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