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
17package com.android.server.am;
18
19import android.annotation.UiThread;
20import android.app.ActivityManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.os.Build;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.Message;
28import android.util.AtomicFile;
29import android.util.DisplayMetrics;
30import android.util.Slog;
31import android.util.Xml;
32
33import com.android.internal.util.FastXmlSerializer;
34
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37import org.xmlpull.v1.XmlSerializer;
38
39import java.io.File;
40import java.io.FileInputStream;
41import java.io.FileOutputStream;
42import java.nio.charset.StandardCharsets;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.Map;
46
47/**
48 * Manages warning dialogs shown during application lifecycle.
49 */
50class 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