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