blob: 21b68095ef67f3dab67fcbc70b29132685a37219 [file] [log] [blame]
/*
* 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.wm;
import android.annotation.UiThread;
import android.content.ComponentName;
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.HashSet;
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 ActivityTaskManagerService mAtm;
private final Context mUiContext;
private final ConfigHandler mHandler;
private final UiHandler mUiHandler;
private final AtomicFile mConfigFile;
private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog;
private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog;
private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog;
/** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities =
new HashSet<>();
/** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */
void alwaysShowUnsupportedCompileSdkWarning(ComponentName activity) {
mAlwaysShowUnsupportedCompileSdkWarningActivities.add(activity);
}
/**
* Creates a new warning dialog manager.
* <p>
* <strong>Note:</strong> Must be called from the ActivityManagerService thread.
*
* @param atm
* @param uiContext
* @param handler
* @param uiHandler
* @param systemDir
*/
public AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler,
Handler uiHandler, File systemDir) {
mAtm = atm;
mUiContext = uiContext;
mHandler = new ConfigHandler(handler.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 = mAtm.getGlobalConfiguration();
if (globalConfig.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE
&& r.info.applicationInfo.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.info.applicationInfo.compileSdkVersion == 0
|| r.info.applicationInfo.compileSdkVersionCodename == null) {
// We don't know enough about this package. Abort!
return;
}
// TODO(b/75318890): Need to move this to when the app actually crashes.
if (/*ActivityManager.isRunningInTestHarness()
&&*/ !mAlwaysShowUnsupportedCompileSdkWarningActivities.contains(
r.mActivityComponent)) {
// Don't show warning if we are running in a test harness and we don't have to always
// show for this activity.
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.info.applicationInfo.compileSdkVersion;
final int platformSdk = Build.VERSION.SDK_INT;
final boolean isCompileSdkPreview =
!"REL".equals(r.info.applicationInfo.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.info.applicationInfo.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.info.applicationInfo.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);
mHandler.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);
}
mHandler.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 ActivityTaskManagerService thread.
*/
private final class ConfigHandler extends Handler {
private static final int MSG_WRITE = 1;
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) {
}
}
}
}
}