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