blob: 1cd0592db267677bad4dab6fcd5041c437907008 [file] [log] [blame]
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001/*
2 * Copyright (C) 2016 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 */
16package com.android.server.pm;
17
18import android.annotation.NonNull;
19import android.annotation.Nullable;
20import android.annotation.UserIdInt;
Makoto Onuki55046222016-03-08 10:49:47 -080021import android.app.ActivityManager;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080022import android.content.ComponentName;
Makoto Onuki55046222016-03-08 10:49:47 -080023import android.content.ContentProvider;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080024import android.content.Context;
25import android.content.Intent;
26import android.content.pm.IShortcutService;
27import android.content.pm.LauncherApps;
28import android.content.pm.LauncherApps.ShortcutQuery;
29import android.content.pm.PackageManager;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.pm.ParceledListSlice;
32import android.content.pm.ShortcutInfo;
33import android.content.pm.ShortcutServiceInternal;
34import android.content.pm.ShortcutServiceInternal.ShortcutChangeListener;
Makoto Onuki55046222016-03-08 10:49:47 -080035import android.graphics.Bitmap;
36import android.graphics.Bitmap.CompressFormat;
37import android.graphics.BitmapFactory;
38import android.graphics.Canvas;
39import android.graphics.RectF;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080040import android.graphics.drawable.Icon;
Makoto Onuki55046222016-03-08 10:49:47 -080041import android.net.Uri;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080042import android.os.Binder;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080043import android.os.Environment;
44import android.os.Handler;
Makoto Onuki55046222016-03-08 10:49:47 -080045import android.os.ParcelFileDescriptor;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080046import android.os.PersistableBundle;
47import android.os.Process;
48import android.os.RemoteException;
49import android.os.ResultReceiver;
Makoto Onuki55046222016-03-08 10:49:47 -080050import android.os.SELinux;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080051import android.os.ShellCommand;
52import android.os.UserHandle;
53import android.text.TextUtils;
Makoto Onuki55046222016-03-08 10:49:47 -080054import android.text.format.Formatter;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080055import android.text.format.Time;
56import android.util.ArrayMap;
57import android.util.ArraySet;
58import android.util.AtomicFile;
Makoto Onuki4362a662016-03-08 18:59:09 -080059import android.util.KeyValueListParser;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080060import android.util.Slog;
61import android.util.SparseArray;
Makoto Onuki55046222016-03-08 10:49:47 -080062import android.util.TypedValue;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080063import android.util.Xml;
64
65import com.android.internal.annotations.GuardedBy;
66import com.android.internal.annotations.VisibleForTesting;
67import com.android.internal.os.BackgroundThread;
68import com.android.internal.util.FastXmlSerializer;
69import com.android.internal.util.Preconditions;
70import com.android.server.LocalServices;
71import com.android.server.SystemService;
72
73import libcore.io.IoUtils;
74
75import org.xmlpull.v1.XmlPullParser;
76import org.xmlpull.v1.XmlPullParserException;
77import org.xmlpull.v1.XmlSerializer;
78
79import java.io.File;
80import java.io.FileDescriptor;
81import java.io.FileInputStream;
82import java.io.FileNotFoundException;
83import java.io.FileOutputStream;
84import java.io.IOException;
Makoto Onuki55046222016-03-08 10:49:47 -080085import java.io.InputStream;
Makoto Onuki6f7362d92016-03-04 13:39:41 -080086import java.io.PrintWriter;
87import java.net.URISyntaxException;
88import java.nio.charset.StandardCharsets;
89import java.util.ArrayList;
90import java.util.List;
91import java.util.function.Predicate;
92
93/**
94 * TODO:
Makoto Onuki6f7362d92016-03-04 13:39:41 -080095 *
Makoto Onuki55046222016-03-08 10:49:47 -080096 * - Detect when already registered instances are passed to APIs again, which might break
97 * internal bitmap handling.
Makoto Onuki6f7362d92016-03-04 13:39:41 -080098 *
99 * - Listen to PACKAGE_*, remove orphan info, update timestamp for icon res
Makoto Onuki55046222016-03-08 10:49:47 -0800100 * -> Need to scan all packages when a user starts too.
101 * -> Clear data -> remove all dynamic? but not the pinned?
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800102 *
103 * - Pinned per each launcher package (multiple launchers)
104 *
Makoto Onuki55046222016-03-08 10:49:47 -0800105 * - Make save async (should we?)
106 *
107 * - Scan and remove orphan bitmaps (just in case).
108 *
109 * - Backup & restore
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800110 */
111public class ShortcutService extends IShortcutService.Stub {
Makoto Onuki55046222016-03-08 10:49:47 -0800112 static final String TAG = "ShortcutService";
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800113
Makoto Onuki41066a62016-03-09 16:18:44 -0800114 static final boolean DEBUG = false; // STOPSHIP if true
115 static final boolean DEBUG_LOAD = false; // STOPSHIP if true
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800116
Makoto Onuki4362a662016-03-08 18:59:09 -0800117 @VisibleForTesting
118 static final long DEFAULT_RESET_INTERVAL_SEC = 24 * 60 * 60; // 1 day
119
120 @VisibleForTesting
121 static final int DEFAULT_MAX_DAILY_UPDATES = 10;
122
123 @VisibleForTesting
124 static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 5;
125
126 @VisibleForTesting
127 static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
128
129 @VisibleForTesting
130 static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48;
131
132 @VisibleForTesting
133 static final String DEFAULT_ICON_PERSIST_FORMAT = CompressFormat.PNG.name();
134
135 @VisibleForTesting
136 static final int DEFAULT_ICON_PERSIST_QUALITY = 100;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800137
138 private static final int SAVE_DELAY_MS = 5000; // in milliseconds.
139
140 @VisibleForTesting
141 static final String FILENAME_BASE_STATE = "shortcut_service.xml";
142
143 @VisibleForTesting
144 static final String DIRECTORY_PER_USER = "shortcut_service";
145
146 @VisibleForTesting
147 static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
148
Makoto Onuki55046222016-03-08 10:49:47 -0800149 static final String DIRECTORY_BITMAPS = "bitmaps";
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800150
Makoto Onuki41066a62016-03-09 16:18:44 -0800151 static final String TAG_ROOT = "root";
152 static final String TAG_PACKAGE = "package";
153 static final String TAG_LAST_RESET_TIME = "last_reset_time";
154 static final String TAG_INTENT_EXTRAS = "intent-extras";
155 static final String TAG_EXTRAS = "extras";
156 static final String TAG_SHORTCUT = "shortcut";
Makoto Onuki55046222016-03-08 10:49:47 -0800157
Makoto Onuki41066a62016-03-09 16:18:44 -0800158 static final String ATTR_VALUE = "value";
159 static final String ATTR_NAME = "name";
160 static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
161 static final String ATTR_CALL_COUNT = "call-count";
162 static final String ATTR_LAST_RESET = "last-reset";
163 static final String ATTR_ID = "id";
164 static final String ATTR_ACTIVITY = "activity";
165 static final String ATTR_TITLE = "title";
166 static final String ATTR_INTENT = "intent";
167 static final String ATTR_WEIGHT = "weight";
168 static final String ATTR_TIMESTAMP = "timestamp";
169 static final String ATTR_FLAGS = "flags";
170 static final String ATTR_ICON_RES = "icon-res";
171 static final String ATTR_BITMAP_PATH = "bitmap-path";
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800172
Makoto Onuki4362a662016-03-08 18:59:09 -0800173 @VisibleForTesting
174 interface ConfigConstants {
175 /**
176 * Key name for the throttling reset interval, in seconds. (long)
177 */
178 String KEY_RESET_INTERVAL_SEC = "reset_interval_sec";
179
180 /**
181 * Key name for the max number of modifying API calls per app for every interval. (int)
182 */
183 String KEY_MAX_DAILY_UPDATES = "max_daily_updates";
184
185 /**
186 * Key name for the max icon dimensions in DP, for non-low-memory devices.
187 */
188 String KEY_MAX_ICON_DIMENSION_DP = "max_icon_dimension_dp";
189
190 /**
191 * Key name for the max icon dimensions in DP, for low-memory devices.
192 */
193 String KEY_MAX_ICON_DIMENSION_DP_LOWRAM = "max_icon_dimension_dp_lowram";
194
195 /**
196 * Key name for the max dynamic shortcuts per app. (int)
197 */
198 String KEY_MAX_SHORTCUTS = "max_shortcuts";
199
200 /**
Makoto Onuki41066a62016-03-09 16:18:44 -0800201 * Key name for icon compression quality, 0-100.
Makoto Onuki4362a662016-03-08 18:59:09 -0800202 */
203 String KEY_ICON_QUALITY = "icon_quality";
204
205 /**
206 * Key name for icon compression format: "PNG", "JPEG" or "WEBP"
207 */
208 String KEY_ICON_FORMAT = "icon_format";
209 }
210
Makoto Onuki41066a62016-03-09 16:18:44 -0800211 final Context mContext;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800212
213 private final Object mLock = new Object();
214
215 private final Handler mHandler;
216
217 @GuardedBy("mLock")
218 private final ArrayList<ShortcutChangeListener> mListeners = new ArrayList<>(1);
219
220 @GuardedBy("mLock")
221 private long mRawLastResetTime;
222
223 /**
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800224 * User ID -> package name -> list of ShortcutInfos.
225 */
226 @GuardedBy("mLock")
227 private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts =
228 new SparseArray<>();
229
230 /**
231 * Max number of dynamic shortcuts that each application can have at a time.
232 */
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800233 private int mMaxDynamicShortcuts;
234
235 /**
236 * Max number of updating API calls that each application can make a day.
237 */
Makoto Onuki41066a62016-03-09 16:18:44 -0800238 int mMaxDailyUpdates;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800239
240 /**
241 * Actual throttling-reset interval. By default it's a day.
242 */
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800243 private long mResetInterval;
244
Makoto Onuki55046222016-03-08 10:49:47 -0800245 /**
246 * Icon max width/height in pixels.
247 */
248 private int mMaxIconDimension;
249
Makoto Onuki4362a662016-03-08 18:59:09 -0800250 private CompressFormat mIconPersistFormat;
251 private int mIconPersistQuality;
Makoto Onuki55046222016-03-08 10:49:47 -0800252
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800253 public ShortcutService(Context context) {
254 mContext = Preconditions.checkNotNull(context);
255 LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
256 mHandler = new Handler(BackgroundThread.get().getLooper());
257 }
258
259 /**
260 * System service lifecycle.
261 */
262 public static final class Lifecycle extends SystemService {
263 final ShortcutService mService;
264
265 public Lifecycle(Context context) {
266 super(context);
267 mService = new ShortcutService(context);
268 }
269
270 @Override
271 public void onStart() {
272 publishBinderService(Context.SHORTCUT_SERVICE, mService);
273 }
274
275 @Override
276 public void onBootPhase(int phase) {
277 mService.onBootPhase(phase);
278 }
279
280 @Override
281 public void onCleanupUser(int userHandle) {
282 synchronized (mService.mLock) {
283 mService.onCleanupUserInner(userHandle);
284 }
285 }
286
287 @Override
Makoto Onukif3a572b2016-03-10 12:28:38 -0800288 public void onUnlockUser(int userId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800289 synchronized (mService.mLock) {
290 mService.onStartUserLocked(userId);
291 }
292 }
293 }
294
295 /** lifecycle event */
296 void onBootPhase(int phase) {
297 if (DEBUG) {
298 Slog.d(TAG, "onBootPhase: " + phase);
299 }
300 switch (phase) {
301 case SystemService.PHASE_LOCK_SETTINGS_READY:
302 initialize();
303 break;
304 }
305 }
306
307 /** lifecycle event */
308 void onStartUserLocked(int userId) {
309 // Preload
310 getUserShortcutsLocked(userId);
311 }
312
313 /** lifecycle event */
314 void onCleanupUserInner(int userId) {
315 // Unload
316 mShortcuts.delete(userId);
317 }
318
319 /** Return the base state file name */
320 private AtomicFile getBaseStateFile() {
321 final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE);
322 path.mkdirs();
323 return new AtomicFile(path);
324 }
325
326 /**
327 * Init the instance. (load the state file, etc)
328 */
329 private void initialize() {
330 synchronized (mLock) {
Makoto Onuki4362a662016-03-08 18:59:09 -0800331 loadConfigurationLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800332 loadBaseStateLocked();
333 }
334 }
335
Makoto Onuki4362a662016-03-08 18:59:09 -0800336 /**
337 * Load the configuration from Settings.
338 */
339 private void loadConfigurationLocked() {
340 updateConfigurationLocked(injectShortcutManagerConstants());
341 }
Makoto Onuki55046222016-03-08 10:49:47 -0800342
Makoto Onuki4362a662016-03-08 18:59:09 -0800343 /**
344 * Load the configuration from Settings.
345 */
346 @VisibleForTesting
347 boolean updateConfigurationLocked(String config) {
348 boolean result = true;
349
350 final KeyValueListParser parser = new KeyValueListParser(',');
351 try {
352 parser.setString(config);
353 } catch (IllegalArgumentException e) {
354 // Failed to parse the settings string, log this and move on
355 // with defaults.
356 Slog.e(TAG, "Bad shortcut manager settings", e);
357 result = false;
358 }
359
360 mResetInterval = parser.getLong(
361 ConfigConstants.KEY_RESET_INTERVAL_SEC, DEFAULT_RESET_INTERVAL_SEC)
362 * 1000L;
363
364 mMaxDailyUpdates = (int) parser.getLong(
365 ConfigConstants.KEY_MAX_DAILY_UPDATES, DEFAULT_MAX_DAILY_UPDATES);
366
367 mMaxDynamicShortcuts = (int) parser.getLong(
368 ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_APP);
369
370 final int iconDimensionDp = injectIsLowRamDevice()
371 ? (int) parser.getLong(
372 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
373 DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
374 : (int) parser.getLong(
375 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
376 DEFAULT_MAX_ICON_DIMENSION_DP);
377
378 mMaxIconDimension = injectDipToPixel(iconDimensionDp);
379
380 mIconPersistFormat = CompressFormat.valueOf(
381 parser.getString(ConfigConstants.KEY_ICON_FORMAT, DEFAULT_ICON_PERSIST_FORMAT));
382
383 mIconPersistQuality = (int) parser.getLong(
384 ConfigConstants.KEY_ICON_QUALITY,
385 DEFAULT_ICON_PERSIST_QUALITY);
386
387 return result;
388 }
389
390 @VisibleForTesting
391 String injectShortcutManagerConstants() {
392 return android.provider.Settings.Global.getString(
393 mContext.getContentResolver(),
394 android.provider.Settings.Global.SHORTCUT_MANAGER_CONSTANTS);
395 }
396
397 @VisibleForTesting
398 int injectDipToPixel(int dip) {
399 return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
400 mContext.getResources().getDisplayMetrics());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800401 }
402
Makoto Onuki55046222016-03-08 10:49:47 -0800403 // === Persisting ===
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800404
405 @Nullable
Makoto Onuki41066a62016-03-09 16:18:44 -0800406 static String parseStringAttribute(XmlPullParser parser, String attribute) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800407 return parser.getAttributeValue(null, attribute);
408 }
409
Makoto Onuki41066a62016-03-09 16:18:44 -0800410 static int parseIntAttribute(XmlPullParser parser, String attribute) {
411 return (int) parseLongAttribute(parser, attribute);
412 }
413
414 static long parseLongAttribute(XmlPullParser parser, String attribute) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800415 final String value = parseStringAttribute(parser, attribute);
416 if (TextUtils.isEmpty(value)) {
417 return 0;
418 }
419 try {
420 return Long.parseLong(value);
421 } catch (NumberFormatException e) {
422 Slog.e(TAG, "Error parsing long " + value);
423 return 0;
424 }
425 }
426
427 @Nullable
Makoto Onuki41066a62016-03-09 16:18:44 -0800428 static ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800429 final String value = parseStringAttribute(parser, attribute);
430 if (TextUtils.isEmpty(value)) {
431 return null;
432 }
433 return ComponentName.unflattenFromString(value);
434 }
435
436 @Nullable
Makoto Onuki41066a62016-03-09 16:18:44 -0800437 static Intent parseIntentAttribute(XmlPullParser parser, String attribute) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800438 final String value = parseStringAttribute(parser, attribute);
439 if (TextUtils.isEmpty(value)) {
440 return null;
441 }
442 try {
443 return Intent.parseUri(value, /* flags =*/ 0);
444 } catch (URISyntaxException e) {
445 Slog.e(TAG, "Error parsing intent", e);
446 return null;
447 }
448 }
449
Makoto Onuki41066a62016-03-09 16:18:44 -0800450 static void writeTagValue(XmlSerializer out, String tag, String value) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800451 if (TextUtils.isEmpty(value)) return;
452
453 out.startTag(null, tag);
454 out.attribute(null, ATTR_VALUE, value);
455 out.endTag(null, tag);
456 }
457
Makoto Onuki41066a62016-03-09 16:18:44 -0800458 static void writeTagValue(XmlSerializer out, String tag, long value) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800459 writeTagValue(out, tag, Long.toString(value));
460 }
461
Makoto Onuki41066a62016-03-09 16:18:44 -0800462 static void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle)
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800463 throws IOException, XmlPullParserException {
464 if (bundle == null) return;
465
466 out.startTag(null, tag);
467 bundle.saveToXml(out);
468 out.endTag(null, tag);
469 }
470
Makoto Onuki41066a62016-03-09 16:18:44 -0800471 static void writeAttr(XmlSerializer out, String name, String value) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800472 if (TextUtils.isEmpty(value)) return;
473
474 out.attribute(null, name, value);
475 }
476
Makoto Onuki41066a62016-03-09 16:18:44 -0800477 static void writeAttr(XmlSerializer out, String name, long value) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800478 writeAttr(out, name, String.valueOf(value));
479 }
480
Makoto Onuki41066a62016-03-09 16:18:44 -0800481 static void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800482 if (comp == null) return;
483 writeAttr(out, name, comp.flattenToString());
484 }
485
Makoto Onuki41066a62016-03-09 16:18:44 -0800486 static void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800487 if (intent == null) return;
488
489 writeAttr(out, name, intent.toUri(/* flags =*/ 0));
490 }
491
492 @VisibleForTesting
493 void saveBaseStateLocked() {
494 final AtomicFile file = getBaseStateFile();
495 if (DEBUG) {
496 Slog.i(TAG, "Saving to " + file.getBaseFile());
497 }
498
499 FileOutputStream outs = null;
500 try {
501 outs = file.startWrite();
502
503 // Write to XML
504 XmlSerializer out = new FastXmlSerializer();
505 out.setOutput(outs, StandardCharsets.UTF_8.name());
506 out.startDocument(null, true);
507 out.startTag(null, TAG_ROOT);
508
509 // Body.
510 writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime);
511
512 // Epilogue.
513 out.endTag(null, TAG_ROOT);
514 out.endDocument();
515
516 // Close.
517 file.finishWrite(outs);
518 } catch (IOException e) {
519 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
520 file.failWrite(outs);
521 }
522 }
523
524 private void loadBaseStateLocked() {
525 mRawLastResetTime = 0;
526
527 final AtomicFile file = getBaseStateFile();
528 if (DEBUG) {
529 Slog.i(TAG, "Loading from " + file.getBaseFile());
530 }
531 try (FileInputStream in = file.openRead()) {
532 XmlPullParser parser = Xml.newPullParser();
533 parser.setInput(in, StandardCharsets.UTF_8.name());
534
535 int type;
536 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
537 if (type != XmlPullParser.START_TAG) {
538 continue;
539 }
540 final int depth = parser.getDepth();
541 // Check the root tag
542 final String tag = parser.getName();
543 if (depth == 1) {
544 if (!TAG_ROOT.equals(tag)) {
545 Slog.e(TAG, "Invalid root tag: " + tag);
546 return;
547 }
548 continue;
549 }
550 // Assume depth == 2
551 switch (tag) {
552 case TAG_LAST_RESET_TIME:
553 mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE);
554 break;
555 default:
556 Slog.e(TAG, "Invalid tag: " + tag);
557 break;
558 }
559 }
560 } catch (FileNotFoundException e) {
561 // Use the default
562 } catch (IOException|XmlPullParserException e) {
563 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
564
565 mRawLastResetTime = 0;
566 }
567 // Adjust the last reset time.
568 getLastResetTimeLocked();
569 }
570
571 private void saveUserLocked(@UserIdInt int userId) {
572 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
573 if (DEBUG) {
574 Slog.i(TAG, "Saving to " + path);
575 }
576 path.mkdirs();
577 final AtomicFile file = new AtomicFile(path);
578 FileOutputStream outs = null;
579 try {
580 outs = file.startWrite();
581
582 // Write to XML
583 XmlSerializer out = new FastXmlSerializer();
584 out.setOutput(outs, StandardCharsets.UTF_8.name());
585 out.startDocument(null, true);
586 out.startTag(null, TAG_ROOT);
587
588 final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId);
589
590 // Body.
591 for (int i = 0; i < packages.size(); i++) {
592 final String packageName = packages.keyAt(i);
Makoto Onuki55046222016-03-08 10:49:47 -0800593 final PackageShortcuts packageShortcuts = packages.valueAt(i);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800594
Makoto Onuki41066a62016-03-09 16:18:44 -0800595 packageShortcuts.saveToXml(out);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800596 }
597
598 // Epilogue.
599 out.endTag(null, TAG_ROOT);
600 out.endDocument();
601
602 // Close.
603 file.finishWrite(outs);
604 } catch (IOException|XmlPullParserException e) {
605 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
606 file.failWrite(outs);
607 }
608 }
609
Makoto Onuki41066a62016-03-09 16:18:44 -0800610 static IOException throwForInvalidTag(int depth, String tag) throws IOException {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800611 throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth));
612 }
613
614 @Nullable
615 private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) {
616 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
617 if (DEBUG) {
618 Slog.i(TAG, "Loading from " + path);
619 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800620 final AtomicFile file = new AtomicFile(path);
621
622 final FileInputStream in;
623 try {
624 in = file.openRead();
625 } catch (FileNotFoundException e) {
626 if (DEBUG) {
627 Slog.i(TAG, "Not found " + path);
628 }
629 return null;
630 }
Makoto Onuki41066a62016-03-09 16:18:44 -0800631 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<>();
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800632 try {
633 XmlPullParser parser = Xml.newPullParser();
634 parser.setInput(in, StandardCharsets.UTF_8.name());
635
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800636 PackageShortcuts shortcuts = null;
637
638 int type;
639 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
640 if (type != XmlPullParser.START_TAG) {
641 continue;
642 }
643 final int depth = parser.getDepth();
644
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800645 final String tag = parser.getName();
646 if (DEBUG_LOAD) {
647 Slog.d(TAG, String.format("depth=%d type=%d name=%s",
648 depth, type, tag));
649 }
650 switch (depth) {
651 case 1: {
652 if (TAG_ROOT.equals(tag)) {
653 continue;
654 }
655 break;
656 }
657 case 2: {
658 switch (tag) {
Makoto Onuki55046222016-03-08 10:49:47 -0800659 case TAG_PACKAGE:
Makoto Onuki41066a62016-03-09 16:18:44 -0800660 shortcuts = PackageShortcuts.loadFromXml(parser, userId);
661 ret.put(shortcuts.mPackageName, shortcuts);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800662 continue;
663 }
664 break;
665 }
666 }
667 throwForInvalidTag(depth, tag);
668 }
669 return ret;
670 } catch (IOException|XmlPullParserException e) {
671 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
672 return null;
673 } finally {
674 IoUtils.closeQuietly(in);
675 }
676 }
677
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800678 // TODO Actually make it async.
679 private void scheduleSaveBaseState() {
680 synchronized (mLock) {
681 saveBaseStateLocked();
682 }
683 }
684
685 // TODO Actually make it async.
686 private void scheduleSaveUser(@UserIdInt int userId) {
687 synchronized (mLock) {
688 saveUserLocked(userId);
689 }
690 }
691
692 /** Return the last reset time. */
693 long getLastResetTimeLocked() {
694 updateTimes();
695 return mRawLastResetTime;
696 }
697
698 /** Return the next reset time. */
699 long getNextResetTimeLocked() {
700 updateTimes();
701 return mRawLastResetTime + mResetInterval;
702 }
703
704 /**
705 * Update the last reset time.
706 */
707 private void updateTimes() {
708
709 final long now = injectCurrentTimeMillis();
710
711 final long prevLastResetTime = mRawLastResetTime;
712
713 if (mRawLastResetTime == 0) { // first launch.
714 // TODO Randomize??
715 mRawLastResetTime = now;
716 } else if (now < mRawLastResetTime) {
717 // Clock rewound.
718 // TODO Randomize??
719 mRawLastResetTime = now;
720 } else {
721 // TODO Do it properly.
722 while ((mRawLastResetTime + mResetInterval) <= now) {
723 mRawLastResetTime += mResetInterval;
724 }
725 }
726 if (prevLastResetTime != mRawLastResetTime) {
727 scheduleSaveBaseState();
728 }
729 }
730
731 /** Return the per-user state. */
732 @GuardedBy("mLock")
733 @NonNull
734 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) {
735 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId);
736 if (userPackages == null) {
737 userPackages = loadUserLocked(userId);
738 if (userPackages == null) {
739 userPackages = new ArrayMap<>();
740 }
741 mShortcuts.put(userId, userPackages);
742 }
743 return userPackages;
744 }
745
746 /** Return the per-user per-package state. */
747 @GuardedBy("mLock")
748 @NonNull
749 private PackageShortcuts getPackageShortcutsLocked(
750 @NonNull String packageName, @UserIdInt int userId) {
751 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId);
752 PackageShortcuts shortcuts = userPackages.get(packageName);
753 if (shortcuts == null) {
Makoto Onuki55046222016-03-08 10:49:47 -0800754 shortcuts = new PackageShortcuts(userId, packageName);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800755 userPackages.put(packageName, shortcuts);
756 }
757 return shortcuts;
758 }
759
760 // === Caller validation ===
761
Makoto Onuki55046222016-03-08 10:49:47 -0800762 void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
763 if (shortcut.getBitmapPath() != null) {
764 if (DEBUG) {
765 Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
766 }
767 new File(shortcut.getBitmapPath()).delete();
768
769 shortcut.setBitmapPath(null);
770 shortcut.setIconResourceId(0);
771 shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
772 }
773 }
774
775 @VisibleForTesting
776 static class FileOutputStreamWithPath extends FileOutputStream {
777 private final File mFile;
778
779 public FileOutputStreamWithPath(File file) throws FileNotFoundException {
780 super(file);
781 mFile = file;
782 }
783
784 public File getFile() {
785 return mFile;
786 }
787 }
788
789 /**
790 * Build the cached bitmap filename for a shortcut icon.
791 *
792 * The filename will be based on the ID, except certain characters will be escaped.
793 */
794 @VisibleForTesting
795 FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
796 throws IOException {
797 final File packagePath = new File(getUserBitmapFilePath(userId),
798 shortcut.getPackageName());
799 if (!packagePath.isDirectory()) {
800 packagePath.mkdirs();
801 if (!packagePath.isDirectory()) {
802 throw new IOException("Unable to create directory " + packagePath);
803 }
804 SELinux.restorecon(packagePath);
805 }
806
807 final String baseName = String.valueOf(injectCurrentTimeMillis());
808 for (int suffix = 0;; suffix++) {
809 final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
810 final File file = new File(packagePath, filename);
811 if (!file.exists()) {
812 if (DEBUG) {
813 Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
814 }
815 return new FileOutputStreamWithPath(file);
816 }
817 }
818 }
819
820 void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
821 if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
822 return;
823 }
824
825 final long token = Binder.clearCallingIdentity();
826 try {
827 // Clear icon info on the shortcut.
828 shortcut.setIconResourceId(0);
829 shortcut.setBitmapPath(null);
830
831 final Icon icon = shortcut.getIcon();
832 if (icon == null) {
833 return; // has no icon
834 }
835
836 Bitmap bitmap = null;
837 try {
838 switch (icon.getType()) {
839 case Icon.TYPE_RESOURCE: {
840 injectValidateIconResPackage(shortcut, icon);
841
842 shortcut.setIconResourceId(icon.getResId());
843 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
844 return;
845 }
846 case Icon.TYPE_BITMAP: {
847 bitmap = icon.getBitmap();
848 break;
849 }
850 case Icon.TYPE_URI: {
851 final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
852
853 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
854
855 bitmap = BitmapFactory.decodeStream(is);
856
857 } catch (IOException e) {
858 Slog.e(TAG, "Unable to load icon from " + uri);
859 return;
860 }
861 break;
862 }
863 default:
864 // This shouldn't happen because we've already validated the icon, but
865 // just in case.
866 throw ShortcutInfo.getInvalidIconException();
867 }
868 if (bitmap == null) {
869 Slog.e(TAG, "Null bitmap detected");
870 return;
871 }
872 // Shrink and write to the file.
873 File path = null;
874 try {
875 final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
876 try {
877 path = out.getFile();
878
879 shrinkBitmap(bitmap, mMaxIconDimension)
880 .compress(mIconPersistFormat, mIconPersistQuality, out);
881
882 shortcut.setBitmapPath(out.getFile().getAbsolutePath());
883 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
884 } finally {
885 IoUtils.closeQuietly(out);
886 }
887 } catch (IOException|RuntimeException e) {
888 // STOPSHIP Change wtf to e
889 Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
890 if (path != null && path.exists()) {
891 path.delete();
892 }
893 }
894 } finally {
895 if (bitmap != null) {
896 bitmap.recycle();
897 }
898 // Once saved, we won't use the original icon information, so null it out.
899 shortcut.clearIcon();
900 }
901 } finally {
902 Binder.restoreCallingIdentity(token);
903 }
904 }
905
906 // Unfortunately we can't do this check in unit tests because we fake creator package names,
907 // so override in unit tests.
908 // TODO CTS this case.
909 void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
910 if (!shortcut.getPackageName().equals(icon.getResPackage())) {
911 throw new IllegalArgumentException(
912 "Icon resource must reside in shortcut owner package");
913 }
914 }
915
916 @VisibleForTesting
917 static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
918 // Original width/height.
919 final int ow = in.getWidth();
920 final int oh = in.getHeight();
921 if ((ow <= maxSize) && (oh <= maxSize)) {
922 if (DEBUG) {
923 Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
924 }
925 return in;
926 }
927 final int longerDimension = Math.max(ow, oh);
928
929 // New width and height.
930 final int nw = ow * maxSize / longerDimension;
931 final int nh = oh * maxSize / longerDimension;
932 if (DEBUG) {
933 Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
934 ow, oh, nw, nh));
935 }
936
937 final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
938 final Canvas c = new Canvas(scaledBitmap);
939
940 final RectF dst = new RectF(0, 0, nw, nh);
941
942 c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
943
944 in.recycle();
945
946 return scaledBitmap;
947 }
948
949 // === Caller validation ===
950
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800951 private boolean isCallerSystem() {
952 final int callingUid = injectBinderCallingUid();
953 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
954 }
955
956 private boolean isCallerShell() {
957 final int callingUid = injectBinderCallingUid();
958 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
959 }
960
961 private void enforceSystemOrShell() {
962 Preconditions.checkState(isCallerSystem() || isCallerShell(),
963 "Caller must be system or shell");
964 }
965
966 private void enforceShell() {
967 Preconditions.checkState(isCallerShell(), "Caller must be shell");
968 }
969
970 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) {
971 Preconditions.checkStringNotEmpty(packageName, "packageName");
972
973 if (isCallerSystem()) {
974 return; // no check
975 }
976
977 final int callingUid = injectBinderCallingUid();
978
979 // Otherwise, make sure the arguments are valid.
980 if (UserHandle.getUserId(callingUid) != userId) {
981 throw new SecurityException("Invalid user-ID");
982 }
Makoto Onuki55046222016-03-08 10:49:47 -0800983 if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800984 return; // Caller is valid.
985 }
986 throw new SecurityException("Caller UID= doesn't own " + packageName);
987 }
988
989 // Test overrides it.
Makoto Onuki55046222016-03-08 10:49:47 -0800990 int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800991 try {
992
993 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
994
Makoto Onuki55046222016-03-08 10:49:47 -0800995 return mContext.getPackageManager().getPackageUidAsUser(packageName,
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800996 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
Makoto Onuki55046222016-03-08 10:49:47 -0800997 | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800998 } catch (NameNotFoundException e) {
999 return -1;
1000 }
1001 }
1002
1003 /**
1004 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
1005 */
1006 void enforceMaxDynamicShortcuts(int numShortcuts) {
1007 if (numShortcuts > mMaxDynamicShortcuts) {
1008 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
1009 }
1010 }
1011
1012 /**
1013 * - Sends a notification to LauncherApps
1014 * - Write to file
1015 */
1016 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
1017 notifyListeners(packageName, userId);
1018 scheduleSaveUser(userId);
1019 }
1020
1021 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
1022 final ArrayList<ShortcutChangeListener> copy;
1023 final List<ShortcutInfo> shortcuts = new ArrayList<>();
1024 synchronized (mLock) {
1025 copy = new ArrayList<>(mListeners);
1026
1027 getPackageShortcutsLocked(packageName, userId)
1028 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
1029 }
1030 for (int i = copy.size() - 1; i >= 0; i--) {
1031 copy.get(i).onShortcutChanged(packageName, shortcuts, userId);
1032 }
1033 }
1034
1035 /**
1036 * Clean up / validate an incoming shortcut.
1037 * - Make sure all mandatory fields are set.
1038 * - Make sure the intent's extras are persistable, and them to set
1039 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras.
1040 * - Clear flags.
Makoto Onuki55046222016-03-08 10:49:47 -08001041 *
1042 * TODO Detailed unit tests
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001043 */
Makoto Onuki55046222016-03-08 10:49:47 -08001044 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001045 Preconditions.checkNotNull(shortcut, "Null shortcut detected");
1046 if (shortcut.getActivityComponent() != null) {
1047 Preconditions.checkState(
1048 shortcut.getPackageName().equals(
1049 shortcut.getActivityComponent().getPackageName()),
1050 "Activity package name mismatch");
1051 }
1052
Makoto Onuki55046222016-03-08 10:49:47 -08001053 if (!forUpdate) {
1054 shortcut.enforceMandatoryFields();
1055 }
1056 if (shortcut.getIcon() != null) {
1057 ShortcutInfo.validateIcon(shortcut.getIcon());
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001058 }
1059
Makoto Onuki55046222016-03-08 10:49:47 -08001060 validateForXml(shortcut.getId());
1061 validateForXml(shortcut.getTitle());
1062 validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
1063 validatePersistableBundleForXml(shortcut.getExtras());
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001064
1065 shortcut.setFlags(0);
1066 }
1067
Makoto Onuki55046222016-03-08 10:49:47 -08001068 // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
1069 // characters.
1070
1071 private static void validatePersistableBundleForXml(PersistableBundle b) {
1072 if (b == null || b.size() == 0) {
1073 return;
1074 }
1075 for (String key : b.keySet()) {
1076 validateForXml(key);
1077 final Object value = b.get(key);
1078 if (value == null) {
1079 continue;
1080 } else if (value instanceof String) {
1081 validateForXml((String) value);
1082 } else if (value instanceof String[]) {
1083 for (String v : (String[]) value) {
1084 validateForXml(v);
1085 }
1086 } else if (value instanceof PersistableBundle) {
1087 validatePersistableBundleForXml((PersistableBundle) value);
1088 }
1089 }
1090 }
1091
1092 private static void validateForXml(String s) {
1093 if (TextUtils.isEmpty(s)) {
1094 return;
1095 }
1096 for (int i = s.length() - 1; i >= 0; i--) {
1097 if (!isAllowedInXml(s.charAt(i))) {
1098 throw new IllegalArgumentException("Unsupported character detected in: " + s);
1099 }
1100 }
1101 }
1102
1103 private static boolean isAllowedInXml(char c) {
1104 return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
1105 }
1106
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001107 // === APIs ===
1108
1109 @Override
1110 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1111 @UserIdInt int userId) {
1112 verifyCaller(packageName, userId);
1113
1114 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1115 final int size = newShortcuts.size();
1116
1117 synchronized (mLock) {
1118 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1119
1120 // Throttling.
1121 if (!ps.tryApiCall(this)) {
1122 return false;
1123 }
1124 enforceMaxDynamicShortcuts(size);
1125
1126 // Validate the shortcuts.
1127 for (int i = 0; i < size; i++) {
Makoto Onuki55046222016-03-08 10:49:47 -08001128 fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001129 }
1130
1131 // First, remove all un-pinned; dynamic shortcuts
Makoto Onuki55046222016-03-08 10:49:47 -08001132 ps.deleteAllDynamicShortcuts(this);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001133
1134 // Then, add/update all. We need to make sure to take over "pinned" flag.
1135 for (int i = 0; i < size; i++) {
1136 final ShortcutInfo newShortcut = newShortcuts.get(i);
1137 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1138 ps.updateShortcutWithCapping(this, newShortcut);
1139 }
1140 }
1141 userPackageChanged(packageName, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001142 return true;
1143 }
1144
1145 @Override
1146 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1147 @UserIdInt int userId) {
1148 verifyCaller(packageName, userId);
1149
1150 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
Makoto Onuki55046222016-03-08 10:49:47 -08001151 final int size = newShortcuts.size();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001152
1153 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001154 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001155
Makoto Onuki55046222016-03-08 10:49:47 -08001156 // Throttling.
1157 if (!ps.tryApiCall(this)) {
1158 return false;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001159 }
1160
Makoto Onuki55046222016-03-08 10:49:47 -08001161 for (int i = 0; i < size; i++) {
1162 final ShortcutInfo source = newShortcuts.get(i);
1163 fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
1164
1165 final ShortcutInfo target = ps.findShortcutById(source.getId());
1166 if (target != null) {
1167 final boolean replacingIcon = (source.getIcon() != null);
1168 if (replacingIcon) {
1169 removeIcon(userId, target);
1170 }
1171
1172 target.copyNonNullFieldsFrom(source);
1173
1174 if (replacingIcon) {
1175 saveIconAndFixUpShortcut(userId, target);
1176 }
1177 }
1178 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001179 }
1180 userPackageChanged(packageName, userId);
1181
1182 return true;
1183 }
1184
1185 @Override
1186 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut,
1187 @UserIdInt int userId) {
1188 verifyCaller(packageName, userId);
1189
1190 synchronized (mLock) {
1191 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1192
1193 // Throttling.
1194 if (!ps.tryApiCall(this)) {
1195 return false;
1196 }
1197
1198 // Validate the shortcut.
Makoto Onuki55046222016-03-08 10:49:47 -08001199 fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001200
1201 // Add it.
1202 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1203 ps.updateShortcutWithCapping(this, newShortcut);
1204 }
1205 userPackageChanged(packageName, userId);
1206
1207 return true;
1208 }
1209
1210 @Override
1211 public void deleteDynamicShortcut(String packageName, String shortcutId,
1212 @UserIdInt int userId) {
1213 verifyCaller(packageName, userId);
1214 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
1215
1216 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001217 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001218 }
1219 userPackageChanged(packageName, userId);
1220 }
1221
1222 @Override
1223 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) {
1224 verifyCaller(packageName, userId);
1225
1226 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001227 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001228 }
1229 userPackageChanged(packageName, userId);
1230 }
1231
1232 @Override
1233 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName,
1234 @UserIdInt int userId) {
1235 verifyCaller(packageName, userId);
1236 synchronized (mLock) {
1237 return getShortcutsWithQueryLocked(
1238 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1239 ShortcutInfo::isDynamic);
1240 }
1241 }
1242
1243 @Override
1244 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
1245 @UserIdInt int userId) {
1246 verifyCaller(packageName, userId);
1247 synchronized (mLock) {
1248 return getShortcutsWithQueryLocked(
1249 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1250 ShortcutInfo::isPinned);
1251 }
1252 }
1253
1254 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName,
1255 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) {
1256
1257 final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1258
1259 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags);
1260
1261 return new ParceledListSlice<>(ret);
1262 }
1263
1264 @Override
1265 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId)
1266 throws RemoteException {
1267 verifyCaller(packageName, userId);
1268
1269 return mMaxDynamicShortcuts;
1270 }
1271
1272 @Override
1273 public int getRemainingCallCount(String packageName, @UserIdInt int userId) {
1274 verifyCaller(packageName, userId);
1275
1276 synchronized (mLock) {
1277 return mMaxDailyUpdates
1278 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
1279 }
1280 }
1281
1282 @Override
1283 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) {
1284 verifyCaller(packageName, userId);
1285
1286 synchronized (mLock) {
1287 return getNextResetTimeLocked();
1288 }
1289 }
1290
Makoto Onuki55046222016-03-08 10:49:47 -08001291 @Override
1292 public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
1293 synchronized (mLock) {
1294 return mMaxIconDimension;
1295 }
1296 }
1297
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001298 /**
1299 * Reset all throttling, for developer options and command line. Only system/shell can call it.
1300 */
1301 @Override
1302 public void resetThrottling() {
1303 enforceSystemOrShell();
1304
1305 resetThrottlingInner();
1306 }
1307
1308 @VisibleForTesting
1309 void resetThrottlingInner() {
1310 synchronized (mLock) {
1311 mRawLastResetTime = injectCurrentTimeMillis();
1312 }
1313 scheduleSaveBaseState();
Makoto Onuki55046222016-03-08 10:49:47 -08001314 Slog.i(TAG, "ShortcutManager: throttling counter reset");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001315 }
1316
1317 /**
1318 * Entry point from {@link LauncherApps}.
1319 */
1320 private class LocalService extends ShortcutServiceInternal {
1321 @Override
1322 public List<ShortcutInfo> getShortcuts(
1323 @NonNull String callingPackage, long changedSince,
1324 @Nullable String packageName, @Nullable ComponentName componentName,
1325 int queryFlags, int userId) {
1326 final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1327 final int cloneFlag =
1328 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0)
1329 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER
1330 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO;
1331
1332 synchronized (mLock) {
1333 if (packageName != null) {
1334 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags,
1335 userId, ret, cloneFlag);
1336 } else {
1337 final ArrayMap<String, PackageShortcuts> packages =
1338 getUserShortcutsLocked(userId);
Makoto Onuki55046222016-03-08 10:49:47 -08001339 for (int i = packages.size() - 1; i >= 0; i--) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001340 getShortcutsInnerLocked(
1341 packages.keyAt(i),
1342 changedSince, componentName, queryFlags, userId, ret, cloneFlag);
1343 }
1344 }
1345 }
1346 return ret;
1347 }
1348
1349 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince,
1350 @Nullable ComponentName componentName, int queryFlags,
1351 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) {
1352 getPackageShortcutsLocked(packageName, userId).findAll(ret,
1353 (ShortcutInfo si) -> {
1354 if (si.getLastChangedTimestamp() < changedSince) {
1355 return false;
1356 }
1357 if (componentName != null
1358 && !componentName.equals(si.getActivityComponent())) {
1359 return false;
1360 }
1361 final boolean matchDynamic =
1362 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
1363 && si.isDynamic();
1364 final boolean matchPinned =
1365 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
1366 && si.isPinned();
1367 return matchDynamic || matchPinned;
1368 }, cloneFlag);
1369 }
1370
1371 @Override
1372 public List<ShortcutInfo> getShortcutInfo(
1373 @NonNull String callingPackage,
1374 @NonNull String packageName, @Nullable List<String> ids, int userId) {
1375 // Calling permission must be checked by LauncherAppsImpl.
1376 Preconditions.checkStringNotEmpty(packageName, "packageName");
1377
1378 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size());
1379 final ArraySet<String> idSet = new ArraySet<>(ids);
1380 synchronized (mLock) {
1381 getPackageShortcutsLocked(packageName, userId).findAll(ret,
1382 (ShortcutInfo si) -> idSet.contains(si.getId()),
1383 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
1384 }
1385 return ret;
1386 }
1387
1388 @Override
1389 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName,
1390 @NonNull List<String> shortcutIds, int userId) {
1391 // Calling permission must be checked by LauncherAppsImpl.
1392 Preconditions.checkStringNotEmpty(packageName, "packageName");
1393 Preconditions.checkNotNull(shortcutIds, "shortcutIds");
1394
1395 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001396 getPackageShortcutsLocked(packageName, userId).replacePinned(
1397 ShortcutService.this, callingPackage, shortcutIds);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001398 }
1399 userPackageChanged(packageName, userId);
1400 }
1401
1402 @Override
1403 public Intent createShortcutIntent(@NonNull String callingPackage,
Makoto Onuki43204b82016-03-08 16:16:44 -08001404 @NonNull String packageName, @NonNull String shortcutId, int userId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001405 // Calling permission must be checked by LauncherAppsImpl.
Makoto Onuki43204b82016-03-08 16:16:44 -08001406 Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1407 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001408
1409 synchronized (mLock) {
1410 final ShortcutInfo fullShortcut =
Makoto Onuki43204b82016-03-08 16:16:44 -08001411 getPackageShortcutsLocked(packageName, userId)
Makoto Onuki55046222016-03-08 10:49:47 -08001412 .findShortcutById(shortcutId);
1413 return fullShortcut == null ? null : fullShortcut.getIntent();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001414 }
1415 }
1416
1417 @Override
1418 public void addListener(@NonNull ShortcutChangeListener listener) {
1419 synchronized (mLock) {
1420 mListeners.add(Preconditions.checkNotNull(listener));
1421 }
1422 }
Makoto Onuki55046222016-03-08 10:49:47 -08001423
1424 @Override
1425 public int getShortcutIconResId(@NonNull String callingPackage,
1426 @NonNull ShortcutInfo shortcut, int userId) {
1427 Preconditions.checkNotNull(shortcut, "shortcut");
1428
1429 synchronized (mLock) {
1430 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1431 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1432 return (shortcutInfo != null && shortcutInfo.hasIconResource())
1433 ? shortcutInfo.getIconResourceId() : 0;
1434 }
1435 }
1436
1437 @Override
1438 public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
Makoto Onuki34d1c912016-03-10 14:24:58 -08001439 @NonNull ShortcutInfo shortcutIn, int userId) {
1440 Preconditions.checkNotNull(shortcutIn, "shortcut");
Makoto Onuki55046222016-03-08 10:49:47 -08001441
1442 synchronized (mLock) {
1443 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
Makoto Onuki34d1c912016-03-10 14:24:58 -08001444 shortcutIn.getPackageName(), userId).findShortcutById(shortcutIn.getId());
Makoto Onuki55046222016-03-08 10:49:47 -08001445 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
1446 return null;
1447 }
1448 try {
Makoto Onuki34d1c912016-03-10 14:24:58 -08001449 if (shortcutInfo.getBitmapPath() == null) {
1450 Slog.w(TAG, "null bitmap detected in getShortcutIconFd()");
1451 return null;
1452 }
Makoto Onuki55046222016-03-08 10:49:47 -08001453 return ParcelFileDescriptor.open(
1454 new File(shortcutInfo.getBitmapPath()),
1455 ParcelFileDescriptor.MODE_READ_ONLY);
1456 } catch (FileNotFoundException e) {
1457 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
1458 return null;
1459 }
1460 }
1461 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001462 }
1463
1464 // === Dump ===
1465
1466 @Override
1467 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1468 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1469 != PackageManager.PERMISSION_GRANTED) {
1470 pw.println("Permission Denial: can't dump UserManager from from pid="
1471 + Binder.getCallingPid()
1472 + ", uid=" + Binder.getCallingUid()
1473 + " without permission "
1474 + android.Manifest.permission.DUMP);
1475 return;
1476 }
1477 dumpInner(pw);
1478 }
1479
1480 @VisibleForTesting
1481 void dumpInner(PrintWriter pw) {
1482 synchronized (mLock) {
1483 final long now = injectCurrentTimeMillis();
1484 pw.print("Now: [");
1485 pw.print(now);
1486 pw.print("] ");
1487 pw.print(formatTime(now));
Makoto Onuki55046222016-03-08 10:49:47 -08001488
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001489 pw.print(" Raw last reset: [");
1490 pw.print(mRawLastResetTime);
1491 pw.print("] ");
1492 pw.print(formatTime(mRawLastResetTime));
1493
1494 final long last = getLastResetTimeLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001495 pw.print(" Last reset: [");
1496 pw.print(last);
1497 pw.print("] ");
1498 pw.print(formatTime(last));
1499
Makoto Onuki55046222016-03-08 10:49:47 -08001500 final long next = getNextResetTimeLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001501 pw.print(" Next reset: [");
1502 pw.print(next);
1503 pw.print("] ");
1504 pw.print(formatTime(next));
1505 pw.println();
1506
Makoto Onuki55046222016-03-08 10:49:47 -08001507 pw.print(" Max icon dim: ");
1508 pw.print(mMaxIconDimension);
1509 pw.print(" Icon format: ");
1510 pw.print(mIconPersistFormat);
1511 pw.print(" Icon quality: ");
1512 pw.print(mIconPersistQuality);
1513 pw.println();
1514
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001515 pw.println();
1516
1517 for (int i = 0; i < mShortcuts.size(); i++) {
1518 dumpUserLocked(pw, mShortcuts.keyAt(i));
1519 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001520 }
1521 }
1522
1523 private void dumpUserLocked(PrintWriter pw, int userId) {
1524 pw.print(" User: ");
1525 pw.print(userId);
1526 pw.println();
1527
1528 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId);
1529 if (packages == null) {
1530 return;
1531 }
1532 for (int j = 0; j < packages.size(); j++) {
1533 dumpPackageLocked(pw, userId, packages.keyAt(j));
1534 }
1535 pw.println();
1536 }
1537
1538 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
Makoto Onuki55046222016-03-08 10:49:47 -08001539 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName);
1540 if (packageShortcuts == null) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001541 return;
1542 }
1543
Makoto Onuki41066a62016-03-09 16:18:44 -08001544 packageShortcuts.dump(this, pw, " ");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001545 }
1546
Makoto Onuki41066a62016-03-09 16:18:44 -08001547 static String formatTime(long time) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001548 Time tobj = new Time();
1549 tobj.set(time);
1550 return tobj.format("%Y-%m-%d %H:%M:%S");
1551 }
1552
1553 // === Shell support ===
1554
1555 @Override
1556 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1557 String[] args, ResultReceiver resultReceiver) throws RemoteException {
1558
1559 enforceShell();
1560
1561 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1562 }
1563
1564 /**
1565 * Handle "adb shell cmd".
1566 */
1567 private class MyShellCommand extends ShellCommand {
1568 @Override
1569 public int onCommand(String cmd) {
1570 if (cmd == null) {
1571 return handleDefaultCommands(cmd);
1572 }
1573 final PrintWriter pw = getOutPrintWriter();
Makoto Onuki4362a662016-03-08 18:59:09 -08001574 int ret = 1;
1575 switch (cmd) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001576 case "reset-package-throttling":
Makoto Onuki4362a662016-03-08 18:59:09 -08001577 ret = handleResetPackageThrottling();
1578 break;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001579 case "reset-throttling":
Makoto Onuki4362a662016-03-08 18:59:09 -08001580 ret = handleResetThrottling();
1581 break;
1582 case "override-config":
1583 ret = handleOverrideConfig();
1584 break;
1585 case "reset-config":
1586 ret = handleResetConfig();
1587 break;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001588 default:
1589 return handleDefaultCommands(cmd);
1590 }
Makoto Onuki4362a662016-03-08 18:59:09 -08001591 if (ret == 0) {
1592 pw.println("Success");
1593 }
1594 return ret;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001595 }
1596
1597 @Override
1598 public void onHelp() {
1599 final PrintWriter pw = getOutPrintWriter();
1600 pw.println("Usage: cmd shortcut COMMAND [options ...]");
1601 pw.println();
1602 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1603 pw.println(" Reset throttling for a package");
1604 pw.println();
1605 pw.println("cmd shortcut reset-throttling");
1606 pw.println(" Reset throttling for all packages and users");
1607 pw.println();
Makoto Onuki4362a662016-03-08 18:59:09 -08001608 pw.println("cmd shortcut override-config CONFIG");
1609 pw.println(" Override the configuration for testing (will last until reboot)");
1610 pw.println();
1611 pw.println("cmd shortcut reset-config");
1612 pw.println(" Reset the configuration set with \"update-config\"");
1613 pw.println();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001614 }
1615
1616 private int handleResetThrottling() {
1617 resetThrottling();
1618 return 0;
1619 }
1620
1621 private int handleResetPackageThrottling() {
1622 final PrintWriter pw = getOutPrintWriter();
1623
1624 int userId = UserHandle.USER_SYSTEM;
1625 String opt;
1626 while ((opt = getNextOption()) != null) {
1627 switch (opt) {
1628 case "--user":
1629 userId = UserHandle.parseUserArg(getNextArgRequired());
1630 break;
1631 default:
1632 pw.println("Error: Unknown option: " + opt);
1633 return 1;
1634 }
1635 }
1636 final String packageName = getNextArgRequired();
1637
1638 synchronized (mLock) {
1639 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine();
1640 saveUserLocked(userId);
1641 }
1642
1643 return 0;
1644 }
Makoto Onuki4362a662016-03-08 18:59:09 -08001645
1646 private int handleOverrideConfig() {
1647 final PrintWriter pw = getOutPrintWriter();
1648 final String config = getNextArgRequired();
1649
1650 synchronized (mLock) {
1651 if (!updateConfigurationLocked(config)) {
1652 pw.println("override-config failed. See logcat for details.");
1653 return 1;
1654 }
1655 }
1656 return 0;
1657 }
1658
1659 private int handleResetConfig() {
1660 synchronized (mLock) {
1661 loadConfigurationLocked();
1662 }
1663 return 0;
1664 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001665 }
1666
1667 // === Unit test support ===
1668
1669 // Injection point.
1670 long injectCurrentTimeMillis() {
1671 return System.currentTimeMillis();
1672 }
1673
1674 // Injection point.
1675 int injectBinderCallingUid() {
1676 return getCallingUid();
1677 }
1678
1679 File injectSystemDataPath() {
1680 return Environment.getDataSystemDirectory();
1681 }
1682
1683 File injectUserDataPath(@UserIdInt int userId) {
Makoto Onuki55046222016-03-08 10:49:47 -08001684 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
1685 }
1686
Makoto Onuki4362a662016-03-08 18:59:09 -08001687 @VisibleForTesting
Makoto Onuki55046222016-03-08 10:49:47 -08001688 boolean injectIsLowRamDevice() {
1689 return ActivityManager.isLowRamDeviceStatic();
1690 }
1691
1692 File getUserBitmapFilePath(@UserIdInt int userId) {
1693 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001694 }
1695
1696 @VisibleForTesting
1697 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() {
1698 return mShortcuts;
1699 }
1700
1701 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08001702 int getMaxDynamicShortcutsForTest() {
1703 return mMaxDynamicShortcuts;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001704 }
1705
1706 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08001707 int getMaxDailyUpdatesForTest() {
1708 return mMaxDailyUpdates;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001709 }
1710
1711 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08001712 long getResetIntervalForTest() {
1713 return mResetInterval;
Makoto Onuki55046222016-03-08 10:49:47 -08001714 }
1715
1716 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08001717 int getMaxIconDimensionForTest() {
1718 return mMaxIconDimension;
1719 }
1720
1721 @VisibleForTesting
1722 CompressFormat getIconPersistFormatForTest() {
1723 return mIconPersistFormat;
1724 }
1725
1726 @VisibleForTesting
1727 int getIconPersistQualityForTest() {
1728 return mIconPersistQuality;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001729 }
Makoto Onuki41066a62016-03-09 16:18:44 -08001730
1731 @VisibleForTesting
1732 ShortcutInfo getPackageShortcutForTest(String packageName, String shortcutId, int userId) {
1733 synchronized (mLock) {
1734 return getPackageShortcutsLocked(packageName, userId).findShortcutById(shortcutId);
1735 }
1736 }
1737}
1738
1739/**
1740 * All the information relevant to shortcuts from a single package (per-user).
1741 */
1742class PackageShortcuts {
1743 private static final String TAG = ShortcutService.TAG;
1744
1745 @UserIdInt
1746 final int mUserId;
1747
1748 @NonNull
1749 final String mPackageName;
1750
1751 /**
1752 * All the shortcuts from the package, keyed on IDs.
1753 */
1754 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
1755
1756 /**
1757 * # of dynamic shortcuts.
1758 */
1759 private int mDynamicShortcutCount = 0;
1760
1761 /**
1762 * # of times the package has called rate-limited APIs.
1763 */
1764 private int mApiCallCount;
1765
1766 /**
1767 * When {@link #mApiCallCount} was reset last time.
1768 */
1769 private long mLastResetTime;
1770
1771 PackageShortcuts(int userId, String packageName) {
1772 mUserId = userId;
1773 mPackageName = packageName;
1774 }
1775
1776 @GuardedBy("mLock")
1777 @Nullable
1778 public ShortcutInfo findShortcutById(String id) {
1779 return mShortcuts.get(id);
1780 }
1781
1782 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
1783 @NonNull String id) {
1784 final ShortcutInfo shortcut = mShortcuts.remove(id);
1785 if (shortcut != null) {
1786 s.removeIcon(mUserId, shortcut);
1787 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
1788 }
1789 return shortcut;
1790 }
1791
1792 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
1793 deleteShortcut(s, newShortcut.getId());
1794 s.saveIconAndFixUpShortcut(mUserId, newShortcut);
1795 mShortcuts.put(newShortcut.getId(), newShortcut);
1796 }
1797
1798 /**
1799 * Add a shortcut, or update one with the same ID, with taking over existing flags.
1800 *
1801 * It checks the max number of dynamic shortcuts.
1802 */
1803 @GuardedBy("mLock")
1804 public void updateShortcutWithCapping(@NonNull ShortcutService s,
1805 @NonNull ShortcutInfo newShortcut) {
1806 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
1807
1808 int oldFlags = 0;
1809 int newDynamicCount = mDynamicShortcutCount;
1810
1811 if (oldShortcut != null) {
1812 oldFlags = oldShortcut.getFlags();
1813 if (oldShortcut.isDynamic()) {
1814 newDynamicCount--;
1815 }
1816 }
1817 if (newShortcut.isDynamic()) {
1818 newDynamicCount++;
1819 }
1820 // Make sure there's still room.
1821 s.enforceMaxDynamicShortcuts(newDynamicCount);
1822
1823 // Okay, make it dynamic and add.
1824 newShortcut.addFlags(oldFlags);
1825
1826 addShortcut(s, newShortcut);
1827 mDynamicShortcutCount = newDynamicCount;
1828 }
1829
1830 /**
1831 * Remove all shortcuts that aren't pinned nor dynamic.
1832 */
1833 private void removeOrphans(@NonNull ShortcutService s) {
1834 ArrayList<String> removeList = null; // Lazily initialize.
1835
1836 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1837 final ShortcutInfo si = mShortcuts.valueAt(i);
1838
1839 if (si.isPinned() || si.isDynamic()) continue;
1840
1841 if (removeList == null) {
1842 removeList = new ArrayList<>();
1843 }
1844 removeList.add(si.getId());
1845 }
1846 if (removeList != null) {
1847 for (int i = removeList.size() - 1 ; i >= 0; i--) {
1848 deleteShortcut(s, removeList.get(i));
1849 }
1850 }
1851 }
1852
1853 @GuardedBy("mLock")
1854 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
1855 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1856 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
1857 }
1858 removeOrphans(s);
1859 mDynamicShortcutCount = 0;
1860 }
1861
1862 @GuardedBy("mLock")
1863 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
1864 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
1865
1866 if (oldShortcut == null) {
1867 return;
1868 }
1869 if (oldShortcut.isDynamic()) {
1870 mDynamicShortcutCount--;
1871 }
1872 if (oldShortcut.isPinned()) {
1873 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
1874 } else {
1875 deleteShortcut(s, shortcutId);
1876 }
1877 }
1878
1879 @GuardedBy("mLock")
1880 public void replacePinned(@NonNull ShortcutService s, String launcherPackage,
1881 List<String> shortcutIds) {
1882
1883 // TODO Should be per launcherPackage.
1884
1885 // First, un-pin all shortcuts
1886 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
1887 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
1888 }
1889
1890 // Then pin ALL
1891 for (int i = shortcutIds.size() - 1; i >= 0; i--) {
1892 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i));
1893 if (shortcut != null) {
1894 shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
1895 }
1896 }
1897
1898 removeOrphans(s);
1899 }
1900
1901 /**
1902 * Number of calls that the caller has made, since the last reset.
1903 */
1904 @GuardedBy("mLock")
1905 public int getApiCallCount(@NonNull ShortcutService s) {
1906 final long last = s.getLastResetTimeLocked();
1907
1908 final long now = s.injectCurrentTimeMillis();
1909 if (mLastResetTime > now) {
1910 // Clock rewound. // TODO Test it
1911 mLastResetTime = now;
1912 }
1913
1914 // If not reset yet, then reset.
1915 if (mLastResetTime < last) {
1916 mApiCallCount = 0;
1917 mLastResetTime = last;
1918 }
1919 return mApiCallCount;
1920 }
1921
1922 /**
1923 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
1924 * and return true. Otherwise just return false.
1925 */
1926 @GuardedBy("mLock")
1927 public boolean tryApiCall(@NonNull ShortcutService s) {
1928 if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
1929 return false;
1930 }
1931 mApiCallCount++;
1932 return true;
1933 }
1934
1935 @GuardedBy("mLock")
1936 public void resetRateLimitingForCommandLine() {
1937 mApiCallCount = 0;
1938 mLastResetTime = 0;
1939 }
1940
1941 /**
1942 * Find all shortcuts that match {@code query}.
1943 */
1944 @GuardedBy("mLock")
1945 public void findAll(@NonNull List<ShortcutInfo> result,
1946 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
1947 for (int i = 0; i < mShortcuts.size(); i++) {
1948 final ShortcutInfo si = mShortcuts.valueAt(i);
1949 if (query == null || query.test(si)) {
1950 result.add(si.clone(cloneFlag));
1951 }
1952 }
1953 }
1954
1955 public void dump(@NonNull ShortcutService s, @NonNull PrintWriter pw, @NonNull String prefix) {
1956 pw.print(prefix);
1957 pw.print("Package: ");
1958 pw.print(mPackageName);
1959 pw.println();
1960
1961 pw.print(prefix);
1962 pw.print(" ");
1963 pw.print("Calls: ");
1964 pw.print(getApiCallCount(s));
1965 pw.println();
1966
1967 // This should be after getApiCallCount(), which may update it.
1968 pw.print(prefix);
1969 pw.print(" ");
1970 pw.print("Last reset: [");
1971 pw.print(mLastResetTime);
1972 pw.print("] ");
1973 pw.print(s.formatTime(mLastResetTime));
1974 pw.println();
1975
1976 pw.println(" Shortcuts:");
1977 long totalBitmapSize = 0;
1978 final ArrayMap<String, ShortcutInfo> shortcuts = mShortcuts;
1979 final int size = shortcuts.size();
1980 for (int i = 0; i < size; i++) {
1981 final ShortcutInfo si = shortcuts.valueAt(i);
1982 pw.print(" ");
1983 pw.println(si.toInsecureString());
Makoto Onuki34d1c912016-03-10 14:24:58 -08001984 if (si.getBitmapPath() != null) {
Makoto Onuki41066a62016-03-09 16:18:44 -08001985 final long len = new File(si.getBitmapPath()).length();
1986 pw.print(" ");
1987 pw.print("bitmap size=");
1988 pw.println(len);
1989
1990 totalBitmapSize += len;
1991 }
1992 }
1993 pw.print(prefix);
1994 pw.print(" ");
1995 pw.print("Total bitmap size: ");
1996 pw.print(totalBitmapSize);
1997 pw.print(" (");
1998 pw.print(Formatter.formatFileSize(s.mContext, totalBitmapSize));
1999 pw.println(")");
2000 }
2001
2002 public void saveToXml(@NonNull XmlSerializer out) throws IOException, XmlPullParserException {
2003 out.startTag(null, ShortcutService.TAG_PACKAGE);
2004
2005 ShortcutService.writeAttr(out, ShortcutService.ATTR_NAME, mPackageName);
2006 ShortcutService.writeAttr(out, ShortcutService.ATTR_DYNAMIC_COUNT, mDynamicShortcutCount);
2007 ShortcutService.writeAttr(out, ShortcutService.ATTR_CALL_COUNT, mApiCallCount);
2008 ShortcutService.writeAttr(out, ShortcutService.ATTR_LAST_RESET, mLastResetTime);
2009
2010 final int size = mShortcuts.size();
2011 for (int j = 0; j < size; j++) {
2012 saveShortcut(out, mShortcuts.valueAt(j));
2013 }
2014
2015 out.endTag(null, ShortcutService.TAG_PACKAGE);
2016 }
2017
2018 private static void saveShortcut(XmlSerializer out, ShortcutInfo si)
2019 throws IOException, XmlPullParserException {
2020 out.startTag(null, ShortcutService.TAG_SHORTCUT);
2021 ShortcutService.writeAttr(out, ShortcutService.ATTR_ID, si.getId());
2022 // writeAttr(out, "package", si.getPackageName()); // not needed
2023 ShortcutService.writeAttr(out, ShortcutService.ATTR_ACTIVITY, si.getActivityComponent());
2024 // writeAttr(out, "icon", si.getIcon()); // We don't save it.
2025 ShortcutService.writeAttr(out, ShortcutService.ATTR_TITLE, si.getTitle());
2026 ShortcutService.writeAttr(out, ShortcutService.ATTR_INTENT, si.getIntentNoExtras());
2027 ShortcutService.writeAttr(out, ShortcutService.ATTR_WEIGHT, si.getWeight());
2028 ShortcutService.writeAttr(out, ShortcutService.ATTR_TIMESTAMP,
2029 si.getLastChangedTimestamp());
2030 ShortcutService.writeAttr(out, ShortcutService.ATTR_FLAGS, si.getFlags());
2031 ShortcutService.writeAttr(out, ShortcutService.ATTR_ICON_RES, si.getIconResourceId());
2032 ShortcutService.writeAttr(out, ShortcutService.ATTR_BITMAP_PATH, si.getBitmapPath());
2033
2034 ShortcutService.writeTagExtra(out, ShortcutService.TAG_INTENT_EXTRAS,
2035 si.getIntentPersistableExtras());
2036 ShortcutService.writeTagExtra(out, ShortcutService.TAG_EXTRAS, si.getExtras());
2037
2038 out.endTag(null, ShortcutService.TAG_SHORTCUT);
2039 }
2040
2041 public static PackageShortcuts loadFromXml(XmlPullParser parser, int userId)
2042 throws IOException, XmlPullParserException {
2043
2044 final String packageName = ShortcutService.parseStringAttribute(parser,
2045 ShortcutService.ATTR_NAME);
2046
2047 final PackageShortcuts ret = new PackageShortcuts(userId, packageName);
2048
2049 ret.mDynamicShortcutCount =
2050 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_DYNAMIC_COUNT);
2051 ret.mApiCallCount =
2052 ShortcutService.parseIntAttribute(parser, ShortcutService.ATTR_CALL_COUNT);
2053 ret.mLastResetTime =
2054 ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_LAST_RESET);
2055
2056 final int outerDepth = parser.getDepth();
2057 int type;
2058 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2059 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2060 if (type != XmlPullParser.START_TAG) {
2061 continue;
2062 }
2063 final int depth = parser.getDepth();
2064 final String tag = parser.getName();
2065 switch (tag) {
2066 case ShortcutService.TAG_SHORTCUT:
2067 final ShortcutInfo si = parseShortcut(parser, packageName);
2068
2069 // Don't use addShortcut(), we don't need to save the icon.
2070 ret.mShortcuts.put(si.getId(), si);
2071 continue;
2072 }
2073 throw ShortcutService.throwForInvalidTag(depth, tag);
2074 }
2075 return ret;
2076 }
2077
2078 private static ShortcutInfo parseShortcut(XmlPullParser parser, String packageName)
2079 throws IOException, XmlPullParserException {
2080 String id;
2081 ComponentName activityComponent;
2082 // Icon icon;
2083 String title;
2084 Intent intent;
2085 PersistableBundle intentPersistableExtras = null;
2086 int weight;
2087 PersistableBundle extras = null;
2088 long lastChangedTimestamp;
2089 int flags;
2090 int iconRes;
2091 String bitmapPath;
2092
2093 id = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_ID);
2094 activityComponent = ShortcutService.parseComponentNameAttribute(parser,
2095 ShortcutService.ATTR_ACTIVITY);
2096 title = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_TITLE);
2097 intent = ShortcutService.parseIntentAttribute(parser, ShortcutService.ATTR_INTENT);
2098 weight = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_WEIGHT);
2099 lastChangedTimestamp = (int) ShortcutService.parseLongAttribute(parser,
2100 ShortcutService.ATTR_TIMESTAMP);
2101 flags = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_FLAGS);
2102 iconRes = (int) ShortcutService.parseLongAttribute(parser, ShortcutService.ATTR_ICON_RES);
2103 bitmapPath = ShortcutService.parseStringAttribute(parser, ShortcutService.ATTR_BITMAP_PATH);
2104
2105 final int outerDepth = parser.getDepth();
2106 int type;
2107 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
2108 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
2109 if (type != XmlPullParser.START_TAG) {
2110 continue;
2111 }
2112 final int depth = parser.getDepth();
2113 final String tag = parser.getName();
2114 if (ShortcutService.DEBUG_LOAD) {
2115 Slog.d(TAG, String.format(" depth=%d type=%d name=%s",
2116 depth, type, tag));
2117 }
2118 switch (tag) {
2119 case ShortcutService.TAG_INTENT_EXTRAS:
2120 intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
2121 continue;
2122 case ShortcutService.TAG_EXTRAS:
2123 extras = PersistableBundle.restoreFromXml(parser);
2124 continue;
2125 }
2126 throw ShortcutService.throwForInvalidTag(depth, tag);
2127 }
2128 return new ShortcutInfo(
2129 id, packageName, activityComponent, /* icon =*/ null, title, intent,
2130 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
2131 iconRes, bitmapPath);
2132 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002133}