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