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