InstantAppRegistry.java revision be9ffa15af9e1906e9ffb505768328d62d4a3793
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.pm;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.UserIdInt;
22import android.content.Intent;
23import android.content.pm.InstantAppInfo;
24import android.content.pm.PackageParser;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.graphics.Canvas;
28import android.graphics.drawable.BitmapDrawable;
29import android.graphics.drawable.Drawable;
30import android.os.Binder;
31import android.os.Environment;
32import android.os.Handler;
33import android.os.Looper;
34import android.os.Message;
35import android.provider.Settings;
36import android.util.ArrayMap;
37import android.util.AtomicFile;
38import android.util.PackageUtils;
39import android.util.Slog;
40import android.util.SparseArray;
41import android.util.SparseBooleanArray;
42import android.util.Xml;
43import com.android.internal.annotations.GuardedBy;
44import com.android.internal.os.BackgroundThread;
45import com.android.internal.util.ArrayUtils;
46import com.android.internal.util.XmlUtils;
47import libcore.io.IoUtils;
48import org.xmlpull.v1.XmlPullParser;
49import org.xmlpull.v1.XmlPullParserException;
50import org.xmlpull.v1.XmlSerializer;
51
52import java.io.File;
53import java.io.FileInputStream;
54import java.io.FileNotFoundException;
55import java.io.FileOutputStream;
56import java.io.IOException;
57import java.nio.charset.StandardCharsets;
58import java.util.ArrayList;
59import java.util.List;
60import java.util.Set;
61import java.util.function.Predicate;
62
63/**
64 * This class is a part of the package manager service that is responsible
65 * for managing data associated with instant apps such as cached uninstalled
66 * instant apps and instant apps' cookies. In addition it is responsible for
67 * pruning installed instant apps and meta-data for uninstalled instant apps
68 * when free space is needed.
69 */
70class InstantAppRegistry {
71    private static final boolean DEBUG = false;
72
73    private static final String LOG_TAG = "InstantAppRegistry";
74
75    private static final long DEFAULT_UNINSTALLED_INSTANT_APP_CACHE_DURATION_MILLIS =
76            DEBUG ? 60 * 1000L /* one min */ : 6 * 30 * 24 * 60 * 60 * 1000L; /* six months */
77
78    private static final String INSTANT_APPS_FOLDER = "instant";
79    private static final String INSTANT_APP_ICON_FILE = "icon.png";
80    private static final String INSTANT_APP_COOKIE_FILE_PREFIX = "cookie_";
81    private static final String INSTANT_APP_COOKIE_FILE_SIFFIX = ".dat";
82    private static final String INSTANT_APP_METADATA_FILE = "metadata.xml";
83
84    private static final String TAG_PACKAGE = "package";
85    private static final String TAG_PERMISSIONS = "permissions";
86    private static final String TAG_PERMISSION = "permission";
87
88    private static final String ATTR_LABEL = "label";
89    private static final String ATTR_NAME = "name";
90    private static final String ATTR_GRANTED = "granted";
91
92    private final PackageManagerService mService;
93    private final CookiePersistence mCookiePersistence;
94
95    /** State for uninstalled instant apps */
96    @GuardedBy("mService.mPackages")
97    private SparseArray<List<UninstalledInstantAppState>> mUninstalledInstantApps;
98
99    /**
100     * Automatic grants for access to instant app metadata.
101     * The key is the target application UID.
102     * The value is a set of instant app UIDs.
103     * UserID -> TargetAppId -> InstantAppId
104     */
105    @GuardedBy("mService.mPackages")
106    private SparseArray<SparseArray<SparseBooleanArray>> mInstantGrants;
107
108    /** The set of all installed instant apps. UserID -> AppID */
109    @GuardedBy("mService.mPackages")
110    private SparseArray<SparseBooleanArray> mInstalledInstantAppUids;
111
112    public InstantAppRegistry(PackageManagerService service) {
113        mService = service;
114        mCookiePersistence = new CookiePersistence(BackgroundThread.getHandler().getLooper());
115    }
116
117    public byte[] getInstantAppCookieLPw(@NonNull String packageName,
118                                         @UserIdInt int userId) {
119        byte[] pendingCookie = mCookiePersistence.getPendingPersistCookie(userId, packageName);
120        if (pendingCookie != null) {
121            return pendingCookie;
122        }
123        File cookieFile = peekInstantCookieFile(packageName, userId);
124        if (cookieFile != null && cookieFile.exists()) {
125            try {
126                return IoUtils.readFileAsByteArray(cookieFile.toString());
127            } catch (IOException e) {
128                Slog.w(LOG_TAG, "Error reading cookie file: " + cookieFile);
129            }
130        }
131        return null;
132    }
133
134    public boolean setInstantAppCookieLPw(@NonNull String packageName,
135                                          @Nullable byte[] cookie, @UserIdInt int userId) {
136        if (cookie != null && cookie.length > 0) {
137            final int maxCookieSize = mService.mContext.getPackageManager()
138                    .getInstantAppCookieMaxSize();
139            if (cookie.length > maxCookieSize) {
140                Slog.e(LOG_TAG, "Instant app cookie for package " + packageName + " size "
141                        + cookie.length + " bytes while max size is " + maxCookieSize);
142                return false;
143            }
144        }
145
146        mCookiePersistence.schedulePersist(userId, packageName, cookie);
147        return true;
148    }
149
150    private void persistInstantApplicationCookie(@Nullable byte[] cookie,
151            @NonNull String packageName, @UserIdInt int userId) {
152        synchronized (mService.mPackages) {
153            PackageParser.Package pkg = mService.mPackages.get(packageName);
154            if (pkg == null) {
155                return;
156            }
157
158            File appDir = getInstantApplicationDir(packageName, userId);
159            if (!appDir.exists() && !appDir.mkdirs()) {
160                Slog.e(LOG_TAG, "Cannot create instant app cookie directory");
161                return;
162            }
163
164            File cookieFile = computeInstantCookieFile(pkg, userId);
165            if (cookieFile.exists() && !cookieFile.delete()) {
166                Slog.e(LOG_TAG, "Cannot delete instant app cookie file");
167            }
168
169            // No cookie or an empty one means delete - done
170            if (cookie == null || cookie.length <= 0) {
171                return;
172            }
173
174            try (FileOutputStream fos = new FileOutputStream(cookieFile)) {
175                fos.write(cookie, 0, cookie.length);
176            } catch (IOException e) {
177                Slog.e(LOG_TAG, "Error writing instant app cookie file: " + cookieFile, e);
178            }
179        }
180    }
181
182    public Bitmap getInstantAppIconLPw(@NonNull String packageName,
183                                       @UserIdInt int userId) {
184        File iconFile = new File(getInstantApplicationDir(packageName, userId),
185                INSTANT_APP_ICON_FILE);
186        if (iconFile.exists()) {
187            return BitmapFactory.decodeFile(iconFile.toString());
188        }
189        return null;
190    }
191
192    public @Nullable List<InstantAppInfo> getInstantAppsLPr(@UserIdInt int userId) {
193        List<InstantAppInfo> installedApps = getInstalledInstantApplicationsLPr(userId);
194        List<InstantAppInfo> uninstalledApps = getUninstalledInstantApplicationsLPr(userId);
195        if (installedApps != null) {
196            if (uninstalledApps != null) {
197                installedApps.addAll(uninstalledApps);
198            }
199            return installedApps;
200        }
201        return uninstalledApps;
202    }
203
204    public void onPackageInstalledLPw(@NonNull PackageParser.Package pkg, @NonNull int[] userIds) {
205        PackageSetting ps = (PackageSetting) pkg.mExtras;
206        if (ps == null) {
207            return;
208        }
209
210        for (int userId : userIds) {
211            // Ignore not installed apps
212            if (mService.mPackages.get(pkg.packageName) == null || !ps.getInstalled(userId)) {
213                continue;
214            }
215
216            // Propagate permissions before removing any state
217            propagateInstantAppPermissionsIfNeeded(pkg.packageName, userId);
218
219            // Track instant apps
220            if (pkg.applicationInfo.isInstantApp()) {
221                addInstantAppLPw(userId, ps.appId);
222            }
223
224            // Remove the in-memory state
225            removeUninstalledInstantAppStateLPw((UninstalledInstantAppState state) ->
226                            state.mInstantAppInfo.getPackageName().equals(pkg.packageName),
227                    userId);
228
229            // Remove the on-disk state except the cookie
230            File instantAppDir = getInstantApplicationDir(pkg.packageName, userId);
231            new File(instantAppDir, INSTANT_APP_METADATA_FILE).delete();
232            new File(instantAppDir, INSTANT_APP_ICON_FILE).delete();
233
234            // If app signature changed - wipe the cookie
235            File currentCookieFile = peekInstantCookieFile(pkg.packageName, userId);
236            if (currentCookieFile == null) {
237                continue;
238            }
239            File expectedCookeFile = computeInstantCookieFile(pkg, userId);
240            if (!currentCookieFile.equals(expectedCookeFile)) {
241                Slog.i(LOG_TAG, "Signature for package " + pkg.packageName
242                        + " changed - dropping cookie");
243                currentCookieFile.delete();
244            }
245        }
246    }
247
248    public void onPackageUninstalledLPw(@NonNull PackageParser.Package pkg,
249            @NonNull int[] userIds) {
250        PackageSetting ps = (PackageSetting) pkg.mExtras;
251        if (ps == null) {
252            return;
253        }
254
255        for (int userId : userIds) {
256            if (mService.mPackages.get(pkg.packageName) != null && ps.getInstalled(userId)) {
257                continue;
258            }
259
260            if (pkg.applicationInfo.isInstantApp()) {
261                // Add a record for an uninstalled instant app
262                addUninstalledInstantAppLPw(pkg, userId);
263                removeInstantAppLPw(userId, ps.appId);
264            } else {
265                // Deleting an app prunes all instant state such as cookie
266                deleteDir(getInstantApplicationDir(pkg.packageName, userId));
267                removeAppLPw(userId, ps.appId);
268            }
269        }
270    }
271
272    public void onUserRemovedLPw(int userId) {
273        if (mUninstalledInstantApps != null) {
274            mUninstalledInstantApps.remove(userId);
275            if (mUninstalledInstantApps.size() <= 0) {
276                mUninstalledInstantApps = null;
277            }
278        }
279        if (mInstalledInstantAppUids != null) {
280            mInstalledInstantAppUids.remove(userId);
281            if (mInstalledInstantAppUids.size() <= 0) {
282                mInstalledInstantAppUids = null;
283            }
284        }
285        if (mInstantGrants != null) {
286            mInstantGrants.remove(userId);
287            if (mInstantGrants.size() <= 0) {
288                mInstantGrants = null;
289            }
290        }
291        deleteDir(getInstantApplicationsDir(userId));
292    }
293
294    public boolean isInstantAccessGranted(@UserIdInt int userId, int targetAppId,
295            int instantAppId) {
296        if (mInstantGrants == null) {
297            return false;
298        }
299        final SparseArray<SparseBooleanArray> targetAppList = mInstantGrants.get(userId);
300        if (targetAppList == null) {
301            return false;
302        }
303        final SparseBooleanArray instantGrantList = targetAppList.get(targetAppId);
304        if (instantGrantList == null) {
305            return false;
306        }
307        return instantGrantList.get(instantAppId);
308    }
309
310    public void grantInstantAccessLPw(@UserIdInt int userId, @Nullable Intent intent,
311            int targetAppId, int instantAppId) {
312        if (mInstalledInstantAppUids == null) {
313            return;     // no instant apps installed; no need to grant
314        }
315        SparseBooleanArray instantAppList = mInstalledInstantAppUids.get(userId);
316        if (instantAppList == null || !instantAppList.get(instantAppId)) {
317            return;     // instant app id isn't installed; no need to grant
318        }
319        if (instantAppList.get(targetAppId)) {
320            return;     // target app id is an instant app; no need to grant
321        }
322        if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) {
323            final Set<String> categories = intent.getCategories();
324            if (categories != null && categories.contains(Intent.CATEGORY_BROWSABLE)) {
325                return;  // launched via VIEW/BROWSABLE intent; no need to grant
326            }
327        }
328        if (mInstantGrants == null) {
329            mInstantGrants = new SparseArray<>();
330        }
331        SparseArray<SparseBooleanArray> targetAppList = mInstantGrants.get(userId);
332        if (targetAppList == null) {
333            targetAppList = new SparseArray<>();
334            mInstantGrants.put(userId, targetAppList);
335        }
336        SparseBooleanArray instantGrantList = targetAppList.get(targetAppId);
337        if (instantGrantList == null) {
338            instantGrantList = new SparseBooleanArray();
339            targetAppList.put(targetAppId, instantGrantList);
340        }
341        instantGrantList.put(instantAppId, true /*granted*/);
342    }
343
344    public void addInstantAppLPw(@UserIdInt int userId, int instantAppId) {
345        if (mInstalledInstantAppUids == null) {
346            mInstalledInstantAppUids = new SparseArray<>();
347        }
348        SparseBooleanArray instantAppList = mInstalledInstantAppUids.get(userId);
349        if (instantAppList == null) {
350            instantAppList = new SparseBooleanArray();
351            mInstalledInstantAppUids.put(userId, instantAppList);
352        }
353        instantAppList.put(instantAppId, true /*installed*/);
354    }
355
356    private void removeInstantAppLPw(@UserIdInt int userId, int instantAppId) {
357        // remove from the installed list
358        if (mInstalledInstantAppUids == null) {
359            return; // no instant apps on the system
360        }
361        final SparseBooleanArray instantAppList = mInstalledInstantAppUids.get(userId);
362        if (instantAppList == null) {
363            return;
364        }
365
366        instantAppList.delete(instantAppId);
367
368        // remove any grants
369        if (mInstantGrants == null) {
370            return; // no grants on the system
371        }
372        final SparseArray<SparseBooleanArray> targetAppList = mInstantGrants.get(userId);
373        if (targetAppList == null) {
374            return; // no grants for this user
375        }
376        for (int i = targetAppList.size() - 1; i >= 0; --i) {
377            targetAppList.valueAt(i).delete(instantAppId);
378        }
379    }
380
381    private void removeAppLPw(@UserIdInt int userId, int targetAppId) {
382        // remove from the installed list
383        if (mInstantGrants == null) {
384            return; // no grants on the system
385        }
386        final SparseArray<SparseBooleanArray> targetAppList = mInstantGrants.get(userId);
387        if (targetAppList == null) {
388            return; // no grants for this user
389        }
390        targetAppList.delete(targetAppId);
391    }
392
393    private void addUninstalledInstantAppLPw(@NonNull PackageParser.Package pkg,
394            @UserIdInt int userId) {
395        InstantAppInfo uninstalledApp = createInstantAppInfoForPackage(
396                pkg, userId, false);
397        if (uninstalledApp == null) {
398            return;
399        }
400        if (mUninstalledInstantApps == null) {
401            mUninstalledInstantApps = new SparseArray<>();
402        }
403        List<UninstalledInstantAppState> uninstalledAppStates =
404                mUninstalledInstantApps.get(userId);
405        if (uninstalledAppStates == null) {
406            uninstalledAppStates = new ArrayList<>();
407            mUninstalledInstantApps.put(userId, uninstalledAppStates);
408        }
409        UninstalledInstantAppState uninstalledAppState = new UninstalledInstantAppState(
410                uninstalledApp, System.currentTimeMillis());
411        uninstalledAppStates.add(uninstalledAppState);
412
413        writeUninstalledInstantAppMetadata(uninstalledApp, userId);
414        writeInstantApplicationIconLPw(pkg, userId);
415    }
416
417    private void writeInstantApplicationIconLPw(@NonNull PackageParser.Package pkg,
418            @UserIdInt int userId) {
419        File appDir = getInstantApplicationDir(pkg.packageName, userId);
420        if (!appDir.exists()) {
421            return;
422        }
423
424        Drawable icon = pkg.applicationInfo.loadIcon(mService.mContext.getPackageManager());
425
426        final Bitmap bitmap;
427        if (icon instanceof BitmapDrawable) {
428            bitmap = ((BitmapDrawable) icon).getBitmap();
429        } else  {
430            bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(),
431                    icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
432            Canvas canvas = new Canvas(bitmap);
433            icon.draw(canvas);
434        }
435
436        File iconFile = new File(getInstantApplicationDir(pkg.packageName, userId),
437                INSTANT_APP_ICON_FILE);
438
439        try (FileOutputStream out = new FileOutputStream(iconFile)) {
440            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
441        } catch (Exception e) {
442            Slog.e(LOG_TAG, "Error writing instant app icon", e);
443        }
444    }
445
446    public void deleteInstantApplicationMetadataLPw(@NonNull String packageName,
447            @UserIdInt int userId) {
448        removeUninstalledInstantAppStateLPw((UninstalledInstantAppState state) ->
449                state.mInstantAppInfo.getPackageName().equals(packageName),
450                userId);
451
452        File instantAppDir = getInstantApplicationDir(packageName, userId);
453        new File(instantAppDir, INSTANT_APP_METADATA_FILE).delete();
454        new File(instantAppDir, INSTANT_APP_ICON_FILE).delete();
455        File cookie = peekInstantCookieFile(packageName, userId);
456        if (cookie != null) {
457            cookie.delete();
458        }
459    }
460
461    private void removeUninstalledInstantAppStateLPw(
462            @NonNull Predicate<UninstalledInstantAppState> criteria, @UserIdInt int userId) {
463        if (mUninstalledInstantApps == null) {
464            return;
465        }
466        List<UninstalledInstantAppState> uninstalledAppStates =
467                mUninstalledInstantApps.get(userId);
468        if (uninstalledAppStates == null) {
469            return;
470        }
471        final int appCount = uninstalledAppStates.size();
472        for (int i = appCount - 1; i >= 0; --i) {
473            UninstalledInstantAppState uninstalledAppState = uninstalledAppStates.get(i);
474            if (!criteria.test(uninstalledAppState)) {
475                continue;
476            }
477            uninstalledAppStates.remove(i);
478            if (uninstalledAppStates.isEmpty()) {
479                mUninstalledInstantApps.remove(userId);
480                if (mUninstalledInstantApps.size() <= 0) {
481                    mUninstalledInstantApps = null;
482                }
483                return;
484            }
485        }
486    }
487
488    public void pruneInstantAppsLPw() {
489        // For now we prune only state for uninstalled instant apps
490        final long maxCacheDurationMillis = Settings.Global.getLong(
491                mService.mContext.getContentResolver(),
492                Settings.Global.UNINSTALLED_INSTANT_APP_CACHE_DURATION_MILLIS,
493                DEFAULT_UNINSTALLED_INSTANT_APP_CACHE_DURATION_MILLIS);
494
495        for (int userId : UserManagerService.getInstance().getUserIds()) {
496            // Prune in-memory state
497            removeUninstalledInstantAppStateLPw((UninstalledInstantAppState state) -> {
498                final long elapsedCachingMillis = System.currentTimeMillis() - state.mTimestamp;
499                return (elapsedCachingMillis > maxCacheDurationMillis);
500            }, userId);
501
502            // Prune on-disk state
503            File instantAppsDir = getInstantApplicationsDir(userId);
504            if (!instantAppsDir.exists()) {
505                continue;
506            }
507            File[] files = instantAppsDir.listFiles();
508            if (files == null) {
509                continue;
510            }
511            for (File instantDir : files) {
512                if (!instantDir.isDirectory()) {
513                    continue;
514                }
515
516                File metadataFile = new File(instantDir, INSTANT_APP_METADATA_FILE);
517                if (!metadataFile.exists()) {
518                    continue;
519                }
520
521                final long elapsedCachingMillis = System.currentTimeMillis()
522                        - metadataFile.lastModified();
523                if (elapsedCachingMillis > maxCacheDurationMillis) {
524                    deleteDir(instantDir);
525                }
526            }
527        }
528    }
529
530    private @Nullable List<InstantAppInfo> getInstalledInstantApplicationsLPr(
531            @UserIdInt int userId) {
532        List<InstantAppInfo> result = null;
533
534        final int packageCount = mService.mPackages.size();
535        for (int i = 0; i < packageCount; i++) {
536            PackageParser.Package pkg = mService.mPackages.valueAt(i);
537            if (!pkg.applicationInfo.isInstantApp()) {
538                continue;
539            }
540            InstantAppInfo info = createInstantAppInfoForPackage(
541                    pkg, userId, true);
542            if (info == null) {
543                continue;
544            }
545            if (result == null) {
546                result = new ArrayList<>();
547            }
548            result.add(info);
549        }
550
551        return result;
552    }
553
554    private @NonNull
555    InstantAppInfo createInstantAppInfoForPackage(
556            @NonNull PackageParser.Package pkg, @UserIdInt int userId,
557            boolean addApplicationInfo) {
558        PackageSetting ps = (PackageSetting) pkg.mExtras;
559        if (ps == null) {
560            return null;
561        }
562        if (!ps.getInstalled(userId)) {
563            return null;
564        }
565
566        String[] requestedPermissions = new String[pkg.requestedPermissions.size()];
567        pkg.requestedPermissions.toArray(requestedPermissions);
568
569        Set<String> permissions = ps.getPermissionsState().getPermissions(userId);
570        String[] grantedPermissions = new String[permissions.size()];
571        permissions.toArray(grantedPermissions);
572
573        if (addApplicationInfo) {
574            return new InstantAppInfo(pkg.applicationInfo,
575                    requestedPermissions, grantedPermissions);
576        } else {
577            return new InstantAppInfo(pkg.applicationInfo.packageName,
578                    pkg.applicationInfo.loadLabel(mService.mContext.getPackageManager()),
579                    requestedPermissions, grantedPermissions);
580        }
581    }
582
583    private @Nullable List<InstantAppInfo> getUninstalledInstantApplicationsLPr(
584            @UserIdInt int userId) {
585        List<UninstalledInstantAppState> uninstalledAppStates =
586                getUninstalledInstantAppStatesLPr(userId);
587        if (uninstalledAppStates == null || uninstalledAppStates.isEmpty()) {
588            return null;
589        }
590
591        List<InstantAppInfo> uninstalledApps = null;
592        final int stateCount = uninstalledAppStates.size();
593        for (int i = 0; i < stateCount; i++) {
594            UninstalledInstantAppState uninstalledAppState = uninstalledAppStates.get(i);
595            if (uninstalledApps == null) {
596                uninstalledApps = new ArrayList<>();
597            }
598            uninstalledApps.add(uninstalledAppState.mInstantAppInfo);
599        }
600        return uninstalledApps;
601    }
602
603    private void propagateInstantAppPermissionsIfNeeded(@NonNull String packageName,
604            @UserIdInt int userId) {
605        InstantAppInfo appInfo = peekOrParseUninstalledInstantAppInfo(
606                packageName, userId);
607        if (appInfo == null) {
608            return;
609        }
610        if (ArrayUtils.isEmpty(appInfo.getGrantedPermissions())) {
611            return;
612        }
613        final long identity = Binder.clearCallingIdentity();
614        try {
615            for (String grantedPermission : appInfo.getGrantedPermissions()) {
616                BasePermission bp = mService.mSettings.mPermissions.get(grantedPermission);
617                if (bp != null && (bp.isRuntime() || bp.isDevelopment()) && bp.isInstant()) {
618                    mService.grantRuntimePermission(packageName, grantedPermission, userId);
619                }
620            }
621        } finally {
622            Binder.restoreCallingIdentity(identity);
623        }
624    }
625
626    private @NonNull
627    InstantAppInfo peekOrParseUninstalledInstantAppInfo(
628            @NonNull String packageName, @UserIdInt int userId) {
629        if (mUninstalledInstantApps != null) {
630            List<UninstalledInstantAppState> uninstalledAppStates =
631                    mUninstalledInstantApps.get(userId);
632            if (uninstalledAppStates != null) {
633                final int appCount = uninstalledAppStates.size();
634                for (int i = 0; i < appCount; i++) {
635                    UninstalledInstantAppState uninstalledAppState = uninstalledAppStates.get(i);
636                    if (uninstalledAppState.mInstantAppInfo
637                            .getPackageName().equals(packageName)) {
638                        return uninstalledAppState.mInstantAppInfo;
639                    }
640                }
641            }
642        }
643
644        File metadataFile = new File(getInstantApplicationDir(packageName, userId),
645                INSTANT_APP_METADATA_FILE);
646        UninstalledInstantAppState uninstalledAppState = parseMetadataFile(metadataFile);
647        if (uninstalledAppState == null) {
648            return null;
649        }
650
651        return uninstalledAppState.mInstantAppInfo;
652    }
653
654    private @Nullable List<UninstalledInstantAppState> getUninstalledInstantAppStatesLPr(
655            @UserIdInt int userId) {
656        List<UninstalledInstantAppState> uninstalledAppStates = null;
657        if (mUninstalledInstantApps != null) {
658            uninstalledAppStates = mUninstalledInstantApps.get(userId);
659            if (uninstalledAppStates != null) {
660                return uninstalledAppStates;
661            }
662        }
663
664        File instantAppsDir = getInstantApplicationsDir(userId);
665        if (instantAppsDir.exists()) {
666            File[] files = instantAppsDir.listFiles();
667            if (files != null) {
668                for (File instantDir : files) {
669                    if (!instantDir.isDirectory()) {
670                        continue;
671                    }
672                    File metadataFile = new File(instantDir,
673                            INSTANT_APP_METADATA_FILE);
674                    UninstalledInstantAppState uninstalledAppState =
675                            parseMetadataFile(metadataFile);
676                    if (uninstalledAppState == null) {
677                        continue;
678                    }
679                    if (uninstalledAppStates == null) {
680                        uninstalledAppStates = new ArrayList<>();
681                    }
682                    uninstalledAppStates.add(uninstalledAppState);
683                }
684            }
685        }
686
687        if (uninstalledAppStates != null) {
688            if (mUninstalledInstantApps == null) {
689                mUninstalledInstantApps = new SparseArray<>();
690            }
691            mUninstalledInstantApps.put(userId, uninstalledAppStates);
692        }
693
694        return uninstalledAppStates;
695    }
696
697    private static @Nullable UninstalledInstantAppState parseMetadataFile(
698            @NonNull File metadataFile) {
699        if (!metadataFile.exists()) {
700            return null;
701        }
702        FileInputStream in;
703        try {
704            in = new AtomicFile(metadataFile).openRead();
705        } catch (FileNotFoundException fnfe) {
706            Slog.i(LOG_TAG, "No instant metadata file");
707            return null;
708        }
709
710        final File instantDir = metadataFile.getParentFile();
711        final long timestamp = metadataFile.lastModified();
712        final String packageName = instantDir.getName();
713
714        try {
715            XmlPullParser parser = Xml.newPullParser();
716            parser.setInput(in, StandardCharsets.UTF_8.name());
717            return new UninstalledInstantAppState(
718                    parseMetadata(parser, packageName), timestamp);
719        } catch (XmlPullParserException | IOException e) {
720            throw new IllegalStateException("Failed parsing instant"
721                    + " metadata file: " + metadataFile, e);
722        } finally {
723            IoUtils.closeQuietly(in);
724        }
725    }
726
727    private static @NonNull File computeInstantCookieFile(@NonNull PackageParser.Package pkg,
728            @UserIdInt int userId) {
729        File appDir = getInstantApplicationDir(pkg.packageName, userId);
730        String cookieFile = INSTANT_APP_COOKIE_FILE_PREFIX + PackageUtils.computeSha256Digest(
731                pkg.mSignatures[0].toByteArray()) + INSTANT_APP_COOKIE_FILE_SIFFIX;
732        return new File(appDir, cookieFile);
733    }
734
735    private static @Nullable File peekInstantCookieFile(@NonNull String packageName,
736            @UserIdInt int userId) {
737        File appDir = getInstantApplicationDir(packageName, userId);
738        if (!appDir.exists()) {
739            return null;
740        }
741        File[] files = appDir.listFiles();
742        if (files == null) {
743            return null;
744        }
745        for (File file : files) {
746            if (!file.isDirectory()
747                    && file.getName().startsWith(INSTANT_APP_COOKIE_FILE_PREFIX)
748                    && file.getName().endsWith(INSTANT_APP_COOKIE_FILE_SIFFIX)) {
749                return file;
750            }
751        }
752        return null;
753    }
754
755    private static @Nullable
756    InstantAppInfo parseMetadata(@NonNull XmlPullParser parser,
757                                 @NonNull String packageName)
758            throws IOException, XmlPullParserException {
759        final int outerDepth = parser.getDepth();
760        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
761            if (TAG_PACKAGE.equals(parser.getName())) {
762                return parsePackage(parser, packageName);
763            }
764        }
765        return null;
766    }
767
768    private static InstantAppInfo parsePackage(@NonNull XmlPullParser parser,
769                                               @NonNull String packageName)
770            throws IOException, XmlPullParserException {
771        String label = parser.getAttributeValue(null, ATTR_LABEL);
772
773        List<String> outRequestedPermissions = new ArrayList<>();
774        List<String> outGrantedPermissions = new ArrayList<>();
775
776        final int outerDepth = parser.getDepth();
777        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
778            if (TAG_PERMISSIONS.equals(parser.getName())) {
779                parsePermissions(parser, outRequestedPermissions, outGrantedPermissions);
780            }
781        }
782
783        String[] requestedPermissions = new String[outRequestedPermissions.size()];
784        outRequestedPermissions.toArray(requestedPermissions);
785
786        String[] grantedPermissions = new String[outGrantedPermissions.size()];
787        outGrantedPermissions.toArray(grantedPermissions);
788
789        return new InstantAppInfo(packageName, label,
790                requestedPermissions, grantedPermissions);
791    }
792
793    private static void parsePermissions(@NonNull XmlPullParser parser,
794            @NonNull List<String> outRequestedPermissions,
795            @NonNull List<String> outGrantedPermissions)
796            throws IOException, XmlPullParserException {
797        final int outerDepth = parser.getDepth();
798        while (XmlUtils.nextElementWithin(parser,outerDepth)) {
799            if (TAG_PERMISSION.equals(parser.getName())) {
800                String permission = XmlUtils.readStringAttribute(parser, ATTR_NAME);
801                outRequestedPermissions.add(permission);
802                if (XmlUtils.readBooleanAttribute(parser, ATTR_GRANTED)) {
803                    outGrantedPermissions.add(permission);
804                }
805            }
806        }
807    }
808
809    private void writeUninstalledInstantAppMetadata(
810            @NonNull InstantAppInfo instantApp, @UserIdInt int userId) {
811        File appDir = getInstantApplicationDir(instantApp.getPackageName(), userId);
812        if (!appDir.exists() && !appDir.mkdirs()) {
813            return;
814        }
815
816        File metadataFile = new File(appDir, INSTANT_APP_METADATA_FILE);
817
818        AtomicFile destination = new AtomicFile(metadataFile);
819        FileOutputStream out = null;
820        try {
821            out = destination.startWrite();
822
823            XmlSerializer serializer = Xml.newSerializer();
824            serializer.setOutput(out, StandardCharsets.UTF_8.name());
825            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
826
827            serializer.startDocument(null, true);
828
829            serializer.startTag(null, TAG_PACKAGE);
830            serializer.attribute(null, ATTR_LABEL, instantApp.loadLabel(
831                    mService.mContext.getPackageManager()).toString());
832
833            serializer.startTag(null, TAG_PERMISSIONS);
834            for (String permission : instantApp.getRequestedPermissions()) {
835                serializer.startTag(null, TAG_PERMISSION);
836                serializer.attribute(null, ATTR_NAME, permission);
837                if (ArrayUtils.contains(instantApp.getGrantedPermissions(), permission)) {
838                    serializer.attribute(null, ATTR_GRANTED, String.valueOf(true));
839                }
840                serializer.endTag(null, TAG_PERMISSION);
841            }
842            serializer.endTag(null, TAG_PERMISSIONS);
843
844            serializer.endTag(null, TAG_PACKAGE);
845
846            serializer.endDocument();
847            destination.finishWrite(out);
848        } catch (Throwable t) {
849            Slog.wtf(LOG_TAG, "Failed to write instant state, restoring backup", t);
850            destination.failWrite(out);
851        } finally {
852            IoUtils.closeQuietly(out);
853        }
854    }
855
856    private static @NonNull File getInstantApplicationsDir(int userId) {
857        return new File(Environment.getUserSystemDirectory(userId),
858                INSTANT_APPS_FOLDER);
859    }
860
861    private static @NonNull File getInstantApplicationDir(String packageName, int userId) {
862        return new File (getInstantApplicationsDir(userId), packageName);
863    }
864
865    private static void deleteDir(@NonNull File dir) {
866        File[] files = dir.listFiles();
867        if (files != null) {
868            for (File file : files) {
869                deleteDir(file);
870            }
871        }
872        dir.delete();
873    }
874
875    private static final class UninstalledInstantAppState {
876        final InstantAppInfo mInstantAppInfo;
877        final long mTimestamp;
878
879        public UninstalledInstantAppState(InstantAppInfo instantApp,
880                long timestamp) {
881            mInstantAppInfo = instantApp;
882            mTimestamp = timestamp;
883        }
884    }
885
886    private final class CookiePersistence extends Handler {
887        private static final long PERSIST_COOKIE_DELAY_MILLIS = 1000L; /* one second */
888
889        // In case you wonder why we stash the cookies aside, we use
890        // the user id for the message id and the package for the payload.
891        // Handler allows removing messages by id and tag where the
892        // tag is is compared using ==. So to allow cancelling the
893        // pending persistence for an app under a given user we use
894        // the fact that package names are interned in the system
895        // process so the == comparison would match and we end up
896        // with a way to cancel persisting the cookie for a user
897        // and package.
898        private final SparseArray<ArrayMap<String, byte[]>> mPendingPersistCookies =
899                new SparseArray<>();
900
901        public CookiePersistence(Looper looper) {
902            super(looper);
903        }
904
905        public void schedulePersist(@UserIdInt int userId,
906                @NonNull String packageName, @NonNull byte[] cookie) {
907            cancelPendingPersist(userId, packageName);
908            addPendingPersistCookie(userId, packageName, cookie);
909            sendMessageDelayed(obtainMessage(userId, packageName),
910                    PERSIST_COOKIE_DELAY_MILLIS);
911        }
912
913        public @Nullable byte[] getPendingPersistCookie(@UserIdInt int userId,
914                @NonNull String packageName) {
915            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
916            if (pendingWorkForUser != null) {
917                return pendingWorkForUser.remove(packageName);
918            }
919            return null;
920        }
921
922        private void cancelPendingPersist(@UserIdInt int userId,
923                @NonNull String packageName) {
924            removePendingPersistCookie(userId, packageName);
925            removeMessages(userId, packageName);
926        }
927
928        private void addPendingPersistCookie(@UserIdInt int userId,
929                @NonNull String packageName, @NonNull byte[] cookie) {
930            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
931            if (pendingWorkForUser == null) {
932                pendingWorkForUser = new ArrayMap<>();
933                mPendingPersistCookies.put(userId, pendingWorkForUser);
934            }
935            pendingWorkForUser.put(packageName, cookie);
936        }
937
938        private byte[] removePendingPersistCookie(@UserIdInt int userId,
939                @NonNull String packageName) {
940            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
941            byte[] cookie = null;
942            if (pendingWorkForUser != null) {
943                cookie = pendingWorkForUser.remove(packageName);
944                if (pendingWorkForUser.isEmpty()) {
945                    mPendingPersistCookies.remove(userId);
946                }
947            }
948            return cookie;
949        }
950
951        @Override
952        public void handleMessage(Message message) {
953            int userId = message.what;
954            String packageName = (String) message.obj;
955            byte[] cookie = removePendingPersistCookie(userId, packageName);
956            persistInstantApplicationCookie(cookie, packageName, userId);
957        }
958    }
959}
960