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