| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.am; |
| |
| import android.annotation.UiThread; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.AtomicFile; |
| import android.util.DisplayMetrics; |
| import android.util.Slog; |
| import android.util.Xml; |
| |
| import com.android.internal.util.FastXmlSerializer; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.nio.charset.StandardCharsets; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * Manages warning dialogs shown during application lifecycle. |
| */ |
| class AppWarnings { |
| private static final String TAG = "AppWarnings"; |
| private static final String CONFIG_FILE_NAME = "packages-warnings.xml"; |
| |
| public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01; |
| public static final int FLAG_HIDE_COMPILE_SDK = 0x02; |
| public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04; |
| |
| private final HashMap<String, Integer> mPackageFlags = new HashMap<>(); |
| |
| private final ActivityManagerService mAms; |
| private final Context mUiContext; |
| private final ConfigHandler mAmsHandler; |
| private final UiHandler mUiHandler; |
| private final AtomicFile mConfigFile; |
| |
| private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog; |
| private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog; |
| private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog; |
| |
| /** |
| * Creates a new warning dialog manager. |
| * <p> |
| * <strong>Note:</strong> Must be called from the ActivityManagerService thread. |
| * |
| * @param ams |
| * @param uiContext |
| * @param amsHandler |
| * @param uiHandler |
| * @param systemDir |
| */ |
| public AppWarnings(ActivityManagerService ams, Context uiContext, Handler amsHandler, |
| Handler uiHandler, File systemDir) { |
| mAms = ams; |
| mUiContext = uiContext; |
| mAmsHandler = new ConfigHandler(amsHandler.getLooper()); |
| mUiHandler = new UiHandler(uiHandler.getLooper()); |
| mConfigFile = new AtomicFile(new File(systemDir, CONFIG_FILE_NAME), "warnings-config"); |
| |
| readConfigFromFileAmsThread(); |
| } |
| |
| /** |
| * Shows the "unsupported display size" warning, if necessary. |
| * |
| * @param r activity record for which the warning may be displayed |
| */ |
| public void showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r) { |
| final Configuration globalConfig = mAms.getGlobalConfiguration(); |
| if (globalConfig.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE |
| && r.appInfo.requiresSmallestWidthDp > globalConfig.smallestScreenWidthDp) { |
| mUiHandler.showUnsupportedDisplaySizeDialog(r); |
| } |
| } |
| |
| /** |
| * Shows the "unsupported compile SDK" warning, if necessary. |
| * |
| * @param r activity record for which the warning may be displayed |
| */ |
| public void showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r) { |
| if (r.appInfo.compileSdkVersion == 0 || r.appInfo.compileSdkVersionCodename == null) { |
| // We don't know enough about this package. Abort! |
| return; |
| } |
| |
| // If the application was built against an pre-release SDK that's older than the current |
| // platform OR if the current platform is pre-release and older than the SDK against which |
| // the application was built OR both are pre-release with the same SDK_INT but different |
| // codenames (e.g. simultaneous pre-release development), then we're likely to run into |
| // compatibility issues. Warn the user and offer to check for an update. |
| final int compileSdk = r.appInfo.compileSdkVersion; |
| final int platformSdk = Build.VERSION.SDK_INT; |
| final boolean isCompileSdkPreview = !"REL".equals(r.appInfo.compileSdkVersionCodename); |
| final boolean isPlatformSdkPreview = !"REL".equals(Build.VERSION.CODENAME); |
| if ((isCompileSdkPreview && compileSdk < platformSdk) |
| || (isPlatformSdkPreview && platformSdk < compileSdk) |
| || (isCompileSdkPreview && isPlatformSdkPreview && platformSdk == compileSdk |
| && !Build.VERSION.CODENAME.equals(r.appInfo.compileSdkVersionCodename))) { |
| mUiHandler.showUnsupportedCompileSdkDialog(r); |
| } |
| } |
| |
| /** |
| * Shows the "deprecated target sdk" warning, if necessary. |
| * |
| * @param r activity record for which the warning may be displayed |
| */ |
| public void showDeprecatedTargetDialogIfNeeded(ActivityRecord r) { |
| if (r.appInfo.targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT) { |
| mUiHandler.showDeprecatedTargetDialog(r); |
| } |
| } |
| |
| /** |
| * Called when an activity is being started. |
| * |
| * @param r record for the activity being started |
| */ |
| public void onStartActivity(ActivityRecord r) { |
| showUnsupportedCompileSdkDialogIfNeeded(r); |
| showUnsupportedDisplaySizeDialogIfNeeded(r); |
| showDeprecatedTargetDialogIfNeeded(r); |
| } |
| |
| /** |
| * Called when an activity was previously started and is being resumed. |
| * |
| * @param r record for the activity being resumed |
| */ |
| public void onResumeActivity(ActivityRecord r) { |
| showUnsupportedDisplaySizeDialogIfNeeded(r); |
| } |
| |
| /** |
| * Called by ActivityManagerService when package data has been cleared. |
| * |
| * @param name the package whose data has been cleared |
| */ |
| public void onPackageDataCleared(String name) { |
| removePackageAndHideDialogs(name); |
| } |
| |
| /** |
| * Called by ActivityManagerService when a package has been uninstalled. |
| * |
| * @param name the package that has been uninstalled |
| */ |
| public void onPackageUninstalled(String name) { |
| removePackageAndHideDialogs(name); |
| } |
| |
| /** |
| * Called by ActivityManagerService when the default display density has changed. |
| */ |
| public void onDensityChanged() { |
| mUiHandler.hideUnsupportedDisplaySizeDialog(); |
| } |
| |
| /** |
| * Does what it says on the tin. |
| */ |
| private void removePackageAndHideDialogs(String name) { |
| mUiHandler.hideDialogsForPackage(name); |
| |
| synchronized (mPackageFlags) { |
| mPackageFlags.remove(name); |
| mAmsHandler.scheduleWrite(); |
| } |
| } |
| |
| /** |
| * Hides the "unsupported display size" warning. |
| * <p> |
| * <strong>Note:</strong> Must be called on the UI thread. |
| */ |
| @UiThread |
| private void hideUnsupportedDisplaySizeDialogUiThread() { |
| if (mUnsupportedDisplaySizeDialog != null) { |
| mUnsupportedDisplaySizeDialog.dismiss(); |
| mUnsupportedDisplaySizeDialog = null; |
| } |
| } |
| |
| /** |
| * Shows the "unsupported display size" warning for the given application. |
| * <p> |
| * <strong>Note:</strong> Must be called on the UI thread. |
| * |
| * @param ar record for the activity that triggered the warning |
| */ |
| @UiThread |
| private void showUnsupportedDisplaySizeDialogUiThread(ActivityRecord ar) { |
| if (mUnsupportedDisplaySizeDialog != null) { |
| mUnsupportedDisplaySizeDialog.dismiss(); |
| mUnsupportedDisplaySizeDialog = null; |
| } |
| if (ar != null && !hasPackageFlag( |
| ar.packageName, FLAG_HIDE_DISPLAY_SIZE)) { |
| mUnsupportedDisplaySizeDialog = new UnsupportedDisplaySizeDialog( |
| AppWarnings.this, mUiContext, ar.info.applicationInfo); |
| mUnsupportedDisplaySizeDialog.show(); |
| } |
| } |
| |
| /** |
| * Shows the "unsupported compile SDK" warning for the given application. |
| * <p> |
| * <strong>Note:</strong> Must be called on the UI thread. |
| * |
| * @param ar record for the activity that triggered the warning |
| */ |
| @UiThread |
| private void showUnsupportedCompileSdkDialogUiThread(ActivityRecord ar) { |
| if (mUnsupportedCompileSdkDialog != null) { |
| mUnsupportedCompileSdkDialog.dismiss(); |
| mUnsupportedCompileSdkDialog = null; |
| } |
| if (ar != null && !hasPackageFlag( |
| ar.packageName, FLAG_HIDE_COMPILE_SDK)) { |
| mUnsupportedCompileSdkDialog = new UnsupportedCompileSdkDialog( |
| AppWarnings.this, mUiContext, ar.info.applicationInfo); |
| mUnsupportedCompileSdkDialog.show(); |
| } |
| } |
| |
| /** |
| * Shows the "deprecated target sdk version" warning for the given application. |
| * <p> |
| * <strong>Note:</strong> Must be called on the UI thread. |
| * |
| * @param ar record for the activity that triggered the warning |
| */ |
| @UiThread |
| private void showDeprecatedTargetSdkDialogUiThread(ActivityRecord ar) { |
| if (mDeprecatedTargetSdkVersionDialog != null) { |
| mDeprecatedTargetSdkVersionDialog.dismiss(); |
| mDeprecatedTargetSdkVersionDialog = null; |
| } |
| if (ar != null && !hasPackageFlag( |
| ar.packageName, FLAG_HIDE_DEPRECATED_SDK)) { |
| mDeprecatedTargetSdkVersionDialog = new DeprecatedTargetSdkVersionDialog( |
| AppWarnings.this, mUiContext, ar.info.applicationInfo); |
| mDeprecatedTargetSdkVersionDialog.show(); |
| } |
| } |
| |
| /** |
| * Dismisses all warnings for the given package. |
| * <p> |
| * <strong>Note:</strong> Must be called on the UI thread. |
| * |
| * @param name the package for which warnings should be dismissed, or {@code null} to dismiss |
| * all warnings |
| */ |
| @UiThread |
| private void hideDialogsForPackageUiThread(String name) { |
| // Hides the "unsupported display" dialog if necessary. |
| if (mUnsupportedDisplaySizeDialog != null && (name == null || name.equals( |
| mUnsupportedDisplaySizeDialog.getPackageName()))) { |
| mUnsupportedDisplaySizeDialog.dismiss(); |
| mUnsupportedDisplaySizeDialog = null; |
| } |
| |
| // Hides the "unsupported compile SDK" dialog if necessary. |
| if (mUnsupportedCompileSdkDialog != null && (name == null || name.equals( |
| mUnsupportedCompileSdkDialog.getPackageName()))) { |
| mUnsupportedCompileSdkDialog.dismiss(); |
| mUnsupportedCompileSdkDialog = null; |
| } |
| |
| // Hides the "deprecated target sdk version" dialog if necessary. |
| if (mDeprecatedTargetSdkVersionDialog != null && (name == null || name.equals( |
| mDeprecatedTargetSdkVersionDialog.getPackageName()))) { |
| mDeprecatedTargetSdkVersionDialog.dismiss(); |
| mDeprecatedTargetSdkVersionDialog = null; |
| } |
| } |
| |
| /** |
| * Returns the value of the flag for the given package. |
| * |
| * @param name the package from which to retrieve the flag |
| * @param flag the bitmask for the flag to retrieve |
| * @return {@code true} if the flag is enabled, {@code false} otherwise |
| */ |
| boolean hasPackageFlag(String name, int flag) { |
| return (getPackageFlags(name) & flag) == flag; |
| } |
| |
| /** |
| * Sets the flag for the given package to the specified value. |
| * |
| * @param name the package on which to set the flag |
| * @param flag the bitmask for flag to set |
| * @param enabled the value to set for the flag |
| */ |
| void setPackageFlag(String name, int flag, boolean enabled) { |
| synchronized (mPackageFlags) { |
| final int curFlags = getPackageFlags(name); |
| final int newFlags = enabled ? (curFlags | flag) : (curFlags & ~flag); |
| if (curFlags != newFlags) { |
| if (newFlags != 0) { |
| mPackageFlags.put(name, newFlags); |
| } else { |
| mPackageFlags.remove(name); |
| } |
| mAmsHandler.scheduleWrite(); |
| } |
| } |
| } |
| |
| /** |
| * Returns the bitmask of flags set for the specified package. |
| */ |
| private int getPackageFlags(String name) { |
| synchronized (mPackageFlags) { |
| return mPackageFlags.getOrDefault(name, 0); |
| } |
| } |
| |
| /** |
| * Handles messages on the system process UI thread. |
| */ |
| private final class UiHandler extends Handler { |
| private static final int MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 1; |
| private static final int MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 2; |
| private static final int MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG = 3; |
| private static final int MSG_HIDE_DIALOGS_FOR_PACKAGE = 4; |
| private static final int MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG = 5; |
| |
| public UiHandler(Looper looper) { |
| super(looper, null, true); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG: { |
| final ActivityRecord ar = (ActivityRecord) msg.obj; |
| showUnsupportedDisplaySizeDialogUiThread(ar); |
| } break; |
| case MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG: { |
| hideUnsupportedDisplaySizeDialogUiThread(); |
| } break; |
| case MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG: { |
| final ActivityRecord ar = (ActivityRecord) msg.obj; |
| showUnsupportedCompileSdkDialogUiThread(ar); |
| } break; |
| case MSG_HIDE_DIALOGS_FOR_PACKAGE: { |
| final String name = (String) msg.obj; |
| hideDialogsForPackageUiThread(name); |
| } break; |
| case MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG: { |
| final ActivityRecord ar = (ActivityRecord) msg.obj; |
| showDeprecatedTargetSdkDialogUiThread(ar); |
| } break; |
| } |
| } |
| |
| public void showUnsupportedDisplaySizeDialog(ActivityRecord r) { |
| removeMessages(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG); |
| obtainMessage(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG, r).sendToTarget(); |
| } |
| |
| public void hideUnsupportedDisplaySizeDialog() { |
| removeMessages(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG); |
| sendEmptyMessage(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG); |
| } |
| |
| public void showUnsupportedCompileSdkDialog(ActivityRecord r) { |
| removeMessages(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG); |
| obtainMessage(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG, r).sendToTarget(); |
| } |
| |
| public void showDeprecatedTargetDialog(ActivityRecord r) { |
| removeMessages(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG); |
| obtainMessage(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG, r).sendToTarget(); |
| } |
| |
| public void hideDialogsForPackage(String name) { |
| obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget(); |
| } |
| } |
| |
| /** |
| * Handles messages on the ActivityManagerService thread. |
| */ |
| private final class ConfigHandler extends Handler { |
| private static final int MSG_WRITE = ActivityManagerService.FIRST_COMPAT_MODE_MSG; |
| |
| private static final int DELAY_MSG_WRITE = 10000; |
| |
| public ConfigHandler(Looper looper) { |
| super(looper, null, true); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_WRITE: |
| writeConfigToFileAmsThread(); |
| break; |
| } |
| } |
| |
| public void scheduleWrite() { |
| removeMessages(MSG_WRITE); |
| sendEmptyMessageDelayed(MSG_WRITE, DELAY_MSG_WRITE); |
| } |
| } |
| |
| /** |
| * Writes the configuration file. |
| * <p> |
| * <strong>Note:</strong> Should be called from the ActivityManagerService thread unless you |
| * don't care where you're doing I/O operations. But you <i>do</i> care, don't you? |
| */ |
| private void writeConfigToFileAmsThread() { |
| // Create a shallow copy so that we don't have to synchronize on config. |
| final HashMap<String, Integer> packageFlags; |
| synchronized (mPackageFlags) { |
| packageFlags = new HashMap<>(mPackageFlags); |
| } |
| |
| FileOutputStream fos = null; |
| try { |
| fos = mConfigFile.startWrite(); |
| |
| final XmlSerializer out = new FastXmlSerializer(); |
| out.setOutput(fos, StandardCharsets.UTF_8.name()); |
| out.startDocument(null, true); |
| out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); |
| out.startTag(null, "packages"); |
| |
| for (Map.Entry<String, Integer> entry : packageFlags.entrySet()) { |
| String pkg = entry.getKey(); |
| int mode = entry.getValue(); |
| if (mode == 0) { |
| continue; |
| } |
| out.startTag(null, "package"); |
| out.attribute(null, "name", pkg); |
| out.attribute(null, "flags", Integer.toString(mode)); |
| out.endTag(null, "package"); |
| } |
| |
| out.endTag(null, "packages"); |
| out.endDocument(); |
| |
| mConfigFile.finishWrite(fos); |
| } catch (java.io.IOException e1) { |
| Slog.w(TAG, "Error writing package metadata", e1); |
| if (fos != null) { |
| mConfigFile.failWrite(fos); |
| } |
| } |
| } |
| |
| /** |
| * Reads the configuration file and populates the package flags. |
| * <p> |
| * <strong>Note:</strong> Must be called from the constructor (and thus on the |
| * ActivityManagerService thread) since we don't synchronize on config. |
| */ |
| private void readConfigFromFileAmsThread() { |
| FileInputStream fis = null; |
| |
| try { |
| fis = mConfigFile.openRead(); |
| |
| final XmlPullParser parser = Xml.newPullParser(); |
| parser.setInput(fis, StandardCharsets.UTF_8.name()); |
| |
| int eventType = parser.getEventType(); |
| while (eventType != XmlPullParser.START_TAG && |
| eventType != XmlPullParser.END_DOCUMENT) { |
| eventType = parser.next(); |
| } |
| if (eventType == XmlPullParser.END_DOCUMENT) { |
| return; |
| } |
| |
| String tagName = parser.getName(); |
| if ("packages".equals(tagName)) { |
| eventType = parser.next(); |
| do { |
| if (eventType == XmlPullParser.START_TAG) { |
| tagName = parser.getName(); |
| if (parser.getDepth() == 2) { |
| if ("package".equals(tagName)) { |
| final String name = parser.getAttributeValue(null, "name"); |
| if (name != null) { |
| final String flags = parser.getAttributeValue( |
| null, "flags"); |
| int flagsInt = 0; |
| if (flags != null) { |
| try { |
| flagsInt = Integer.parseInt(flags); |
| } catch (NumberFormatException e) { |
| } |
| } |
| mPackageFlags.put(name, flagsInt); |
| } |
| } |
| } |
| } |
| eventType = parser.next(); |
| } while (eventType != XmlPullParser.END_DOCUMENT); |
| } |
| } catch (XmlPullParserException e) { |
| Slog.w(TAG, "Error reading package metadata", e); |
| } catch (java.io.IOException e) { |
| if (fis != null) Slog.w(TAG, "Error reading package metadata", e); |
| } finally { |
| if (fis != null) { |
| try { |
| fis.close(); |
| } catch (java.io.IOException e1) { |
| } |
| } |
| } |
| } |
| } |