InstantAppResolver.java revision 7dd99e3d463eb2354e5ddb0cbeed1333ec590235
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.pm;
18
19import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE;
20import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO;
21import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_LAUNCH_TOKEN;
22import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_DELAY_MS;
23import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_INSTANT_APP_RESOLUTION_STATUS;
24
25import android.annotation.NonNull;
26import android.annotation.Nullable;
27import android.app.ActivityManager;
28import android.app.PendingIntent;
29import android.content.ComponentName;
30import android.content.Context;
31import android.content.IIntentSender;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.IntentSender;
35import android.content.pm.ActivityInfo;
36import android.content.pm.InstantAppRequest;
37import android.content.pm.AuxiliaryResolveInfo;
38import android.content.pm.InstantAppIntentFilter;
39import android.content.pm.InstantAppResolveInfo;
40import android.content.pm.InstantAppResolveInfo.InstantAppDigest;
41import android.metrics.LogMaker;
42import android.os.Binder;
43import android.os.Build;
44import android.os.Bundle;
45import android.os.Handler;
46import android.os.RemoteException;
47import android.util.Log;
48import android.util.Pair;
49import android.util.Slog;
50
51import com.android.internal.logging.MetricsLogger;
52import com.android.internal.logging.nano.MetricsProto;
53import com.android.server.pm.EphemeralResolverConnection.PhaseTwoCallback;
54
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.List;
58import java.util.UUID;
59
60/** @hide */
61public abstract class InstantAppResolver {
62    private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE;
63    private static final String TAG = "PackageManager";
64
65    private static int RESOLUTION_SUCCESS = 0;
66    private static int RESOLUTION_FAILURE = 1;
67
68    private static MetricsLogger sMetricsLogger;
69    private static MetricsLogger getLogger() {
70        if (sMetricsLogger == null) {
71            sMetricsLogger = new MetricsLogger();
72        }
73        return sMetricsLogger;
74    }
75
76    public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(Context context,
77            EphemeralResolverConnection connection, InstantAppRequest requestObj) {
78        final long startTime = System.currentTimeMillis();
79        final String token = UUID.randomUUID().toString();
80        if (DEBUG_EPHEMERAL) {
81            Log.d(TAG, "[" + token + "] Resolving phase 1");
82        }
83        final Intent intent = requestObj.origIntent;
84        final InstantAppDigest digest =
85                new InstantAppDigest(intent.getData().getHost(), 5 /*maxDigests*/);
86        final int[] shaPrefix = digest.getDigestPrefix();
87        final List<InstantAppResolveInfo> instantAppResolveInfoList =
88                connection.getInstantAppResolveInfoList(shaPrefix, token);
89
90        if (instantAppResolveInfoList == null || instantAppResolveInfoList.size() == 0) {
91            // No hash prefix match; there are no instant apps for this domain.
92            if (DEBUG_EPHEMERAL) {
93                Log.d(TAG, "[" + token + "] No results returned");
94            }
95            return null;
96        }
97        final AuxiliaryResolveInfo resolveInfo = InstantAppResolver.filterInstantAppIntent(
98                instantAppResolveInfoList, intent, requestObj.resolvedType, requestObj.userId,
99                intent.getPackage(), digest, token);
100        logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token,
101                RESOLUTION_SUCCESS);
102        if (DEBUG_EPHEMERAL && resolveInfo == null) {
103            Log.d(TAG, "[" + token + "] No results matched");
104        }
105        return resolveInfo;
106    }
107
108    public static void doInstantAppResolutionPhaseTwo(Context context,
109            EphemeralResolverConnection connection, InstantAppRequest requestObj,
110            ActivityInfo instantAppInstaller, Handler callbackHandler) {
111        final long startTime = System.currentTimeMillis();
112        final String token = requestObj.responseObj.token;
113        if (DEBUG_EPHEMERAL) {
114            Log.d(TAG, "[" + token + "] Resolving phase 2");
115        }
116        final Intent intent = requestObj.origIntent;
117        final String hostName = intent.getData().getHost();
118        final InstantAppDigest digest = new InstantAppDigest(hostName, 5 /*maxDigests*/);
119        final int[] shaPrefix = digest.getDigestPrefix();
120
121        final PhaseTwoCallback callback = new PhaseTwoCallback() {
122            @Override
123            void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList,
124                    long startTime) {
125                final String packageName;
126                final String splitName;
127                final int versionCode;
128                final Intent failureIntent;
129                if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
130                    final AuxiliaryResolveInfo instantAppIntentInfo =
131                            InstantAppResolver.filterInstantAppIntent(
132                                    instantAppResolveInfoList, intent, null /*resolvedType*/,
133                                    0 /*userId*/, intent.getPackage(), digest, token);
134                    if (instantAppIntentInfo != null
135                            && instantAppIntentInfo.resolveInfo != null) {
136                        packageName = instantAppIntentInfo.resolveInfo.getPackageName();
137                        splitName = instantAppIntentInfo.splitName;
138                        versionCode = instantAppIntentInfo.resolveInfo.getVersionCode();
139                        failureIntent = instantAppIntentInfo.failureIntent;
140                    } else {
141                        packageName = null;
142                        splitName = null;
143                        versionCode = -1;
144                        failureIntent = null;
145                    }
146                } else {
147                    packageName = null;
148                    splitName = null;
149                    versionCode = -1;
150                    failureIntent = null;
151                }
152                final Intent installerIntent = buildEphemeralInstallerIntent(
153                        requestObj.origIntent,
154                        failureIntent,
155                        requestObj.callingPackage,
156                        requestObj.verificationBundle,
157                        requestObj.resolvedType,
158                        requestObj.userId,
159                        packageName,
160                        splitName,
161                        versionCode,
162                        token,
163                        false /*needsPhaseTwo*/);
164                installerIntent.setComponent(new ComponentName(
165                        instantAppInstaller.packageName, instantAppInstaller.name));
166
167                logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
168                        packageName != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
169
170                context.startActivity(installerIntent);
171            }
172        };
173        connection.getInstantAppIntentFilterList(
174                shaPrefix, token, hostName, callback, callbackHandler, startTime);
175    }
176
177    /**
178     * Builds and returns an intent to launch the instant installer.
179     */
180    public static Intent buildEphemeralInstallerIntent(
181            @NonNull Intent origIntent,
182            @NonNull Intent failureIntent,
183            @NonNull String callingPackage,
184            @Nullable Bundle verificationBundle,
185            @NonNull String resolvedType,
186            int userId,
187            @NonNull String instantAppPackageName,
188            @Nullable String instantAppSplitName,
189            int versionCode,
190            @Nullable String token,
191            boolean needsPhaseTwo) {
192        // Construct the intent that launches the instant installer
193        int flags = origIntent.getFlags();
194        final Intent intent = new Intent();
195        intent.setFlags(flags
196                | Intent.FLAG_ACTIVITY_NEW_TASK
197                | Intent.FLAG_ACTIVITY_CLEAR_TASK
198                | Intent.FLAG_ACTIVITY_NO_HISTORY
199                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
200        if (token != null) {
201            intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token);
202        }
203        if (origIntent.getData() != null) {
204            intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
205        }
206
207        // We have all of the data we need; just start the installer without a second phase
208        if (!needsPhaseTwo) {
209            // Intent that is launched if the package couldn't be installed for any reason.
210            if (failureIntent != null) {
211                try {
212                    final IIntentSender failureIntentTarget = ActivityManager.getService()
213                            .getIntentSender(
214                                    ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
215                                    null /*token*/, null /*resultWho*/, 1 /*requestCode*/,
216                                    new Intent[] { failureIntent },
217                                    new String[] { resolvedType },
218                                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
219                                            | PendingIntent.FLAG_IMMUTABLE,
220                                    null /*bOptions*/, userId);
221                    intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE,
222                            new IntentSender(failureIntentTarget));
223                } catch (RemoteException ignore) { /* ignore; same process */ }
224            }
225
226            // Intent that is launched if the package was installed successfully.
227            final Intent successIntent = new Intent(origIntent);
228            successIntent.setLaunchToken(token);
229            try {
230                final IIntentSender successIntentTarget = ActivityManager.getService()
231                        .getIntentSender(
232                                ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
233                                null /*token*/, null /*resultWho*/, 0 /*requestCode*/,
234                                new Intent[] { successIntent },
235                                new String[] { resolvedType },
236                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
237                                        | PendingIntent.FLAG_IMMUTABLE,
238                                null /*bOptions*/, userId);
239                intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS,
240                        new IntentSender(successIntentTarget));
241            } catch (RemoteException ignore) { /* ignore; same process */ }
242
243            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName);
244            intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
245            intent.putExtra(Intent.EXTRA_VERSION_CODE, versionCode);
246            intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
247            if (verificationBundle != null) {
248                intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
249            }
250        }
251
252        return intent;
253    }
254
255    private static AuxiliaryResolveInfo filterInstantAppIntent(
256            List<InstantAppResolveInfo> instantAppResolveInfoList,
257            Intent origIntent, String resolvedType, int userId, String packageName,
258            InstantAppDigest digest, String token) {
259        final int[] shaPrefix = digest.getDigestPrefix();
260        final byte[][] digestBytes = digest.getDigestBytes();
261        final Intent failureIntent = new Intent(origIntent);
262        failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
263        failureIntent.setLaunchToken(token);
264        // Go in reverse order so we match the narrowest scope first.
265        for (int i = shaPrefix.length - 1; i >= 0 ; --i) {
266            for (InstantAppResolveInfo instantAppInfo : instantAppResolveInfoList) {
267                if (!Arrays.equals(digestBytes[i], instantAppInfo.getDigestBytes())) {
268                    continue;
269                }
270                if (packageName != null
271                        && !packageName.equals(instantAppInfo.getPackageName())) {
272                    continue;
273                }
274                final List<InstantAppIntentFilter> instantAppFilters =
275                        instantAppInfo.getIntentFilters();
276                // No filters; we need to start phase two
277                if (instantAppFilters == null || instantAppFilters.isEmpty()) {
278                    if (DEBUG_EPHEMERAL) {
279                        Log.d(TAG, "No app filters; go to phase 2");
280                    }
281                    return new AuxiliaryResolveInfo(instantAppInfo,
282                            new IntentFilter(Intent.ACTION_VIEW) /*intentFilter*/,
283                            null /*splitName*/, token, true /*needsPhase2*/,
284                            null /*failureIntent*/);
285                }
286                // We have a domain match; resolve the filters to see if anything matches.
287                final PackageManagerService.EphemeralIntentResolver instantAppResolver =
288                        new PackageManagerService.EphemeralIntentResolver();
289                for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
290                    final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
291                    final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
292                    if (splitFilters == null || splitFilters.isEmpty()) {
293                        continue;
294                    }
295                    for (int k = splitFilters.size() - 1; k >= 0; --k) {
296                        final AuxiliaryResolveInfo intentInfo =
297                                new AuxiliaryResolveInfo(instantAppInfo,
298                                        splitFilters.get(k), instantAppFilter.getSplitName(),
299                                        token, false /*needsPhase2*/, failureIntent);
300                        instantAppResolver.addFilter(intentInfo);
301                    }
302                }
303                List<AuxiliaryResolveInfo> matchedResolveInfoList = instantAppResolver.queryIntent(
304                        origIntent, resolvedType, false /*defaultOnly*/, userId);
305                if (!matchedResolveInfoList.isEmpty()) {
306                    if (DEBUG_EPHEMERAL) {
307                        final AuxiliaryResolveInfo info = matchedResolveInfoList.get(0);
308                        Log.d(TAG, "[" + token + "] Found match;"
309                                + " package: " + info.packageName
310                                + ", split: " + info.splitName
311                                + ", versionCode: " + info.versionCode);
312                    }
313                    return matchedResolveInfoList.get(0);
314                } else if (DEBUG_EPHEMERAL) {
315                    Log.d(TAG, "[" + token + "] No matches found"
316                            + " package: " + instantAppInfo.getPackageName()
317                            + ", versionCode: " + instantAppInfo.getVersionCode());
318                }
319            }
320        }
321        // Hash or filter mis-match; no instant apps for this domain.
322        return null;
323    }
324
325    private static void logMetrics(int action, long startTime, String token, int status) {
326        final LogMaker logMaker = new LogMaker(action)
327                .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
328                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS,
329                        new Long(System.currentTimeMillis() - startTime))
330                .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token)
331                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status));
332        getLogger().write(logMaker);
333    }
334}
335