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 startQuickContact(activity, contactLookupUri, previousScreenType, /* requestCode */ -1); 106 } 107 108 /** 109 * Starts QuickContact for result with the default mode and specified previous screen type. 110 */ startQuickContactForResult(Activity activity, Uri contactLookupUri, int previousScreenType, int requestCode)111 public static void startQuickContactForResult(Activity activity, Uri contactLookupUri, 112 int previousScreenType, int requestCode) { 113 startQuickContact(activity, contactLookupUri, previousScreenType, requestCode); 114 } 115 startQuickContact(Activity activity, Uri contactLookupUri, int previousScreenType, int requestCode)116 private static void startQuickContact(Activity activity, Uri contactLookupUri, 117 int previousScreenType, int requestCode) { 118 final Intent intent = ImplicitIntentsUtil.composeQuickContactIntent( 119 activity, contactLookupUri, previousScreenType); 120 121 // We only start "for result" if specifically requested. 122 if (requestCode >= 0) { 123 intent.setPackage(activity.getPackageName()); 124 activity.startActivityForResult(intent, requestCode); 125 } else { 126 startActivityInApp(activity, intent); 127 } 128 } 129 130 /** 131 * Returns an implicit intent for opening QuickContacts with the default mode and specified 132 * previous screen type. 133 */ composeQuickContactIntent(Context context, Uri contactLookupUri, int previousScreenType)134 public static Intent composeQuickContactIntent(Context context, Uri contactLookupUri, 135 int previousScreenType) { 136 return composeQuickContactIntent(context, contactLookupUri, 137 QuickContactActivity.MODE_FULLY_EXPANDED, previousScreenType); 138 } 139 140 /** 141 * Returns an implicit intent for opening QuickContacts. 142 */ composeQuickContactIntent(Context context, Uri contactLookupUri, int mode, int previousScreenType)143 public static Intent composeQuickContactIntent(Context context, Uri contactLookupUri, 144 int mode, int previousScreenType) { 145 final Intent intent = new Intent(context, QuickContactActivity.class); 146 intent.setAction(QuickContact.ACTION_QUICK_CONTACT); 147 intent.setData(contactLookupUri); 148 intent.putExtra(QuickContact.EXTRA_MODE, mode); 149 // Make sure not to show QuickContacts on top of another QuickContacts. 150 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 151 intent.putExtra(QuickContactActivity.EXTRA_PREVIOUS_SCREEN_TYPE, previousScreenType); 152 return intent; 153 } 154 155 /** 156 * Returns an Intent to open the Settings add account activity filtered to only 157 * display contact provider account types. 158 */ getIntentForAddingAccount()159 public static Intent getIntentForAddingAccount() { 160 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 161 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 162 intent.putExtra(Settings.EXTRA_AUTHORITIES, 163 new String[]{ContactsContract.AUTHORITY}); 164 return intent; 165 } 166 167 /** 168 * Returns an Intent to add a google account. 169 */ getIntentForAddingGoogleAccount()170 public static Intent getIntentForAddingGoogleAccount() { 171 final Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT); 172 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 173 intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, 174 new String[]{GoogleAccountType.ACCOUNT_TYPE}); 175 return intent; 176 } 177 getIntentForQuickContactLauncherShortcut(Context context, Uri contactUri)178 public static Intent getIntentForQuickContactLauncherShortcut(Context context, Uri contactUri) { 179 final Intent intent = composeQuickContactIntent(context, contactUri, 180 QuickContact.MODE_LARGE, ScreenType.UNKNOWN); 181 intent.setPackage(context.getPackageName()); 182 183 // When starting from the launcher, start in a new, cleared task. 184 // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we 185 // clear the whole thing preemptively here since QuickContactActivity will 186 // finish itself when launching other detail activities. We need to use 187 // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect 188 // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra. 189 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK 190 | Intent.FLAG_ACTIVITY_NO_ANIMATION); 191 192 // Tell the launcher to not do its animation, because we are doing our own 193 intent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true); 194 195 intent.putExtra(QuickContact.EXTRA_EXCLUDE_MIMES, (String[])null); 196 197 return intent; 198 } 199 200 /** 201 * Returns a copy of {@param intent} with a class name set, if a class inside this app 202 * has a corresponding intent filter. 203 */ getIntentInAppIfExists(Context context, Intent intent)204 private static Intent getIntentInAppIfExists(Context context, Intent intent) { 205 try { 206 final Intent intentCopy = new Intent(intent); 207 // Force this intentCopy to open inside the current app. 208 intentCopy.setPackage(context.getPackageName()); 209 final List<ResolveInfo> list = context.getPackageManager().queryIntentActivities( 210 intentCopy, PackageManager.MATCH_DEFAULT_ONLY); 211 if (list != null && list.size() != 0) { 212 // Now that we know the intentCopy will work inside the current app, we 213 // can return this intent non-null. 214 if (list.get(0).activityInfo != null 215 && !TextUtils.isEmpty(list.get(0).activityInfo.name)) { 216 // Now that we know the class name, we may as well attach it to intentCopy 217 // to prevent the package manager from needing to find it again inside 218 // startActivity(). This is only needed for efficiency. 219 intentCopy.setClassName(context.getPackageName(), 220 list.get(0).activityInfo.name); 221 } 222 return intentCopy; 223 } 224 return null; 225 } catch (Exception e) { 226 // Don't let the package manager crash our app. If the package manager can't resolve the 227 // intent here, then we can still call startActivity without calling setClass() first. 228 return null; 229 } 230 } 231 } 232