InstantAppRegistry.java revision be0b8896d1bc385d4c8fb54c21929745935dcbea
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 (ps.getInstantApp(userId)) {
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 (ps.getInstantApp(userId)) {
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            final PackageParser.Package pkg = mService.mPackages.valueAt(i);
537            final PackageSetting ps = (PackageSetting) pkg.mExtras;
538            if (ps == null || !ps.getInstantApp(userId)) {
539                continue;
540            }
541            final InstantAppInfo info = createInstantAppInfoForPackage(
542                    pkg, userId, true);
543            if (info == null) {
544                continue;
545            }
546            if (result == null) {
547                result = new ArrayList<>();
548            }
549            result.add(info);
550        }
551
552        return result;
553    }
554
555    private @NonNull
556    InstantAppInfo createInstantAppInfoForPackage(
557            @NonNull PackageParser.Package pkg, @UserIdInt int userId,
558            boolean addApplicationInfo) {
559        PackageSetting ps = (PackageSetting) pkg.mExtras;
560        if (ps == null) {
561            return null;
562        }
563        if (!ps.getInstalled(userId)) {
564            return null;
565        }
566
567        String[] requestedPermissions = new String[pkg.requestedPermissions.size()];
568        pkg.requestedPermissions.toArray(requestedPermissions);
569
570        Set<String> permissions = ps.getPermissionsState().getPermissions(userId);
571        String[] grantedPermissions = new String[permissions.size()];
572        permissions.toArray(grantedPermissions);
573
574        if (addApplicationInfo) {
575            return new InstantAppInfo(pkg.applicationInfo,
576                    requestedPermissions, grantedPermissions);
577        } else {
578            return new InstantAppInfo(pkg.applicationInfo.packageName,
579                    pkg.applicationInfo.loadLabel(mService.mContext.getPackageManager()),
580                    requestedPermissions, grantedPermissions);
581        }
582    }
583
584    private @Nullable List<InstantAppInfo> getUninstalledInstantApplicationsLPr(
585            @UserIdInt int userId) {
586        List<UninstalledInstantAppState> uninstalledAppStates =
587                getUninstalledInstantAppStatesLPr(userId);
588        if (uninstalledAppStates == null || uninstalledAppStates.isEmpty()) {
589            return null;
590        }
591
592        List<InstantAppInfo> uninstalledApps = null;
593        final int stateCount = uninstalledAppStates.size();
594        for (int i = 0; i < stateCount; i++) {
595            UninstalledInstantAppState uninstalledAppState = uninstalledAppStates.get(i);
596            if (uninstalledApps == null) {
597                uninstalledApps = new ArrayList<>();
598            }
599            uninstalledApps.add(uninstalledAppState.mInstantAppInfo);
600        }
601        return uninstalledApps;
602    }
603
604    private void propagateInstantAppPermissionsIfNeeded(@NonNull String packageName,
605            @UserIdInt int userId) {
606        InstantAppInfo appInfo = peekOrParseUninstalledInstantAppInfo(
607                packageName, userId);
608        if (appInfo == null) {
609            return;
610        }
611        if (ArrayUtils.isEmpty(appInfo.getGrantedPermissions())) {
612            return;
613        }
614        final long identity = Binder.clearCallingIdentity();
615        try {
616            for (String grantedPermission : appInfo.getGrantedPermissions()) {
617                BasePermission bp = mService.mSettings.mPermissions.get(grantedPermission);
618                if (bp != null && (bp.isRuntime() || bp.isDevelopment()) && bp.isInstant()) {
619                    mService.grantRuntimePermission(packageName, grantedPermission, userId);
620                }
621            }
622        } finally {
623            Binder.restoreCallingIdentity(identity);
624        }
625    }
626
627    private @NonNull
628    InstantAppInfo peekOrParseUninstalledInstantAppInfo(
629            @NonNull String packageName, @UserIdInt int userId) {
630        if (mUninstalledInstantApps != null) {
631            List<UninstalledInstantAppState> uninstalledAppStates =
632                    mUninstalledInstantApps.get(userId);
633            if (uninstalledAppStates != null) {
634                final int appCount = uninstalledAppStates.size();
635                for (int i = 0; i < appCount; i++) {
636                    UninstalledInstantAppState uninstalledAppState = uninstalledAppStates.get(i);
637                    if (uninstalledAppState.mInstantAppInfo
638                            .getPackageName().equals(packageName)) {
639                        return uninstalledAppState.mInstantAppInfo;
640                    }
641                }
642            }
643        }
644
645        File metadataFile = new File(getInstantApplicationDir(packageName, userId),
646                INSTANT_APP_METADATA_FILE);
647        UninstalledInstantAppState uninstalledAppState = parseMetadataFile(metadataFile);
648        if (uninstalledAppState == null) {
649            return null;
650        }
651
652        return uninstalledAppState.mInstantAppInfo;
653    }
654
655    private @Nullable List<UninstalledInstantAppState> getUninstalledInstantAppStatesLPr(
656            @UserIdInt int userId) {
657        List<UninstalledInstantAppState> uninstalledAppStates = null;
658        if (mUninstalledInstantApps != null) {
659            uninstalledAppStates = mUninstalledInstantApps.get(userId);
660            if (uninstalledAppStates != null) {
661                return uninstalledAppStates;
662            }
663        }
664
665        File instantAppsDir = getInstantApplicationsDir(userId);
666        if (instantAppsDir.exists()) {
667            File[] files = instantAppsDir.listFiles();
668            if (files != null) {
669                for (File instantDir : files) {
670                    if (!instantDir.isDirectory()) {
671                        continue;
672                    }
673                    File metadataFile = new File(instantDir,
674                            INSTANT_APP_METADATA_FILE);
675                    UninstalledInstantAppState uninstalledAppState =
676                            parseMetadataFile(metadataFile);
677                    if (uninstalledAppState == null) {
678                        continue;
679                    }
680                    if (uninstalledAppStates == null) {
681                        uninstalledAppStates = new ArrayList<>();
682                    }
683                    uninstalledAppStates.add(uninstalledAppState);
684                }
685            }
686        }
687
688        if (uninstalledAppStates != null) {
689            if (mUninstalledInstantApps == null) {
690                mUninstalledInstantApps = new SparseArray<>();
691            }
692            mUninstalledInstantApps.put(userId, uninstalledAppStates);
693        }
694
695        return uninstalledAppStates;
696    }
697
698    private static @Nullable UninstalledInstantAppState parseMetadataFile(
699            @NonNull File metadataFile) {
700        if (!metadataFile.exists()) {
701            return null;
702        }
703        FileInputStream in;
704        try {
705            in = new AtomicFile(metadataFile).openRead();
706        } catch (FileNotFoundException fnfe) {
707            Slog.i(LOG_TAG, "No instant metadata file");
708            return null;
709        }
710
711        final File instantDir = metadataFile.getParentFile();
712        final long timestamp = metadataFile.lastModified();
713        final String packageName = instantDir.getName();
714
715        try {
716            XmlPullParser parser = Xml.newPullParser();
717            parser.setInput(in, StandardCharsets.UTF_8.name());
718            return new UninstalledInstantAppState(
719                    parseMetadata(parser, packageName), timestamp);
720        } catch (XmlPullParserException | IOException e) {
721            throw new IllegalStateException("Failed parsing instant"
722                    + " metadata file: " + metadataFile, e);
723        } finally {
724            IoUtils.closeQuietly(in);
725        }
726    }
727
728    private static @NonNull File computeInstantCookieFile(@NonNull PackageParser.Package pkg,
729            @UserIdInt int userId) {
730        File appDir = getInstantApplicationDir(pkg.packageName, userId);
731        String cookieFile = INSTANT_APP_COOKIE_FILE_PREFIX + PackageUtils.computeSha256Digest(
732                pkg.mSignatures[0].toByteArray()) + INSTANT_APP_COOKIE_FILE_SIFFIX;
733        return new File(appDir, cookieFile);
734    }
735
736    private static @Nullable File peekInstantCookieFile(@NonNull String packageName,
737            @UserIdInt int userId) {
738        File appDir = getInstantApplicationDir(packageName, userId);
739        if (!appDir.exists()) {
740            return null;
741        }
742        File[] files = appDir.listFiles();
743        if (files == null) {
744            return null;
745        }
746        for (File file : files) {
747            if (!file.isDirectory()
748                    && file.getName().startsWith(INSTANT_APP_COOKIE_FILE_PREFIX)
749                    && file.getName().endsWith(INSTANT_APP_COOKIE_FILE_SIFFIX)) {
750                return file;
751            }
752        }
753        return null;
754    }
755
756    private static @Nullable
757    InstantAppInfo parseMetadata(@NonNull XmlPullParser parser,
758                                 @NonNull String packageName)
759            throws IOException, XmlPullParserException {
760        final int outerDepth = parser.getDepth();
761        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
762            if (TAG_PACKAGE.equals(parser.getName())) {
763                return parsePackage(parser, packageName);
764            }
765        }
766        return null;
767    }
768
769    private static InstantAppInfo parsePackage(@NonNull XmlPullParser parser,
770                                               @NonNull String packageName)
771            throws IOException, XmlPullParserException {
772        String label = parser.getAttributeValue(null, ATTR_LABEL);
773
774        List<String> outRequestedPermissions = new ArrayList<>();
775        List<String> outGrantedPermissions = new ArrayList<>();
776
777        final int outerDepth = parser.getDepth();
778        while (XmlUtils.nextElementWithin(parser, outerDepth)) {
779            if (TAG_PERMISSIONS.equals(parser.getName())) {
780                parsePermissions(parser, outRequestedPermissions, outGrantedPermissions);
781            }
782        }
783
784        String[] requestedPermissions = new String[outRequestedPermissions.size()];
785        outRequestedPermissions.toArray(requestedPermissions);
786
787        String[] grantedPermissions = new String[outGrantedPermissions.size()];
788        outGrantedPermissions.toArray(grantedPermissions);
789
790        return new InstantAppInfo(packageName, label,
791                requestedPermissions, grantedPermissions);
792    }
793
794    private static void parsePermissions(@NonNull XmlPullParser parser,
795            @NonNull List<String> outRequestedPermissions,
796            @NonNull List<String> outGrantedPermissions)
797            throws IOException, XmlPullParserException {
798        final int outerDepth = parser.getDepth();
799        while (XmlUtils.nextElementWithin(parser,outerDepth)) {
800            if (TAG_PERMISSION.equals(parser.getName())) {
801                String permission = XmlUtils.readStringAttribute(parser, ATTR_NAME);
802                outRequestedPermissions.add(permission);
803                if (XmlUtils.readBooleanAttribute(parser, ATTR_GRANTED)) {
804                    outGrantedPermissions.add(permission);
805                }
806            }
807        }
808    }
809
810    private void writeUninstalledInstantAppMetadata(
811            @NonNull InstantAppInfo instantApp, @UserIdInt int userId) {
812        File appDir = getInstantApplicationDir(instantApp.getPackageName(), userId);
813        if (!appDir.exists() && !appDir.mkdirs()) {
814            return;
815        }
816
817        File metadataFile = new File(appDir, INSTANT_APP_METADATA_FILE);
818
819        AtomicFile destination = new AtomicFile(metadataFile);
820        FileOutputStream out = null;
821        try {
822            out = destination.startWrite();
823
824            XmlSerializer serializer = Xml.newSerializer();
825            serializer.setOutput(out, StandardCharsets.UTF_8.name());
826            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
827
828            serializer.startDocument(null, true);
829
830            serializer.startTag(null, TAG_PACKAGE);
831            serializer.attribute(null, ATTR_LABEL, instantApp.loadLabel(
832                    mService.mContext.getPackageManager()).toString());
833
834            serializer.startTag(null, TAG_PERMISSIONS);
835            for (String permission : instantApp.getRequestedPermissions()) {
836                serializer.startTag(null, TAG_PERMISSION);
837                serializer.attribute(null, ATTR_NAME, permission);
838                if (ArrayUtils.contains(instantApp.getGrantedPermissions(), permission)) {
839                    serializer.attribute(null, ATTR_GRANTED, String.valueOf(true));
840                }
841                serializer.endTag(null, TAG_PERMISSION);
842            }
843            serializer.endTag(null, TAG_PERMISSIONS);
844
845            serializer.endTag(null, TAG_PACKAGE);
846
847            serializer.endDocument();
848            destination.finishWrite(out);
849        } catch (Throwable t) {
850            Slog.wtf(LOG_TAG, "Failed to write instant state, restoring backup", t);
851            destination.failWrite(out);
852        } finally {
853            IoUtils.closeQuietly(out);
854        }
855    }
856
857    private static @NonNull File getInstantApplicationsDir(int userId) {
858        return new File(Environment.getUserSystemDirectory(userId),
859                INSTANT_APPS_FOLDER);
860    }
861
862    private static @NonNull File getInstantApplicationDir(String packageName, int userId) {
863        return new File (getInstantApplicationsDir(userId), packageName);
864    }
865
866    private static void deleteDir(@NonNull File dir) {
867        File[] files = dir.listFiles();
868        if (files != null) {
869            for (File file : files) {
870                deleteDir(file);
871            }
872        }
873        dir.delete();
874    }
875
876    private static final class UninstalledInstantAppState {
877        final InstantAppInfo mInstantAppInfo;
878        final long mTimestamp;
879
880        public UninstalledInstantAppState(InstantAppInfo instantApp,
881                long timestamp) {
882            mInstantAppInfo = instantApp;
883            mTimestamp = timestamp;
884        }
885    }
886
887    private final class CookiePersistence extends Handler {
888        private static final long PERSIST_COOKIE_DELAY_MILLIS = 1000L; /* one second */
889
890        // In case you wonder why we stash the cookies aside, we use
891        // the user id for the message id and the package for the payload.
892        // Handler allows removing messages by id and tag where the
893        // tag is is compared using ==. So to allow cancelling the
894        // pending persistence for an app under a given user we use
895        // the fact that package names are interned in the system
896        // process so the == comparison would match and we end up
897        // with a way to cancel persisting the cookie for a user
898        // and package.
899        private final SparseArray<ArrayMap<String, byte[]>> mPendingPersistCookies =
900                new SparseArray<>();
901
902        public CookiePersistence(Looper looper) {
903            super(looper);
904        }
905
906        public void schedulePersist(@UserIdInt int userId,
907                @NonNull String packageName, @NonNull byte[] cookie) {
908            cancelPendingPersist(userId, packageName);
909            addPendingPersistCookie(userId, packageName, cookie);
910            sendMessageDelayed(obtainMessage(userId, packageName),
911                    PERSIST_COOKIE_DELAY_MILLIS);
912        }
913
914        public @Nullable byte[] getPendingPersistCookie(@UserIdInt int userId,
915                @NonNull String packageName) {
916            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
917            if (pendingWorkForUser != null) {
918                return pendingWorkForUser.remove(packageName);
919            }
920            return null;
921        }
922
923        private void cancelPendingPersist(@UserIdInt int userId,
924                @NonNull String packageName) {
925            removePendingPersistCookie(userId, packageName);
926            removeMessages(userId, packageName);
927        }
928
929        private void addPendingPersistCookie(@UserIdInt int userId,
930                @NonNull String packageName, @NonNull byte[] cookie) {
931            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
932            if (pendingWorkForUser == null) {
933                pendingWorkForUser = new ArrayMap<>();
934                mPendingPersistCookies.put(userId, pendingWorkForUser);
935            }
936            pendingWorkForUser.put(packageName, cookie);
937        }
938
939        private byte[] removePendingPersistCookie(@UserIdInt int userId,
940                @NonNull String packageName) {
941            ArrayMap<String, byte[]> pendingWorkForUser = mPendingPersistCookies.get(userId);
942            byte[] cookie = null;
943            if (pendingWorkForUser != null) {
944                cookie = pendingWorkForUser.remove(packageName);
945                if (pendingWorkForUser.isEmpty()) {
946                    mPendingPersistCookies.remove(userId);
947                }
948            }
949            return cookie;
950        }
951
952        @Override
953        public void handleMessage(Message message) {
954            int userId = message.what;
955            String packageName = (String) message.obj;
956            byte[] cookie = removePendingPersistCookie(userId, packageName);
957            persistInstantApplicationCookie(cookie, packageName, userId);
958        }
959    }
960}
961