Implement editor springboard activity

Have the springboard handle edit intents.
Show a dialog if the contact is made of multiple raw contacts.
Go straight to editor if:
  * Single raw contact
  * Given a raw contact Uri
In the case of 1 read 1 writable, we still show the dialog
since there would otherwise be no way to view what data comes
from the read only raw contact.
The springboard does not handle legacy contact Uris and will throw
an exception if one is received.

Test:
Tested these scenarios:
 1) Edit a single raw contact
 2) Edit a single read only raw contact
 3) Edit a contact made of >2 raws
 4) Add new contact
 5) Edit a contact made of one read only, one writable
 6) Made edits and checked if quick contact continued to update
 7) The relevant edit intents from the Test app

Bug: 31826229
Bug: 31088704
Change-Id: I4c1c44accc86521efce2081744189d25f00ec541
diff --git a/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
new file mode 100644
index 0000000..f32ee5b
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditorSpringBoardActivity.java
@@ -0,0 +1,182 @@
+package com.android.contacts.activities;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.app.LoaderManager;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.widget.Toast;
+
+import com.android.contacts.AppCompatContactsActivity;
+import com.android.contacts.R;
+import com.android.contacts.common.activity.RequestPermissionsActivity;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+import com.android.contacts.editor.ContactEditorFragment;
+import com.android.contacts.editor.EditorIntents;
+import com.android.contacts.editor.PickRawContactDialogFragment;
+import com.android.contacts.editor.PickRawContactLoader;
+
+/**
+ * Transparent springboard activity that hosts a dialog to select a raw contact to edit.
+ * This activity has noHistory set to true, and all intents coming out from it have
+ * {@code FLAG_ACTIVITY_FORWARD_RESULT} set.
+ */
+public class ContactEditorSpringBoardActivity extends AppCompatContactsActivity  {
+    private static final String TAG = "EditorSpringBoard";
+    private static final String TAG_RAW_CONTACTS_DIALOG = "rawContactsDialog";
+    private static final int LOADER_RAW_CONTACTS = 1;
+
+    private Uri mUri;
+    private Cursor mCursor;
+    private MaterialPalette mMaterialPalette;
+
+    /**
+     * The contact data loader listener.
+     */
+    protected final LoaderManager.LoaderCallbacks<Cursor> mRawContactLoaderListener =
+            new LoaderManager.LoaderCallbacks<Cursor>() {
+
+                @Override
+                public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+                    return new PickRawContactLoader(ContactEditorSpringBoardActivity.this, mUri);
+                }
+
+                @Override
+                public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+                    if (cursor == null) {
+                        Toast.makeText(ContactEditorSpringBoardActivity.this,
+                                R.string.editor_failed_to_load, Toast.LENGTH_SHORT).show();
+                        finish();
+                        return;
+                    }
+                    mCursor = cursor;
+                    if (mCursor.getCount() == 1) {
+                        loadEditor();
+                    } else {
+                        showDialog();
+                    }
+                }
+
+                @Override
+                public void onLoaderReset(Loader<Cursor> loader) {
+                    mCursor = null;
+                }
+            };
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (RequestPermissionsActivity.startPermissionActivity(this)) {
+            return;
+        }
+
+        final Intent intent = getIntent();
+        final String action = intent.getAction();
+
+        if (!Intent.ACTION_EDIT.equals(action)) {
+            finish();
+            return;
+        }
+        // Just for shorter variable names.
+        final String primary = ContactEditorFragment.INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR;
+        final String secondary =
+                ContactEditorFragment.INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR;
+        if (intent.hasExtra(primary) && intent.hasExtra(secondary)) {
+            mMaterialPalette = new MaterialPalette(intent.getIntExtra(primary, -1),
+                    intent.getIntExtra(secondary, -1));
+        }
+
+        mUri = intent.getData();
+        final String authority = mUri.getAuthority();
+        final String type = getContentResolver().getType(mUri);
+        // Go straight to editor if we're passed a raw contact Uri.
+        if (ContactsContract.AUTHORITY.equals(authority) &&
+                RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+            final long rawContactId = ContentUris.parseId(mUri);
+            final Intent editorIntent = getIntentForRawContact(rawContactId);
+            ImplicitIntentsUtil.startActivityInApp(this, editorIntent);
+        } else {
+            getLoaderManager().initLoader(LOADER_RAW_CONTACTS, null, mRawContactLoaderListener);
+        }
+    }
+
+    /**
+     * Start the dialog to pick the raw contact to edit.
+     */
+    private void showDialog() {
+        final FragmentManager fm = getFragmentManager();
+        final PickRawContactDialogFragment oldFragment = (PickRawContactDialogFragment)
+                fm.findFragmentByTag(TAG_RAW_CONTACTS_DIALOG);
+        final FragmentTransaction ft = fm.beginTransaction();
+        if (oldFragment != null) {
+            ft.remove(oldFragment);
+        }
+        final PickRawContactDialogFragment newFragment =
+                PickRawContactDialogFragment.getInstance(mUri, mCursor, mMaterialPalette);
+        ft.add(newFragment, TAG_RAW_CONTACTS_DIALOG);
+        // commitAllowingStateLoss is safe in this activity because the fragment entirely depends
+        // on the result of the loader. Even if we lose the fragment because the activity was
+        // in the background, when it comes back onLoadFinished will be called again which will
+        // have all the state the picker needs. This situation should be very rare, since the load
+        // should be quick.
+        ft.commitAllowingStateLoss();
+    }
+
+    /**
+     * Starts the editor for the first (only) raw contact in the cursor.
+     */
+    private void loadEditor() {
+        final Intent intent;
+        if (isSingleWritableAccount()) {
+            mCursor.moveToFirst();
+            final long rawContactId = mCursor.getLong(PickRawContactLoader.RAW_CONTACT_ID);
+            intent = getIntentForRawContact(rawContactId);
+
+        } else {
+            // If it's a single read-only raw contact, we'll want to let the editor create
+            // the writable raw contact for it.
+            intent = EditorIntents.createEditContactIntent(this, mUri, mMaterialPalette, -1);
+            intent.setClass(this, ContactEditorActivity.class);
+        }
+        ImplicitIntentsUtil.startActivityInApp(this, intent);
+    }
+
+    /**
+     * @return true if there is only one raw contact in the contact and it is from a writable
+     * account.
+     */
+    private boolean isSingleWritableAccount() {
+        if (mCursor.getCount() != 1) {
+            return false;
+        }
+        mCursor.moveToFirst();
+        final String accountType = mCursor.getString(PickRawContactLoader.ACCOUNT_TYPE);
+        final String dataSet = mCursor.getString(PickRawContactLoader.DATA_SET);
+        final AccountType account = AccountTypeManager.getInstance(this)
+                .getAccountType(accountType, dataSet);
+        return account.areContactsWritable();
+    }
+
+    /**
+     * Returns an intent to load the editor for the given raw contact. Sets
+     * {@code FLAG_ACTIVITY_FORWARD_RESULT} in case the activity that started us expects a result.
+     * @param rawContactId Raw contact to edit
+     */
+    private Intent getIntentForRawContact(long rawContactId) {
+        final Intent intent = EditorIntents.createEditContactIntentForRawContact(
+                this, mUri, rawContactId, mMaterialPalette);
+        intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+        return intent;
+    }
+}
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 80e1b82..0542436 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -203,6 +203,13 @@
     public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
 
     /**
+     * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
+     * by itself.
+     */
+    public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
+            "raw_contact_id_to_display_alone";
+
+    /**
      * Intent extra to specify a {@link ContactEditor.SaveMode}.
      */
     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
@@ -1478,6 +1485,8 @@
                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
             }
+            mRawContactIdToDisplayAlone = mIntentExtras
+                    .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
         }
     }
 
diff --git a/src/com/android/contacts/editor/EditorIntents.java b/src/com/android/contacts/editor/EditorIntents.java
index c4f48e6..c903b84 100644
--- a/src/com/android/contacts/editor/EditorIntents.java
+++ b/src/com/android/contacts/editor/EditorIntents.java
@@ -24,6 +24,7 @@
 import android.text.TextUtils;
 
 import com.android.contacts.activities.ContactEditorActivity;
+import com.android.contacts.activities.ContactEditorSpringBoardActivity;
 import com.android.contacts.common.model.RawContactDeltaList;
 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
 
@@ -38,19 +39,32 @@
     }
 
     /**
-     * Returns an Intent to start the {@link ContactEditorActivity} for an
+     * Returns an Intent to start the {@link ContactEditorSpringBoardActivity} for an
      * existing contact.
      */
-    public static Intent createEditContactIntent(Context context, Uri contactLookupUri,
+    public static Intent createEditContactIntent(Context context, Uri uri,
             MaterialPalette materialPalette, long photoId) {
-        final Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri, context,
-                ContactEditorActivity.class);
+        final Intent intent = new Intent(Intent.ACTION_EDIT, uri, context,
+                ContactEditorSpringBoardActivity.class);
         putMaterialPalette(intent, materialPalette);
         putPhotoId(intent, photoId);
         return intent;
     }
 
     /**
+     * Returns an Intent to start the {@link ContactEditorActivity} for the given raw contact.
+     */
+    public static Intent createEditContactIntentForRawContact(Context context,
+            Uri uri, long rawContactId, MaterialPalette materialPalette) {
+        final Intent intent = new Intent(Intent.ACTION_EDIT, uri, context,
+                ContactEditorActivity.class);
+        intent.putExtra(ContactEditorFragment.INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE,
+                rawContactId);
+        putMaterialPalette(intent, materialPalette);
+        return intent;
+    }
+
+    /**
      * Returns an Intent to start the {@link ContactEditorActivity} for a new contact with
      * the field values specified by rawContactDeltaList pre-populate in the form.
      */
@@ -71,9 +85,9 @@
      * Returns an Intent to edit a different contact in the editor with whatever
      * values were already entered on the current editor.
      */
-    public static Intent createEditOtherContactIntent(Context context, Uri contactLookupUri,
+    public static Intent createEditOtherContactIntent(Context context, Uri uri,
             ArrayList<ContentValues> contentValues) {
-        final Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri, context,
+        final Intent intent = new Intent(Intent.ACTION_EDIT, uri, context,
                 ContactEditorActivity.class);
         intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
                 | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
diff --git a/src/com/android/contacts/editor/PickRawContactDialogFragment.java b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
new file mode 100644
index 0000000..20e8f35
--- /dev/null
+++ b/src/com/android/contacts/editor/PickRawContactDialogFragment.java
@@ -0,0 +1,162 @@
+package com.android.contacts.editor;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ImplicitIntentsUtil;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+
+/**
+ * Dialog containing the raw contacts that make up a contact. On selection the editor is loaded
+ * for the chosen raw contact.
+ */
+public class PickRawContactDialogFragment extends DialogFragment {
+    /**
+     * Used to list the account info for the given raw contacts list.
+     */
+    private static final class RawContactAccountListAdapter extends CursorAdapter {
+        private final LayoutInflater mInflater;
+        private final Context mContext;
+
+        public RawContactAccountListAdapter(Context context, Cursor cursor) {
+            super(context, cursor, 0);
+            mContext = context;
+            mInflater = LayoutInflater.from(context);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            final long rawContactId = cursor.getLong(PickRawContactLoader.RAW_CONTACT_ID);
+            final String accountName = cursor.getString(PickRawContactLoader.ACCOUNT_NAME);
+            final String accountType = cursor.getString(PickRawContactLoader.ACCOUNT_TYPE);
+            final String dataSet = cursor.getString(PickRawContactLoader.DATA_SET);
+            final AccountType account = AccountTypeManager.getInstance(mContext)
+                    .getAccountType(accountType, dataSet);
+
+            final ContactsPreferences prefs = new ContactsPreferences(mContext);
+            final int displayNameColumn =
+                    prefs.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+                            ? PickRawContactLoader.DISPLAY_NAME_PRIMARY
+                            : PickRawContactLoader.DISPLAY_NAME_ALTERNATIVE;
+            String displayName = cursor.getString(displayNameColumn);
+
+            final TextView nameView = (TextView) view.findViewById(
+                    R.id.display_name);
+            final TextView accountTextView = (TextView) view.findViewById(
+                    R.id.account_name);
+            final ImageView accountIconView = (ImageView) view.findViewById(
+                    R.id.account_icon);
+
+            if (!account.areContactsWritable()) {
+                displayName = mContext
+                        .getString(R.string.contact_editor_pick_raw_contact_read_only, displayName);
+                view.setAlpha(.38f);
+            } else {
+                view.setAlpha(1f);
+            }
+
+            nameView.setText(displayName);
+            accountTextView.setText(accountName);
+            accountIconView.setImageDrawable(account.getDisplayIcon(mContext));
+
+            final ContactPhotoManager.DefaultImageRequest
+                    request = new ContactPhotoManager.DefaultImageRequest(
+                    displayName, String.valueOf(rawContactId), /* isCircular = */ true);
+            final ImageView photoView = (ImageView) view.findViewById(
+                    R.id.photo);
+            ContactPhotoManager.getInstance(mContext).loadDirectoryPhoto(photoView,
+                    ContactPhotoManager.getDefaultAvatarUriForContact(request),
+                    /* darkTheme = */ false,
+                    /* isCircular = */ true,
+                    request);
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            return mInflater.inflate(R.layout.raw_contact_list_item, parent, false);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            getCursor().moveToPosition(position);
+            return getCursor().getLong(PickRawContactLoader.RAW_CONTACT_ID);
+        }
+    }
+
+    // Cursor holding all raw contact rows for the given Contact.
+    private Cursor mCursor;
+    // Uri for the whole Contact.
+    private Uri mUri;
+    private MaterialPalette mMaterialPalette;
+
+    public static PickRawContactDialogFragment getInstance(Uri uri, Cursor cursor,
+            MaterialPalette materialPalette) {
+        final PickRawContactDialogFragment fragment = new PickRawContactDialogFragment();
+        fragment.setUri(uri);
+        fragment.setCursor(cursor);
+        fragment.setMaterialPalette(materialPalette);
+        return fragment;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        final CursorAdapter adapter = new RawContactAccountListAdapter(getContext(), mCursor);
+        builder.setTitle(R.string.contact_editor_pick_raw_contact_dialog_title);
+        builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final long rawContactId = adapter.getItemId(which);
+                final Intent intent = EditorIntents.createEditContactIntentForRawContact(
+                        getActivity(), mUri, rawContactId, mMaterialPalette);
+                intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+                ImplicitIntentsUtil.startActivityInApp(getActivity(), intent);
+            }
+        });
+        builder.setCancelable(true);
+        return builder.create();
+    }
+
+    @Override
+    public void onDismiss(DialogInterface dialog) {
+        super.onDismiss(dialog);
+        mCursor = null;
+        finishActivity();
+    }
+
+    private void setUri(Uri uri) {
+        mUri = uri;
+    }
+
+    private void setCursor(Cursor cursor) {
+        mCursor = cursor;
+    }
+
+    private void setMaterialPalette(MaterialPalette materialPalette) {
+        mMaterialPalette = materialPalette;
+    }
+
+    private void finishActivity() {
+        if (getActivity() != null && !getActivity().isFinishing()) {
+            getActivity().finish();
+        }
+    }
+}
diff --git a/src/com/android/contacts/editor/PickRawContactLoader.java b/src/com/android/contacts/editor/PickRawContactLoader.java
new file mode 100644
index 0000000..62be517
--- /dev/null
+++ b/src/com/android/contacts/editor/PickRawContactLoader.java
@@ -0,0 +1,78 @@
+package com.android.contacts.editor;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * Loader for the pick a raw contact to edit activity. Loads all raw contact metadata for the
+ * given Contact {@link Uri}.
+ */
+public class PickRawContactLoader extends CursorLoader {
+    private Uri mContactUri;
+
+    public static final String[] COLUMNS = new String[] {
+            RawContacts.ACCOUNT_NAME,
+            RawContacts.ACCOUNT_TYPE,
+            RawContacts.DATA_SET,
+            RawContacts._ID,
+            RawContacts.DISPLAY_NAME_PRIMARY,
+            RawContacts.DISPLAY_NAME_ALTERNATIVE
+    };
+
+    public static final String SELECTION = RawContacts.CONTACT_ID + "=?";
+
+    public static final int ACCOUNT_NAME = 0;
+    public static final int ACCOUNT_TYPE = 1;
+    public static final int DATA_SET = 2;
+    public static final int RAW_CONTACT_ID = 3;
+    public static final int DISPLAY_NAME_PRIMARY = 4;
+    public static final int DISPLAY_NAME_ALTERNATIVE = 5;
+
+    public PickRawContactLoader(Context context, Uri contactUri) {
+        super(context, ensureIsContactUri(contactUri), COLUMNS, SELECTION, null, RawContacts._ID);
+        mContactUri = contactUri;
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        // Get the id of the contact we're looking at.
+        final Cursor cursor = getContext().getContentResolver()
+                .query(mContactUri, new String[] { Contacts._ID }, null,
+                null, null);
+
+        if (cursor == null) {
+            return null;
+        }
+
+        if (cursor.getCount() < 1) {
+            cursor.close();
+            return null;
+        }
+
+        cursor.moveToFirst();
+        final long contactId = cursor.getLong(0);
+        cursor.close();
+        // Update selection arguments and uri.
+        setSelectionArgs(new String[]{ Long.toString(contactId) });
+        setUri(RawContacts.CONTENT_URI);
+        return super.loadInBackground();
+    }
+
+    /**
+     * Ensures that this is a valid contact URI. If invalid, then an exception is
+     * thrown. Otherwise, the original URI is returned.
+     */
+    private static Uri ensureIsContactUri(final Uri uri) {
+        if (uri == null) {
+            throw new IllegalArgumentException("Uri must not be null");
+        }
+        if (!uri.toString().startsWith(Contacts.CONTENT_URI.toString())) {
+            throw new IllegalArgumentException("Invalid contact Uri: " + uri);
+        }
+        return uri;
+    }
+}