blob: 9d8def66c72ad3f18a6fb399cb4e3d4245ebf6dd [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
114 private static final boolean DEBUG = true; // STOPSHIP if true
115 private static final boolean DEBUG_LOAD = true; // STOPSHIP if true
116
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
151 private static final String TAG_ROOT = "root";
Makoto Onuki55046222016-03-08 10:49:47 -0800152 private static final String TAG_PACKAGE = "package";
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800153 private static final String TAG_LAST_RESET_TIME = "last_reset_time";
Makoto Onuki55046222016-03-08 10:49:47 -0800154 private static final String TAG_INTENT_EXTRAS = "intent-extras";
155 private static final String TAG_EXTRAS = "extras";
156 private static final String TAG_SHORTCUT = "shortcut";
157
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800158 private static final String ATTR_VALUE = "value";
Makoto Onuki55046222016-03-08 10:49:47 -0800159 private static final String ATTR_NAME = "name";
160 private static final String ATTR_DYNAMIC_COUNT = "dynamic-count";
161 private static final String ATTR_CALL_COUNT = "call-count";
162 private static final String ATTR_LAST_RESET = "last-reset";
163 private static final String ATTR_ID = "id";
164 private static final String ATTR_ACTIVITY = "activity";
165 private static final String ATTR_TITLE = "title";
166 private static final String ATTR_INTENT = "intent";
167 private static final String ATTR_WEIGHT = "weight";
168 private static final String ATTR_TIMESTAMP = "timestamp";
169 private static final String ATTR_FLAGS = "flags";
170 private static final String ATTR_ICON_RES = "icon-res";
171 private 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 /**
201 * Key name for icom compression quality, 0-100.
202 */
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 Onuki6f7362d92016-03-04 13:39:41 -0800211 private final Context mContext;
212
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 /**
224 * All the information relevant to shortcuts from a single package (per-user).
225 *
226 * TODO Move the persisting code to this class.
Makoto Onuki55046222016-03-08 10:49:47 -0800227 *
228 * Only save/load/dump should look/touch inside this class.
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800229 */
230 private static class PackageShortcuts {
Makoto Onuki55046222016-03-08 10:49:47 -0800231 @UserIdInt
232 private final int mUserId;
233
234 @NonNull
235 private final String mPackageName;
236
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800237 /**
238 * All the shortcuts from the package, keyed on IDs.
239 */
240 final private ArrayMap<String, ShortcutInfo> mShortcuts = new ArrayMap<>();
241
242 /**
243 * # of dynamic shortcuts.
244 */
245 private int mDynamicShortcutCount = 0;
246
247 /**
248 * # of times the package has called rate-limited APIs.
249 */
Makoto Onuki55046222016-03-08 10:49:47 -0800250 private int mApiCallCount;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800251
252 /**
Makoto Onuki55046222016-03-08 10:49:47 -0800253 * When {@link #mApiCallCount} was reset last time.
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800254 */
255 private long mLastResetTime;
256
Makoto Onuki55046222016-03-08 10:49:47 -0800257 private PackageShortcuts(int userId, String packageName) {
258 mUserId = userId;
259 mPackageName = packageName;
260 }
261
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800262 @GuardedBy("mLock")
Makoto Onuki55046222016-03-08 10:49:47 -0800263 @Nullable
264 public ShortcutInfo findShortcutById(String id) {
265 return mShortcuts.get(id);
266 }
267
268 private ShortcutInfo deleteShortcut(@NonNull ShortcutService s,
269 @NonNull String id) {
270 final ShortcutInfo shortcut = mShortcuts.remove(id);
271 if (shortcut != null) {
272 s.removeIcon(mUserId, shortcut);
273 shortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC | ShortcutInfo.FLAG_PINNED);
274 }
275 return shortcut;
276 }
277
278 void addShortcut(@NonNull ShortcutService s, @NonNull ShortcutInfo newShortcut) {
279 deleteShortcut(s, newShortcut.getId());
280 s.saveIconAndFixUpShortcut(mUserId, newShortcut);
281 mShortcuts.put(newShortcut.getId(), newShortcut);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800282 }
283
284 /**
285 * Add a shortcut, or update one with the same ID, with taking over existing flags.
286 *
287 * It checks the max number of dynamic shortcuts.
288 */
289 @GuardedBy("mLock")
290 public void updateShortcutWithCapping(@NonNull ShortcutService s,
291 @NonNull ShortcutInfo newShortcut) {
292 final ShortcutInfo oldShortcut = mShortcuts.get(newShortcut.getId());
293
294 int oldFlags = 0;
295 int newDynamicCount = mDynamicShortcutCount;
296
297 if (oldShortcut != null) {
298 oldFlags = oldShortcut.getFlags();
299 if (oldShortcut.isDynamic()) {
300 newDynamicCount--;
301 }
302 }
303 if (newShortcut.isDynamic()) {
304 newDynamicCount++;
305 }
306 // Make sure there's still room.
307 s.enforceMaxDynamicShortcuts(newDynamicCount);
308
309 // Okay, make it dynamic and add.
310 newShortcut.addFlags(oldFlags);
311
Makoto Onuki55046222016-03-08 10:49:47 -0800312 addShortcut(s, newShortcut);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800313 mDynamicShortcutCount = newDynamicCount;
314 }
315
Makoto Onuki55046222016-03-08 10:49:47 -0800316 /**
317 * Remove all shortcuts that aren't pinned nor dynamic.
318 */
319 private void removeOrphans(@NonNull ShortcutService s) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800320 ArrayList<String> removeList = null; // Lazily initialize.
321
322 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
323 final ShortcutInfo si = mShortcuts.valueAt(i);
324
Makoto Onuki55046222016-03-08 10:49:47 -0800325 if (si.isPinned() || si.isDynamic()) continue;
326
327 if (removeList == null) {
328 removeList = new ArrayList<>();
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800329 }
Makoto Onuki55046222016-03-08 10:49:47 -0800330 removeList.add(si.getId());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800331 }
332 if (removeList != null) {
333 for (int i = removeList.size() - 1 ; i >= 0; i--) {
Makoto Onuki55046222016-03-08 10:49:47 -0800334 deleteShortcut(s, removeList.get(i));
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800335 }
336 }
Makoto Onuki55046222016-03-08 10:49:47 -0800337 }
338
339 @GuardedBy("mLock")
340 public void deleteAllDynamicShortcuts(@NonNull ShortcutService s) {
341 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
342 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_DYNAMIC);
343 }
344 removeOrphans(s);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800345 mDynamicShortcutCount = 0;
346 }
347
348 @GuardedBy("mLock")
Makoto Onuki55046222016-03-08 10:49:47 -0800349 public void deleteDynamicWithId(@NonNull ShortcutService s, @NonNull String shortcutId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800350 final ShortcutInfo oldShortcut = mShortcuts.get(shortcutId);
351
352 if (oldShortcut == null) {
353 return;
354 }
355 if (oldShortcut.isDynamic()) {
356 mDynamicShortcutCount--;
357 }
358 if (oldShortcut.isPinned()) {
359 oldShortcut.clearFlags(ShortcutInfo.FLAG_DYNAMIC);
360 } else {
Makoto Onuki55046222016-03-08 10:49:47 -0800361 deleteShortcut(s, shortcutId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800362 }
363 }
364
365 @GuardedBy("mLock")
Makoto Onuki55046222016-03-08 10:49:47 -0800366 public void replacePinned(@NonNull ShortcutService s, String launcherPackage,
367 List<String> shortcutIds) {
368
369 // TODO Should be per launcherPackage.
370
371 // First, un-pin all shortcuts
372 for (int i = mShortcuts.size() - 1; i >= 0; i--) {
373 mShortcuts.valueAt(i).clearFlags(ShortcutInfo.FLAG_PINNED);
374 }
375
376 // Then pin ALL
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800377 for (int i = shortcutIds.size() - 1; i >= 0; i--) {
378 final ShortcutInfo shortcut = mShortcuts.get(shortcutIds.get(i));
379 if (shortcut != null) {
380 shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
381 }
382 }
Makoto Onuki55046222016-03-08 10:49:47 -0800383
384 removeOrphans(s);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800385 }
386
387 /**
388 * Number of calls that the caller has made, since the last reset.
389 */
390 @GuardedBy("mLock")
391 public int getApiCallCount(@NonNull ShortcutService s) {
392 final long last = s.getLastResetTimeLocked();
393
Makoto Onuki55046222016-03-08 10:49:47 -0800394 final long now = s.injectCurrentTimeMillis();
395 if (mLastResetTime > now) {
396 // Clock rewound. // TODO Test it
397 mLastResetTime = now;
398 }
399
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800400 // If not reset yet, then reset.
401 if (mLastResetTime < last) {
Makoto Onuki55046222016-03-08 10:49:47 -0800402 mApiCallCount = 0;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800403 mLastResetTime = last;
404 }
Makoto Onuki55046222016-03-08 10:49:47 -0800405 return mApiCallCount;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800406 }
407
408 /**
Makoto Onuki55046222016-03-08 10:49:47 -0800409 * If the caller app hasn't been throttled yet, increment {@link #mApiCallCount}
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800410 * and return true. Otherwise just return false.
411 */
412 @GuardedBy("mLock")
413 public boolean tryApiCall(@NonNull ShortcutService s) {
414 if (getApiCallCount(s) >= s.mMaxDailyUpdates) {
415 return false;
416 }
Makoto Onuki55046222016-03-08 10:49:47 -0800417 mApiCallCount++;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800418 return true;
419 }
420
421 @GuardedBy("mLock")
422 public void resetRateLimitingForCommandLine() {
Makoto Onuki55046222016-03-08 10:49:47 -0800423 mApiCallCount = 0;
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800424 mLastResetTime = 0;
425 }
426
427 /**
428 * Find all shortcuts that match {@code query}.
429 */
430 @GuardedBy("mLock")
431 public void findAll(@NonNull List<ShortcutInfo> result,
432 @Nullable Predicate<ShortcutInfo> query, int cloneFlag) {
433 for (int i = 0; i < mShortcuts.size(); i++) {
434 final ShortcutInfo si = mShortcuts.valueAt(i);
435 if (query == null || query.test(si)) {
436 result.add(si.clone(cloneFlag));
437 }
438 }
439 }
440 }
441
442 /**
443 * User ID -> package name -> list of ShortcutInfos.
444 */
445 @GuardedBy("mLock")
446 private final SparseArray<ArrayMap<String, PackageShortcuts>> mShortcuts =
447 new SparseArray<>();
448
449 /**
450 * Max number of dynamic shortcuts that each application can have at a time.
451 */
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800452 private int mMaxDynamicShortcuts;
453
454 /**
455 * Max number of updating API calls that each application can make a day.
456 */
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800457 private int mMaxDailyUpdates;
458
459 /**
460 * Actual throttling-reset interval. By default it's a day.
461 */
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800462 private long mResetInterval;
463
Makoto Onuki55046222016-03-08 10:49:47 -0800464 /**
465 * Icon max width/height in pixels.
466 */
467 private int mMaxIconDimension;
468
Makoto Onuki4362a662016-03-08 18:59:09 -0800469 private CompressFormat mIconPersistFormat;
470 private int mIconPersistQuality;
Makoto Onuki55046222016-03-08 10:49:47 -0800471
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800472 public ShortcutService(Context context) {
473 mContext = Preconditions.checkNotNull(context);
474 LocalServices.addService(ShortcutServiceInternal.class, new LocalService());
475 mHandler = new Handler(BackgroundThread.get().getLooper());
476 }
477
478 /**
479 * System service lifecycle.
480 */
481 public static final class Lifecycle extends SystemService {
482 final ShortcutService mService;
483
484 public Lifecycle(Context context) {
485 super(context);
486 mService = new ShortcutService(context);
487 }
488
489 @Override
490 public void onStart() {
491 publishBinderService(Context.SHORTCUT_SERVICE, mService);
492 }
493
494 @Override
495 public void onBootPhase(int phase) {
496 mService.onBootPhase(phase);
497 }
498
499 @Override
500 public void onCleanupUser(int userHandle) {
501 synchronized (mService.mLock) {
502 mService.onCleanupUserInner(userHandle);
503 }
504 }
505
506 @Override
507 public void onStartUser(int userId) {
508 synchronized (mService.mLock) {
509 mService.onStartUserLocked(userId);
510 }
511 }
512 }
513
514 /** lifecycle event */
515 void onBootPhase(int phase) {
516 if (DEBUG) {
517 Slog.d(TAG, "onBootPhase: " + phase);
518 }
519 switch (phase) {
520 case SystemService.PHASE_LOCK_SETTINGS_READY:
521 initialize();
522 break;
523 }
524 }
525
526 /** lifecycle event */
527 void onStartUserLocked(int userId) {
528 // Preload
529 getUserShortcutsLocked(userId);
530 }
531
532 /** lifecycle event */
533 void onCleanupUserInner(int userId) {
534 // Unload
535 mShortcuts.delete(userId);
536 }
537
538 /** Return the base state file name */
539 private AtomicFile getBaseStateFile() {
540 final File path = new File(injectSystemDataPath(), FILENAME_BASE_STATE);
541 path.mkdirs();
542 return new AtomicFile(path);
543 }
544
545 /**
546 * Init the instance. (load the state file, etc)
547 */
548 private void initialize() {
549 synchronized (mLock) {
Makoto Onuki4362a662016-03-08 18:59:09 -0800550 loadConfigurationLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800551 loadBaseStateLocked();
552 }
553 }
554
Makoto Onuki4362a662016-03-08 18:59:09 -0800555 /**
556 * Load the configuration from Settings.
557 */
558 private void loadConfigurationLocked() {
559 updateConfigurationLocked(injectShortcutManagerConstants());
560 }
Makoto Onuki55046222016-03-08 10:49:47 -0800561
Makoto Onuki4362a662016-03-08 18:59:09 -0800562 /**
563 * Load the configuration from Settings.
564 */
565 @VisibleForTesting
566 boolean updateConfigurationLocked(String config) {
567 boolean result = true;
568
569 final KeyValueListParser parser = new KeyValueListParser(',');
570 try {
571 parser.setString(config);
572 } catch (IllegalArgumentException e) {
573 // Failed to parse the settings string, log this and move on
574 // with defaults.
575 Slog.e(TAG, "Bad shortcut manager settings", e);
576 result = false;
577 }
578
579 mResetInterval = parser.getLong(
580 ConfigConstants.KEY_RESET_INTERVAL_SEC, DEFAULT_RESET_INTERVAL_SEC)
581 * 1000L;
582
583 mMaxDailyUpdates = (int) parser.getLong(
584 ConfigConstants.KEY_MAX_DAILY_UPDATES, DEFAULT_MAX_DAILY_UPDATES);
585
586 mMaxDynamicShortcuts = (int) parser.getLong(
587 ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_APP);
588
589 final int iconDimensionDp = injectIsLowRamDevice()
590 ? (int) parser.getLong(
591 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
592 DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP)
593 : (int) parser.getLong(
594 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP,
595 DEFAULT_MAX_ICON_DIMENSION_DP);
596
597 mMaxIconDimension = injectDipToPixel(iconDimensionDp);
598
599 mIconPersistFormat = CompressFormat.valueOf(
600 parser.getString(ConfigConstants.KEY_ICON_FORMAT, DEFAULT_ICON_PERSIST_FORMAT));
601
602 mIconPersistQuality = (int) parser.getLong(
603 ConfigConstants.KEY_ICON_QUALITY,
604 DEFAULT_ICON_PERSIST_QUALITY);
605
606 return result;
607 }
608
609 @VisibleForTesting
610 String injectShortcutManagerConstants() {
611 return android.provider.Settings.Global.getString(
612 mContext.getContentResolver(),
613 android.provider.Settings.Global.SHORTCUT_MANAGER_CONSTANTS);
614 }
615
616 @VisibleForTesting
617 int injectDipToPixel(int dip) {
618 return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
619 mContext.getResources().getDisplayMetrics());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800620 }
621
Makoto Onuki55046222016-03-08 10:49:47 -0800622 // === Persisting ===
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800623
624 @Nullable
625 private String parseStringAttribute(XmlPullParser parser, String attribute) {
626 return parser.getAttributeValue(null, attribute);
627 }
628
629 private long parseLongAttribute(XmlPullParser parser, String attribute) {
630 final String value = parseStringAttribute(parser, attribute);
631 if (TextUtils.isEmpty(value)) {
632 return 0;
633 }
634 try {
635 return Long.parseLong(value);
636 } catch (NumberFormatException e) {
637 Slog.e(TAG, "Error parsing long " + value);
638 return 0;
639 }
640 }
641
642 @Nullable
643 private ComponentName parseComponentNameAttribute(XmlPullParser parser, String attribute) {
644 final String value = parseStringAttribute(parser, attribute);
645 if (TextUtils.isEmpty(value)) {
646 return null;
647 }
648 return ComponentName.unflattenFromString(value);
649 }
650
651 @Nullable
652 private Intent parseIntentAttribute(XmlPullParser parser, String attribute) {
653 final String value = parseStringAttribute(parser, attribute);
654 if (TextUtils.isEmpty(value)) {
655 return null;
656 }
657 try {
658 return Intent.parseUri(value, /* flags =*/ 0);
659 } catch (URISyntaxException e) {
660 Slog.e(TAG, "Error parsing intent", e);
661 return null;
662 }
663 }
664
665 private void writeTagValue(XmlSerializer out, String tag, String value) throws IOException {
666 if (TextUtils.isEmpty(value)) return;
667
668 out.startTag(null, tag);
669 out.attribute(null, ATTR_VALUE, value);
670 out.endTag(null, tag);
671 }
672
673 private void writeTagValue(XmlSerializer out, String tag, long value) throws IOException {
674 writeTagValue(out, tag, Long.toString(value));
675 }
676
677 private void writeTagExtra(XmlSerializer out, String tag, PersistableBundle bundle)
678 throws IOException, XmlPullParserException {
679 if (bundle == null) return;
680
681 out.startTag(null, tag);
682 bundle.saveToXml(out);
683 out.endTag(null, tag);
684 }
685
686 private void writeAttr(XmlSerializer out, String name, String value) throws IOException {
687 if (TextUtils.isEmpty(value)) return;
688
689 out.attribute(null, name, value);
690 }
691
692 private void writeAttr(XmlSerializer out, String name, long value) throws IOException {
693 writeAttr(out, name, String.valueOf(value));
694 }
695
696 private void writeAttr(XmlSerializer out, String name, ComponentName comp) throws IOException {
697 if (comp == null) return;
698 writeAttr(out, name, comp.flattenToString());
699 }
700
701 private void writeAttr(XmlSerializer out, String name, Intent intent) throws IOException {
702 if (intent == null) return;
703
704 writeAttr(out, name, intent.toUri(/* flags =*/ 0));
705 }
706
707 @VisibleForTesting
708 void saveBaseStateLocked() {
709 final AtomicFile file = getBaseStateFile();
710 if (DEBUG) {
711 Slog.i(TAG, "Saving to " + file.getBaseFile());
712 }
713
714 FileOutputStream outs = null;
715 try {
716 outs = file.startWrite();
717
718 // Write to XML
719 XmlSerializer out = new FastXmlSerializer();
720 out.setOutput(outs, StandardCharsets.UTF_8.name());
721 out.startDocument(null, true);
722 out.startTag(null, TAG_ROOT);
723
724 // Body.
725 writeTagValue(out, TAG_LAST_RESET_TIME, mRawLastResetTime);
726
727 // Epilogue.
728 out.endTag(null, TAG_ROOT);
729 out.endDocument();
730
731 // Close.
732 file.finishWrite(outs);
733 } catch (IOException e) {
734 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
735 file.failWrite(outs);
736 }
737 }
738
739 private void loadBaseStateLocked() {
740 mRawLastResetTime = 0;
741
742 final AtomicFile file = getBaseStateFile();
743 if (DEBUG) {
744 Slog.i(TAG, "Loading from " + file.getBaseFile());
745 }
746 try (FileInputStream in = file.openRead()) {
747 XmlPullParser parser = Xml.newPullParser();
748 parser.setInput(in, StandardCharsets.UTF_8.name());
749
750 int type;
751 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
752 if (type != XmlPullParser.START_TAG) {
753 continue;
754 }
755 final int depth = parser.getDepth();
756 // Check the root tag
757 final String tag = parser.getName();
758 if (depth == 1) {
759 if (!TAG_ROOT.equals(tag)) {
760 Slog.e(TAG, "Invalid root tag: " + tag);
761 return;
762 }
763 continue;
764 }
765 // Assume depth == 2
766 switch (tag) {
767 case TAG_LAST_RESET_TIME:
768 mRawLastResetTime = parseLongAttribute(parser, ATTR_VALUE);
769 break;
770 default:
771 Slog.e(TAG, "Invalid tag: " + tag);
772 break;
773 }
774 }
775 } catch (FileNotFoundException e) {
776 // Use the default
777 } catch (IOException|XmlPullParserException e) {
778 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
779
780 mRawLastResetTime = 0;
781 }
782 // Adjust the last reset time.
783 getLastResetTimeLocked();
784 }
785
786 private void saveUserLocked(@UserIdInt int userId) {
787 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
788 if (DEBUG) {
789 Slog.i(TAG, "Saving to " + path);
790 }
791 path.mkdirs();
792 final AtomicFile file = new AtomicFile(path);
793 FileOutputStream outs = null;
794 try {
795 outs = file.startWrite();
796
797 // Write to XML
798 XmlSerializer out = new FastXmlSerializer();
799 out.setOutput(outs, StandardCharsets.UTF_8.name());
800 out.startDocument(null, true);
801 out.startTag(null, TAG_ROOT);
802
803 final ArrayMap<String, PackageShortcuts> packages = getUserShortcutsLocked(userId);
804
805 // Body.
806 for (int i = 0; i < packages.size(); i++) {
807 final String packageName = packages.keyAt(i);
Makoto Onuki55046222016-03-08 10:49:47 -0800808 final PackageShortcuts packageShortcuts = packages.valueAt(i);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800809
810 // TODO Move this to PackageShortcuts.
811
Makoto Onuki55046222016-03-08 10:49:47 -0800812 out.startTag(null, TAG_PACKAGE);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800813
Makoto Onuki55046222016-03-08 10:49:47 -0800814 writeAttr(out, ATTR_NAME, packageName);
815 writeAttr(out, ATTR_DYNAMIC_COUNT, packageShortcuts.mDynamicShortcutCount);
816 writeAttr(out, ATTR_CALL_COUNT, packageShortcuts.mApiCallCount);
817 writeAttr(out, ATTR_LAST_RESET, packageShortcuts.mLastResetTime);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800818
Makoto Onuki55046222016-03-08 10:49:47 -0800819 final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
820 final int size = shortcuts.size();
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800821 for (int j = 0; j < size; j++) {
Makoto Onuki55046222016-03-08 10:49:47 -0800822 saveShortcut(out, shortcuts.valueAt(j));
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800823 }
824
Makoto Onuki55046222016-03-08 10:49:47 -0800825 out.endTag(null, TAG_PACKAGE);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800826 }
827
828 // Epilogue.
829 out.endTag(null, TAG_ROOT);
830 out.endDocument();
831
832 // Close.
833 file.finishWrite(outs);
834 } catch (IOException|XmlPullParserException e) {
835 Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
836 file.failWrite(outs);
837 }
838 }
839
840 private void saveShortcut(XmlSerializer out, ShortcutInfo si)
841 throws IOException, XmlPullParserException {
Makoto Onuki55046222016-03-08 10:49:47 -0800842 out.startTag(null, TAG_SHORTCUT);
843 writeAttr(out, ATTR_ID, si.getId());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800844 // writeAttr(out, "package", si.getPackageName()); // not needed
Makoto Onuki55046222016-03-08 10:49:47 -0800845 writeAttr(out, ATTR_ACTIVITY, si.getActivityComponent());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800846 // writeAttr(out, "icon", si.getIcon()); // We don't save it.
Makoto Onuki55046222016-03-08 10:49:47 -0800847 writeAttr(out, ATTR_TITLE, si.getTitle());
848 writeAttr(out, ATTR_INTENT, si.getIntentNoExtras());
849 writeAttr(out, ATTR_WEIGHT, si.getWeight());
850 writeAttr(out, ATTR_TIMESTAMP, si.getLastChangedTimestamp());
851 writeAttr(out, ATTR_FLAGS, si.getFlags());
852 writeAttr(out, ATTR_ICON_RES, si.getIconResourceId());
853 writeAttr(out, ATTR_BITMAP_PATH, si.getBitmapPath());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800854
Makoto Onuki55046222016-03-08 10:49:47 -0800855 writeTagExtra(out, TAG_INTENT_EXTRAS, si.getIntentPersistableExtras());
856 writeTagExtra(out, TAG_EXTRAS, si.getExtras());
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800857
Makoto Onuki55046222016-03-08 10:49:47 -0800858 out.endTag(null, TAG_SHORTCUT);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800859 }
860
861 private static IOException throwForInvalidTag(int depth, String tag) throws IOException {
862 throw new IOException(String.format("Invalid tag '%s' found at depth %d", tag, depth));
863 }
864
865 @Nullable
866 private ArrayMap<String, PackageShortcuts> loadUserLocked(@UserIdInt int userId) {
867 final File path = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
868 if (DEBUG) {
869 Slog.i(TAG, "Loading from " + path);
870 }
871 path.mkdirs();
872 final AtomicFile file = new AtomicFile(path);
873
874 final FileInputStream in;
875 try {
876 in = file.openRead();
877 } catch (FileNotFoundException e) {
878 if (DEBUG) {
879 Slog.i(TAG, "Not found " + path);
880 }
881 return null;
882 }
883 final ArrayMap<String, PackageShortcuts> ret = new ArrayMap<String, PackageShortcuts>();
884 try {
885 XmlPullParser parser = Xml.newPullParser();
886 parser.setInput(in, StandardCharsets.UTF_8.name());
887
888 String packageName = null;
889 PackageShortcuts shortcuts = null;
890
891 int type;
892 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
893 if (type != XmlPullParser.START_TAG) {
894 continue;
895 }
896 final int depth = parser.getDepth();
897
898 // TODO Move some of this to PackageShortcuts.
899
900 final String tag = parser.getName();
901 if (DEBUG_LOAD) {
902 Slog.d(TAG, String.format("depth=%d type=%d name=%s",
903 depth, type, tag));
904 }
905 switch (depth) {
906 case 1: {
907 if (TAG_ROOT.equals(tag)) {
908 continue;
909 }
910 break;
911 }
912 case 2: {
913 switch (tag) {
Makoto Onuki55046222016-03-08 10:49:47 -0800914 case TAG_PACKAGE:
915 packageName = parseStringAttribute(parser, ATTR_NAME);
916 shortcuts = new PackageShortcuts(userId, packageName);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800917 ret.put(packageName, shortcuts);
918
919 shortcuts.mDynamicShortcutCount =
Makoto Onuki55046222016-03-08 10:49:47 -0800920 (int) parseLongAttribute(parser, ATTR_DYNAMIC_COUNT);
921 shortcuts.mApiCallCount =
922 (int) parseLongAttribute(parser, ATTR_CALL_COUNT);
923 shortcuts.mLastResetTime = parseLongAttribute(parser,
924 ATTR_LAST_RESET);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800925 continue;
926 }
927 break;
928 }
929 case 3: {
930 switch (tag) {
Makoto Onuki55046222016-03-08 10:49:47 -0800931 case TAG_SHORTCUT:
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800932 final ShortcutInfo si = parseShortcut(parser, packageName);
Makoto Onuki55046222016-03-08 10:49:47 -0800933
934 // Don't use addShortcut(), we don't need to save the icon.
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800935 shortcuts.mShortcuts.put(si.getId(), si);
936 continue;
937 }
938 break;
939 }
940 }
941 throwForInvalidTag(depth, tag);
942 }
943 return ret;
944 } catch (IOException|XmlPullParserException e) {
945 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
946 return null;
947 } finally {
948 IoUtils.closeQuietly(in);
949 }
950 }
951
952 private ShortcutInfo parseShortcut(XmlPullParser parser, String packgeName)
953 throws IOException, XmlPullParserException {
954 String id;
955 ComponentName activityComponent;
956 Icon icon;
957 String title;
958 Intent intent;
959 PersistableBundle intentPersistableExtras = null;
960 int weight;
961 PersistableBundle extras = null;
962 long lastChangedTimestamp;
963 int flags;
964 int iconRes;
965 String bitmapPath;
966
Makoto Onuki55046222016-03-08 10:49:47 -0800967 id = parseStringAttribute(parser, ATTR_ID);
968 activityComponent = parseComponentNameAttribute(parser, ATTR_ACTIVITY);
969 title = parseStringAttribute(parser, ATTR_TITLE);
970 intent = parseIntentAttribute(parser, ATTR_INTENT);
971 weight = (int) parseLongAttribute(parser, ATTR_WEIGHT);
972 lastChangedTimestamp = (int) parseLongAttribute(parser, ATTR_TIMESTAMP);
973 flags = (int) parseLongAttribute(parser, ATTR_FLAGS);
974 iconRes = (int) parseLongAttribute(parser, ATTR_ICON_RES);
975 bitmapPath = parseStringAttribute(parser, ATTR_BITMAP_PATH);
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800976
977 final int outerDepth = parser.getDepth();
978 int type;
979 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
980 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
981 if (type != XmlPullParser.START_TAG) {
982 continue;
983 }
984 final int depth = parser.getDepth();
985 final String tag = parser.getName();
986 if (DEBUG_LOAD) {
987 Slog.d(TAG, String.format(" depth=%d type=%d name=%s",
988 depth, type, tag));
989 }
990 switch (tag) {
Makoto Onuki55046222016-03-08 10:49:47 -0800991 case TAG_INTENT_EXTRAS:
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800992 intentPersistableExtras = PersistableBundle.restoreFromXml(parser);
993 continue;
Makoto Onuki55046222016-03-08 10:49:47 -0800994 case TAG_EXTRAS:
Makoto Onuki6f7362d92016-03-04 13:39:41 -0800995 extras = PersistableBundle.restoreFromXml(parser);
996 continue;
997 }
998 throw throwForInvalidTag(depth, tag);
999 }
1000 return new ShortcutInfo(
1001 id, packgeName, activityComponent, /* icon =*/ null, title, intent,
1002 intentPersistableExtras, weight, extras, lastChangedTimestamp, flags,
1003 iconRes, bitmapPath);
1004 }
1005
1006 // TODO Actually make it async.
1007 private void scheduleSaveBaseState() {
1008 synchronized (mLock) {
1009 saveBaseStateLocked();
1010 }
1011 }
1012
1013 // TODO Actually make it async.
1014 private void scheduleSaveUser(@UserIdInt int userId) {
1015 synchronized (mLock) {
1016 saveUserLocked(userId);
1017 }
1018 }
1019
1020 /** Return the last reset time. */
1021 long getLastResetTimeLocked() {
1022 updateTimes();
1023 return mRawLastResetTime;
1024 }
1025
1026 /** Return the next reset time. */
1027 long getNextResetTimeLocked() {
1028 updateTimes();
1029 return mRawLastResetTime + mResetInterval;
1030 }
1031
1032 /**
1033 * Update the last reset time.
1034 */
1035 private void updateTimes() {
1036
1037 final long now = injectCurrentTimeMillis();
1038
1039 final long prevLastResetTime = mRawLastResetTime;
1040
1041 if (mRawLastResetTime == 0) { // first launch.
1042 // TODO Randomize??
1043 mRawLastResetTime = now;
1044 } else if (now < mRawLastResetTime) {
1045 // Clock rewound.
1046 // TODO Randomize??
1047 mRawLastResetTime = now;
1048 } else {
1049 // TODO Do it properly.
1050 while ((mRawLastResetTime + mResetInterval) <= now) {
1051 mRawLastResetTime += mResetInterval;
1052 }
1053 }
1054 if (prevLastResetTime != mRawLastResetTime) {
1055 scheduleSaveBaseState();
1056 }
1057 }
1058
1059 /** Return the per-user state. */
1060 @GuardedBy("mLock")
1061 @NonNull
1062 private ArrayMap<String, PackageShortcuts> getUserShortcutsLocked(@UserIdInt int userId) {
1063 ArrayMap<String, PackageShortcuts> userPackages = mShortcuts.get(userId);
1064 if (userPackages == null) {
1065 userPackages = loadUserLocked(userId);
1066 if (userPackages == null) {
1067 userPackages = new ArrayMap<>();
1068 }
1069 mShortcuts.put(userId, userPackages);
1070 }
1071 return userPackages;
1072 }
1073
1074 /** Return the per-user per-package state. */
1075 @GuardedBy("mLock")
1076 @NonNull
1077 private PackageShortcuts getPackageShortcutsLocked(
1078 @NonNull String packageName, @UserIdInt int userId) {
1079 final ArrayMap<String, PackageShortcuts> userPackages = getUserShortcutsLocked(userId);
1080 PackageShortcuts shortcuts = userPackages.get(packageName);
1081 if (shortcuts == null) {
Makoto Onuki55046222016-03-08 10:49:47 -08001082 shortcuts = new PackageShortcuts(userId, packageName);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001083 userPackages.put(packageName, shortcuts);
1084 }
1085 return shortcuts;
1086 }
1087
1088 // === Caller validation ===
1089
Makoto Onuki55046222016-03-08 10:49:47 -08001090 void removeIcon(@UserIdInt int userId, ShortcutInfo shortcut) {
1091 if (shortcut.getBitmapPath() != null) {
1092 if (DEBUG) {
1093 Slog.d(TAG, "Removing " + shortcut.getBitmapPath());
1094 }
1095 new File(shortcut.getBitmapPath()).delete();
1096
1097 shortcut.setBitmapPath(null);
1098 shortcut.setIconResourceId(0);
1099 shortcut.clearFlags(ShortcutInfo.FLAG_HAS_ICON_FILE | ShortcutInfo.FLAG_HAS_ICON_RES);
1100 }
1101 }
1102
1103 @VisibleForTesting
1104 static class FileOutputStreamWithPath extends FileOutputStream {
1105 private final File mFile;
1106
1107 public FileOutputStreamWithPath(File file) throws FileNotFoundException {
1108 super(file);
1109 mFile = file;
1110 }
1111
1112 public File getFile() {
1113 return mFile;
1114 }
1115 }
1116
1117 /**
1118 * Build the cached bitmap filename for a shortcut icon.
1119 *
1120 * The filename will be based on the ID, except certain characters will be escaped.
1121 */
1122 @VisibleForTesting
1123 FileOutputStreamWithPath openIconFileForWrite(@UserIdInt int userId, ShortcutInfo shortcut)
1124 throws IOException {
1125 final File packagePath = new File(getUserBitmapFilePath(userId),
1126 shortcut.getPackageName());
1127 if (!packagePath.isDirectory()) {
1128 packagePath.mkdirs();
1129 if (!packagePath.isDirectory()) {
1130 throw new IOException("Unable to create directory " + packagePath);
1131 }
1132 SELinux.restorecon(packagePath);
1133 }
1134
1135 final String baseName = String.valueOf(injectCurrentTimeMillis());
1136 for (int suffix = 0;; suffix++) {
1137 final String filename = (suffix == 0 ? baseName : baseName + "_" + suffix) + ".png";
1138 final File file = new File(packagePath, filename);
1139 if (!file.exists()) {
1140 if (DEBUG) {
1141 Slog.d(TAG, "Saving icon to " + file.getAbsolutePath());
1142 }
1143 return new FileOutputStreamWithPath(file);
1144 }
1145 }
1146 }
1147
1148 void saveIconAndFixUpShortcut(@UserIdInt int userId, ShortcutInfo shortcut) {
1149 if (shortcut.hasIconFile() || shortcut.hasIconResource()) {
1150 return;
1151 }
1152
1153 final long token = Binder.clearCallingIdentity();
1154 try {
1155 // Clear icon info on the shortcut.
1156 shortcut.setIconResourceId(0);
1157 shortcut.setBitmapPath(null);
1158
1159 final Icon icon = shortcut.getIcon();
1160 if (icon == null) {
1161 return; // has no icon
1162 }
1163
1164 Bitmap bitmap = null;
1165 try {
1166 switch (icon.getType()) {
1167 case Icon.TYPE_RESOURCE: {
1168 injectValidateIconResPackage(shortcut, icon);
1169
1170 shortcut.setIconResourceId(icon.getResId());
1171 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_RES);
1172 return;
1173 }
1174 case Icon.TYPE_BITMAP: {
1175 bitmap = icon.getBitmap();
1176 break;
1177 }
1178 case Icon.TYPE_URI: {
1179 final Uri uri = ContentProvider.maybeAddUserId(icon.getUri(), userId);
1180
1181 try (InputStream is = mContext.getContentResolver().openInputStream(uri)) {
1182
1183 bitmap = BitmapFactory.decodeStream(is);
1184
1185 } catch (IOException e) {
1186 Slog.e(TAG, "Unable to load icon from " + uri);
1187 return;
1188 }
1189 break;
1190 }
1191 default:
1192 // This shouldn't happen because we've already validated the icon, but
1193 // just in case.
1194 throw ShortcutInfo.getInvalidIconException();
1195 }
1196 if (bitmap == null) {
1197 Slog.e(TAG, "Null bitmap detected");
1198 return;
1199 }
1200 // Shrink and write to the file.
1201 File path = null;
1202 try {
1203 final FileOutputStreamWithPath out = openIconFileForWrite(userId, shortcut);
1204 try {
1205 path = out.getFile();
1206
1207 shrinkBitmap(bitmap, mMaxIconDimension)
1208 .compress(mIconPersistFormat, mIconPersistQuality, out);
1209
1210 shortcut.setBitmapPath(out.getFile().getAbsolutePath());
1211 shortcut.addFlags(ShortcutInfo.FLAG_HAS_ICON_FILE);
1212 } finally {
1213 IoUtils.closeQuietly(out);
1214 }
1215 } catch (IOException|RuntimeException e) {
1216 // STOPSHIP Change wtf to e
1217 Slog.wtf(ShortcutService.TAG, "Unable to write bitmap to file", e);
1218 if (path != null && path.exists()) {
1219 path.delete();
1220 }
1221 }
1222 } finally {
1223 if (bitmap != null) {
1224 bitmap.recycle();
1225 }
1226 // Once saved, we won't use the original icon information, so null it out.
1227 shortcut.clearIcon();
1228 }
1229 } finally {
1230 Binder.restoreCallingIdentity(token);
1231 }
1232 }
1233
1234 // Unfortunately we can't do this check in unit tests because we fake creator package names,
1235 // so override in unit tests.
1236 // TODO CTS this case.
1237 void injectValidateIconResPackage(ShortcutInfo shortcut, Icon icon) {
1238 if (!shortcut.getPackageName().equals(icon.getResPackage())) {
1239 throw new IllegalArgumentException(
1240 "Icon resource must reside in shortcut owner package");
1241 }
1242 }
1243
1244 @VisibleForTesting
1245 static Bitmap shrinkBitmap(Bitmap in, int maxSize) {
1246 // Original width/height.
1247 final int ow = in.getWidth();
1248 final int oh = in.getHeight();
1249 if ((ow <= maxSize) && (oh <= maxSize)) {
1250 if (DEBUG) {
1251 Slog.d(TAG, String.format("Icon size %dx%d, no need to shrink", ow, oh));
1252 }
1253 return in;
1254 }
1255 final int longerDimension = Math.max(ow, oh);
1256
1257 // New width and height.
1258 final int nw = ow * maxSize / longerDimension;
1259 final int nh = oh * maxSize / longerDimension;
1260 if (DEBUG) {
1261 Slog.d(TAG, String.format("Icon size %dx%d, shrinking to %dx%d",
1262 ow, oh, nw, nh));
1263 }
1264
1265 final Bitmap scaledBitmap = Bitmap.createBitmap(nw, nh, Bitmap.Config.ARGB_8888);
1266 final Canvas c = new Canvas(scaledBitmap);
1267
1268 final RectF dst = new RectF(0, 0, nw, nh);
1269
1270 c.drawBitmap(in, /*src=*/ null, dst, /* paint =*/ null);
1271
1272 in.recycle();
1273
1274 return scaledBitmap;
1275 }
1276
1277 // === Caller validation ===
1278
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001279 private boolean isCallerSystem() {
1280 final int callingUid = injectBinderCallingUid();
1281 return UserHandle.isSameApp(callingUid, Process.SYSTEM_UID);
1282 }
1283
1284 private boolean isCallerShell() {
1285 final int callingUid = injectBinderCallingUid();
1286 return callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
1287 }
1288
1289 private void enforceSystemOrShell() {
1290 Preconditions.checkState(isCallerSystem() || isCallerShell(),
1291 "Caller must be system or shell");
1292 }
1293
1294 private void enforceShell() {
1295 Preconditions.checkState(isCallerShell(), "Caller must be shell");
1296 }
1297
1298 private void verifyCaller(@NonNull String packageName, @UserIdInt int userId) {
1299 Preconditions.checkStringNotEmpty(packageName, "packageName");
1300
1301 if (isCallerSystem()) {
1302 return; // no check
1303 }
1304
1305 final int callingUid = injectBinderCallingUid();
1306
1307 // Otherwise, make sure the arguments are valid.
1308 if (UserHandle.getUserId(callingUid) != userId) {
1309 throw new SecurityException("Invalid user-ID");
1310 }
Makoto Onuki55046222016-03-08 10:49:47 -08001311 if (injectGetPackageUid(packageName, userId) == injectBinderCallingUid()) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001312 return; // Caller is valid.
1313 }
1314 throw new SecurityException("Caller UID= doesn't own " + packageName);
1315 }
1316
1317 // Test overrides it.
Makoto Onuki55046222016-03-08 10:49:47 -08001318 int injectGetPackageUid(@NonNull String packageName, @UserIdInt int userId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001319 try {
1320
1321 // TODO Is MATCH_UNINSTALLED_PACKAGES correct to get SD card app info?
1322
Makoto Onuki55046222016-03-08 10:49:47 -08001323 return mContext.getPackageManager().getPackageUidAsUser(packageName,
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001324 PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE
Makoto Onuki55046222016-03-08 10:49:47 -08001325 | PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001326 } catch (NameNotFoundException e) {
1327 return -1;
1328 }
1329 }
1330
1331 /**
1332 * Throw if {@code numShortcuts} is bigger than {@link #mMaxDynamicShortcuts}.
1333 */
1334 void enforceMaxDynamicShortcuts(int numShortcuts) {
1335 if (numShortcuts > mMaxDynamicShortcuts) {
1336 throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
1337 }
1338 }
1339
1340 /**
1341 * - Sends a notification to LauncherApps
1342 * - Write to file
1343 */
1344 private void userPackageChanged(@NonNull String packageName, @UserIdInt int userId) {
1345 notifyListeners(packageName, userId);
1346 scheduleSaveUser(userId);
1347 }
1348
1349 private void notifyListeners(@NonNull String packageName, @UserIdInt int userId) {
1350 final ArrayList<ShortcutChangeListener> copy;
1351 final List<ShortcutInfo> shortcuts = new ArrayList<>();
1352 synchronized (mLock) {
1353 copy = new ArrayList<>(mListeners);
1354
1355 getPackageShortcutsLocked(packageName, userId)
1356 .findAll(shortcuts, /* query =*/ null, ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO);
1357 }
1358 for (int i = copy.size() - 1; i >= 0; i--) {
1359 copy.get(i).onShortcutChanged(packageName, shortcuts, userId);
1360 }
1361 }
1362
1363 /**
1364 * Clean up / validate an incoming shortcut.
1365 * - Make sure all mandatory fields are set.
1366 * - Make sure the intent's extras are persistable, and them to set
1367 * {@link ShortcutInfo#mIntentPersistableExtras}. Also clear its extras.
1368 * - Clear flags.
Makoto Onuki55046222016-03-08 10:49:47 -08001369 *
1370 * TODO Detailed unit tests
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001371 */
Makoto Onuki55046222016-03-08 10:49:47 -08001372 private void fixUpIncomingShortcutInfo(@NonNull ShortcutInfo shortcut, boolean forUpdate) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001373 Preconditions.checkNotNull(shortcut, "Null shortcut detected");
1374 if (shortcut.getActivityComponent() != null) {
1375 Preconditions.checkState(
1376 shortcut.getPackageName().equals(
1377 shortcut.getActivityComponent().getPackageName()),
1378 "Activity package name mismatch");
1379 }
1380
Makoto Onuki55046222016-03-08 10:49:47 -08001381 if (!forUpdate) {
1382 shortcut.enforceMandatoryFields();
1383 }
1384 if (shortcut.getIcon() != null) {
1385 ShortcutInfo.validateIcon(shortcut.getIcon());
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001386 }
1387
Makoto Onuki55046222016-03-08 10:49:47 -08001388 validateForXml(shortcut.getId());
1389 validateForXml(shortcut.getTitle());
1390 validatePersistableBundleForXml(shortcut.getIntentPersistableExtras());
1391 validatePersistableBundleForXml(shortcut.getExtras());
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001392
1393 shortcut.setFlags(0);
1394 }
1395
Makoto Onuki55046222016-03-08 10:49:47 -08001396 // KXmlSerializer is strict and doesn't allow certain characters, so we disallow those
1397 // characters.
1398
1399 private static void validatePersistableBundleForXml(PersistableBundle b) {
1400 if (b == null || b.size() == 0) {
1401 return;
1402 }
1403 for (String key : b.keySet()) {
1404 validateForXml(key);
1405 final Object value = b.get(key);
1406 if (value == null) {
1407 continue;
1408 } else if (value instanceof String) {
1409 validateForXml((String) value);
1410 } else if (value instanceof String[]) {
1411 for (String v : (String[]) value) {
1412 validateForXml(v);
1413 }
1414 } else if (value instanceof PersistableBundle) {
1415 validatePersistableBundleForXml((PersistableBundle) value);
1416 }
1417 }
1418 }
1419
1420 private static void validateForXml(String s) {
1421 if (TextUtils.isEmpty(s)) {
1422 return;
1423 }
1424 for (int i = s.length() - 1; i >= 0; i--) {
1425 if (!isAllowedInXml(s.charAt(i))) {
1426 throw new IllegalArgumentException("Unsupported character detected in: " + s);
1427 }
1428 }
1429 }
1430
1431 private static boolean isAllowedInXml(char c) {
1432 return (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
1433 }
1434
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001435 // === APIs ===
1436
1437 @Override
1438 public boolean setDynamicShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1439 @UserIdInt int userId) {
1440 verifyCaller(packageName, userId);
1441
1442 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
1443 final int size = newShortcuts.size();
1444
1445 synchronized (mLock) {
1446 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1447
1448 // Throttling.
1449 if (!ps.tryApiCall(this)) {
1450 return false;
1451 }
1452 enforceMaxDynamicShortcuts(size);
1453
1454 // Validate the shortcuts.
1455 for (int i = 0; i < size; i++) {
Makoto Onuki55046222016-03-08 10:49:47 -08001456 fixUpIncomingShortcutInfo(newShortcuts.get(i), /* forUpdate= */ false);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001457 }
1458
1459 // First, remove all un-pinned; dynamic shortcuts
Makoto Onuki55046222016-03-08 10:49:47 -08001460 ps.deleteAllDynamicShortcuts(this);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001461
1462 // Then, add/update all. We need to make sure to take over "pinned" flag.
1463 for (int i = 0; i < size; i++) {
1464 final ShortcutInfo newShortcut = newShortcuts.get(i);
1465 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1466 ps.updateShortcutWithCapping(this, newShortcut);
1467 }
1468 }
1469 userPackageChanged(packageName, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001470 return true;
1471 }
1472
1473 @Override
1474 public boolean updateShortcuts(String packageName, ParceledListSlice shortcutInfoList,
1475 @UserIdInt int userId) {
1476 verifyCaller(packageName, userId);
1477
1478 final List<ShortcutInfo> newShortcuts = (List<ShortcutInfo>) shortcutInfoList.getList();
Makoto Onuki55046222016-03-08 10:49:47 -08001479 final int size = newShortcuts.size();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001480
1481 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001482 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001483
Makoto Onuki55046222016-03-08 10:49:47 -08001484 // Throttling.
1485 if (!ps.tryApiCall(this)) {
1486 return false;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001487 }
1488
Makoto Onuki55046222016-03-08 10:49:47 -08001489 for (int i = 0; i < size; i++) {
1490 final ShortcutInfo source = newShortcuts.get(i);
1491 fixUpIncomingShortcutInfo(source, /* forUpdate= */ true);
1492
1493 final ShortcutInfo target = ps.findShortcutById(source.getId());
1494 if (target != null) {
1495 final boolean replacingIcon = (source.getIcon() != null);
1496 if (replacingIcon) {
1497 removeIcon(userId, target);
1498 }
1499
1500 target.copyNonNullFieldsFrom(source);
1501
1502 if (replacingIcon) {
1503 saveIconAndFixUpShortcut(userId, target);
1504 }
1505 }
1506 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001507 }
1508 userPackageChanged(packageName, userId);
1509
1510 return true;
1511 }
1512
1513 @Override
1514 public boolean addDynamicShortcut(String packageName, ShortcutInfo newShortcut,
1515 @UserIdInt int userId) {
1516 verifyCaller(packageName, userId);
1517
1518 synchronized (mLock) {
1519 final PackageShortcuts ps = getPackageShortcutsLocked(packageName, userId);
1520
1521 // Throttling.
1522 if (!ps.tryApiCall(this)) {
1523 return false;
1524 }
1525
1526 // Validate the shortcut.
Makoto Onuki55046222016-03-08 10:49:47 -08001527 fixUpIncomingShortcutInfo(newShortcut, /* forUpdate= */ false);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001528
1529 // Add it.
1530 newShortcut.addFlags(ShortcutInfo.FLAG_DYNAMIC);
1531 ps.updateShortcutWithCapping(this, newShortcut);
1532 }
1533 userPackageChanged(packageName, userId);
1534
1535 return true;
1536 }
1537
1538 @Override
1539 public void deleteDynamicShortcut(String packageName, String shortcutId,
1540 @UserIdInt int userId) {
1541 verifyCaller(packageName, userId);
1542 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId must be provided");
1543
1544 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001545 getPackageShortcutsLocked(packageName, userId).deleteDynamicWithId(this, shortcutId);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001546 }
1547 userPackageChanged(packageName, userId);
1548 }
1549
1550 @Override
1551 public void deleteAllDynamicShortcuts(String packageName, @UserIdInt int userId) {
1552 verifyCaller(packageName, userId);
1553
1554 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001555 getPackageShortcutsLocked(packageName, userId).deleteAllDynamicShortcuts(this);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001556 }
1557 userPackageChanged(packageName, userId);
1558 }
1559
1560 @Override
1561 public ParceledListSlice<ShortcutInfo> getDynamicShortcuts(String packageName,
1562 @UserIdInt int userId) {
1563 verifyCaller(packageName, userId);
1564 synchronized (mLock) {
1565 return getShortcutsWithQueryLocked(
1566 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1567 ShortcutInfo::isDynamic);
1568 }
1569 }
1570
1571 @Override
1572 public ParceledListSlice<ShortcutInfo> getPinnedShortcuts(String packageName,
1573 @UserIdInt int userId) {
1574 verifyCaller(packageName, userId);
1575 synchronized (mLock) {
1576 return getShortcutsWithQueryLocked(
1577 packageName, userId, ShortcutInfo.CLONE_REMOVE_FOR_CREATOR,
1578 ShortcutInfo::isPinned);
1579 }
1580 }
1581
1582 private ParceledListSlice<ShortcutInfo> getShortcutsWithQueryLocked(@NonNull String packageName,
1583 @UserIdInt int userId, int cloneFlags, @NonNull Predicate<ShortcutInfo> query) {
1584
1585 final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1586
1587 getPackageShortcutsLocked(packageName, userId).findAll(ret, query, cloneFlags);
1588
1589 return new ParceledListSlice<>(ret);
1590 }
1591
1592 @Override
1593 public int getMaxDynamicShortcutCount(String packageName, @UserIdInt int userId)
1594 throws RemoteException {
1595 verifyCaller(packageName, userId);
1596
1597 return mMaxDynamicShortcuts;
1598 }
1599
1600 @Override
1601 public int getRemainingCallCount(String packageName, @UserIdInt int userId) {
1602 verifyCaller(packageName, userId);
1603
1604 synchronized (mLock) {
1605 return mMaxDailyUpdates
1606 - getPackageShortcutsLocked(packageName, userId).getApiCallCount(this);
1607 }
1608 }
1609
1610 @Override
1611 public long getRateLimitResetTime(String packageName, @UserIdInt int userId) {
1612 verifyCaller(packageName, userId);
1613
1614 synchronized (mLock) {
1615 return getNextResetTimeLocked();
1616 }
1617 }
1618
Makoto Onuki55046222016-03-08 10:49:47 -08001619 @Override
1620 public int getIconMaxDimensions(String packageName, int userId) throws RemoteException {
1621 synchronized (mLock) {
1622 return mMaxIconDimension;
1623 }
1624 }
1625
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001626 /**
1627 * Reset all throttling, for developer options and command line. Only system/shell can call it.
1628 */
1629 @Override
1630 public void resetThrottling() {
1631 enforceSystemOrShell();
1632
1633 resetThrottlingInner();
1634 }
1635
1636 @VisibleForTesting
1637 void resetThrottlingInner() {
1638 synchronized (mLock) {
1639 mRawLastResetTime = injectCurrentTimeMillis();
1640 }
1641 scheduleSaveBaseState();
Makoto Onuki55046222016-03-08 10:49:47 -08001642 Slog.i(TAG, "ShortcutManager: throttling counter reset");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001643 }
1644
1645 /**
1646 * Entry point from {@link LauncherApps}.
1647 */
1648 private class LocalService extends ShortcutServiceInternal {
1649 @Override
1650 public List<ShortcutInfo> getShortcuts(
1651 @NonNull String callingPackage, long changedSince,
1652 @Nullable String packageName, @Nullable ComponentName componentName,
1653 int queryFlags, int userId) {
1654 final ArrayList<ShortcutInfo> ret = new ArrayList<>();
1655 final int cloneFlag =
1656 ((queryFlags & ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY) == 0)
1657 ? ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER
1658 : ShortcutInfo.CLONE_REMOVE_NON_KEY_INFO;
1659
1660 synchronized (mLock) {
1661 if (packageName != null) {
1662 getShortcutsInnerLocked(packageName, changedSince, componentName, queryFlags,
1663 userId, ret, cloneFlag);
1664 } else {
1665 final ArrayMap<String, PackageShortcuts> packages =
1666 getUserShortcutsLocked(userId);
Makoto Onuki55046222016-03-08 10:49:47 -08001667 for (int i = packages.size() - 1; i >= 0; i--) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001668 getShortcutsInnerLocked(
1669 packages.keyAt(i),
1670 changedSince, componentName, queryFlags, userId, ret, cloneFlag);
1671 }
1672 }
1673 }
1674 return ret;
1675 }
1676
1677 private void getShortcutsInnerLocked(@Nullable String packageName,long changedSince,
1678 @Nullable ComponentName componentName, int queryFlags,
1679 int userId, ArrayList<ShortcutInfo> ret, int cloneFlag) {
1680 getPackageShortcutsLocked(packageName, userId).findAll(ret,
1681 (ShortcutInfo si) -> {
1682 if (si.getLastChangedTimestamp() < changedSince) {
1683 return false;
1684 }
1685 if (componentName != null
1686 && !componentName.equals(si.getActivityComponent())) {
1687 return false;
1688 }
1689 final boolean matchDynamic =
1690 ((queryFlags & ShortcutQuery.FLAG_GET_DYNAMIC) != 0)
1691 && si.isDynamic();
1692 final boolean matchPinned =
1693 ((queryFlags & ShortcutQuery.FLAG_GET_PINNED) != 0)
1694 && si.isPinned();
1695 return matchDynamic || matchPinned;
1696 }, cloneFlag);
1697 }
1698
1699 @Override
1700 public List<ShortcutInfo> getShortcutInfo(
1701 @NonNull String callingPackage,
1702 @NonNull String packageName, @Nullable List<String> ids, int userId) {
1703 // Calling permission must be checked by LauncherAppsImpl.
1704 Preconditions.checkStringNotEmpty(packageName, "packageName");
1705
1706 final ArrayList<ShortcutInfo> ret = new ArrayList<>(ids.size());
1707 final ArraySet<String> idSet = new ArraySet<>(ids);
1708 synchronized (mLock) {
1709 getPackageShortcutsLocked(packageName, userId).findAll(ret,
1710 (ShortcutInfo si) -> idSet.contains(si.getId()),
1711 ShortcutInfo.CLONE_REMOVE_FOR_LAUNCHER);
1712 }
1713 return ret;
1714 }
1715
1716 @Override
1717 public void pinShortcuts(@NonNull String callingPackage, @NonNull String packageName,
1718 @NonNull List<String> shortcutIds, int userId) {
1719 // Calling permission must be checked by LauncherAppsImpl.
1720 Preconditions.checkStringNotEmpty(packageName, "packageName");
1721 Preconditions.checkNotNull(shortcutIds, "shortcutIds");
1722
1723 synchronized (mLock) {
Makoto Onuki55046222016-03-08 10:49:47 -08001724 getPackageShortcutsLocked(packageName, userId).replacePinned(
1725 ShortcutService.this, callingPackage, shortcutIds);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001726 }
1727 userPackageChanged(packageName, userId);
1728 }
1729
1730 @Override
1731 public Intent createShortcutIntent(@NonNull String callingPackage,
Makoto Onuki43204b82016-03-08 16:16:44 -08001732 @NonNull String packageName, @NonNull String shortcutId, int userId) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001733 // Calling permission must be checked by LauncherAppsImpl.
Makoto Onuki43204b82016-03-08 16:16:44 -08001734 Preconditions.checkStringNotEmpty(packageName, "packageName can't be empty");
1735 Preconditions.checkStringNotEmpty(shortcutId, "shortcutId can't be empty");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001736
1737 synchronized (mLock) {
1738 final ShortcutInfo fullShortcut =
Makoto Onuki43204b82016-03-08 16:16:44 -08001739 getPackageShortcutsLocked(packageName, userId)
Makoto Onuki55046222016-03-08 10:49:47 -08001740 .findShortcutById(shortcutId);
1741 return fullShortcut == null ? null : fullShortcut.getIntent();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001742 }
1743 }
1744
1745 @Override
1746 public void addListener(@NonNull ShortcutChangeListener listener) {
1747 synchronized (mLock) {
1748 mListeners.add(Preconditions.checkNotNull(listener));
1749 }
1750 }
Makoto Onuki55046222016-03-08 10:49:47 -08001751
1752 @Override
1753 public int getShortcutIconResId(@NonNull String callingPackage,
1754 @NonNull ShortcutInfo shortcut, int userId) {
1755 Preconditions.checkNotNull(shortcut, "shortcut");
1756
1757 synchronized (mLock) {
1758 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1759 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1760 return (shortcutInfo != null && shortcutInfo.hasIconResource())
1761 ? shortcutInfo.getIconResourceId() : 0;
1762 }
1763 }
1764
1765 @Override
1766 public ParcelFileDescriptor getShortcutIconFd(@NonNull String callingPackage,
1767 @NonNull ShortcutInfo shortcut, int userId) {
1768 Preconditions.checkNotNull(shortcut, "shortcut");
1769
1770 synchronized (mLock) {
1771 final ShortcutInfo shortcutInfo = getPackageShortcutsLocked(
1772 shortcut.getPackageName(), userId).findShortcutById(shortcut.getId());
1773 if (shortcutInfo == null || !shortcutInfo.hasIconFile()) {
1774 return null;
1775 }
1776 try {
1777 return ParcelFileDescriptor.open(
1778 new File(shortcutInfo.getBitmapPath()),
1779 ParcelFileDescriptor.MODE_READ_ONLY);
1780 } catch (FileNotFoundException e) {
1781 Slog.e(TAG, "Icon file not found: " + shortcutInfo.getBitmapPath());
1782 return null;
1783 }
1784 }
1785 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001786 }
1787
1788 // === Dump ===
1789
1790 @Override
1791 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1792 if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
1793 != PackageManager.PERMISSION_GRANTED) {
1794 pw.println("Permission Denial: can't dump UserManager from from pid="
1795 + Binder.getCallingPid()
1796 + ", uid=" + Binder.getCallingUid()
1797 + " without permission "
1798 + android.Manifest.permission.DUMP);
1799 return;
1800 }
1801 dumpInner(pw);
1802 }
1803
1804 @VisibleForTesting
1805 void dumpInner(PrintWriter pw) {
1806 synchronized (mLock) {
1807 final long now = injectCurrentTimeMillis();
1808 pw.print("Now: [");
1809 pw.print(now);
1810 pw.print("] ");
1811 pw.print(formatTime(now));
Makoto Onuki55046222016-03-08 10:49:47 -08001812
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001813 pw.print(" Raw last reset: [");
1814 pw.print(mRawLastResetTime);
1815 pw.print("] ");
1816 pw.print(formatTime(mRawLastResetTime));
1817
1818 final long last = getLastResetTimeLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001819 pw.print(" Last reset: [");
1820 pw.print(last);
1821 pw.print("] ");
1822 pw.print(formatTime(last));
1823
Makoto Onuki55046222016-03-08 10:49:47 -08001824 final long next = getNextResetTimeLocked();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001825 pw.print(" Next reset: [");
1826 pw.print(next);
1827 pw.print("] ");
1828 pw.print(formatTime(next));
1829 pw.println();
1830
Makoto Onuki55046222016-03-08 10:49:47 -08001831 pw.print(" Max icon dim: ");
1832 pw.print(mMaxIconDimension);
1833 pw.print(" Icon format: ");
1834 pw.print(mIconPersistFormat);
1835 pw.print(" Icon quality: ");
1836 pw.print(mIconPersistQuality);
1837 pw.println();
1838
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001839 pw.println();
1840
1841 for (int i = 0; i < mShortcuts.size(); i++) {
1842 dumpUserLocked(pw, mShortcuts.keyAt(i));
1843 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001844 }
1845 }
1846
1847 private void dumpUserLocked(PrintWriter pw, int userId) {
1848 pw.print(" User: ");
1849 pw.print(userId);
1850 pw.println();
1851
1852 final ArrayMap<String, PackageShortcuts> packages = mShortcuts.get(userId);
1853 if (packages == null) {
1854 return;
1855 }
1856 for (int j = 0; j < packages.size(); j++) {
1857 dumpPackageLocked(pw, userId, packages.keyAt(j));
1858 }
1859 pw.println();
1860 }
1861
1862 private void dumpPackageLocked(PrintWriter pw, int userId, String packageName) {
Makoto Onuki55046222016-03-08 10:49:47 -08001863 final PackageShortcuts packageShortcuts = mShortcuts.get(userId).get(packageName);
1864 if (packageShortcuts == null) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001865 return;
1866 }
1867
1868 pw.print(" Package: ");
1869 pw.print(packageName);
1870 pw.println();
1871
1872 pw.print(" Calls: ");
Makoto Onuki55046222016-03-08 10:49:47 -08001873 pw.print(packageShortcuts.getApiCallCount(this));
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001874 pw.println();
1875
1876 // This should be after getApiCallCount(), which may update it.
1877 pw.print(" Last reset: [");
Makoto Onuki55046222016-03-08 10:49:47 -08001878 pw.print(packageShortcuts.mLastResetTime);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001879 pw.print("] ");
Makoto Onuki55046222016-03-08 10:49:47 -08001880 pw.print(formatTime(packageShortcuts.mLastResetTime));
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001881 pw.println();
1882
1883 pw.println(" Shortcuts:");
Makoto Onuki55046222016-03-08 10:49:47 -08001884 long totalBitmapSize = 0;
1885 final ArrayMap<String, ShortcutInfo> shortcuts = packageShortcuts.mShortcuts;
1886 final int size = shortcuts.size();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001887 for (int i = 0; i < size; i++) {
Makoto Onuki55046222016-03-08 10:49:47 -08001888 final ShortcutInfo si = shortcuts.valueAt(i);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001889 pw.print(" ");
Makoto Onuki55046222016-03-08 10:49:47 -08001890 pw.println(si.toInsecureString());
1891 if (si.hasIconFile()) {
1892 final long len = new File(si.getBitmapPath()).length();
1893 pw.print(" ");
1894 pw.print("bitmap size=");
1895 pw.println(len);
1896
1897 totalBitmapSize += len;
1898 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001899 }
Makoto Onuki55046222016-03-08 10:49:47 -08001900 pw.print(" Total bitmap size: ");
1901 pw.print(totalBitmapSize);
1902 pw.print(" (");
1903 pw.print(Formatter.formatFileSize(mContext, totalBitmapSize));
1904 pw.println(")");
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001905 }
1906
1907 private static String formatTime(long time) {
1908 Time tobj = new Time();
1909 tobj.set(time);
1910 return tobj.format("%Y-%m-%d %H:%M:%S");
1911 }
1912
1913 // === Shell support ===
1914
1915 @Override
1916 public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
1917 String[] args, ResultReceiver resultReceiver) throws RemoteException {
1918
1919 enforceShell();
1920
1921 (new MyShellCommand()).exec(this, in, out, err, args, resultReceiver);
1922 }
1923
1924 /**
1925 * Handle "adb shell cmd".
1926 */
1927 private class MyShellCommand extends ShellCommand {
1928 @Override
1929 public int onCommand(String cmd) {
1930 if (cmd == null) {
1931 return handleDefaultCommands(cmd);
1932 }
1933 final PrintWriter pw = getOutPrintWriter();
Makoto Onuki4362a662016-03-08 18:59:09 -08001934 int ret = 1;
1935 switch (cmd) {
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001936 case "reset-package-throttling":
Makoto Onuki4362a662016-03-08 18:59:09 -08001937 ret = handleResetPackageThrottling();
1938 break;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001939 case "reset-throttling":
Makoto Onuki4362a662016-03-08 18:59:09 -08001940 ret = handleResetThrottling();
1941 break;
1942 case "override-config":
1943 ret = handleOverrideConfig();
1944 break;
1945 case "reset-config":
1946 ret = handleResetConfig();
1947 break;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001948 default:
1949 return handleDefaultCommands(cmd);
1950 }
Makoto Onuki4362a662016-03-08 18:59:09 -08001951 if (ret == 0) {
1952 pw.println("Success");
1953 }
1954 return ret;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001955 }
1956
1957 @Override
1958 public void onHelp() {
1959 final PrintWriter pw = getOutPrintWriter();
1960 pw.println("Usage: cmd shortcut COMMAND [options ...]");
1961 pw.println();
1962 pw.println("cmd shortcut reset-package-throttling [--user USER_ID] PACKAGE");
1963 pw.println(" Reset throttling for a package");
1964 pw.println();
1965 pw.println("cmd shortcut reset-throttling");
1966 pw.println(" Reset throttling for all packages and users");
1967 pw.println();
Makoto Onuki4362a662016-03-08 18:59:09 -08001968 pw.println("cmd shortcut override-config CONFIG");
1969 pw.println(" Override the configuration for testing (will last until reboot)");
1970 pw.println();
1971 pw.println("cmd shortcut reset-config");
1972 pw.println(" Reset the configuration set with \"update-config\"");
1973 pw.println();
Makoto Onuki6f7362d92016-03-04 13:39:41 -08001974 }
1975
1976 private int handleResetThrottling() {
1977 resetThrottling();
1978 return 0;
1979 }
1980
1981 private int handleResetPackageThrottling() {
1982 final PrintWriter pw = getOutPrintWriter();
1983
1984 int userId = UserHandle.USER_SYSTEM;
1985 String opt;
1986 while ((opt = getNextOption()) != null) {
1987 switch (opt) {
1988 case "--user":
1989 userId = UserHandle.parseUserArg(getNextArgRequired());
1990 break;
1991 default:
1992 pw.println("Error: Unknown option: " + opt);
1993 return 1;
1994 }
1995 }
1996 final String packageName = getNextArgRequired();
1997
1998 synchronized (mLock) {
1999 getPackageShortcutsLocked(packageName, userId).resetRateLimitingForCommandLine();
2000 saveUserLocked(userId);
2001 }
2002
2003 return 0;
2004 }
Makoto Onuki4362a662016-03-08 18:59:09 -08002005
2006 private int handleOverrideConfig() {
2007 final PrintWriter pw = getOutPrintWriter();
2008 final String config = getNextArgRequired();
2009
2010 synchronized (mLock) {
2011 if (!updateConfigurationLocked(config)) {
2012 pw.println("override-config failed. See logcat for details.");
2013 return 1;
2014 }
2015 }
2016 return 0;
2017 }
2018
2019 private int handleResetConfig() {
2020 synchronized (mLock) {
2021 loadConfigurationLocked();
2022 }
2023 return 0;
2024 }
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002025 }
2026
2027 // === Unit test support ===
2028
2029 // Injection point.
2030 long injectCurrentTimeMillis() {
2031 return System.currentTimeMillis();
2032 }
2033
2034 // Injection point.
2035 int injectBinderCallingUid() {
2036 return getCallingUid();
2037 }
2038
2039 File injectSystemDataPath() {
2040 return Environment.getDataSystemDirectory();
2041 }
2042
2043 File injectUserDataPath(@UserIdInt int userId) {
Makoto Onuki55046222016-03-08 10:49:47 -08002044 return new File(Environment.getDataSystemCeDirectory(userId), DIRECTORY_PER_USER);
2045 }
2046
Makoto Onuki4362a662016-03-08 18:59:09 -08002047 @VisibleForTesting
Makoto Onuki55046222016-03-08 10:49:47 -08002048 boolean injectIsLowRamDevice() {
2049 return ActivityManager.isLowRamDeviceStatic();
2050 }
2051
2052 File getUserBitmapFilePath(@UserIdInt int userId) {
2053 return new File(injectUserDataPath(userId), DIRECTORY_BITMAPS);
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002054 }
2055
2056 @VisibleForTesting
2057 SparseArray<ArrayMap<String, PackageShortcuts>> getShortcutsForTest() {
2058 return mShortcuts;
2059 }
2060
2061 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08002062 int getMaxDynamicShortcutsForTest() {
2063 return mMaxDynamicShortcuts;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002064 }
2065
2066 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08002067 int getMaxDailyUpdatesForTest() {
2068 return mMaxDailyUpdates;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002069 }
2070
2071 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08002072 long getResetIntervalForTest() {
2073 return mResetInterval;
Makoto Onuki55046222016-03-08 10:49:47 -08002074 }
2075
2076 @VisibleForTesting
Makoto Onuki4362a662016-03-08 18:59:09 -08002077 int getMaxIconDimensionForTest() {
2078 return mMaxIconDimension;
2079 }
2080
2081 @VisibleForTesting
2082 CompressFormat getIconPersistFormatForTest() {
2083 return mIconPersistFormat;
2084 }
2085
2086 @VisibleForTesting
2087 int getIconPersistQualityForTest() {
2088 return mIconPersistQuality;
Makoto Onuki6f7362d92016-03-04 13:39:41 -08002089 }
2090}