blob: 806e95d71cd15f516145c7d976521ae1090737bd [file] [log] [blame]
Alan Viveretteb6a25732017-11-21 14:49:24 -05001/*
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.content.Context;
21import android.content.res.Configuration;
22import android.os.Build;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.util.AtomicFile;
27import android.util.DisplayMetrics;
28import android.util.Slog;
29import android.util.Xml;
30
31import com.android.internal.util.FastXmlSerializer;
32
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35import org.xmlpull.v1.XmlSerializer;
36
37import java.io.File;
38import java.io.FileInputStream;
39import java.io.FileOutputStream;
40import java.nio.charset.StandardCharsets;
41import java.util.HashMap;
42import java.util.Map;
43
44/**
45 * Manages warning dialogs shown during application lifecycle.
46 */
47class AppWarnings {
48 private static final String TAG = "AppWarnings";
49 private static final String CONFIG_FILE_NAME = "packages-warnings.xml";
50
51 public static final int FLAG_HIDE_DISPLAY_SIZE = 0x01;
52 public static final int FLAG_HIDE_COMPILE_SDK = 0x02;
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +000053 public static final int FLAG_HIDE_DEPRECATED_SDK = 0x04;
Alan Viveretteb6a25732017-11-21 14:49:24 -050054
55 private final HashMap<String, Integer> mPackageFlags = new HashMap<>();
56
57 private final ActivityManagerService mAms;
58 private final Context mUiContext;
59 private final ConfigHandler mAmsHandler;
60 private final UiHandler mUiHandler;
61 private final AtomicFile mConfigFile;
62
63 private UnsupportedDisplaySizeDialog mUnsupportedDisplaySizeDialog;
64 private UnsupportedCompileSdkDialog mUnsupportedCompileSdkDialog;
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +000065 private DeprecatedTargetSdkVersionDialog mDeprecatedTargetSdkVersionDialog;
Alan Viveretteb6a25732017-11-21 14:49:24 -050066
67 /**
68 * Creates a new warning dialog manager.
69 * <p>
70 * <strong>Note:</strong> Must be called from the ActivityManagerService thread.
71 *
72 * @param ams
73 * @param uiContext
74 * @param amsHandler
75 * @param uiHandler
76 * @param systemDir
77 */
78 public AppWarnings(ActivityManagerService ams, Context uiContext, Handler amsHandler,
79 Handler uiHandler, File systemDir) {
80 mAms = ams;
81 mUiContext = uiContext;
82 mAmsHandler = new ConfigHandler(amsHandler.getLooper());
83 mUiHandler = new UiHandler(uiHandler.getLooper());
84 mConfigFile = new AtomicFile(new File(systemDir, CONFIG_FILE_NAME));
85
86 readConfigFromFileAmsThread();
87 }
88
89 /**
90 * Shows the "unsupported display size" warning, if necessary.
91 *
92 * @param r activity record for which the warning may be displayed
93 */
94 public void showUnsupportedDisplaySizeDialogIfNeeded(ActivityRecord r) {
95 final Configuration globalConfig = mAms.getGlobalConfiguration();
96 if (globalConfig.densityDpi != DisplayMetrics.DENSITY_DEVICE_STABLE
97 && r.appInfo.requiresSmallestWidthDp > globalConfig.smallestScreenWidthDp) {
98 mUiHandler.showUnsupportedDisplaySizeDialog(r);
99 }
100 }
101
102 /**
103 * Shows the "unsupported compile SDK" warning, if necessary.
104 *
105 * @param r activity record for which the warning may be displayed
106 */
107 public void showUnsupportedCompileSdkDialogIfNeeded(ActivityRecord r) {
108 if (r.appInfo.compileSdkVersion == 0 || r.appInfo.compileSdkVersionCodename == null) {
109 // We don't know enough about this package. Abort!
110 return;
111 }
112
113 // If the application was built against an pre-release SDK that's older than the current
114 // platform OR if the current platform is pre-release and older than the SDK against which
115 // the application was built OR both are pre-release with the same SDK_INT but different
116 // codenames (e.g. simultaneous pre-release development), then we're likely to run into
117 // compatibility issues. Warn the user and offer to check for an update.
118 final int compileSdk = r.appInfo.compileSdkVersion;
119 final int platformSdk = Build.VERSION.SDK_INT;
120 final boolean isCompileSdkPreview = !"REL".equals(r.appInfo.compileSdkVersionCodename);
121 final boolean isPlatformSdkPreview = !"REL".equals(Build.VERSION.CODENAME);
122 if ((isCompileSdkPreview && compileSdk < platformSdk)
123 || (isPlatformSdkPreview && platformSdk < compileSdk)
124 || (isCompileSdkPreview && isPlatformSdkPreview && platformSdk == compileSdk
125 && !Build.VERSION.CODENAME.equals(r.appInfo.compileSdkVersionCodename))) {
126 mUiHandler.showUnsupportedCompileSdkDialog(r);
127 }
128 }
129
130 /**
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000131 * Shows the "deprecated target sdk" warning, if necessary.
132 *
133 * @param r activity record for which the warning may be displayed
134 */
135 public void showDeprecatedTargetDialogIfNeeded(ActivityRecord r) {
136 if (r.appInfo.targetSdkVersion < Build.VERSION.MIN_SUPPORTED_TARGET_SDK_INT) {
137 mUiHandler.showDeprecatedTargetDialog(r);
138 }
139 }
140
141 /**
Alan Viveretteb6a25732017-11-21 14:49:24 -0500142 * Called when an activity is being started.
143 *
144 * @param r record for the activity being started
145 */
146 public void onStartActivity(ActivityRecord r) {
147 showUnsupportedCompileSdkDialogIfNeeded(r);
148 showUnsupportedDisplaySizeDialogIfNeeded(r);
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000149 showDeprecatedTargetDialogIfNeeded(r);
Alan Viveretteb6a25732017-11-21 14:49:24 -0500150 }
151
152 /**
153 * Called when an activity was previously started and is being resumed.
154 *
155 * @param r record for the activity being resumed
156 */
157 public void onResumeActivity(ActivityRecord r) {
158 showUnsupportedDisplaySizeDialogIfNeeded(r);
159 }
160
161 /**
162 * Called by ActivityManagerService when package data has been cleared.
163 *
164 * @param name the package whose data has been cleared
165 */
166 public void onPackageDataCleared(String name) {
167 removePackageAndHideDialogs(name);
168 }
169
170 /**
171 * Called by ActivityManagerService when a package has been uninstalled.
172 *
173 * @param name the package that has been uninstalled
174 */
175 public void onPackageUninstalled(String name) {
176 removePackageAndHideDialogs(name);
177 }
178
179 /**
180 * Called by ActivityManagerService when the default display density has changed.
181 */
182 public void onDensityChanged() {
183 mUiHandler.hideUnsupportedDisplaySizeDialog();
184 }
185
186 /**
187 * Does what it says on the tin.
188 */
189 private void removePackageAndHideDialogs(String name) {
190 mUiHandler.hideDialogsForPackage(name);
191
192 synchronized (mPackageFlags) {
193 mPackageFlags.remove(name);
194 mAmsHandler.scheduleWrite();
195 }
196 }
197
198 /**
199 * Hides the "unsupported display size" warning.
200 * <p>
201 * <strong>Note:</strong> Must be called on the UI thread.
202 */
203 @UiThread
204 private void hideUnsupportedDisplaySizeDialogUiThread() {
205 if (mUnsupportedDisplaySizeDialog != null) {
206 mUnsupportedDisplaySizeDialog.dismiss();
207 mUnsupportedDisplaySizeDialog = null;
208 }
209 }
210
211 /**
212 * Shows the "unsupported display size" warning for the given application.
213 * <p>
214 * <strong>Note:</strong> Must be called on the UI thread.
215 *
216 * @param ar record for the activity that triggered the warning
217 */
218 @UiThread
219 private void showUnsupportedDisplaySizeDialogUiThread(ActivityRecord ar) {
220 if (mUnsupportedDisplaySizeDialog != null) {
221 mUnsupportedDisplaySizeDialog.dismiss();
222 mUnsupportedDisplaySizeDialog = null;
223 }
224 if (ar != null && !hasPackageFlag(
225 ar.packageName, FLAG_HIDE_DISPLAY_SIZE)) {
226 mUnsupportedDisplaySizeDialog = new UnsupportedDisplaySizeDialog(
227 AppWarnings.this, mUiContext, ar.info.applicationInfo);
228 mUnsupportedDisplaySizeDialog.show();
229 }
230 }
231
232 /**
233 * Shows the "unsupported compile SDK" warning for the given application.
234 * <p>
235 * <strong>Note:</strong> Must be called on the UI thread.
236 *
237 * @param ar record for the activity that triggered the warning
238 */
239 @UiThread
240 private void showUnsupportedCompileSdkDialogUiThread(ActivityRecord ar) {
241 if (mUnsupportedCompileSdkDialog != null) {
242 mUnsupportedCompileSdkDialog.dismiss();
243 mUnsupportedCompileSdkDialog = null;
244 }
245 if (ar != null && !hasPackageFlag(
246 ar.packageName, FLAG_HIDE_COMPILE_SDK)) {
247 mUnsupportedCompileSdkDialog = new UnsupportedCompileSdkDialog(
248 AppWarnings.this, mUiContext, ar.info.applicationInfo);
249 mUnsupportedCompileSdkDialog.show();
250 }
251 }
252
253 /**
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000254 * Shows the "deprecated target sdk version" warning for the given application.
255 * <p>
256 * <strong>Note:</strong> Must be called on the UI thread.
257 *
258 * @param ar record for the activity that triggered the warning
259 */
260 @UiThread
261 private void showDeprecatedTargetSdkDialogUiThread(ActivityRecord ar) {
262 if (mDeprecatedTargetSdkVersionDialog != null) {
263 mDeprecatedTargetSdkVersionDialog.dismiss();
264 mDeprecatedTargetSdkVersionDialog = null;
265 }
266 if (ar != null && !hasPackageFlag(
267 ar.packageName, FLAG_HIDE_DEPRECATED_SDK)) {
268 mDeprecatedTargetSdkVersionDialog = new DeprecatedTargetSdkVersionDialog(
269 AppWarnings.this, mUiContext, ar.info.applicationInfo);
270 mDeprecatedTargetSdkVersionDialog.show();
271 }
272 }
273
274 /**
Alan Viveretteb6a25732017-11-21 14:49:24 -0500275 * Dismisses all warnings for the given package.
276 * <p>
277 * <strong>Note:</strong> Must be called on the UI thread.
278 *
279 * @param name the package for which warnings should be dismissed, or {@code null} to dismiss
280 * all warnings
281 */
282 @UiThread
283 private void hideDialogsForPackageUiThread(String name) {
284 // Hides the "unsupported display" dialog if necessary.
285 if (mUnsupportedDisplaySizeDialog != null && (name == null || name.equals(
286 mUnsupportedDisplaySizeDialog.getPackageName()))) {
287 mUnsupportedDisplaySizeDialog.dismiss();
288 mUnsupportedDisplaySizeDialog = null;
289 }
290
291 // Hides the "unsupported compile SDK" dialog if necessary.
292 if (mUnsupportedCompileSdkDialog != null && (name == null || name.equals(
293 mUnsupportedCompileSdkDialog.getPackageName()))) {
294 mUnsupportedCompileSdkDialog.dismiss();
295 mUnsupportedCompileSdkDialog = null;
296 }
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000297
298 // Hides the "deprecated target sdk version" dialog if necessary.
299 if (mDeprecatedTargetSdkVersionDialog != null && (name == null || name.equals(
300 mDeprecatedTargetSdkVersionDialog.getPackageName()))) {
301 mDeprecatedTargetSdkVersionDialog.dismiss();
302 mDeprecatedTargetSdkVersionDialog = null;
303 }
Alan Viveretteb6a25732017-11-21 14:49:24 -0500304 }
305
306 /**
307 * Returns the value of the flag for the given package.
308 *
309 * @param name the package from which to retrieve the flag
310 * @param flag the bitmask for the flag to retrieve
311 * @return {@code true} if the flag is enabled, {@code false} otherwise
312 */
313 boolean hasPackageFlag(String name, int flag) {
314 return (getPackageFlags(name) & flag) == flag;
315 }
316
317 /**
318 * Sets the flag for the given package to the specified value.
319 *
320 * @param name the package on which to set the flag
321 * @param flag the bitmask for flag to set
322 * @param enabled the value to set for the flag
323 */
324 void setPackageFlag(String name, int flag, boolean enabled) {
325 synchronized (mPackageFlags) {
326 final int curFlags = getPackageFlags(name);
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000327 final int newFlags = enabled ? (curFlags | flag) : (curFlags & ~flag);
Alan Viveretteb6a25732017-11-21 14:49:24 -0500328 if (curFlags != newFlags) {
329 if (newFlags != 0) {
330 mPackageFlags.put(name, newFlags);
331 } else {
332 mPackageFlags.remove(name);
333 }
334 mAmsHandler.scheduleWrite();
335 }
336 }
337 }
338
339 /**
340 * Returns the bitmask of flags set for the specified package.
341 */
342 private int getPackageFlags(String name) {
343 synchronized (mPackageFlags) {
344 return mPackageFlags.getOrDefault(name, 0);
345 }
346 }
347
348 /**
349 * Handles messages on the system process UI thread.
350 */
351 private final class UiHandler extends Handler {
352 private static final int MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 1;
353 private static final int MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG = 2;
354 private static final int MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG = 3;
355 private static final int MSG_HIDE_DIALOGS_FOR_PACKAGE = 4;
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000356 private static final int MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG = 5;
Alan Viveretteb6a25732017-11-21 14:49:24 -0500357
358 public UiHandler(Looper looper) {
359 super(looper, null, true);
360 }
361
362 @Override
363 public void handleMessage(Message msg) {
364 switch (msg.what) {
365 case MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG: {
366 final ActivityRecord ar = (ActivityRecord) msg.obj;
367 showUnsupportedDisplaySizeDialogUiThread(ar);
368 } break;
369 case MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG: {
370 hideUnsupportedDisplaySizeDialogUiThread();
371 } break;
372 case MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG: {
373 final ActivityRecord ar = (ActivityRecord) msg.obj;
374 showUnsupportedCompileSdkDialogUiThread(ar);
375 } break;
376 case MSG_HIDE_DIALOGS_FOR_PACKAGE: {
377 final String name = (String) msg.obj;
378 hideDialogsForPackageUiThread(name);
379 } break;
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000380 case MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG: {
381 final ActivityRecord ar = (ActivityRecord) msg.obj;
382 showDeprecatedTargetSdkDialogUiThread(ar);
383 } break;
Alan Viveretteb6a25732017-11-21 14:49:24 -0500384 }
385 }
386
387 public void showUnsupportedDisplaySizeDialog(ActivityRecord r) {
388 removeMessages(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
389 obtainMessage(MSG_SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG, r).sendToTarget();
390 }
391
392 public void hideUnsupportedDisplaySizeDialog() {
393 removeMessages(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
394 sendEmptyMessage(MSG_HIDE_UNSUPPORTED_DISPLAY_SIZE_DIALOG);
395 }
396
397 public void showUnsupportedCompileSdkDialog(ActivityRecord r) {
398 removeMessages(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG);
399 obtainMessage(MSG_SHOW_UNSUPPORTED_COMPILE_SDK_DIALOG, r).sendToTarget();
400 }
401
Przemyslaw Szczepaniakce73c7e2018-01-22 11:00:41 +0000402 public void showDeprecatedTargetDialog(ActivityRecord r) {
403 removeMessages(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG);
404 obtainMessage(MSG_SHOW_DEPRECATED_TARGET_SDK_DIALOG, r).sendToTarget();
405 }
406
Alan Viveretteb6a25732017-11-21 14:49:24 -0500407 public void hideDialogsForPackage(String name) {
408 obtainMessage(MSG_HIDE_DIALOGS_FOR_PACKAGE, name).sendToTarget();
409 }
410 }
411
412 /**
413 * Handles messages on the ActivityManagerService thread.
414 */
415 private final class ConfigHandler extends Handler {
416 private static final int MSG_WRITE = ActivityManagerService.FIRST_COMPAT_MODE_MSG;
417
418 private static final int DELAY_MSG_WRITE = 10000;
419
420 public ConfigHandler(Looper looper) {
421 super(looper, null, true);
422 }
423
424 @Override
425 public void handleMessage(Message msg) {
426 switch (msg.what) {
427 case MSG_WRITE:
428 writeConfigToFileAmsThread();
429 break;
430 }
431 }
432
433 public void scheduleWrite() {
434 removeMessages(MSG_WRITE);
435 sendEmptyMessageDelayed(MSG_WRITE, DELAY_MSG_WRITE);
436 }
437 }
438
439 /**
440 * Writes the configuration file.
441 * <p>
442 * <strong>Note:</strong> Should be called from the ActivityManagerService thread unless you
443 * don't care where you're doing I/O operations. But you <i>do</i> care, don't you?
444 */
445 private void writeConfigToFileAmsThread() {
446 // Create a shallow copy so that we don't have to synchronize on config.
447 final HashMap<String, Integer> packageFlags;
448 synchronized (mPackageFlags) {
449 packageFlags = new HashMap<>(mPackageFlags);
450 }
451
452 FileOutputStream fos = null;
453 try {
454 fos = mConfigFile.startWrite();
455
456 final XmlSerializer out = new FastXmlSerializer();
457 out.setOutput(fos, StandardCharsets.UTF_8.name());
458 out.startDocument(null, true);
459 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
460 out.startTag(null, "packages");
461
462 for (Map.Entry<String, Integer> entry : packageFlags.entrySet()) {
463 String pkg = entry.getKey();
464 int mode = entry.getValue();
465 if (mode == 0) {
466 continue;
467 }
468 out.startTag(null, "package");
469 out.attribute(null, "name", pkg);
470 out.attribute(null, "flags", Integer.toString(mode));
471 out.endTag(null, "package");
472 }
473
474 out.endTag(null, "packages");
475 out.endDocument();
476
477 mConfigFile.finishWrite(fos);
478 } catch (java.io.IOException e1) {
479 Slog.w(TAG, "Error writing package metadata", e1);
480 if (fos != null) {
481 mConfigFile.failWrite(fos);
482 }
483 }
484 }
485
486 /**
487 * Reads the configuration file and populates the package flags.
488 * <p>
489 * <strong>Note:</strong> Must be called from the constructor (and thus on the
490 * ActivityManagerService thread) since we don't synchronize on config.
491 */
492 private void readConfigFromFileAmsThread() {
493 FileInputStream fis = null;
494
495 try {
496 fis = mConfigFile.openRead();
497
498 final XmlPullParser parser = Xml.newPullParser();
499 parser.setInput(fis, StandardCharsets.UTF_8.name());
500
501 int eventType = parser.getEventType();
502 while (eventType != XmlPullParser.START_TAG &&
503 eventType != XmlPullParser.END_DOCUMENT) {
504 eventType = parser.next();
505 }
506 if (eventType == XmlPullParser.END_DOCUMENT) {
507 return;
508 }
509
510 String tagName = parser.getName();
511 if ("packages".equals(tagName)) {
512 eventType = parser.next();
513 do {
514 if (eventType == XmlPullParser.START_TAG) {
515 tagName = parser.getName();
516 if (parser.getDepth() == 2) {
517 if ("package".equals(tagName)) {
518 final String name = parser.getAttributeValue(null, "name");
519 if (name != null) {
520 final String flags = parser.getAttributeValue(
521 null, "flags");
522 int flagsInt = 0;
523 if (flags != null) {
524 try {
525 flagsInt = Integer.parseInt(flags);
526 } catch (NumberFormatException e) {
527 }
528 }
529 mPackageFlags.put(name, flagsInt);
530 }
531 }
532 }
533 }
534 eventType = parser.next();
535 } while (eventType != XmlPullParser.END_DOCUMENT);
536 }
537 } catch (XmlPullParserException e) {
538 Slog.w(TAG, "Error reading package metadata", e);
539 } catch (java.io.IOException e) {
540 if (fis != null) Slog.w(TAG, "Error reading package metadata", e);
541 } finally {
542 if (fis != null) {
543 try {
544 fis.close();
545 } catch (java.io.IOException e1) {
546 }
547 }
548 }
549 }
550}