1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wm;
18 
19 import android.annotation.UiThread;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.os.Build;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.os.Message;
27 import android.util.AtomicFile;
28 import android.util.DisplayMetrics;
29 import android.util.Slog;
30 import android.util.TypedXmlPullParser;
31 import android.util.TypedXmlSerializer;
32 import android.util.Xml;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.FileOutputStream;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.Map;
43 
44 /**
45  * Manages warning dialogs shown during application lifecycle.
46  */
47 class AppWarnings {
48     private static final String TAG = "AppWarnings";
49     private static final String CONFIG_FILE_NAME = "packages-warnings.xml";
50 
51     public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01;
52     public static final int FLAG_HIDE_COMPILE_SDK = 0x02;
53     public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04;
54 
55     private final HashMap<String, Integer> mPackageFlags = new HashMap<>();
56 
57     private final ActivityTaskManagerService mAtm;
58     private final Context mUiContext;
59     private final ConfigHandler mHandler;
60     private final UiHandler mUiHandler;
61     private final AtomicFile mConfigFile;
62 
63     private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog;
64     private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog;
65     private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog;
66 
67     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
68     private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
69             new HashSet<>();
70 
71     /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
alwaysShowUnsupportedCompileSdkWarning(ComponentName activity)72     void alwaysShowUnsupportedCompileSdkWarning(ComponentName activity) {
73         mAlwaysShowUnsupportedCompileSdkWarningActivities.add(activity);
74     }
75 
76     /**
77      * Creates a new warning dialog manager.
78      * <p>
79      * <strong>Note:</strong> Must be called from the ActivityManagerService thread.
80      *
81      * @param atm
82      * @param uiContext
83      * @param handler
84      * @param uiHandler
85      * @param systemDir
86      */
AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler, Handler uiHandler, File systemDir)87     public AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler,
88             Handler uiHandler, File systemDir) {
89         mAtm = atm;
90         mUiContext = uiContext;
91         mHandler = new ConfigHandler(handler.getLooper());
92         mUiHandler = new UiHandler(uiHandler.getLooper());
93         mConfigFile = new AtomicFile(new File(systemDir, CONFIG_FILE_NAME), "warnings-config");
94 
95         readConfigFromFileAmsThread();
96     }
97 
98     /**
99      * Shows the "unsupported display size" warning, if necessary.
100      *
101      * @param r activity record for which the warning may be displayed
102      */
showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r)103     public void showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r) {
104         final Configuration globalConfig = mAtm.getGlobalConfiguration();
105         if (globalConfig.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE
106                 && r.info.applicationInfo.requiresSmallestWidthDp
107                 > globalConfig.smallestScreenWidthDp) {
108             mUiHandler.showUnsupportedDisplaySizeDialog(r);
109         }
110     }
111 
112     /**
113      * Shows the "unsupported compile SDK" warning, if necessary.
114      *
115      * @param r activity record for which the warning may be displayed
116      */
showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r)117     public void showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r) {
118         if (r.info.applicationInfo.compileSdkVersion == 0
119                 || r.info.applicationInfo.compileSdkVersionCodename == null) {
120             // We don't know enough about this package. Abort!
121             return;
122         }
123 
124         // TODO(b/75318890): Need to move this to when the app actually crashes.
125         if (/*ActivityManager.isRunningInTestHarness()
126                 &&*/ !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(
127                         r.mActivityComponent)) {
128             // Don't show warning if we are running in a test harness and we don't have to always
129             // show for this activity.
130             return;
131         }
132 
133         // If the application was built against an pre-release SDK that's older than the current
134         // platform OR if the current platform is pre-release and older than the SDK against which
135         // the application was built OR both are pre-release with the same SDK_INT but different
136         // codenames (e.g. simultaneous pre-release development), then we're likely to run into
137         // compatibility issues. Warn the user and offer to check for an update.
138         final int compileSdk = r.info.applicationInfo.compileSdkVersion;
139         final int platformSdk = Build.VERSION.SDK_INT;
140         final boolean isCompileSdkPreview =
141                 !"REL".equals(r.info.applicationInfo.compileSdkVersionCodename);
142         final boolean isPlatformSdkPreview = !"REL".equals(Build.VERSION.CODENAME);
143         if ((isCompileSdkPreview && compileSdk < platformSdk)
144                 || (isPlatformSdkPreview && platformSdk < compileSdk)
145                 || (isCompileSdkPreview && isPlatformSdkPreview && platformSdk == compileSdk
146                     && !Build.VERSION.CODENAME.equals(
147                             r.info.applicationInfo.compileSdkVersionCodename))) {
148             mUiHandler.showUnsupportedCompileSdkDialog(r);
149         }
150     }
151 
152     /**
153      * Shows the "deprecated target sdk" warning, if necessary.
154      *
155      * @param r activity record for which the warning may be displayed
156      */
showDeprecatedTargetDialogIfNeeded(ActivityRecord r)157     public void showDeprecatedTargetDialogIfNeeded(ActivityRecord r) {
158         if (r.info.applicationInfo.targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT) {
159             mUiHandler.showDeprecatedTargetDialog(r);
160         }
161     }
162 
163     /**
164      * Called when an activity is being started.
165      *
166      * @param r record for the activity being started
167      */
onStartActivity(ActivityRecord r)168     public void onStartActivity(ActivityRecord r) {
169         showUnsupportedCompileSdkDialogIfNeeded(r);
170         showUnsupportedDisplaySizeDialogIfNeeded(r);
171         showDeprecatedTargetDialogIfNeeded(r);
172     }
173 
174     /**
175      * Called when an activity was previously started and is being resumed.
176      *
177      * @param r record for the activity being resumed
178      */
onResumeActivity(ActivityRecord r)179     public void onResumeActivity(ActivityRecord r) {
180         showUnsupportedDisplaySizeDialogIfNeeded(r);
181     }
182 
183     /**
184      * Called by ActivityManagerService when package data has been cleared.
185      *
186      * @param name the package whose data has been cleared
187      */
onPackageDataCleared(String name)188     public void onPackageDataCleared(String name) {
189         removePackageAndHideDialogs(name);
190     }
191 
192     /**
193      * Called by ActivityManagerService when a package has been uninstalled.
194      *
195      * @param name the package that has been uninstalled
196      */
onPackageUninstalled(String name)197     public void onPackageUninstalled(String name) {
198         removePackageAndHideDialogs(name);
199     }
200 
201     /**
202      * Called by ActivityManagerService when the default display density has changed.
203      */
onDensityChanged()204     public void onDensityChanged() {
205         mUiHandler.hideUnsupportedDisplaySizeDialog();
206     }
207 
208     /**
209      * Does what it says on the tin.
210      */
removePackageAndHideDialogs(String name)211     private void removePackageAndHideDialogs(String name) {
212         mUiHandler.hideDialogsForPackage(name);
213 
214         synchronized (mPackageFlags) {
215             mPackageFlags.remove(name);
216             mHandler.scheduleWrite();
217         }
218     }
219 
220     /**
221      * Hides the "unsupported display size" warning.
222      * <p>
223      * <strong>Note:</strong> Must be called on the UI thread.
224      */
225     @UiThread
hideUnsupportedDisplaySizeDialogUiThread()226     private void hideUnsupportedDisplaySizeDialogUiThread() {
227         if (mUnsupportedDisplaySizeDialog != null) {
228             mUnsupportedDisplaySizeDialog.dismiss();
229             mUnsupportedDisplaySizeDialog = null;
230         }
231     }
232 
233     /**
234      * Shows the "unsupported display size" warning for the given application.
235      * <p>
236      * <strong>Note:</strong> Must be called on the UI thread.
237      *
238      * @param ar record for the activity that triggered the warning
239      */
240     @UiThread
showUnsupportedDisplaySizeDialogUiThread(ActivityRecord ar)241     private void showUnsupportedDisplaySizeDialogUiThread(ActivityRecord ar) {
242         if (mUnsupportedDisplaySizeDialog != null) {
243             mUnsupportedDisplaySizeDialog.dismiss();
244             mUnsupportedDisplaySizeDialog = null;
245         }
246         if (ar != null && !hasPackageFlag(
247                 ar.packageName, FLAG_HIDE_DISPLAY_SIZE)) {
248             mUnsupportedDisplaySizeDialog = new UnsupportedDisplaySizeDialog(
249                     AppWarnings.this, mUiContext, ar.info.applicationInfo);
250             mUnsupportedDisplaySizeDialog.show();
251         }
252     }
253 
254     /**
255      * Shows the "unsupported compile SDK" warning for the given application.
256      * <p>
257      * <strong>Note:</strong> Must be called on the UI thread.
258      *
259      * @param ar record for the activity that triggered the warning
260      */
261     @UiThread
showUnsupportedCompileSdkDialogUiThread(ActivityRecord ar)262     private void showUnsupportedCompileSdkDialogUiThread(ActivityRecord ar) {
263         if (mUnsupportedCompileSdkDialog != null) {
264             mUnsupportedCompileSdkDialog.dismiss();
265             mUnsupportedCompileSdkDialog = null;
266         }
267         if (ar != null && !hasPackageFlag(
268                 ar.packageName, FLAG_HIDE_COMPILE_SDK)) {
269             mUnsupportedCompileSdkDialog = new UnsupportedCompileSdkDialog(
270                     AppWarnings.this, mUiContext, ar.info.applicationInfo);
271             mUnsupportedCompileSdkDialog.show();
272         }
273     }
274 
275     /**
276      * Shows the "deprecated target sdk version" warning for the given application.
277      * <p>
278      * <strong>Note:</strong> Must be called on the UI thread.
279      *
280      * @param ar record for the activity that triggered the warning
281      */
282     @UiThread
showDeprecatedTargetSdkDialogUiThread(ActivityRecord ar)283     private void showDeprecatedTargetSdkDialogUiThread(ActivityRecord ar) {
284         if (mDeprecatedTargetSdkVersionDialog != null) {
285             mDeprecatedTargetSdkVersionDialog.dismiss();
286             mDeprecatedTargetSdkVersionDialog = null;
287         }
288         if (ar != null && !hasPackageFlag(
289                 ar.packageName, FLAG_HIDE_DEPRECATED_SDK)) {
290             mDeprecatedTargetSdkVersionDialog = new DeprecatedTargetSdkVersionDialog(
291                     AppWarnings.this, mUiContext, ar.info.applicationInfo);
292             mDeprecatedTargetSdkVersionDialog.show();
293         }
294     }
295 
296     /**
297      * Dismisses all warnings for the given package.
298      * <p>
299      * <strong>Note:</strong> Must be called on the UI thread.
300      *
301      * @param name the package for which warnings should be dismissed, or {@code null} to dismiss
302      *             all warnings
303      */
304     @UiThread
hideDialogsForPackageUiThread(String name)305     private void hideDialogsForPackageUiThread(String name) {
306         // Hides the "unsupported display" dialog if necessary.
307         if (mUnsupportedDisplaySizeDialog != null && (name == null || name.equals(
308                 mUnsupportedDisplaySizeDialog.getPackageName()))) {
309             mUnsupportedDisplaySizeDialog.dismiss();
310             mUnsupportedDisplaySizeDialog = null;
311         }
312 
313         // Hides the "unsupported compile SDK" dialog if necessary.
314         if (mUnsupportedCompileSdkDialog != null && (name == null || name.equals(
315                 mUnsupportedCompileSdkDialog.getPackageName()))) {
316             mUnsupportedCompileSdkDialog.dismiss();
317             mUnsupportedCompileSdkDialog = null;
318         }
319 
320         // Hides the "deprecated target sdk version" dialog if necessary.
321         if (mDeprecatedTargetSdkVersionDialog != null && (name == null || name.equals(
322                 mDeprecatedTargetSdkVersionDialog.getPackageName()))) {
323             mDeprecatedTargetSdkVersionDialog.dismiss();
324             mDeprecatedTargetSdkVersionDialog = null;
325         }
326     }
327 
328     /**
329      * Returns the value of the flag for the given package.
330      *
331      * @param name the package from which to retrieve the flag
332      * @param flag the bitmask for the flag to retrieve
333      * @return {@code true} if the flag is enabled, {@code false} otherwise
334      */
hasPackageFlag(String name, int flag)335     boolean hasPackageFlag(String name, int flag) {
336         return (getPackageFlags(name) & flag) == flag;
337     }
338 
339     /**
340      * Sets the flag for the given package to the specified value.
341      *
342      * @param name the package on which to set the flag
343      * @param flag the bitmask for flag to set
344      * @param enabled the value to set for the flag
345      */
setPackageFlag(String name, int flag, boolean enabled)346     void setPackageFlag(String name, int flag, boolean enabled) {
347         synchronized (mPackageFlags) {
348             final int curFlags = getPackageFlags(name);
349             final int newFlags = enabled ? (curFlags | flag) : (curFlags & ~flag);
350             if (curFlags != newFlags) {
351                 if (newFlags != 0) {
352                     mPackageFlags.put(name, newFlags);
353                 } else {
354                     mPackageFlags.remove(name);
355                 }
356                 mHandler.scheduleWrite();
357             }
358         }
359     }
360 
361     /**
362      * Returns the bitmask of flags set for the specified package.
363      */
getPackageFlags(String name)364     private int getPackageFlags(String name) {
365         synchronized (mPackageFlags) {
366             return mPackageFlags.getOrDefault(name, 0);
367         }
368     }
369 
370     /**
371      * Handles messages on the system process UI thread.
372      */
373     private final class UiHandler extends Handler {
374         private static final int MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 1;
375         private static final int MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 2;
376         private static final int MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG = 3;
377         private static final int MSG_HIDE_DIALOGS_FOR_PACKAGE = 4;
378         private static final int MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG = 5;
379 
UiHandler(Looper looper)380         public UiHandler(Looper looper) {
381             super(looper, null, true);
382         }
383 
384         @Override
handleMessage(Message msg)385         public void handleMessage(Message msg) {
386             switch (msg.what) {
387                 case MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG: {
388                     final ActivityRecord ar = (ActivityRecord) msg.obj;
389                     showUnsupportedDisplaySizeDialogUiThread(ar);
390                 } break;
391                 case MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG: {
392                     hideUnsupportedDisplaySizeDialogUiThread();
393                 } break;
394                 case MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG: {
395                     final ActivityRecord ar = (ActivityRecord) msg.obj;
396                     showUnsupportedCompileSdkDialogUiThread(ar);
397                 } break;
398                 case MSG_HIDE_DIALOGS_FOR_PACKAGE: {
399                     final String name = (String) msg.obj;
400                     hideDialogsForPackageUiThread(name);
401                 } break;
402                 case MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG: {
403                     final ActivityRecord ar = (ActivityRecord) msg.obj;
404                     showDeprecatedTargetSdkDialogUiThread(ar);
405                 } break;
406             }
407         }
408 
showUnsupportedDisplaySizeDialog(ActivityRecord r)409         public void showUnsupportedDisplaySizeDialog(ActivityRecord r) {
410             removeMessages(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
411             obtainMessage(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG, r).sendToTarget();
412         }
413 
hideUnsupportedDisplaySizeDialog()414         public void hideUnsupportedDisplaySizeDialog() {
415             removeMessages(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
416             sendEmptyMessage(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
417         }
418 
showUnsupportedCompileSdkDialog(ActivityRecord r)419         public void showUnsupportedCompileSdkDialog(ActivityRecord r) {
420             removeMessages(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG);
421             obtainMessage(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG, r).sendToTarget();
422         }
423 
showDeprecatedTargetDialog(ActivityRecord r)424         public void showDeprecatedTargetDialog(ActivityRecord r) {
425             removeMessages(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG);
426             obtainMessage(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG, r).sendToTarget();
427         }
428 
hideDialogsForPackage(String name)429         public void hideDialogsForPackage(String name) {
430             obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget();
431         }
432     }
433 
434     /**
435      * Handles messages on the ActivityTaskManagerService thread.
436      */
437     private final class ConfigHandler extends Handler {
438         private static final int MSG_WRITE = 1;
439 
440         private static final int DELAY_MSG_WRITE = 10000;
441 
ConfigHandler(Looper looper)442         public ConfigHandler(Looper looper) {
443             super(looper, null, true);
444         }
445 
446         @Override
handleMessage(Message msg)447         public void handleMessage(Message msg) {
448             switch (msg.what) {
449                 case MSG_WRITE:
450                     writeConfigToFileAmsThread();
451                     break;
452             }
453         }
454 
scheduleWrite()455         public void scheduleWrite() {
456             removeMessages(MSG_WRITE);
457             sendEmptyMessageDelayed(MSG_WRITE, DELAY_MSG_WRITE);
458         }
459     }
460 
461     /**
462      * Writes the configuration file.
463      * <p>
464      * <strong>Note:</strong> Should be called from the ActivityManagerService thread unless you
465      * don't care where you're doing I/O operations. But you <i>do</i> care, don't you?
466      */
writeConfigToFileAmsThread()467     private void writeConfigToFileAmsThread() {
468         // Create a shallow copy so that we don't have to synchronize on config.
469         final HashMap<String, Integer> packageFlags;
470         synchronized (mPackageFlags) {
471             packageFlags = new HashMap<>(mPackageFlags);
472         }
473 
474         FileOutputStream fos = null;
475         try {
476             fos = mConfigFile.startWrite();
477 
478             final TypedXmlSerializer out = Xml.resolveSerializer(fos);
479             out.startDocument(null, true);
480             out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
481             out.startTag(null, "packages");
482 
483             for (Map.Entry<String, Integer> entry : packageFlags.entrySet()) {
484                 String pkg = entry.getKey();
485                 int mode = entry.getValue();
486                 if (mode == 0) {
487                     continue;
488                 }
489                 out.startTag(null, "package");
490                 out.attribute(null, "name", pkg);
491                 out.attributeInt(null, "flags", mode);
492                 out.endTag(null, "package");
493             }
494 
495             out.endTag(null, "packages");
496             out.endDocument();
497 
498             mConfigFile.finishWrite(fos);
499         } catch (java.io.IOException e1) {
500             Slog.w(TAG, "Error writing package metadata", e1);
501             if (fos != null) {
502                 mConfigFile.failWrite(fos);
503             }
504         }
505     }
506 
507     /**
508      * Reads the configuration file and populates the package flags.
509      * <p>
510      * <strong>Note:</strong> Must be called from the constructor (and thus on the
511      * ActivityManagerService thread) since we don't synchronize on config.
512      */
readConfigFromFileAmsThread()513     private void readConfigFromFileAmsThread() {
514         FileInputStream fis = null;
515 
516         try {
517             fis = mConfigFile.openRead();
518 
519             final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
520 
521             int eventType = parser.getEventType();
522             while (eventType != XmlPullParser.START_TAG &&
523                     eventType != XmlPullParser.END_DOCUMENT) {
524                 eventType = parser.next();
525             }
526             if (eventType == XmlPullParser.END_DOCUMENT) {
527                 return;
528             }
529 
530             String tagName = parser.getName();
531             if ("packages".equals(tagName)) {
532                 eventType = parser.next();
533                 do {
534                     if (eventType == XmlPullParser.START_TAG) {
535                         tagName = parser.getName();
536                         if (parser.getDepth() == 2) {
537                             if ("package".equals(tagName)) {
538                                 final String name = parser.getAttributeValue(null, "name");
539                                 if (name != null) {
540                                     int flagsInt = parser.getAttributeInt(null, "flags", 0);
541                                     mPackageFlags.put(name, flagsInt);
542                                 }
543                             }
544                         }
545                     }
546                     eventType = parser.next();
547                 } while (eventType != XmlPullParser.END_DOCUMENT);
548             }
549         } catch (XmlPullParserException e) {
550             Slog.w(TAG, "Error reading package metadata", e);
551         } catch (java.io.IOException e) {
552             if (fis != null) Slog.w(TAG, "Error reading package metadata", e);
553         } finally {
554             if (fis != null) {
555                 try {
556                     fis.close();
557                 } catch (java.io.IOException e1) {
558                 }
559             }
560         }
561     }
562 }
563