InstantAppResolver.java revision 4db6bc16d0cc3c3b60a6b5305118f266ab2028a8
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.IntDef;
26import android.annotation.NonNull;
27import android.annotation.Nullable;
28import android.app.ActivityManager;
29import android.app.PendingIntent;
30import android.content.ComponentName;
31import android.content.Context;
32import android.content.IIntentSender;
33import android.content.Intent;
34import android.content.IntentFilter;
35import android.content.IntentSender;
36import android.content.pm.ActivityInfo;
37import android.content.pm.InstantAppRequest;
38import android.content.pm.AuxiliaryResolveInfo;
39import android.content.pm.InstantAppIntentFilter;
40import android.content.pm.InstantAppResolveInfo;
41import android.content.pm.InstantAppResolveInfo.InstantAppDigest;
42import android.metrics.LogMaker;
43import android.net.Uri;
44import android.os.Build;
45import android.os.Bundle;
46import android.os.Handler;
47import android.os.RemoteException;
48import android.text.TextUtils;
49import android.util.Log;
50import android.util.Slog;
51
52import com.android.internal.logging.MetricsLogger;
53import com.android.internal.logging.nano.MetricsProto;
54import com.android.server.pm.InstantAppResolverConnection.ConnectionException;
55import com.android.server.pm.InstantAppResolverConnection.PhaseTwoCallback;
56
57import java.lang.annotation.Retention;
58import java.lang.annotation.RetentionPolicy;
59import java.util.ArrayList;
60import java.util.Arrays;
61import java.util.Collections;
62import java.util.Iterator;
63import java.util.List;
64import java.util.Set;
65import java.util.UUID;
66
67/** @hide */
68public abstract class InstantAppResolver {
69    private static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE;
70    private static final String TAG = "PackageManager";
71
72    private static final int RESOLUTION_SUCCESS = 0;
73    private static final int RESOLUTION_FAILURE = 1;
74    /** Binding to the external service timed out */
75    private static final int RESOLUTION_BIND_TIMEOUT = 2;
76    /** The call to retrieve an instant application response timed out */
77    private static final int RESOLUTION_CALL_TIMEOUT = 3;
78
79    @IntDef(flag = true, prefix = { "RESOLUTION_" }, value = {
80            RESOLUTION_SUCCESS,
81            RESOLUTION_FAILURE,
82            RESOLUTION_BIND_TIMEOUT,
83            RESOLUTION_CALL_TIMEOUT,
84    })
85    @Retention(RetentionPolicy.SOURCE)
86    public @interface ResolutionStatus {}
87
88    private static MetricsLogger sMetricsLogger;
89
90    private static MetricsLogger getLogger() {
91        if (sMetricsLogger == null) {
92            sMetricsLogger = new MetricsLogger();
93        }
94        return sMetricsLogger;
95    }
96
97    /**
98     * Returns an intent with potential PII removed from the original intent. Fields removed
99     * include extras and the host + path of the data, if defined.
100     */
101    public static Intent sanitizeIntent(Intent origIntent) {
102        final Intent sanitizedIntent;
103        sanitizedIntent = new Intent(origIntent.getAction());
104        Set<String> categories = origIntent.getCategories();
105        if (categories != null) {
106            for (String category : categories) {
107                sanitizedIntent.addCategory(category);
108            }
109        }
110        Uri sanitizedUri = origIntent.getData() == null
111                ? null
112                : Uri.fromParts(origIntent.getScheme(), "", "");
113        sanitizedIntent.setDataAndType(sanitizedUri, origIntent.getType());
114        sanitizedIntent.addFlags(origIntent.getFlags());
115        sanitizedIntent.setPackage(origIntent.getPackage());
116        return sanitizedIntent;
117    }
118
119    public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(
120            InstantAppResolverConnection connection, InstantAppRequest requestObj) {
121        final long startTime = System.currentTimeMillis();
122        final String token = UUID.randomUUID().toString();
123        if (DEBUG_INSTANT) {
124            Log.d(TAG, "[" + token + "] Phase1; resolving");
125        }
126        final Intent origIntent = requestObj.origIntent;
127        final Intent sanitizedIntent = sanitizeIntent(origIntent);
128
129        AuxiliaryResolveInfo resolveInfo = null;
130        @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS;
131        try {
132            final List<InstantAppResolveInfo> instantAppResolveInfoList =
133                    connection.getInstantAppResolveInfoList(sanitizedIntent,
134                            requestObj.digest.getDigestPrefixSecure(), token);
135            if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
136                resolveInfo = InstantAppResolver.filterInstantAppIntent(
137                        instantAppResolveInfoList, origIntent, requestObj.resolvedType,
138                        requestObj.userId, origIntent.getPackage(), requestObj.digest, token);
139            }
140        } catch (ConnectionException e) {
141            if (e.failure == ConnectionException.FAILURE_BIND) {
142                resolutionStatus = RESOLUTION_BIND_TIMEOUT;
143            } else if (e.failure == ConnectionException.FAILURE_CALL) {
144                resolutionStatus = RESOLUTION_CALL_TIMEOUT;
145            } else {
146                resolutionStatus = RESOLUTION_FAILURE;
147            }
148        }
149        // Only log successful instant application resolution
150        if (requestObj.resolveForStart && resolutionStatus == RESOLUTION_SUCCESS) {
151            logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token,
152                    resolutionStatus);
153        }
154        if (DEBUG_INSTANT && resolveInfo == null) {
155            if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
156                Log.d(TAG, "[" + token + "] Phase1; bind timed out");
157            } else if (resolutionStatus == RESOLUTION_CALL_TIMEOUT) {
158                Log.d(TAG, "[" + token + "] Phase1; call timed out");
159            } else if (resolutionStatus != RESOLUTION_SUCCESS) {
160                Log.d(TAG, "[" + token + "] Phase1; service connection error");
161            } else {
162                Log.d(TAG, "[" + token + "] Phase1; No results matched");
163            }
164        }
165        return resolveInfo;
166    }
167
168    public static void doInstantAppResolutionPhaseTwo(Context context,
169            InstantAppResolverConnection connection, InstantAppRequest requestObj,
170            ActivityInfo instantAppInstaller, Handler callbackHandler) {
171        final long startTime = System.currentTimeMillis();
172        final String token = requestObj.responseObj.token;
173        if (DEBUG_INSTANT) {
174            Log.d(TAG, "[" + token + "] Phase2; resolving");
175        }
176        final Intent origIntent = requestObj.origIntent;
177        final Intent sanitizedIntent = sanitizeIntent(origIntent);
178
179        final PhaseTwoCallback callback = new PhaseTwoCallback() {
180            @Override
181            void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList,
182                    long startTime) {
183                final Intent failureIntent;
184                if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
185                    final AuxiliaryResolveInfo instantAppIntentInfo =
186                            InstantAppResolver.filterInstantAppIntent(
187                                    instantAppResolveInfoList, origIntent, null /*resolvedType*/,
188                                    0 /*userId*/, origIntent.getPackage(), requestObj.digest,
189                                    token);
190                    if (instantAppIntentInfo != null) {
191                        failureIntent = instantAppIntentInfo.failureIntent;
192                    } else {
193                        failureIntent = null;
194                    }
195                } else {
196                    failureIntent = null;
197                }
198                final Intent installerIntent = buildEphemeralInstallerIntent(
199                        requestObj.origIntent,
200                        sanitizedIntent,
201                        failureIntent,
202                        requestObj.callingPackage,
203                        requestObj.verificationBundle,
204                        requestObj.resolvedType,
205                        requestObj.userId,
206                        requestObj.responseObj.installFailureActivity,
207                        token,
208                        false /*needsPhaseTwo*/,
209                        requestObj.responseObj.filters);
210                installerIntent.setComponent(new ComponentName(
211                        instantAppInstaller.packageName, instantAppInstaller.name));
212
213                logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
214                        requestObj.responseObj.filters != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
215
216                context.startActivity(installerIntent);
217            }
218        };
219        try {
220            connection.getInstantAppIntentFilterList(sanitizedIntent,
221                    requestObj.digest.getDigestPrefixSecure(), token, callback, callbackHandler,
222                    startTime);
223        } catch (ConnectionException e) {
224            @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE;
225            if (e.failure == ConnectionException.FAILURE_BIND) {
226                resolutionStatus = RESOLUTION_BIND_TIMEOUT;
227            }
228            logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
229                    resolutionStatus);
230            if (DEBUG_INSTANT) {
231                if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
232                    Log.d(TAG, "[" + token + "] Phase2; bind timed out");
233                } else {
234                    Log.d(TAG, "[" + token + "] Phase2; service connection error");
235                }
236            }
237        }
238    }
239
240    /**
241     * Builds and returns an intent to launch the instant installer.
242     */
243    public static Intent buildEphemeralInstallerIntent(
244            @NonNull Intent origIntent,
245            @NonNull Intent sanitizedIntent,
246            @Nullable Intent failureIntent,
247            @NonNull String callingPackage,
248            @Nullable Bundle verificationBundle,
249            @NonNull String resolvedType,
250            int userId,
251            @Nullable ComponentName installFailureActivity,
252            @Nullable String token,
253            boolean needsPhaseTwo,
254            List<AuxiliaryResolveInfo.AuxiliaryFilter> filters) {
255        // Construct the intent that launches the instant installer
256        int flags = origIntent.getFlags();
257        final Intent intent = new Intent();
258        intent.setFlags(flags
259                | Intent.FLAG_ACTIVITY_NEW_TASK
260                | Intent.FLAG_ACTIVITY_CLEAR_TASK
261                | Intent.FLAG_ACTIVITY_NO_HISTORY
262                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
263        if (token != null) {
264            // TODO(b/72700831): remove populating old extra
265            intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token);
266            intent.putExtra(Intent.EXTRA_INSTANT_APP_TOKEN, token);
267        }
268        if (origIntent.getData() != null) {
269            // TODO(b/72700831): remove populating old extra
270            intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
271            intent.putExtra(Intent.EXTRA_INSTANT_APP_HOSTNAME, origIntent.getData().getHost());
272        }
273        intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction());
274        intent.putExtra(Intent.EXTRA_INTENT, sanitizedIntent);
275
276        if (needsPhaseTwo) {
277            intent.setAction(Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE);
278        } else {
279            // We have all of the data we need; just start the installer without a second phase
280            if (failureIntent != null || installFailureActivity != null) {
281                // Intent that is launched if the package couldn't be installed for any reason.
282                try {
283                    final Intent onFailureIntent;
284                    if (installFailureActivity != null) {
285                        onFailureIntent = new Intent();
286                        onFailureIntent.setComponent(installFailureActivity);
287                        if (filters != null && filters.size() == 1) {
288                            onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME,
289                                    filters.get(0).splitName);
290                        }
291                        onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent);
292                    } else {
293                        onFailureIntent = failureIntent;
294                    }
295                    final IIntentSender failureIntentTarget = ActivityManager.getService()
296                            .getIntentSender(
297                                    ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
298                                    null /*token*/, null /*resultWho*/, 1 /*requestCode*/,
299                                    new Intent[] { onFailureIntent },
300                                    new String[] { resolvedType },
301                                    PendingIntent.FLAG_CANCEL_CURRENT
302                                            | PendingIntent.FLAG_ONE_SHOT
303                                            | PendingIntent.FLAG_IMMUTABLE,
304                                    null /*bOptions*/, userId);
305                    IntentSender failureSender = new IntentSender(failureIntentTarget);
306                    // TODO(b/72700831): remove populating old extra
307                    intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE, failureSender);
308                    intent.putExtra(Intent.EXTRA_INSTANT_APP_FAILURE, failureSender);
309                } catch (RemoteException ignore) { /* ignore; same process */ }
310            }
311
312            // Intent that is launched if the package was installed successfully.
313            final Intent successIntent = new Intent(origIntent);
314            successIntent.setLaunchToken(token);
315            try {
316                final IIntentSender successIntentTarget = ActivityManager.getService()
317                        .getIntentSender(
318                                ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
319                                null /*token*/, null /*resultWho*/, 0 /*requestCode*/,
320                                new Intent[] { successIntent },
321                                new String[] { resolvedType },
322                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
323                                        | PendingIntent.FLAG_IMMUTABLE,
324                                null /*bOptions*/, userId);
325                IntentSender successSender = new IntentSender(successIntentTarget);
326                // TODO(b/72700831): remove populating old extra
327                intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS, successSender);
328                intent.putExtra(Intent.EXTRA_INSTANT_APP_SUCCESS, successSender);
329            } catch (RemoteException ignore) { /* ignore; same process */ }
330            if (verificationBundle != null) {
331                intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
332            }
333            intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
334
335            if (filters != null) {
336                Bundle resolvableFilters[] = new Bundle[filters.size()];
337                for (int i = 0, max = filters.size(); i < max; i++) {
338                    Bundle resolvableFilter = new Bundle();
339                    AuxiliaryResolveInfo.AuxiliaryFilter filter = filters.get(i);
340                    resolvableFilter.putBoolean(Intent.EXTRA_UNKNOWN_INSTANT_APP,
341                            filter.resolveInfo != null
342                                    && filter.resolveInfo.shouldLetInstallerDecide());
343                    resolvableFilter.putString(Intent.EXTRA_PACKAGE_NAME, filter.packageName);
344                    resolvableFilter.putString(Intent.EXTRA_SPLIT_NAME, filter.splitName);
345                    resolvableFilter.putLong(Intent.EXTRA_LONG_VERSION_CODE, filter.versionCode);
346                    resolvableFilter.putBundle(Intent.EXTRA_INSTANT_APP_EXTRAS, filter.extras);
347                    resolvableFilters[i] = resolvableFilter;
348                    if (i == 0) {
349                        // for backwards compat, always set the first result on the intent and add
350                        // the int version code
351                        intent.putExtras(resolvableFilter);
352                        intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) filter.versionCode);
353                    }
354                }
355                intent.putExtra(Intent.EXTRA_INSTANT_APP_BUNDLES, resolvableFilters);
356            }
357            intent.setAction(Intent.ACTION_INSTALL_INSTANT_APP_PACKAGE);
358        }
359        return intent;
360    }
361
362    private static AuxiliaryResolveInfo filterInstantAppIntent(
363            List<InstantAppResolveInfo> instantAppResolveInfoList,
364            Intent origIntent, String resolvedType, int userId, String packageName,
365            InstantAppDigest digest, String token) {
366        final int[] shaPrefix = digest.getDigestPrefix();
367        final byte[][] digestBytes = digest.getDigestBytes();
368        final Intent failureIntent = new Intent(origIntent);
369        boolean requiresSecondPhase = false;
370        failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
371        failureIntent.setLaunchToken(token);
372        ArrayList<AuxiliaryResolveInfo.AuxiliaryFilter> filters = null;
373        boolean isWebIntent = origIntent.isBrowsableWebIntent();
374        for (InstantAppResolveInfo instantAppResolveInfo : instantAppResolveInfoList) {
375            if (shaPrefix.length > 0 && instantAppResolveInfo.shouldLetInstallerDecide()) {
376                Slog.e(TAG, "InstantAppResolveInfo with mShouldLetInstallerDecide=true when digest"
377                        + " provided; ignoring");
378                continue;
379            }
380            byte[] filterDigestBytes = instantAppResolveInfo.getDigestBytes();
381            // Only include matching digests if we have a prefix and we're either dealing with a
382            // web intent or the resolveInfo specifies digest details.
383            if (shaPrefix.length > 0 && (isWebIntent || filterDigestBytes.length > 0)) {
384                boolean matchFound = false;
385                // Go in reverse order so we match the narrowest scope first.
386                for (int i = shaPrefix.length - 1; i >= 0; --i) {
387                    if (Arrays.equals(digestBytes[i], filterDigestBytes)) {
388                        matchFound = true;
389                        break;
390                    }
391                }
392                if (!matchFound) {
393                    continue;
394                }
395            }
396            // We matched a resolve info; resolve the filters to see if anything matches completely.
397            List<AuxiliaryResolveInfo.AuxiliaryFilter> matchFilters = computeResolveFilters(
398                    origIntent, resolvedType, userId, packageName, token, instantAppResolveInfo);
399            if (matchFilters != null) {
400                if (matchFilters.isEmpty()) {
401                    requiresSecondPhase = true;
402                }
403                if (filters == null) {
404                    filters = new ArrayList<>(matchFilters);
405                } else {
406                    filters.addAll(matchFilters);
407                }
408            }
409        }
410        if (filters != null && !filters.isEmpty()) {
411            return new AuxiliaryResolveInfo(token, requiresSecondPhase, failureIntent, filters);
412        }
413        // Hash or filter mis-match; no instant apps for this domain.
414        return null;
415    }
416
417    /**
418     * Returns one of three states: <p/>
419     * <ul>
420     *     <li>{@code null} if there are no matches will not be; resolution is unnecessary.</li>
421     *     <li>An empty list signifying that a 2nd phase of resolution is required.</li>
422     *     <li>A populated list meaning that matches were found and should be sent directly to the
423     *     installer</li>
424     * </ul>
425     *
426     */
427    private static List<AuxiliaryResolveInfo.AuxiliaryFilter> computeResolveFilters(
428            Intent origIntent, String resolvedType, int userId, String packageName, String token,
429            InstantAppResolveInfo instantAppInfo) {
430        if (instantAppInfo.shouldLetInstallerDecide()) {
431            return Collections.singletonList(
432                    new AuxiliaryResolveInfo.AuxiliaryFilter(
433                            instantAppInfo, null /* splitName */,
434                            instantAppInfo.getExtras()));
435        }
436        if (packageName != null
437                && !packageName.equals(instantAppInfo.getPackageName())) {
438            return null;
439        }
440        final List<InstantAppIntentFilter> instantAppFilters =
441                instantAppInfo.getIntentFilters();
442        if (instantAppFilters == null || instantAppFilters.isEmpty()) {
443            // No filters on web intent; no matches, 2nd phase unnecessary.
444            if (origIntent.isBrowsableWebIntent()) {
445                return null;
446            }
447            // No filters; we need to start phase two
448            if (DEBUG_INSTANT) {
449                Log.d(TAG, "No app filters; go to phase 2");
450            }
451            return Collections.emptyList();
452        }
453        final PackageManagerService.InstantAppIntentResolver instantAppResolver =
454                new PackageManagerService.InstantAppIntentResolver();
455        for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
456            final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
457            final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
458            if (splitFilters == null || splitFilters.isEmpty()) {
459                continue;
460            }
461            for (int k = splitFilters.size() - 1; k >= 0; --k) {
462                IntentFilter filter = splitFilters.get(k);
463                Iterator<IntentFilter.AuthorityEntry> authorities =
464                        filter.authoritiesIterator();
465                // ignore http/s-only filters.
466                if ((authorities == null || !authorities.hasNext())
467                        && (filter.hasDataScheme("http") || filter.hasDataScheme("https"))
468                        && filter.hasAction(Intent.ACTION_VIEW)
469                        && filter.hasCategory(Intent.CATEGORY_BROWSABLE)) {
470                    continue;
471                }
472                instantAppResolver.addFilter(
473                        new AuxiliaryResolveInfo.AuxiliaryFilter(
474                                filter,
475                                instantAppInfo,
476                                instantAppFilter.getSplitName(),
477                                instantAppInfo.getExtras()
478                        ));
479            }
480        }
481        List<AuxiliaryResolveInfo.AuxiliaryFilter> matchedResolveInfoList =
482                instantAppResolver.queryIntent(
483                        origIntent, resolvedType, false /*defaultOnly*/, userId);
484        if (!matchedResolveInfoList.isEmpty()) {
485            if (DEBUG_INSTANT) {
486                Log.d(TAG, "[" + token + "] Found match(es); " + matchedResolveInfoList);
487            }
488            return matchedResolveInfoList;
489        } else if (DEBUG_INSTANT) {
490            Log.d(TAG, "[" + token + "] No matches found"
491                    + " package: " + instantAppInfo.getPackageName()
492                    + ", versionCode: " + instantAppInfo.getVersionCode());
493        }
494        return null;
495    }
496
497    private static void logMetrics(int action, long startTime, String token,
498            @ResolutionStatus int status) {
499        final LogMaker logMaker = new LogMaker(action)
500                .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
501                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS,
502                        new Long(System.currentTimeMillis() - startTime))
503                .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token)
504                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status));
505        getLogger().write(logMaker);
506    }
507}
508