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.Xml; 31 32 import com.android.internal.util.FastXmlSerializer; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 import org.xmlpull.v1.XmlSerializer; 37 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileOutputStream; 41 import java.nio.charset.StandardCharsets; 42 import java.util.HashMap; 43 import java.util.HashSet; 44 import java.util.Map; 45 46 /** 47 * Manages warning dialogs shown during application lifecycle. 48 */ 49 class AppWarnings { 50 private static final String TAG = "AppWarnings"; 51 private static final String CONFIG_FILE_NAME = "packages-warnings.xml"; 52 53 public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01; 54 public static final int FLAG_HIDE_COMPILE_SDK = 0x02; 55 public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04; 56 57 private final HashMap<String, Integer> mPackageFlags = new HashMap<>(); 58 59 private final ActivityTaskManagerService mAtm; 60 private final Context mUiContext; 61 private final ConfigHandler mHandler; 62 private final UiHandler mUiHandler; 63 private final AtomicFile mConfigFile; 64 65 private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog; 66 private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog; 67 private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog; 68 69 /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */ 70 private HashSet<ComponentName> mAlwaysShowUnsupportedCompileSdkWarningActivities = 71 new HashSet<>(); 72 73 /** @see android.app.ActivityManager#alwaysShowUnsupportedCompileSdkWarning */ alwaysShowUnsupportedCompileSdkWarning(ComponentName activity)74 void alwaysShowUnsupportedCompileSdkWarning(ComponentName activity) { 75 mAlwaysShowUnsupportedCompileSdkWarningActivities.add(activity); 76 } 77 78 /** 79 * Creates a new warning dialog manager. 80 * <p> 81 * <strong>Note:</strong> Must be called from the ActivityManagerService thread. 82 * 83 * @param atm 84 * @param uiContext 85 * @param handler 86 * @param uiHandler 87 * @param systemDir 88 */ AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler, Handler uiHandler, File systemDir)89 public AppWarnings(ActivityTaskManagerService atm, Context uiContext, Handler handler, 90 Handler uiHandler, File systemDir) { 91 mAtm = atm; 92 mUiContext = uiContext; 93 mHandler = new ConfigHandler(handler.getLooper()); 94 mUiHandler = new UiHandler(uiHandler.getLooper()); 95 mConfigFile = new AtomicFile(new File(systemDir, CONFIG_FILE_NAME), "warnings-config"); 96 97 readConfigFromFileAmsThread(); 98 } 99 100 /** 101 * Shows the "unsupported display size" warning, if necessary. 102 * 103 * @param r activity record for which the warning may be displayed 104 */ showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r)105 public void showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r) { 106 final Configuration globalConfig = mAtm.getGlobalConfiguration(); 107 if (globalConfig.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE 108 && r.appInfo.requiresSmallestWidthDp > globalConfig.smallestScreenWidthDp) { 109 mUiHandler.showUnsupportedDisplaySizeDialog(r); 110 } 111 } 112 113 /** 114 * Shows the "unsupported compile SDK" warning, if necessary. 115 * 116 * @param r activity record for which the warning may be displayed 117 */ showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r)118 public void showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r) { 119 if (r.appInfo.compileSdkVersion == 0 || r.appInfo.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.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 */ showDeprecatedTargetDialogIfNeeded(ActivityRecord r)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 */ onStartActivity(ActivityRecord r)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 */ onResumeActivity(ActivityRecord r)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 */ onPackageDataCleared(String name)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 */ onPackageUninstalled(String name)195 public void onPackageUninstalled(String name) { 196 removePackageAndHideDialogs(name); 197 } 198 199 /** 200 * Called by ActivityManagerService when the default display density has changed. 201 */ onDensityChanged()202 public void onDensityChanged() { 203 mUiHandler.hideUnsupportedDisplaySizeDialog(); 204 } 205 206 /** 207 * Does what it says on the tin. 208 */ removePackageAndHideDialogs(String name)209 private void removePackageAndHideDialogs(String name) { 210 mUiHandler.hideDialogsForPackage(name); 211 212 synchronized (mPackageFlags) { 213 mPackageFlags.remove(name); 214 mHandler.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 hideUnsupportedDisplaySizeDialogUiThread()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 showUnsupportedDisplaySizeDialogUiThread(ActivityRecord ar)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 showUnsupportedCompileSdkDialogUiThread(ActivityRecord ar)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 showDeprecatedTargetSdkDialogUiThread(ActivityRecord ar)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 hideDialogsForPackageUiThread(String name)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 */ hasPackageFlag(String name, int flag)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 */ setPackageFlag(String name, int flag, boolean enabled)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 mHandler.scheduleWrite(); 355 } 356 } 357 } 358 359 /** 360 * Returns the bitmask of flags set for the specified package. 361 */ getPackageFlags(String name)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 UiHandler(Looper looper)378 public UiHandler(Looper looper) { 379 super(looper, null, true); 380 } 381 382 @Override handleMessage(Message msg)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 showUnsupportedDisplaySizeDialog(ActivityRecord r)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 hideUnsupportedDisplaySizeDialog()412 public void hideUnsupportedDisplaySizeDialog() { 413 removeMessages(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG); 414 sendEmptyMessage(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG); 415 } 416 showUnsupportedCompileSdkDialog(ActivityRecord r)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 showDeprecatedTargetDialog(ActivityRecord r)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 hideDialogsForPackage(String name)427 public void hideDialogsForPackage(String name) { 428 obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget(); 429 } 430 } 431 432 /** 433 * Handles messages on the ActivityTaskManagerService thread. 434 */ 435 private final class ConfigHandler extends Handler { 436 private static final int MSG_WRITE = 1; 437 438 private static final int DELAY_MSG_WRITE = 10000; 439 ConfigHandler(Looper looper)440 public ConfigHandler(Looper looper) { 441 super(looper, null, true); 442 } 443 444 @Override handleMessage(Message msg)445 public void handleMessage(Message msg) { 446 switch (msg.what) { 447 case MSG_WRITE: 448 writeConfigToFileAmsThread(); 449 break; 450 } 451 } 452 scheduleWrite()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 */ writeConfigToFileAmsThread()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 */ readConfigFromFileAmsThread()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