Home | History | Annotate | Download | only in util
      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      */
     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      */
     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      */
     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      */
    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      */
    111     public static void startQuickContactForResult(Activity activity, Uri contactLookupUri,
    112             int previousScreenType, int requestCode) {
    113         startQuickContact(activity, contactLookupUri, previousScreenType, requestCode);
    114     }
    115 
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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