1/*
2 * Copyright (C) 2015 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.contacts.common.util;
18
19import android.content.Context;
20import android.content.Intent;
21import android.content.pm.PackageManager;
22import android.content.pm.ResolveInfo;
23import android.net.Uri;
24import android.os.Build;
25import android.provider.ContactsContract.QuickContact;
26import android.text.TextUtils;
27
28import java.util.List;
29
30/**
31 * Utility for forcing intents to be started inside the current app. This is useful for avoiding
32 * senseless disambiguation dialogs. Ie, if a user clicks a contact inside Contacts we assume
33 * they want to view the contact inside the Contacts app as opposed to a 3rd party contacts app.
34 *
35 * Methods are designed to replace the use of startActivity() for implicit intents. This class isn't
36 * necessary for explicit intents. No attempt is made to replace startActivityForResult(), since
37 * startActivityForResult() is always used with explicit intents in this project.
38 *
39 * Why not just always use explicit intents? The Contacts/Dialer app implements standard intent
40 * actions used by others apps. We want to continue exercising these intent filters to make sure
41 * they still work. Plus we sometimes don't know an explicit intent would work. See
42 * {@link #startActivityInAppIfPossible}.
43 *
44 * Some ContactsCommon code that is only used by Dialer doesn't use ImplicitIntentsUtil.
45 */
46public class ImplicitIntentsUtil {
47
48    /**
49     * Start an intent. If it is possible for this app to handle the intent, force this app's
50     * activity to handle the intent. Sometimes it is impossible to know whether this app
51     * can handle an intent while coding since the code is used inside both Dialer and Contacts.
52     * This method is particularly useful in such circumstances.
53     *
54     * On a Nexus 5 with a small number of apps, this method consistently added 3-16ms of delay
55     * in order to talk to the package manager.
56     */
57    public static void startActivityInAppIfPossible(Context context, Intent intent) {
58        final Intent appIntent = getIntentInAppIfExists(context, intent);
59        if (appIntent != null) {
60            context.startActivity(appIntent);
61        } else {
62            context.startActivity(intent);
63        }
64    }
65
66    /**
67     * Start intent using an activity inside this app. This method is useful if you are certain
68     * that the intent can be handled inside this app, and you care about shaving milliseconds.
69     */
70    public static void startActivityInApp(Context context, Intent intent) {
71        String packageName = context.getPackageName();
72        intent.setPackage(packageName);
73        context.startActivity(intent);
74    }
75
76    /**
77     * Start an intent normally. Assert that the intent can't be opened inside this app.
78     */
79    public static void startActivityOutsideApp(Context context, Intent intent) {
80        final boolean isPlatformDebugBuild = Build.TYPE.equals("eng")
81                || Build.TYPE.equals("userdebug");
82        if (isPlatformDebugBuild) {
83            if (getIntentInAppIfExists(context, intent) != null) {
84                throw new AssertionError("startActivityOutsideApp() was called for an intent" +
85                        " that can be handled inside the app");
86            }
87        }
88        context.startActivity(intent);
89    }
90
91    /**
92     * Returns an implicit intent for opening QuickContacts.
93     */
94    public static Intent composeQuickContactIntent(Uri contactLookupUri,
95            int extraMode) {
96        final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
97        intent.setData(contactLookupUri);
98        intent.putExtra(QuickContact.EXTRA_MODE, extraMode);
99        // Make sure not to show QuickContacts on top of another QuickContacts.
100        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
101        return intent;
102    }
103
104    /**
105     * Returns a copy of {@param intent} with a class name set, if a class inside this app
106     * has a corresponding intent filter.
107     */
108    private static Intent getIntentInAppIfExists(Context context, Intent intent) {
109        try {
110            final Intent intentCopy = new Intent(intent);
111            // Force this intentCopy to open inside the current app.
112            intentCopy.setPackage(context.getPackageName());
113            final List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(
114                    intentCopy, PackageManager.MATCH_DEFAULT_ONLY);
115            if (list != null && list.size() != 0) {
116                // Now that we know the intentCopy will work inside the current app, we
117                // can return this intent non-null.
118                if (list.get(0).activityInfo != null
119                        && !TextUtils.isEmpty(list.get(0).activityInfo.name)) {
120                    // Now that we know the class name, we may as well attach it to intentCopy
121                    // to prevent the package manager from needing to find it again inside
122                    // startActivity(). This is only needed for efficiency.
123                    intentCopy.setClassName(context.getPackageName(),
124                            list.get(0).activityInfo.name);
125                }
126                return intentCopy;
127            }
128            return null;
129        } catch (Exception e) {
130            // Don't let the package manager crash our app. If the package manager can't resolve the
131            // intent here, then we can still call startActivity without calling setClass() first.
132            return null;
133        }
134    }
135}
136