InstantAppResolver.java revision b3b43130db1c02493ca881ad95adf27ec0cbe8ad
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.Handler;
45import android.os.RemoteException;
46import android.util.Log;
47import android.util.Pair;
48import android.util.Slog;
49
50import com.android.internal.logging.MetricsLogger;
51import com.android.internal.logging.nano.MetricsProto;
52import com.android.server.pm.EphemeralResolverConnection.PhaseTwoCallback;
53
54import java.util.ArrayList;
55import java.util.Arrays;
56import java.util.List;
57import java.util.UUID;
58
59/** @hide */
60public abstract class InstantAppResolver {
61    private static final boolean DEBUG_EPHEMERAL = Build.IS_DEBUGGABLE;
62    private static final String TAG = "PackageManager";
63
64    private static int RESOLUTION_SUCCESS = 0;
65    private static int RESOLUTION_FAILURE = 1;
66
67    private static MetricsLogger sMetricsLogger;
68    private static MetricsLogger getLogger() {
69        if (sMetricsLogger == null) {
70            sMetricsLogger = new MetricsLogger();
71        }
72        return sMetricsLogger;
73    }
74
75    public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(Context context,
76            EphemeralResolverConnection connection, InstantAppRequest requestObj) {
77        if (DEBUG_EPHEMERAL) {
78            Log.d(TAG, "Resolving phase 1");
79        }
80        final long startTime = System.currentTimeMillis();
81        final String token = UUID.randomUUID().toString();
82        final Intent intent = requestObj.origIntent;
83        final InstantAppDigest digest =
84                new InstantAppDigest(intent.getData().getHost(), 5 /*maxDigests*/);
85        final int[] shaPrefix = digest.getDigestPrefix();
86        final List<InstantAppResolveInfo> instantAppResolveInfoList =
87                connection.getInstantAppResolveInfoList(shaPrefix, token);
88
89        final AuxiliaryResolveInfo resolveInfo;
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, "No results returned");
94            }
95            resolveInfo = null;
96        } else {
97            resolveInfo = InstantAppResolver.filterInstantAppIntent(instantAppResolveInfoList,
98                    intent, requestObj.resolvedType, requestObj.userId,
99                    intent.getPackage(), digest, token);
100        }
101
102        logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token,
103                resolveInfo != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
104
105        return resolveInfo;
106    }
107
108    public static void doInstantAppResolutionPhaseTwo(Context context,
109            EphemeralResolverConnection connection, InstantAppRequest requestObj,
110            ActivityInfo instantAppInstaller, Handler callbackHandler) {
111        if (DEBUG_EPHEMERAL) {
112            Log.d(TAG, "Resolving phase 2");
113        }
114        final long startTime = System.currentTimeMillis();
115        final Intent intent = requestObj.origIntent;
116        final String hostName = intent.getData().getHost();
117        final InstantAppDigest digest = new InstantAppDigest(hostName, 5 /*maxDigests*/);
118        final int[] shaPrefix = digest.getDigestPrefix();
119        final String token = requestObj.responseObj.token;
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                if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
129                    final AuxiliaryResolveInfo instantAppIntentInfo =
130                            InstantAppResolver.filterInstantAppIntent(
131                                    instantAppResolveInfoList, intent, null /*resolvedType*/,
132                                    0 /*userId*/, intent.getPackage(), digest, token);
133                    if (instantAppIntentInfo != null
134                            && instantAppIntentInfo.resolveInfo != null) {
135                        packageName = instantAppIntentInfo.resolveInfo.getPackageName();
136                        splitName = instantAppIntentInfo.splitName;
137                        versionCode = instantAppIntentInfo.resolveInfo.getVersionCode();
138                    } else {
139                        packageName = null;
140                        splitName = null;
141                        versionCode = -1;
142                    }
143                } else {
144                    packageName = null;
145                    splitName = null;
146                    versionCode = -1;
147                }
148                final Intent installerIntent = buildEphemeralInstallerIntent(
149                        requestObj.origIntent,
150                        requestObj.callingPackage,
151                        requestObj.resolvedType,
152                        requestObj.userId,
153                        packageName,
154                        splitName,
155                        versionCode,
156                        token,
157                        false /*needsPhaseTwo*/);
158                installerIntent.setComponent(new ComponentName(
159                        instantAppInstaller.packageName, instantAppInstaller.name));
160
161                logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
162                        packageName != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
163
164                context.startActivity(installerIntent);
165            }
166        };
167        connection.getInstantAppIntentFilterList(
168                shaPrefix, token, hostName, callback, callbackHandler, startTime);
169    }
170
171    /**
172     * Builds and returns an intent to launch the instant installer.
173     */
174    public static Intent buildEphemeralInstallerIntent(@NonNull Intent origIntent,
175            @NonNull String callingPackage,
176            @NonNull String resolvedType,
177            int userId,
178            @NonNull String instantAppPackageName,
179            @Nullable String instantAppSplitName,
180            int versionCode,
181            @Nullable String token,
182            boolean needsPhaseTwo) {
183        // Construct the intent that launches the instant installer
184        int flags = origIntent.getFlags();
185        final Intent intent = new Intent();
186        intent.setFlags(flags
187                | Intent.FLAG_ACTIVITY_NEW_TASK
188                | Intent.FLAG_ACTIVITY_CLEAR_TASK
189                | Intent.FLAG_ACTIVITY_NO_HISTORY
190                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
191        if (token != null) {
192            intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token);
193        }
194        if (origIntent.getData() != null) {
195            intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
196        }
197
198        // We have all of the data we need; just start the installer without a second phase
199        if (!needsPhaseTwo) {
200            // Intent that is launched if the package couldn't be installed for any reason.
201            final Intent failureIntent = new Intent(origIntent);
202            failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
203            failureIntent.setLaunchToken(token);
204            try {
205                final IIntentSender failureIntentTarget = ActivityManager.getService()
206                        .getIntentSender(
207                                ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
208                                null /*token*/, null /*resultWho*/, 1 /*requestCode*/,
209                                new Intent[] { failureIntent },
210                                new String[] { resolvedType },
211                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
212                                        | PendingIntent.FLAG_IMMUTABLE,
213                                null /*bOptions*/, userId);
214                intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE,
215                        new IntentSender(failureIntentTarget));
216            } catch (RemoteException ignore) { /* ignore; same process */ }
217
218            // Intent that is launched if the package was installed successfully.
219            final Intent successIntent = new Intent(origIntent);
220            successIntent.setLaunchToken(token);
221            try {
222                final IIntentSender successIntentTarget = ActivityManager.getService()
223                        .getIntentSender(
224                                ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
225                                null /*token*/, null /*resultWho*/, 0 /*requestCode*/,
226                                new Intent[] { successIntent },
227                                new String[] { resolvedType },
228                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
229                                        | PendingIntent.FLAG_IMMUTABLE,
230                                null /*bOptions*/, userId);
231                intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS,
232                        new IntentSender(successIntentTarget));
233            } catch (RemoteException ignore) { /* ignore; same process */ }
234
235            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName);
236            intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
237            intent.putExtra(Intent.EXTRA_VERSION_CODE, versionCode);
238        }
239
240        return intent;
241    }
242
243    private static AuxiliaryResolveInfo filterInstantAppIntent(
244            List<InstantAppResolveInfo> instantAppResolveInfoList,
245            Intent intent, String resolvedType, int userId, String packageName,
246            InstantAppDigest digest, String token) {
247        final int[] shaPrefix = digest.getDigestPrefix();
248        final byte[][] digestBytes = digest.getDigestBytes();
249        // Go in reverse order so we match the narrowest scope first.
250        for (int i = shaPrefix.length - 1; i >= 0 ; --i) {
251            for (InstantAppResolveInfo instantAppInfo : instantAppResolveInfoList) {
252                if (!Arrays.equals(digestBytes[i], instantAppInfo.getDigestBytes())) {
253                    continue;
254                }
255                if (packageName != null
256                        && !packageName.equals(instantAppInfo.getPackageName())) {
257                    continue;
258                }
259                final List<InstantAppIntentFilter> instantAppFilters =
260                        instantAppInfo.getIntentFilters();
261                // No filters; we need to start phase two
262                if (instantAppFilters == null || instantAppFilters.isEmpty()) {
263                    if (DEBUG_EPHEMERAL) {
264                        Log.d(TAG, "No app filters; go to phase 2");
265                    }
266                    return new AuxiliaryResolveInfo(instantAppInfo,
267                            new IntentFilter(Intent.ACTION_VIEW) /*intentFilter*/,
268                            null /*splitName*/, token, true /*needsPhase2*/);
269                }
270                // We have a domain match; resolve the filters to see if anything matches.
271                final PackageManagerService.EphemeralIntentResolver instantAppResolver =
272                        new PackageManagerService.EphemeralIntentResolver();
273                for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
274                    final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
275                    final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
276                    if (splitFilters == null || splitFilters.isEmpty()) {
277                        continue;
278                    }
279                    for (int k = splitFilters.size() - 1; k >= 0; --k) {
280                        final AuxiliaryResolveInfo intentInfo =
281                                new AuxiliaryResolveInfo(instantAppInfo,
282                                        splitFilters.get(k), instantAppFilter.getSplitName(),
283                                        token, false /*needsPhase2*/);
284                        instantAppResolver.addFilter(intentInfo);
285                    }
286                }
287                List<AuxiliaryResolveInfo> matchedResolveInfoList = instantAppResolver.queryIntent(
288                        intent, resolvedType, false /*defaultOnly*/, userId);
289                if (!matchedResolveInfoList.isEmpty()) {
290                    if (DEBUG_EPHEMERAL) {
291                        final AuxiliaryResolveInfo info = matchedResolveInfoList.get(0);
292                        Log.d(TAG, "Found match;"
293                                + " package: " + info.packageName
294                                + ", split: " + info.splitName
295                                + ", versionCode: " + info.versionCode);
296                    }
297                    return matchedResolveInfoList.get(0);
298                }
299            }
300        }
301        // Hash or filter mis-match; no instant apps for this domain.
302        return null;
303    }
304
305    private static void logMetrics(int action, long startTime, String token, int status) {
306        final LogMaker logMaker = new LogMaker(action)
307                .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
308                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS,
309                        new Long(System.currentTimeMillis() - startTime))
310                .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token)
311                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status));
312        getLogger().write(logMaker);
313    }
314}
315