1 /*
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  */
16 package com.android.server.pm;
17 
18 import android.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.annotation.UserIdInt;
21 import android.content.pm.PackageInfo;
22 import android.content.pm.ShortcutInfo;
23 import android.content.pm.UserPackage;
24 import android.util.ArrayMap;
25 import android.util.ArraySet;
26 import android.util.Slog;
27 import android.util.Xml;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.modules.utils.TypedXmlPullParser;
31 import com.android.modules.utils.TypedXmlSerializer;
32 import com.android.server.pm.ShortcutService.DumpFilter;
33 
34 import org.json.JSONException;
35 import org.json.JSONObject;
36 import org.xmlpull.v1.XmlPullParser;
37 import org.xmlpull.v1.XmlPullParserException;
38 
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.IOException;
42 import java.io.PrintWriter;
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 /**
47  * Launcher information used by {@link ShortcutService}.
48  *
49  * All methods should be guarded by {@code ShortcutPackageItem#mPackageItemLock}.
50  */
51 class ShortcutLauncher extends ShortcutPackageItem {
52     private static final String TAG = ShortcutService.TAG;
53 
54     static final String TAG_ROOT = "launcher-pins";
55 
56     private static final String TAG_PACKAGE = "package";
57     private static final String TAG_PIN = "pin";
58 
59     private static final String ATTR_LAUNCHER_USER_ID = "launcher-user";
60     private static final String ATTR_VALUE = "value";
61     private static final String ATTR_PACKAGE_NAME = "package-name";
62     private static final String ATTR_PACKAGE_USER_ID = "package-user";
63 
64     private final int mOwnerUserId;
65 
66     /**
67      * Package name -> IDs.
68      */
69     @GuardedBy("mPackageItemLock")
70     private final ArrayMap<UserPackage, ArraySet<String>> mPinnedShortcuts = new ArrayMap<>();
71 
ShortcutLauncher(@onNull ShortcutUser shortcutUser, @UserIdInt int ownerUserId, @NonNull String packageName, @UserIdInt int launcherUserId, ShortcutPackageInfo spi)72     private ShortcutLauncher(@NonNull ShortcutUser shortcutUser,
73             @UserIdInt int ownerUserId, @NonNull String packageName,
74             @UserIdInt int launcherUserId, ShortcutPackageInfo spi) {
75         super(shortcutUser, launcherUserId, packageName,
76                 spi != null ? spi : ShortcutPackageInfo.newEmpty());
77         mOwnerUserId = ownerUserId;
78     }
79 
ShortcutLauncher(@onNull ShortcutUser shortcutUser, @UserIdInt int ownerUserId, @NonNull String packageName, @UserIdInt int launcherUserId)80     public ShortcutLauncher(@NonNull ShortcutUser shortcutUser,
81             @UserIdInt int ownerUserId, @NonNull String packageName,
82             @UserIdInt int launcherUserId) {
83         this(shortcutUser, ownerUserId, packageName, launcherUserId, null);
84     }
85 
86     @Override
getOwnerUserId()87     public int getOwnerUserId() {
88         return mOwnerUserId;
89     }
90 
91     @Override
canRestoreAnyVersion()92     protected boolean canRestoreAnyVersion() {
93         // Launcher's pinned shortcuts can be restored to an older version.
94         return true;
95     }
96 
97     /**
98      * Called when the new package can't receive the backup, due to signature or version mismatch.
99      */
onRestoreBlocked()100     private void onRestoreBlocked() {
101         final ArrayList<UserPackage> pinnedPackages;
102         synchronized (mPackageItemLock) {
103             pinnedPackages = new ArrayList<>(mPinnedShortcuts.keySet());
104             mPinnedShortcuts.clear();
105         }
106         for (int i = pinnedPackages.size() - 1; i >= 0; i--) {
107             final UserPackage up = pinnedPackages.get(i);
108             final ShortcutPackage p = mShortcutUser.getPackageShortcutsIfExists(up.packageName);
109             if (p != null) {
110                 p.refreshPinnedFlags();
111             }
112         }
113     }
114 
115     @Override
onRestored(int restoreBlockReason)116     protected void onRestored(int restoreBlockReason) {
117         // For launcher, possible reasons here are DISABLED_REASON_SIGNATURE_MISMATCH or
118         // DISABLED_REASON_BACKUP_NOT_SUPPORTED.
119         // DISABLED_REASON_VERSION_LOWER will NOT happen because we don't check version
120         // code for launchers.
121         if (restoreBlockReason != ShortcutInfo.DISABLED_REASON_NOT_DISABLED) {
122             onRestoreBlocked();
123         }
124     }
125 
126     /**
127      * Pin the given shortcuts, replacing the current pinned ones.
128      */
pinShortcuts(@serIdInt int packageUserId, @NonNull String packageName, @NonNull List<String> ids, boolean forPinRequest)129     public void pinShortcuts(@UserIdInt int packageUserId,
130             @NonNull String packageName, @NonNull List<String> ids, boolean forPinRequest) {
131         final ShortcutPackage packageShortcuts =
132                 mShortcutUser.getPackageShortcutsIfExists(packageName);
133         if (packageShortcuts == null) {
134             return; // No need to instantiate.
135         }
136 
137         final UserPackage up = UserPackage.of(packageUserId, packageName);
138 
139         final int idSize = ids.size();
140         if (idSize == 0) {
141             synchronized (mPackageItemLock) {
142                 mPinnedShortcuts.remove(up);
143             }
144         } else {
145             // Actually pin shortcuts.
146             // This logic here is to make sure a launcher cannot pin a shortcut that is not
147             // dynamic nor long-lived nor manifest but is pinned.
148             // In this case, technically the shortcut doesn't exist to this launcher, so it
149             // can't pin it.
150             // (Maybe unnecessarily strict...)
151             final ArraySet<String> floatingSet = new ArraySet<>();
152             final ArraySet<String> newSet = new ArraySet<>();
153 
154             for (int i = 0; i < idSize; i++) {
155                 final String id = ids.get(i);
156                 final ShortcutInfo si = packageShortcuts.findShortcutById(id);
157                 if (si == null) {
158                     continue;
159                 }
160                 if (si.isDynamic() || si.isLongLived()
161                         || si.isManifestShortcut()
162                         || forPinRequest) {
163                     newSet.add(id);
164                 } else {
165                     floatingSet.add(id);
166                 }
167             }
168             synchronized (mPackageItemLock) {
169                 final ArraySet<String> prevSet = mPinnedShortcuts.get(up);
170                 if (prevSet != null) {
171                     for (String id : floatingSet) {
172                         if (prevSet.contains(id)) {
173                             newSet.add(id);
174                         }
175                     }
176                 }
177                 mPinnedShortcuts.put(up, newSet);
178             }
179         }
180 
181         packageShortcuts.refreshPinnedFlags();
182     }
183 
184     /**
185      * Return the pinned shortcut IDs for the publisher package.
186      */
187     @Nullable
getPinnedShortcutIds(@onNull String packageName, @UserIdInt int packageUserId)188     public ArraySet<String> getPinnedShortcutIds(@NonNull String packageName,
189             @UserIdInt int packageUserId) {
190         synchronized (mPackageItemLock) {
191             final ArraySet<String> pinnedShortcuts = mPinnedShortcuts.get(
192                     UserPackage.of(packageUserId, packageName));
193             return pinnedShortcuts == null ? null : new ArraySet<>(pinnedShortcuts);
194         }
195     }
196 
197     /**
198      * Return true if the given shortcut is pinned by this launcher.<code></code>
199      */
hasPinned(ShortcutInfo shortcut)200     public boolean hasPinned(ShortcutInfo shortcut) {
201         synchronized (mPackageItemLock) {
202             final ArraySet<String> pinned = mPinnedShortcuts.get(
203                     UserPackage.of(shortcut.getUserId(), shortcut.getPackage()));
204             return (pinned != null) && pinned.contains(shortcut.getId());
205         }
206     }
207 
208     /**
209      * Additionally pin a shortcut. c.f. {@link #pinShortcuts(int, String, List, boolean)}
210      */
addPinnedShortcut(@onNull String packageName, @UserIdInt int packageUserId, String id, boolean forPinRequest)211     public void addPinnedShortcut(@NonNull String packageName, @UserIdInt int packageUserId,
212             String id, boolean forPinRequest) {
213         final ArrayList<String> pinnedList;
214         synchronized (mPackageItemLock) {
215             final ArraySet<String> pinnedSet = mPinnedShortcuts.get(
216                     UserPackage.of(packageUserId, packageName));
217             if (pinnedSet != null) {
218                 pinnedList = new ArrayList<>(pinnedSet.size() + 1);
219                 pinnedList.addAll(pinnedSet);
220             } else {
221                 pinnedList = new ArrayList<>(1);
222             }
223         }
224         pinnedList.add(id);
225 
226         pinShortcuts(packageUserId, packageName, pinnedList, forPinRequest);
227     }
228 
cleanUpPackage(String packageName, @UserIdInt int packageUserId)229     boolean cleanUpPackage(String packageName, @UserIdInt int packageUserId) {
230         synchronized (mPackageItemLock) {
231             return mPinnedShortcuts.remove(UserPackage.of(packageUserId, packageName)) != null;
232         }
233     }
234 
ensurePackageInfo()235     public void ensurePackageInfo() {
236         final PackageInfo pi = mShortcutUser.mService.getPackageInfoWithSignatures(
237                 getPackageName(), getPackageUserId());
238         if (pi == null) {
239             Slog.w(TAG, "Package not found: " + getPackageName());
240             return;
241         }
242         getPackageInfo().updateFromPackageInfo(pi);
243     }
244 
245     /**
246      * Persist.
247      */
248     @Override
saveToXml(TypedXmlSerializer out, boolean forBackup)249     public void saveToXml(TypedXmlSerializer out, boolean forBackup)
250             throws IOException {
251         if (forBackup && !getPackageInfo().isBackupAllowed()) {
252             // If an launcher app doesn't support backup&restore, then nothing to do.
253             return;
254         }
255         final ArrayMap<UserPackage, ArraySet<String>> pinnedShortcuts;
256         synchronized (mPackageItemLock) {
257             pinnedShortcuts = new ArrayMap<>(mPinnedShortcuts);
258         }
259         final int size = pinnedShortcuts.size();
260         if (size == 0) {
261             return; // Nothing to write.
262         }
263 
264         out.startTag(null, TAG_ROOT);
265         ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, getPackageName());
266         ShortcutService.writeAttr(out, ATTR_LAUNCHER_USER_ID, getPackageUserId());
267         getPackageInfo().saveToXml(mShortcutUser.mService, out, forBackup);
268 
269         for (int i = 0; i < size; i++) {
270             final UserPackage up = pinnedShortcuts.keyAt(i);
271 
272             if (forBackup && (up.userId != getOwnerUserId())) {
273                 continue; // Target package on a different user, skip. (i.e. work profile)
274             }
275 
276             out.startTag(null, TAG_PACKAGE);
277             ShortcutService.writeAttr(out, ATTR_PACKAGE_NAME, up.packageName);
278             ShortcutService.writeAttr(out, ATTR_PACKAGE_USER_ID, up.userId);
279 
280             final ArraySet<String> ids = pinnedShortcuts.valueAt(i);
281             final int idSize = ids.size();
282             for (int j = 0; j < idSize; j++) {
283                 ShortcutService.writeTagValue(out, TAG_PIN, ids.valueAt(j));
284             }
285             out.endTag(null, TAG_PACKAGE);
286         }
287 
288         out.endTag(null, TAG_ROOT);
289     }
290 
loadFromFile(File path, ShortcutUser shortcutUser, int ownerUserId, boolean fromBackup)291     public static ShortcutLauncher loadFromFile(File path, ShortcutUser shortcutUser,
292             int ownerUserId, boolean fromBackup) {
293         try (ResilientAtomicFile file = getResilientFile(path)) {
294             FileInputStream in = null;
295             try {
296                 in = file.openRead();
297                 if (in == null) {
298                     Slog.d(TAG, "Not found " + path);
299                     return null;
300                 }
301 
302                 ShortcutLauncher ret = null;
303                 TypedXmlPullParser parser = Xml.resolvePullParser(in);
304 
305                 int type;
306                 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
307                     if (type != XmlPullParser.START_TAG) {
308                         continue;
309                     }
310                     final int depth = parser.getDepth();
311 
312                     final String tag = parser.getName();
313                     if (ShortcutService.DEBUG_LOAD) {
314                         Slog.d(TAG, String.format("depth=%d type=%d name=%s", depth, type, tag));
315                     }
316                     if ((depth == 1) && TAG_ROOT.equals(tag)) {
317                         ret = loadFromXml(parser, shortcutUser, ownerUserId, fromBackup);
318                         continue;
319                     }
320                     ShortcutService.throwForInvalidTag(depth, tag);
321                 }
322                 return ret;
323             } catch (Exception e) {
324                 Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
325                 file.failRead(in, e);
326                 return loadFromFile(path, shortcutUser, ownerUserId, fromBackup);
327             }
328         }
329     }
330 
331     /**
332      * Load.
333      */
loadFromXml(TypedXmlPullParser parser, ShortcutUser shortcutUser, int ownerUserId, boolean fromBackup)334     public static ShortcutLauncher loadFromXml(TypedXmlPullParser parser, ShortcutUser shortcutUser,
335             int ownerUserId, boolean fromBackup) throws IOException, XmlPullParserException {
336         final String launcherPackageName = ShortcutService.parseStringAttribute(parser,
337                 ATTR_PACKAGE_NAME);
338 
339         // If restoring, just use the real user ID.
340         final int launcherUserId =
341                 fromBackup ? ownerUserId
342                 : ShortcutService.parseIntAttribute(parser, ATTR_LAUNCHER_USER_ID, ownerUserId);
343 
344         final ShortcutLauncher ret = new ShortcutLauncher(shortcutUser, ownerUserId,
345                 launcherPackageName, launcherUserId);
346 
347         ArraySet<String> ids = null;
348         final int outerDepth = parser.getDepth();
349         int type;
350         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
351                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
352             if (type != XmlPullParser.START_TAG) {
353                 continue;
354             }
355             final int depth = parser.getDepth();
356             final String tag = parser.getName();
357             if (depth == outerDepth + 1) {
358                 switch (tag) {
359                     case ShortcutPackageInfo.TAG_ROOT:
360                         ret.getPackageInfo().loadFromXml(parser, fromBackup);
361                         continue;
362                     case TAG_PACKAGE: {
363                         final String packageName = ShortcutService.parseStringAttribute(parser,
364                                 ATTR_PACKAGE_NAME);
365                         final int packageUserId = fromBackup ? ownerUserId
366                                 : ShortcutService.parseIntAttribute(parser,
367                                 ATTR_PACKAGE_USER_ID, ownerUserId);
368                         ids = new ArraySet<>();
369                         synchronized (ret.mPackageItemLock) {
370                             ret.mPinnedShortcuts.put(
371                                     UserPackage.of(packageUserId, packageName), ids);
372                         }
373                         continue;
374                     }
375                 }
376             }
377             if (depth == outerDepth + 2) {
378                 switch (tag) {
379                     case TAG_PIN: {
380                         if (ids == null) {
381                             Slog.w(TAG, TAG_PIN + " in invalid place");
382                         } else {
383                             ids.add(ShortcutService.parseStringAttribute(parser, ATTR_VALUE));
384                         }
385                         continue;
386                     }
387                 }
388             }
389             ShortcutService.warnForInvalidTag(depth, tag);
390         }
391         return ret;
392     }
393 
dump(@onNull PrintWriter pw, @NonNull String prefix, DumpFilter filter)394     public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) {
395         pw.println();
396 
397         pw.print(prefix);
398         pw.print("Launcher: ");
399         pw.print(getPackageName());
400         pw.print("  Package user: ");
401         pw.print(getPackageUserId());
402         pw.print("  Owner user: ");
403         pw.print(getOwnerUserId());
404         pw.println();
405 
406         getPackageInfo().dump(pw, prefix + "  ");
407         pw.println();
408 
409         final ArrayMap<UserPackage, ArraySet<String>> pinnedShortcuts;
410         synchronized (mPackageItemLock) {
411             pinnedShortcuts = new ArrayMap<>(mPinnedShortcuts);
412         }
413         final int size = pinnedShortcuts.size();
414         for (int i = 0; i < size; i++) {
415             pw.println();
416 
417             final UserPackage up = pinnedShortcuts.keyAt(i);
418 
419             pw.print(prefix);
420             pw.print("  ");
421             pw.print("Package: ");
422             pw.print(up.packageName);
423             pw.print("  User: ");
424             pw.println(up.userId);
425 
426             final ArraySet<String> ids = pinnedShortcuts.valueAt(i);
427             final int idSize = ids.size();
428 
429             for (int j = 0; j < idSize; j++) {
430                 pw.print(prefix);
431                 pw.print("    Pinned: ");
432                 pw.print(ids.valueAt(j));
433                 pw.println();
434             }
435         }
436     }
437 
438     @Override
dumpCheckin(boolean clear)439     public JSONObject dumpCheckin(boolean clear) throws JSONException {
440         final JSONObject result = super.dumpCheckin(clear);
441 
442         // Nothing really interesting to dump.
443 
444         return result;
445     }
446 
447     @Override
getShortcutPackageItemFile()448     protected File getShortcutPackageItemFile() {
449         final File path = new File(mShortcutUser.mService.injectUserDataPath(
450                 mShortcutUser.getUserId()), ShortcutUser.DIRECTORY_LUANCHERS);
451         // Package user id and owner id can have different values for ShortcutLaunchers. Adding
452         // user Id to the file name to create a unique path. Owner id is used in the root path.
453         final String fileName = getPackageName() + getPackageUserId() + ".xml";
454         return new File(path, fileName);
455     }
456 }
457