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