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