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