InstantAppResolver.java revision 709ee1551db935adb2026fae9dd799e60f784499
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.os.Build;
44import android.os.Bundle;
45import android.os.Handler;
46import android.os.RemoteException;
47import android.util.Log;
48
49import com.android.internal.logging.MetricsLogger;
50import com.android.internal.logging.nano.MetricsProto;
51import com.android.server.pm.EphemeralResolverConnection.ConnectionException;
52import com.android.server.pm.EphemeralResolverConnection.PhaseTwoCallback;
53
54import java.lang.annotation.Retention;
55import java.lang.annotation.RetentionPolicy;
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 final int RESOLUTION_SUCCESS = 0;
66    private static final int RESOLUTION_FAILURE = 1;
67    /** Binding to the external service timed out */
68    private static final int RESOLUTION_BIND_TIMEOUT = 2;
69    /** The call to retrieve an instant application response timed out */
70    private static final int RESOLUTION_CALL_TIMEOUT = 3;
71
72    @IntDef(flag = true, prefix = { "RESOLUTION_" }, value = {
73            RESOLUTION_SUCCESS,
74            RESOLUTION_FAILURE,
75            RESOLUTION_BIND_TIMEOUT,
76            RESOLUTION_CALL_TIMEOUT,
77    })
78    @Retention(RetentionPolicy.SOURCE)
79    public @interface ResolutionStatus {}
80
81    private static MetricsLogger sMetricsLogger;
82    private static MetricsLogger getLogger() {
83        if (sMetricsLogger == null) {
84            sMetricsLogger = new MetricsLogger();
85        }
86        return sMetricsLogger;
87    }
88
89    public static AuxiliaryResolveInfo doInstantAppResolutionPhaseOne(Context context,
90            EphemeralResolverConnection connection, InstantAppRequest requestObj) {
91        final long startTime = System.currentTimeMillis();
92        final String token = UUID.randomUUID().toString();
93        if (DEBUG_EPHEMERAL) {
94            Log.d(TAG, "[" + token + "] Phase1; resolving");
95        }
96        final Intent intent = requestObj.origIntent;
97        final InstantAppDigest digest =
98                new InstantAppDigest(intent.getData().getHost(), 5 /*maxDigests*/);
99        final int[] shaPrefix = digest.getDigestPrefix();
100        AuxiliaryResolveInfo resolveInfo = null;
101        @ResolutionStatus int resolutionStatus = RESOLUTION_SUCCESS;
102        try {
103            final List<InstantAppResolveInfo> instantAppResolveInfoList =
104                    connection.getInstantAppResolveInfoList(shaPrefix, token);
105            if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
106                resolveInfo = InstantAppResolver.filterInstantAppIntent(
107                        instantAppResolveInfoList, intent, requestObj.resolvedType,
108                        requestObj.userId, intent.getPackage(), digest, token);
109            }
110        } catch (ConnectionException e) {
111            if (e.failure == ConnectionException.FAILURE_BIND) {
112                resolutionStatus = RESOLUTION_BIND_TIMEOUT;
113            } else if (e.failure == ConnectionException.FAILURE_CALL) {
114                resolutionStatus = RESOLUTION_CALL_TIMEOUT;
115            } else {
116                resolutionStatus = RESOLUTION_FAILURE;
117            }
118        }
119        // Only log successful instant application resolution
120        if (requestObj.resolveForStart && resolutionStatus == RESOLUTION_SUCCESS) {
121            logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_ONE, startTime, token,
122                    resolutionStatus);
123        }
124        if (DEBUG_EPHEMERAL && resolveInfo == null) {
125            if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
126                Log.d(TAG, "[" + token + "] Phase1; bind timed out");
127            } else if (resolutionStatus == RESOLUTION_CALL_TIMEOUT) {
128                Log.d(TAG, "[" + token + "] Phase1; call timed out");
129            } else if (resolutionStatus != RESOLUTION_SUCCESS) {
130                Log.d(TAG, "[" + token + "] Phase1; service connection error");
131            } else {
132                Log.d(TAG, "[" + token + "] Phase1; No results matched");
133            }
134        }
135        return resolveInfo;
136    }
137
138    public static void doInstantAppResolutionPhaseTwo(Context context,
139            EphemeralResolverConnection connection, InstantAppRequest requestObj,
140            ActivityInfo instantAppInstaller, Handler callbackHandler) {
141        final long startTime = System.currentTimeMillis();
142        final String token = requestObj.responseObj.token;
143        if (DEBUG_EPHEMERAL) {
144            Log.d(TAG, "[" + token + "] Phase2; resolving");
145        }
146        final Intent intent = requestObj.origIntent;
147        final String hostName = intent.getData().getHost();
148        final InstantAppDigest digest = new InstantAppDigest(hostName, 5 /*maxDigests*/);
149        final int[] shaPrefix = digest.getDigestPrefix();
150
151        final PhaseTwoCallback callback = new PhaseTwoCallback() {
152            @Override
153            void onPhaseTwoResolved(List<InstantAppResolveInfo> instantAppResolveInfoList,
154                    long startTime) {
155                final String packageName;
156                final String splitName;
157                final long versionCode;
158                final Intent failureIntent;
159                final Bundle extras;
160                if (instantAppResolveInfoList != null && instantAppResolveInfoList.size() > 0) {
161                    final AuxiliaryResolveInfo instantAppIntentInfo =
162                            InstantAppResolver.filterInstantAppIntent(
163                                    instantAppResolveInfoList, intent, null /*resolvedType*/,
164                                    0 /*userId*/, intent.getPackage(), digest, token);
165                    if (instantAppIntentInfo != null
166                            && instantAppIntentInfo.resolveInfo != null) {
167                        packageName = instantAppIntentInfo.resolveInfo.getPackageName();
168                        splitName = instantAppIntentInfo.splitName;
169                        versionCode = instantAppIntentInfo.resolveInfo.getVersionCode();
170                        failureIntent = instantAppIntentInfo.failureIntent;
171                        extras = instantAppIntentInfo.resolveInfo.getExtras();
172                    } else {
173                        packageName = null;
174                        splitName = null;
175                        versionCode = -1;
176                        failureIntent = null;
177                        extras = null;
178                    }
179                } else {
180                    packageName = null;
181                    splitName = null;
182                    versionCode = -1;
183                    failureIntent = null;
184                    extras = null;
185                }
186                final Intent installerIntent = buildEphemeralInstallerIntent(
187                        Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE,
188                        requestObj.origIntent,
189                        failureIntent,
190                        requestObj.callingPackage,
191                        requestObj.verificationBundle,
192                        requestObj.resolvedType,
193                        requestObj.userId,
194                        packageName,
195                        splitName,
196                        requestObj.responseObj.installFailureActivity,
197                        versionCode,
198                        token,
199                        extras,
200                        false /*needsPhaseTwo*/);
201                installerIntent.setComponent(new ComponentName(
202                        instantAppInstaller.packageName, instantAppInstaller.name));
203
204                logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
205                        packageName != null ? RESOLUTION_SUCCESS : RESOLUTION_FAILURE);
206
207                context.startActivity(installerIntent);
208            }
209        };
210        try {
211            connection.getInstantAppIntentFilterList(
212                    shaPrefix, token, hostName, callback, callbackHandler, startTime);
213        } catch (ConnectionException e) {
214            @ResolutionStatus int resolutionStatus = RESOLUTION_FAILURE;
215            if (e.failure == ConnectionException.FAILURE_BIND) {
216                resolutionStatus = RESOLUTION_BIND_TIMEOUT;
217            }
218            logMetrics(ACTION_INSTANT_APP_RESOLUTION_PHASE_TWO, startTime, token,
219                    resolutionStatus);
220            if (DEBUG_EPHEMERAL) {
221                if (resolutionStatus == RESOLUTION_BIND_TIMEOUT) {
222                    Log.d(TAG, "[" + token + "] Phase2; bind timed out");
223                } else {
224                    Log.d(TAG, "[" + token + "] Phase2; service connection error");
225                }
226            }
227        }
228    }
229
230    /**
231     * Builds and returns an intent to launch the instant installer.
232     */
233    public static Intent buildEphemeralInstallerIntent(
234            @NonNull String action,
235            @NonNull Intent origIntent,
236            @NonNull Intent failureIntent,
237            @NonNull String callingPackage,
238            @Nullable Bundle verificationBundle,
239            @NonNull String resolvedType,
240            int userId,
241            @NonNull String instantAppPackageName,
242            @Nullable String instantAppSplitName,
243            @Nullable ComponentName installFailureActivity,
244            long versionCode,
245            @Nullable String token,
246            @Nullable Bundle extras,
247            boolean needsPhaseTwo) {
248        // Construct the intent that launches the instant installer
249        int flags = origIntent.getFlags();
250        final Intent intent = new Intent(action);
251        intent.setFlags(flags
252                | Intent.FLAG_ACTIVITY_NEW_TASK
253                | Intent.FLAG_ACTIVITY_CLEAR_TASK
254                | Intent.FLAG_ACTIVITY_NO_HISTORY
255                | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
256        if (token != null) {
257            intent.putExtra(Intent.EXTRA_EPHEMERAL_TOKEN, token);
258        }
259        if (origIntent.getData() != null) {
260            intent.putExtra(Intent.EXTRA_EPHEMERAL_HOSTNAME, origIntent.getData().getHost());
261        }
262        intent.putExtra(Intent.EXTRA_INSTANT_APP_ACTION, origIntent.getAction());
263        if (extras != null) {
264            intent.putExtra(Intent.EXTRA_INSTANT_APP_EXTRAS, extras);
265        }
266
267        // We have all of the data we need; just start the installer without a second phase
268        if (!needsPhaseTwo) {
269            // Intent that is launched if the package couldn't be installed for any reason.
270            if (failureIntent != null || installFailureActivity != null) {
271                try {
272                    final Intent onFailureIntent;
273                    if (installFailureActivity != null) {
274                        onFailureIntent = new Intent();
275                        onFailureIntent.setComponent(installFailureActivity);
276                        onFailureIntent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
277                        onFailureIntent.putExtra(Intent.EXTRA_INTENT, origIntent);
278                    } else {
279                        onFailureIntent = failureIntent;
280                    }
281                    final IIntentSender failureIntentTarget = ActivityManager.getService()
282                            .getIntentSender(
283                                    ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
284                                    null /*token*/, null /*resultWho*/, 1 /*requestCode*/,
285                                    new Intent[] { onFailureIntent },
286                                    new String[] { resolvedType },
287                                    PendingIntent.FLAG_CANCEL_CURRENT
288                                            | PendingIntent.FLAG_ONE_SHOT
289                                            | PendingIntent.FLAG_IMMUTABLE,
290                                    null /*bOptions*/, userId);
291                    intent.putExtra(Intent.EXTRA_EPHEMERAL_FAILURE,
292                            new IntentSender(failureIntentTarget));
293                } catch (RemoteException ignore) { /* ignore; same process */ }
294            }
295
296            // Intent that is launched if the package was installed successfully.
297            final Intent successIntent = new Intent(origIntent);
298            successIntent.setLaunchToken(token);
299            try {
300                final IIntentSender successIntentTarget = ActivityManager.getService()
301                        .getIntentSender(
302                                ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage,
303                                null /*token*/, null /*resultWho*/, 0 /*requestCode*/,
304                                new Intent[] { successIntent },
305                                new String[] { resolvedType },
306                                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
307                                        | PendingIntent.FLAG_IMMUTABLE,
308                                null /*bOptions*/, userId);
309                intent.putExtra(Intent.EXTRA_EPHEMERAL_SUCCESS,
310                        new IntentSender(successIntentTarget));
311            } catch (RemoteException ignore) { /* ignore; same process */ }
312
313            intent.putExtra(Intent.EXTRA_PACKAGE_NAME, instantAppPackageName);
314            intent.putExtra(Intent.EXTRA_SPLIT_NAME, instantAppSplitName);
315            intent.putExtra(Intent.EXTRA_VERSION_CODE, (int) (versionCode & 0x7fffffff));
316            intent.putExtra(Intent.EXTRA_LONG_VERSION_CODE, versionCode);
317            intent.putExtra(Intent.EXTRA_CALLING_PACKAGE, callingPackage);
318            if (verificationBundle != null) {
319                intent.putExtra(Intent.EXTRA_VERIFICATION_BUNDLE, verificationBundle);
320            }
321        }
322
323        return intent;
324    }
325
326    private static AuxiliaryResolveInfo filterInstantAppIntent(
327            List<InstantAppResolveInfo> instantAppResolveInfoList,
328            Intent origIntent, String resolvedType, int userId, String packageName,
329            InstantAppDigest digest, String token) {
330        final int[] shaPrefix = digest.getDigestPrefix();
331        final byte[][] digestBytes = digest.getDigestBytes();
332        final Intent failureIntent = new Intent(origIntent);
333        failureIntent.setFlags(failureIntent.getFlags() | Intent.FLAG_IGNORE_EPHEMERAL);
334        failureIntent.setLaunchToken(token);
335        // Go in reverse order so we match the narrowest scope first.
336        for (int i = shaPrefix.length - 1; i >= 0 ; --i) {
337            for (InstantAppResolveInfo instantAppInfo : instantAppResolveInfoList) {
338                if (!Arrays.equals(digestBytes[i], instantAppInfo.getDigestBytes())) {
339                    continue;
340                }
341                if (packageName != null
342                        && !packageName.equals(instantAppInfo.getPackageName())) {
343                    continue;
344                }
345                final List<InstantAppIntentFilter> instantAppFilters =
346                        instantAppInfo.getIntentFilters();
347                // No filters; we need to start phase two
348                if (instantAppFilters == null || instantAppFilters.isEmpty()) {
349                    if (DEBUG_EPHEMERAL) {
350                        Log.d(TAG, "No app filters; go to phase 2");
351                    }
352                    return new AuxiliaryResolveInfo(instantAppInfo,
353                            new IntentFilter(Intent.ACTION_VIEW) /*intentFilter*/,
354                            null /*splitName*/, token, true /*needsPhase2*/,
355                            null /*failureIntent*/);
356                }
357                // We have a domain match; resolve the filters to see if anything matches.
358                final PackageManagerService.EphemeralIntentResolver instantAppResolver =
359                        new PackageManagerService.EphemeralIntentResolver();
360                for (int j = instantAppFilters.size() - 1; j >= 0; --j) {
361                    final InstantAppIntentFilter instantAppFilter = instantAppFilters.get(j);
362                    final List<IntentFilter> splitFilters = instantAppFilter.getFilters();
363                    if (splitFilters == null || splitFilters.isEmpty()) {
364                        continue;
365                    }
366                    for (int k = splitFilters.size() - 1; k >= 0; --k) {
367                        final AuxiliaryResolveInfo intentInfo =
368                                new AuxiliaryResolveInfo(instantAppInfo,
369                                        splitFilters.get(k), instantAppFilter.getSplitName(),
370                                        token, false /*needsPhase2*/, failureIntent);
371                        instantAppResolver.addFilter(intentInfo);
372                    }
373                }
374                List<AuxiliaryResolveInfo> matchedResolveInfoList = instantAppResolver.queryIntent(
375                        origIntent, resolvedType, false /*defaultOnly*/, userId);
376                if (!matchedResolveInfoList.isEmpty()) {
377                    if (DEBUG_EPHEMERAL) {
378                        final AuxiliaryResolveInfo info = matchedResolveInfoList.get(0);
379                        Log.d(TAG, "[" + token + "] Found match;"
380                                + " package: " + info.packageName
381                                + ", split: " + info.splitName
382                                + ", versionCode: " + info.versionCode);
383                    }
384                    return matchedResolveInfoList.get(0);
385                } else if (DEBUG_EPHEMERAL) {
386                    Log.d(TAG, "[" + token + "] No matches found"
387                            + " package: " + instantAppInfo.getPackageName()
388                            + ", versionCode: " + instantAppInfo.getVersionCode());
389                }
390            }
391        }
392        // Hash or filter mis-match; no instant apps for this domain.
393        return null;
394    }
395
396    private static void logMetrics(int action, long startTime, String token,
397            @ResolutionStatus int status) {
398        final LogMaker logMaker = new LogMaker(action)
399                .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
400                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_DELAY_MS,
401                        new Long(System.currentTimeMillis() - startTime))
402                .addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, token)
403                .addTaggedData(FIELD_INSTANT_APP_RESOLUTION_STATUS, new Integer(status));
404        getLogger().write(logMaker);
405    }
406}
407