• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util;
18 
19 import static com.android.contacts.ShortcutIntentBuilder.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION;
20 
21 import android.app.Activity;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.provider.ContactsContract;
29 import android.provider.ContactsContract.QuickContact;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 
33 import com.android.contacts.logging.ScreenEvent.ScreenType;
34 import com.android.contacts.model.account.GoogleAccountType;
35 import com.android.contacts.quickcontact.QuickContactActivity;
36 
37 import java.util.List;
38 
39 /**
40  * Utility for forcing intents to be started inside the current app. This is useful for avoiding
41  * senseless disambiguation dialogs. Ie, if a user clicks a contact inside Contacts we assume
42  * they want to view the contact inside the Contacts app as opposed to a 3rd party contacts app.
43  *
44  * Methods are designed to replace the use of startActivity() for implicit intents. This class isn't
45  * necessary for explicit intents. No attempt is made to replace startActivityForResult(), since
46  * startActivityForResult() is always used with explicit intents in this project.
47  *
48  * Why not just always use explicit intents? The Contacts/Dialer app implements standard intent
49  * actions used by others apps. We want to continue exercising these intent filters to make sure
50  * they still work. Plus we sometimes don't know an explicit intent would work. See
51  * {@link #startActivityInAppIfPossible}.
52  *
53  * Some ContactsCommon code that is only used by Dialer doesn't use ImplicitIntentsUtil.
54  */
55 public class ImplicitIntentsUtil {
56 
57     /**
58      * Start an intent. If it is possible for this app to handle the intent, force this app's
59      * activity to handle the intent. Sometimes it is impossible to know whether this app
60      * can handle an intent while coding since the code is used inside both Dialer and Contacts.
61      * This method is particularly useful in such circumstances.
62      *
63      * On a Nexus 5 with a small number of apps, this method consistently added 3-16ms of delay
64      * in order to talk to the package manager.
65      */
startActivityInAppIfPossible(Context context, Intent intent)66     public static void startActivityInAppIfPossible(Context context, Intent intent) {
67         final Intent appIntent = getIntentInAppIfExists(context, intent);
68         if (appIntent != null) {
69             context.startActivity(appIntent);
70         } else {
71             context.startActivity(intent);
72         }
73     }
74 
75     /**
76      * Start intent using an activity inside this app. This method is useful if you are certain
77      * that the intent can be handled inside this app, and you care about shaving milliseconds.
78      */
startActivityInApp(Context context, Intent intent)79     public static void startActivityInApp(Context context, Intent intent) {
80         String packageName = context.getPackageName();
81         intent.setPackage(packageName);
82         context.startActivity(intent);
83     }
84 
85     /**
86      * Start an intent normally. Assert that the intent can't be opened inside this app.
87      */
startActivityOutsideApp(Context context, Intent intent)88     public static void startActivityOutsideApp(Context context, Intent intent) {
89         final boolean isPlatformDebugBuild = Build.TYPE.equals("eng")
90                 || Build.TYPE.equals("userdebug");
91         if (isPlatformDebugBuild) {
92             if (getIntentInAppIfExists(context, intent) != null) {
93                 throw new AssertionError("startActivityOutsideApp() was called for an intent" +
94                         " that can be handled inside the app");
95             }
96         }
97         context.startActivity(intent);
98     }
99 
100     /**
101      * Starts QuickContact in app with the default mode and specified previous screen type.
102      */
startQuickContact(Activity activity, Uri contactLookupUri, int previousScreenType)103     public static void startQuickContact(Activity activity, Uri contactLookupUri,
104             int previousScreenType) {
105         final Intent intent = ImplicitIntentsUtil.composeQuickContactIntent(
106                 activity, contactLookupUri, previousScreenType);
107 
108         startActivityInApp(activity, intent);
109     }
110 
111     /**
112      * Returns an implicit intent for opening QuickContacts with the default mode and specified
113      * previous screen type.
114      */
composeQuickContactIntent(Context context, Uri contactLookupUri, int previousScreenType)115     public static Intent composeQuickContactIntent(Context context, Uri contactLookupUri,
116             int previousScreenType) {
117         return composeQuickContactIntent(context, contactLookupUri,
118                 QuickContactActivity.MODE_FULLY_EXPANDED, previousScreenType);
119     }
120 
121     /**
122      * Returns an implicit intent for opening QuickContacts.
123      */
composeQuickContactIntent(Context context, Uri contactLookupUri, int mode, int previousScreenType)124     public static Intent composeQuickContactIntent(Context context, Uri contactLookupUri,
125             int mode, int previousScreenType) {
126         final Intent intent = new Intent(context, QuickContactActivity.class);
127         intent.setAction(QuickContact.ACTION_QUICK_CONTACT);
128         intent.setData(contactLookupUri);
129         intent.putExtra(QuickContact.EXTRA_MODE, mode);
130         // Make sure not to show QuickContacts on top of another QuickContacts.
131         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
132         intent.putExtra(QuickContactActivity.EXTRA_PREVIOUS_SCREEN_TYPE, previousScreenType);
133         return intent;
134     }
135 
136     /**
137      * Returns an Intent to open the Settings add account activity filtered to only
138      * display contact provider account types.
139      */
getIntentForAddingAccount()140     public static Intent getIntentForAddingAccount() {
141         final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
142         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
143         intent.putExtra(Settings.EXTRA_AUTHORITIES,
144                 new String[]{ContactsContract.AUTHORITY});
145         return intent;
146     }
147 
148     /**
149      * Returns an Intent to add a google account.
150      */
getIntentForAddingGoogleAccount()151     public static Intent getIntentForAddingGoogleAccount() {
152         final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
153         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
154         intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,
155                 new String[]{GoogleAccountType.ACCOUNT_TYPE});
156         return intent;
157     }
158 
getIntentForQuickContactLauncherShortcut(Context context, Uri contactUri)159     public static Intent getIntentForQuickContactLauncherShortcut(Context context, Uri contactUri) {
160         final Intent intent = composeQuickContactIntent(context, contactUri,
161                 QuickContact.MODE_LARGE, ScreenType.UNKNOWN);
162         intent.setPackage(context.getPackageName());
163 
164         // When starting from the launcher, start in a new, cleared task.
165         // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
166         // clear the whole thing preemptively here since QuickContactActivity will
167         // finish itself when launching other detail activities. We need to use
168         // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect
169         // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra.
170         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
171                 | Intent.FLAG_ACTIVITY_NO_ANIMATION);
172 
173         // Tell the launcher to not do its animation, because we are doing our own
174         intent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
175 
176         intent.putExtra(QuickContact.EXTRA_EXCLUDE_MIMES, (String[])null);
177 
178         return intent;
179     }
180 
181     /**
182      * Returns a copy of {@param intent} with a class name set, if a class inside this app
183      * has a corresponding intent filter.
184      */
getIntentInAppIfExists(Context context, Intent intent)185     private static Intent getIntentInAppIfExists(Context context, Intent intent) {
186         try {
187             final Intent intentCopy = new Intent(intent);
188             // Force this intentCopy to open inside the current app.
189             intentCopy.setPackage(context.getPackageName());
190             final List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(
191                     intentCopy, PackageManager.MATCH_DEFAULT_ONLY);
192             if (list != null && list.size() != 0) {
193                 // Now that we know the intentCopy will work inside the current app, we
194                 // can return this intent non-null.
195                 if (list.get(0).activityInfo != null
196                         && !TextUtils.isEmpty(list.get(0).activityInfo.name)) {
197                     // Now that we know the class name, we may as well attach it to intentCopy
198                     // to prevent the package manager from needing to find it again inside
199                     // startActivity(). This is only needed for efficiency.
200                     intentCopy.setClassName(context.getPackageName(),
201                             list.get(0).activityInfo.name);
202                 }
203                 return intentCopy;
204             }
205             return null;
206         } catch (Exception e) {
207             // Don't let the package manager crash our app. If the package manager can't resolve the
208             // intent here, then we can still call startActivity without calling setClass() first.
209             return null;
210         }
211     }
212 }
213