Add dynamic launcher shortcuts.

Currently the shortcuts are created for the top 3 contacts returned from
Contacts.CONTENT_STREQUENT_URI

Test:
Added unit tests for DynamicShortcuts but currently suppressed because they
require AndroidJUnitRunner
Manual:
* Use N_MR1 device with recent dogfood Nexus launcher installed.
* launch app
* star some contacts if needed
* press home
* long press launcher icon
* verify that starred contacts show in list of shortcuts
* unstar some contacts
* verify that shortcuts change
* pin a shortcut
* remove contact for pinned shortcut
* verify that pinned shortcut is disabled
* pin a shortcut
* change name of contact for pinned shortcut
* verify that name on pinned shortcut changes

Also prevent disambiguation dialog for other home screen shortcuts

Bug 30189449
Bug 31628994
Change-Id: Iace4b1c88b51ba1f7973c6f4ef90002fb92d0784
diff --git a/src/com/android/contacts/ContactsJobService.java b/src/com/android/contacts/ContactsJobService.java
new file mode 100644
index 0000000..c60a0a7
--- /dev/null
+++ b/src/com/android/contacts/ContactsJobService.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+
+/**
+ * Service to run {@link android.app.job.JobScheduler} jobs.
+ */
+public class ContactsJobService extends JobService {
+
+    public static final int DYNAMIC_SHORTCUTS_JOB_ID = 1;
+
+    @Override
+    public boolean onStartJob(JobParameters jobParameters) {
+        switch (jobParameters.getJobId()) {
+            case DYNAMIC_SHORTCUTS_JOB_ID:
+                DynamicShortcuts.updateFromJob(this, jobParameters);
+                return true;
+            default:
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters jobParameters) {
+        return false;
+    }
+}
diff --git a/src/com/android/contacts/DynamicShortcuts.java b/src/com/android/contacts/DynamicShortcuts.java
new file mode 100644
index 0000000..2e873d8
--- /dev/null
+++ b/src/com/android/contacts/DynamicShortcuts.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.Experiments;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contactsbind.experiments.Flags;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.android.contacts.common.list.ShortcutIntentBuilder.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION;
+
+/**
+ * This class creates and updates the dynamic shortcuts displayed on the Nexus launcher for the
+ * Contacts app.
+ *
+ * Currently it adds shortcuts for the top 3 contacts in the {@link Contacts#CONTENT_STREQUENT_URI}
+ *
+ * Usage: DynamicShortcuts.initialize should be called during Application creation. This will
+ * schedule a Job to keep the shortcuts up-to-date so no further interations should be necessary.
+ */
+@TargetApi(Build.VERSION_CODES.N_MR1)
+public class DynamicShortcuts {
+    private static final String TAG = "DynamicShortcuts";
+
+    // Note the Nexus launcher automatically truncates shortcut labels if they exceed these limits
+    // however, we implement our own truncation in case the shortcut is shown on a launcher that
+    // has different behavior
+    private static final int SHORT_LABEL_MAX_LENGTH = 12;
+    private static final int LONG_LABEL_MAX_LENGTH = 30;
+    private static final int MAX_SHORTCUTS = 3;
+
+    /**
+     * How long  to wait after a change to the contacts content uri before updating the shortcuts
+     * This increases the likelihood that multiple updates will be coalesced in the case that
+     * the updates are happening rapidly
+     *
+     * TODO: this should probably be externally configurable to make it easier to manually test the
+     * behavior
+     */
+    private static final int CONTENT_CHANGE_MIN_UPDATE_DELAY_MILLIS = 10000; // 10 seconds
+    /**
+     * The maximum time to wait before updating the shortcuts that may have changed.
+     *
+     * TODO: this should probably be externally configurable to make it easier to manually test the
+     * behavior
+     */
+    private static final int CONTENT_CHANGE_MAX_UPDATE_DELAY_MILLIS = 24*60*60*1000; // 1 day
+
+    // The spec specifies that it should be 44dp @ xxxhdpi
+    // Note that ShortcutManager.getIconMaxWidth and ShortcutManager.getMaxHeight return different
+    // (larger) values.
+    private static final int RECOMMENDED_ICON_PIXEL_LENGTH = 176;
+
+    @VisibleForTesting
+    static final String[] PROJECTION = new String[] {
+            Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME_PRIMARY
+    };
+
+    private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final ShortcutManager mShortcutManager;
+    private int mShortLabelMaxLength = SHORT_LABEL_MAX_LENGTH;
+    private int mLongLabelMaxLength = LONG_LABEL_MAX_LENGTH;
+
+    public DynamicShortcuts(Context context) {
+        this(context, context.getContentResolver(), (ShortcutManager)
+                context.getSystemService(Context.SHORTCUT_SERVICE));
+    }
+
+    public DynamicShortcuts(Context context, ContentResolver contentResolver,
+            ShortcutManager shortcutManager) {
+        mContext = context;
+        mContentResolver = contentResolver;
+        mShortcutManager = shortcutManager;
+    }
+
+    @VisibleForTesting
+    void setShortLabelMaxLength(int length) {
+        this.mShortLabelMaxLength = length;
+    }
+
+    @VisibleForTesting
+    void setLongLabelMaxLength(int length) {
+        this.mLongLabelMaxLength = length;
+    }
+
+    @VisibleForTesting
+    void refresh() {
+        mShortcutManager.setDynamicShortcuts(getStrequentShortcuts());
+        updatePinned();
+    }
+
+    @VisibleForTesting
+    void updatePinned() {
+        final List<ShortcutInfo> updates = new ArrayList<>();
+        final List<String> removedIds = new ArrayList<>();
+        final List<String> enable = new ArrayList<>();
+
+        for (ShortcutInfo shortcut : mShortcutManager.getPinnedShortcuts()) {
+
+            final PersistableBundle extras = shortcut.getExtras();
+            // The contact ID may have changed but that's OK because it is just an optimization
+            final long contactId = extras == null ? 0 : extras.getLong(Contacts._ID);
+
+            final ShortcutInfo update = createShortcutForUri(
+                    Contacts.getLookupUri(contactId, shortcut.getId()));
+            if (update != null) {
+                updates.add(update);
+                if (!shortcut.isEnabled()) {
+                    // Handle the case that a contact is disabled because it doesn't exist but
+                    // later is created (for instance by a sync)
+                    enable.add(update.getId());
+                }
+            } else if (shortcut.isEnabled()) {
+                removedIds.add(shortcut.getId());
+            }
+        }
+
+        mShortcutManager.updateShortcuts(updates);
+        mShortcutManager.enableShortcuts(enable);
+        mShortcutManager.disableShortcuts(removedIds,
+                mContext.getString(R.string.dynamic_shortcut_contact_removed_message));
+    }
+
+    private ShortcutInfo createShortcutForUri(Uri contactUri) {
+        final Cursor cursor = mContentResolver.query(contactUri, PROJECTION, null, null, null);
+        if (cursor == null) return null;
+
+        try {
+            if (cursor.moveToFirst()) {
+                return createShortcutFromRow(cursor);
+            }
+        } finally {
+            cursor.close();
+        }
+        return null;
+    }
+
+    public List<ShortcutInfo> getStrequentShortcuts() {
+        // The limit query parameter doesn't seem to work for this uri but we'll leave it because in
+        // case it does work on some phones or platform versions.
+        final Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
+                .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
+                        String.valueOf(MAX_SHORTCUTS))
+                .build();
+        final Cursor cursor = mContentResolver.query(uri, PROJECTION, null, null, null);
+
+        if (cursor == null) return Collections.emptyList();
+
+        final List<ShortcutInfo> result = new ArrayList<>();
+
+        try {
+            // For some reason the limit query parameter is ignored for the strequent content uri
+            for (int i = 0; i < MAX_SHORTCUTS && cursor.moveToNext(); i++) {
+                result.add(createShortcutFromRow(cursor));
+            }
+        } finally {
+            cursor.close();
+        }
+        return result;
+    }
+
+
+    @VisibleForTesting
+    ShortcutInfo createShortcutFromRow(Cursor cursor) {
+        final ShortcutInfo.Builder builder = builderForContactShortcut(cursor);
+        addIconForContact(cursor, builder);
+        return builder.build();
+    }
+
+    @VisibleForTesting
+    ShortcutInfo.Builder builderForContactShortcut(Cursor cursor) {
+        final long id = cursor.getLong(0);
+        final String lookupKey = cursor.getString(1);
+        final String displayName = cursor.getString(2);
+        return builderForContactShortcut(id, lookupKey, displayName);
+    }
+
+    @VisibleForTesting
+    ShortcutInfo.Builder builderForContactShortcut(long id, String lookupKey, String displayName) {
+        final PersistableBundle extras = new PersistableBundle();
+        extras.putLong(Contacts._ID, id);
+
+        final ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, lookupKey)
+                .setIntent(ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut(mContext,
+                        Contacts.getLookupUri(id, lookupKey)))
+                .setDisabledMessage(mContext.getString(R.string.dynamic_shortcut_disabled_message))
+                .setExtras(extras);
+
+        if (displayName.length() < mLongLabelMaxLength) {
+            builder.setLongLabel(displayName);
+        } else {
+            builder.setLongLabel(displayName.substring(0, mLongLabelMaxLength - 1).trim() + "…");
+        }
+
+        if (displayName.length() < mShortLabelMaxLength) {
+            builder.setShortLabel(displayName);
+        } else {
+            builder.setShortLabel(displayName.substring(0, mShortLabelMaxLength - 1).trim() + "…");
+        }
+        return builder;
+    }
+
+    private void addIconForContact(Cursor cursor, ShortcutInfo.Builder builder) {
+        final long id = cursor.getLong(0);
+        final String lookupKey = cursor.getString(1);
+        final String displayName = cursor.getString(2);
+
+        final Bitmap bitmap = getContactPhoto(id);
+        if (bitmap != null) {
+            builder.setIcon(Icon.createWithBitmap(bitmap));
+        } else {
+            builder.setIcon(Icon.createWithBitmap(getFallbackAvatar(displayName, lookupKey)));
+        }
+    }
+
+    private Bitmap getContactPhoto(long id) {
+        final InputStream photoStream = Contacts.openContactPhotoInputStream(
+                mContext.getContentResolver(),
+                ContentUris.withAppendedId(Contacts.CONTENT_URI, id), true);
+
+        if (photoStream == null) return null;
+        try {
+            final Bitmap bitmap = decodeStreamForShortcut(photoStream);
+            photoStream.close();
+            return bitmap;
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to decode contact photo for shortcut. ID=" + id, e);
+            return null;
+        } finally {
+            try {
+                photoStream.close();
+            } catch (IOException e) {
+                // swallow
+            }
+        }
+    }
+
+    private Bitmap decodeStreamForShortcut(InputStream stream) throws IOException {
+        final BitmapRegionDecoder bitmapDecoder = BitmapRegionDecoder.newInstance(stream, false);
+
+        final int sourceWidth = bitmapDecoder.getWidth();
+        final int sourceHeight = bitmapDecoder.getHeight();
+
+        final int iconMaxWidth = mShortcutManager.getIconMaxWidth();;
+        final int iconMaxHeight = mShortcutManager.getIconMaxHeight();
+
+        final int sampleSize = Math.min(
+                BitmapUtil.findOptimalSampleSize(sourceWidth,
+                        RECOMMENDED_ICON_PIXEL_LENGTH),
+                BitmapUtil.findOptimalSampleSize(sourceHeight,
+                        RECOMMENDED_ICON_PIXEL_LENGTH));
+        final BitmapFactory.Options opts = new BitmapFactory.Options();
+        opts.inSampleSize = sampleSize;
+
+        final int scaledWidth = sourceWidth / opts.inSampleSize;
+        final int scaledHeight = sourceHeight / opts.inSampleSize;
+
+        final int targetWidth = Math.min(scaledWidth, iconMaxWidth);
+        final int targetHeight = Math.min(scaledHeight, iconMaxHeight);
+
+        // Make it square.
+        final int targetSize = Math.min(targetWidth, targetHeight);
+
+        // The region is defined in the coordinates of the source image then the sampling is
+        // done on the extracted region.
+        final int prescaledXOffset = ((scaledWidth - targetSize) * opts.inSampleSize) / 2;
+        final int prescaledYOffset = ((scaledHeight - targetSize) * opts.inSampleSize) / 2;
+
+        final Bitmap bitmap = bitmapDecoder.decodeRegion(new Rect(
+                prescaledXOffset, prescaledYOffset,
+                sourceWidth - prescaledXOffset, sourceHeight - prescaledYOffset
+        ), opts);
+
+        bitmapDecoder.recycle();
+
+        return BitmapUtil.getRoundedBitmap(bitmap, targetSize, targetSize);
+    }
+
+    private Bitmap getFallbackAvatar(String displayName, String lookupKey) {
+        final int w = RECOMMENDED_ICON_PIXEL_LENGTH;
+        final int h = RECOMMENDED_ICON_PIXEL_LENGTH;
+
+        final ContactPhotoManager.DefaultImageRequest request =
+                new ContactPhotoManager.DefaultImageRequest(displayName, lookupKey, true);
+        final Drawable avatar = ContactPhotoManager.getDefaultAvatarDrawableForContact(
+                mContext.getResources(), true, request);
+        final Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+        // The avatar won't draw unless it thinks it is visible
+        avatar.setVisible(true, true);
+        final Canvas canvas = new Canvas(result);
+        avatar.setBounds(0, 0, w, h);
+        avatar.draw(canvas);
+        return result;
+    }
+
+    private void handleFlagDisabled() {
+        mShortcutManager.removeAllDynamicShortcuts();
+
+        final List<ShortcutInfo> pinned = mShortcutManager.getPinnedShortcuts();
+        final List<String> ids = new ArrayList<>(pinned.size());
+        for (ShortcutInfo shortcut : pinned) {
+            ids.add(shortcut.getId());
+        }
+        mShortcutManager.disableShortcuts(ids, mContext
+                .getString(R.string.dynamic_shortcut_disabled_message));
+    }
+
+    @VisibleForTesting
+    void scheduleUpdateJob() {
+        final JobInfo job = new JobInfo.Builder(
+                ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID,
+                new ComponentName(mContext, ContactsJobService.class))
+                // We just observe all changes to contacts. It would be better to be more granular
+                // but CP2 only notifies using this URI anyway so there isn't any point in adding
+                // that complexity.
+                .addTriggerContentUri(new JobInfo.TriggerContentUri(ContactsContract.AUTHORITY_URI,
+                        JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
+                .setTriggerContentUpdateDelay(CONTENT_CHANGE_MIN_UPDATE_DELAY_MILLIS)
+                .setTriggerContentMaxDelay(CONTENT_CHANGE_MAX_UPDATE_DELAY_MILLIS).build();
+        final JobScheduler scheduler = (JobScheduler)
+                mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        scheduler.schedule(job);
+    }
+
+    public synchronized static void initialize(Context context) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return;
+
+        final DynamicShortcuts shortcuts = new DynamicShortcuts(context);
+        if (!Flags.getInstance(context).getBoolean(Experiments.DYNAMIC_SHORTCUTS)) {
+            // Clear dynamic shortcuts if the flag is not enabled. This prevents shortcuts from
+            // staying around if it is enabled then later disabled (due to bugs for instance).
+            shortcuts.handleFlagDisabled();
+        } else if (!isJobScheduled(context)) {
+            // Update the shortcuts. If the job is already scheduled then either the app is being
+            // launched to run the job in which case the shortcuts will get updated when it runs or
+            // it has been launched for some other reason and the data we care about for shortcuts
+            // hasn't changed. Because the job reschedules itself after completion this check
+            // essentially means that this will run on each app launch that happens after a reboot.
+            // Note: the task schedules the job after completing.
+            new ShortcutUpdateTask(shortcuts).execute();
+        }
+    }
+
+    public static void updateFromJob(final JobService service, final JobParameters jobParams) {
+        new ShortcutUpdateTask(new DynamicShortcuts(service)) {
+            @Override
+            protected void onPostExecute(Void aVoid) {
+                // Must call super first which will reschedule the job before we call jobFinished
+                super.onPostExecute(aVoid);
+                service.jobFinished(jobParams, false);
+            }
+        }.execute();
+    }
+
+    @VisibleForTesting
+    public static boolean isJobScheduled(Context context) {
+        final JobScheduler scheduler = (JobScheduler) context
+                .getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        return scheduler.getPendingJob(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID) != null;
+    }
+
+    private static class ShortcutUpdateTask extends AsyncTask<Void, Void, Void> {
+        private DynamicShortcuts mDynamicShortcuts;
+
+        public ShortcutUpdateTask(DynamicShortcuts shortcuts) {
+            mDynamicShortcuts = shortcuts;
+        }
+
+        @Override
+        protected Void doInBackground(Void... voids) {
+            mDynamicShortcuts.refresh();
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            // The shortcuts may have changed so update the job so that we are observing the
+            // correct Uris
+            mDynamicShortcuts.scheduleUpdateJob();
+        }
+    }
+}
diff --git a/src/com/android/contacts/common/list/ShortcutIntentBuilder.java b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
index f30a176..e230996 100644
--- a/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
+++ b/src/com/android/contacts/common/list/ShortcutIntentBuilder.java
@@ -47,6 +47,7 @@
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.R;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
 
 /**
  * Constructs shortcut intents.
@@ -265,26 +266,8 @@
             String lookupKey, byte[] bitmapData) {
         Drawable drawable = getPhotoDrawable(bitmapData, displayName, lookupKey);
 
-        // Use an implicit intent without a package name set. It is reasonable for a disambiguation
-        // dialog to appear when opening QuickContacts from the launcher. Plus, this will be more
-        // resistant to future package name changes done to Contacts.
-        Intent shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
-
-        // When starting from the launcher, start in a new, cleared task.
-        // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
-        // clear the whole thing preemptively here since QuickContactActivity will
-        // finish itself when launching other detail activities. We need to use
-        // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect
-        // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra.
-        shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
-                | Intent.FLAG_ACTIVITY_NO_ANIMATION);
-
-        // Tell the launcher to not do its animation, because we are doing our own
-        shortcutIntent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
-
-        shortcutIntent.setDataAndType(contactUri, contentType);
-        shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
-                (String[]) null);
+        final Intent shortcutIntent = ImplicitIntentsUtil.getIntentForQuickContactLauncherShortcut(
+                mContext, contactUri);
 
         final Bitmap icon = generateQuickContactIcon(drawable);
 
diff --git a/src/com/android/contacts/common/util/ImplicitIntentsUtil.java b/src/com/android/contacts/common/util/ImplicitIntentsUtil.java
index 47bf605..19d171c 100644
--- a/src/com/android/contacts/common/util/ImplicitIntentsUtil.java
+++ b/src/com/android/contacts/common/util/ImplicitIntentsUtil.java
@@ -32,6 +32,8 @@
 
 import java.util.List;
 
+import static com.android.contacts.common.list.ShortcutIntentBuilder.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION;
+
 /**
  * Utility for forcing intents to be started inside the current app. This is useful for avoiding
  * senseless disambiguation dialogs. Ie, if a user clicks a contact inside Contacts we assume
@@ -130,6 +132,28 @@
         return intent;
     }
 
+    public static Intent getIntentForQuickContactLauncherShortcut(Context context, Uri contactUri) {
+        final Intent intent = composeQuickContactIntent(context, contactUri,
+                QuickContact.MODE_LARGE);
+        intent.setPackage(context.getPackageName());
+
+        // When starting from the launcher, start in a new, cleared task.
+        // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
+        // clear the whole thing preemptively here since QuickContactActivity will
+        // finish itself when launching other detail activities. We need to use
+        // Intent.FLAG_ACTIVITY_NO_ANIMATION since not all versions of launcher will respect
+        // the INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION intent extra.
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK
+                | Intent.FLAG_ACTIVITY_NO_ANIMATION);
+
+        // Tell the launcher to not do its animation, because we are doing our own
+        intent.putExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
+
+        intent.putExtra(QuickContact.EXTRA_EXCLUDE_MIMES, (String[])null);
+
+        return intent;
+    }
+
     /**
      * Returns a copy of {@param intent} with a class name set, if a class inside this app
      * has a corresponding intent filter.