Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d26a825e authored by Marcus Hagerott's avatar Marcus Hagerott Committed by Android (Google) Code Review
Browse files

Merge "Show snackbar with "undo" action when deleting group." into ub-contactsdialer-g-dev

parents 4139f908 bea2b850
Loading
Loading
Loading
Loading
+35 −27
Original line number Diff line number Diff line
@@ -14,6 +14,12 @@
     limitations under the License.
-->

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    android:id="@+id/list_container"
                    android:layout_width="match_parent"
@@ -44,5 +50,7 @@
                android:layout_width="match_parent" />
        </FrameLayout>

    <include layout="@layout/floating_action_button" />
    </RelativeLayout>

    <include layout="@layout/floating_action_button" />
</android.support.design.widget.CoordinatorLayout>
+5 −1
Original line number Diff line number Diff line
@@ -348,7 +348,7 @@
    <!-- Toast displayed when a label is saved [CHAR LIMIT=30] -->
    <string name="groupSavedToast">Label saved</string>

    <!-- Toast displayed when a label name is deleted. [CHAR LIMIT=50] -->
    <!-- Toast or snackbar displayed when a label name is deleted. [CHAR LIMIT=50] -->
    <string name="groupDeletedToast">Label deleted</string>

    <!-- Toast displayed when a new label name is created. [CHAR LIMIT=50] -->
@@ -967,6 +967,10 @@
    <!-- The body text for hamburger promo [CHAR LIMIT=200]-->
    <string name="hamburger_feature_highlight_body">Clean up duplicates &amp; group contacts by label</string>

    <!-- The label for the action shown in a snackbar after an operation that modifies some data is performed.
         The user can click on the action to rollback the modification-->
    <string name="undo">Undo</string>

    <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
    <string name="toast_text_copied">Text copied</string>
    <!-- Option displayed in context menu to copy long pressed item to clipboard [CHAR LIMIT=64] -->
+155 −27
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -45,8 +46,8 @@ import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

@@ -59,6 +60,7 @@ import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.RawContactDeltaList;
import com.android.contacts.common.model.RawContactModifier;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.testing.NeededForTesting;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.compat.PinnedPositionsCompat;
import com.android.contacts.util.ContactPhotoUtils;
@@ -131,6 +133,12 @@ public class ContactSaveService extends IntentService {
    public static final String ACTION_SET_RINGTONE = "setRingtone";
    public static final String EXTRA_CUSTOM_RINGTONE = "customRingtone";

    public static final String ACTION_UNDO = "undo";
    public static final String EXTRA_UNDO_ACTION = "undoAction";
    public static final String EXTRA_UNDO_DATA = "undoData";

    public static final String BROADCAST_ACTION_GROUP_DELETED = "groupDeleted";

    public static final int CP2_ERROR = 0;
    public static final int CONTACTS_LINKED = 1;
    public static final int CONTACTS_SPLIT = 2;
@@ -168,6 +176,7 @@ public class ContactSaveService extends IntentService {
            new CopyOnWriteArrayList<Listener>();

    private Handler mMainHandler;
    private GroupsDao mGroupsDao;

    public ContactSaveService() {
        super(TAG);
@@ -175,6 +184,12 @@ public class ContactSaveService extends IntentService {
        mMainHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mGroupsDao = new GroupsDaoImpl(this);
    }

    public static void registerListener(Listener listener) {
        if (!(listener instanceof Activity)) {
            throw new ClassCastException("Only activities can be registered to"
@@ -183,6 +198,10 @@ public class ContactSaveService extends IntentService {
        sListeners.add(0, listener);
    }

    public static boolean canUndo(Intent resultIntent) {
        return resultIntent.hasExtra(EXTRA_UNDO_DATA);
    }

    public static void unregisterListener(Listener listener) {
        sListeners.remove(listener);
    }
@@ -285,6 +304,8 @@ public class ContactSaveService extends IntentService {
            setSendToVoicemail(intent);
        } else if (ACTION_SET_RINGTONE.equals(action)) {
            setRingtone(intent);
        } else if (ACTION_UNDO.equals(action)) {
            undo(intent);
        }
    }

@@ -706,16 +727,10 @@ public class ContactSaveService extends IntentService {
        String label = intent.getStringExtra(EXTRA_GROUP_LABEL);
        final long[] rawContactsToAdd = intent.getLongArrayExtra(EXTRA_RAW_CONTACTS_TO_ADD);

        ContentValues values = new ContentValues();
        values.put(Groups.ACCOUNT_TYPE, accountType);
        values.put(Groups.ACCOUNT_NAME, accountName);
        values.put(Groups.DATA_SET, dataSet);
        values.put(Groups.TITLE, label);

        final ContentResolver resolver = getContentResolver();

        // Create the new group
        final Uri groupUri = resolver.insert(Groups.CONTENT_URI, values);
        final Uri groupUri = mGroupsDao.create(label,
                new AccountWithDataSet(accountName, accountType, dataSet));
        final ContentResolver resolver = getContentResolver();

        // If there's no URI, then the insertion failed. Abort early because group members can't be
        // added if the group doesn't exist
@@ -727,6 +742,7 @@ public class ContactSaveService extends IntentService {
        // Add new group members
        addMembersToGroup(resolver, rawContactsToAdd, ContentUris.parseId(groupUri));

        ContentValues values = new ContentValues();
        // TODO: Move this into the contact editor where it belongs. This needs to be integrated
        // with the way other intent extras that are passed to the {@link ContactEditorActivity}.
        values.clear();
@@ -780,19 +796,11 @@ public class ContactSaveService extends IntentService {
    /**
     * Creates an intent that can be sent to this service to delete a group.
     */
    public static Intent createGroupDeletionIntent(Context context, long groupId,
            Class<? extends Activity> callbackActivity, String callbackAction) {
        Intent serviceIntent = new Intent(context, ContactSaveService.class);
    public static Intent createGroupDeletionIntent(Context context, long groupId) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_DELETE_GROUP);
        serviceIntent.putExtra(ContactSaveService.EXTRA_GROUP_ID, groupId);

        // Callback intent will be invoked by the service once the group is updated
        if (callbackActivity != null && !TextUtils.isEmpty(callbackAction)) {
            final Intent callbackIntent = new Intent(context, callbackActivity);
            callbackIntent.setAction(callbackAction);
            serviceIntent.putExtra(ContactSaveService.EXTRA_CALLBACK_INTENT, callbackIntent);
        }

        return serviceIntent;
    }

@@ -802,18 +810,33 @@ public class ContactSaveService extends IntentService {
            Log.e(TAG, "Invalid arguments for deleteGroup request");
            return;
        }
        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);

        getContentResolver().delete(
                ContentUris.withAppendedId(Groups.CONTENT_URI, groupId), null, null);
        final Intent callbackIntent = new Intent(BROADCAST_ACTION_GROUP_DELETED);
        final Bundle undoData = mGroupsDao.captureDeletionUndoData(groupUri);
        callbackIntent.putExtra(EXTRA_UNDO_ACTION, ACTION_DELETE_GROUP);
        callbackIntent.putExtra(EXTRA_UNDO_DATA, undoData);

        final Intent callbackIntent = intent.getParcelableExtra(EXTRA_CALLBACK_INTENT);
        if (callbackIntent != null) {
            final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
            callbackIntent.setData(groupUri);
            deliverCallback(callbackIntent);
        mGroupsDao.delete(groupUri);

        LocalBroadcastManager.getInstance(this).sendBroadcast(callbackIntent);
    }

    public static Intent createUndoIntent(Context context, Intent resultIntent) {
        final Intent serviceIntent = new Intent(context, ContactSaveService.class);
        serviceIntent.setAction(ContactSaveService.ACTION_UNDO);
        serviceIntent.putExtras(resultIntent);
        return serviceIntent;
    }

    private void undo(Intent intent) {
        final String actionToUndo = intent.getStringExtra(EXTRA_UNDO_ACTION);
        if (ACTION_DELETE_GROUP.equals(actionToUndo)) {
            mGroupsDao.undoDeletion(intent.getBundleExtra(EXTRA_UNDO_DATA));
        }
    }


    /**
     * Creates an intent that can be sent to this service to rename a group as
     * well as add and remove members from the group.
@@ -1620,4 +1643,109 @@ public class ContactSaveService extends IntentService {
            }
        }
    }

    public interface GroupsDao {
        Uri create(String title, AccountWithDataSet account);
        int delete(Uri groupUri);
        Bundle captureDeletionUndoData(Uri groupUri);
        Uri undoDeletion(Bundle undoData);
    }

    @NeededForTesting
    public static class GroupsDaoImpl implements GroupsDao {
        @NeededForTesting
        public static final String KEY_GROUP_DATA = "groupData";
        @NeededForTesting
        public static final String KEY_GROUP_MEMBERS = "groupMemberIds";

        private static final String TAG = "GroupsDao";
        private final Context context;
        private final ContentResolver contentResolver;

        public GroupsDaoImpl(Context context) {
            this(context, context.getContentResolver());
        }

        public GroupsDaoImpl(Context context, ContentResolver contentResolver) {
            this.context = context;
            this.contentResolver = contentResolver;
        }

        public Bundle captureDeletionUndoData(Uri groupUri) {
            final long groupId = ContentUris.parseId(groupUri);
            final Bundle result = new Bundle();

            final Cursor cursor = contentResolver.query(groupUri,
                    new String[]{
                            Groups.TITLE, Groups.NOTES, Groups.GROUP_VISIBLE,
                            Groups.ACCOUNT_TYPE, Groups.ACCOUNT_NAME, Groups.DATA_SET,
                            Groups.SHOULD_SYNC
                    },
                    Groups.DELETED + "=?", new String[] { "0" }, null);
            try {
                if (cursor.moveToFirst()) {
                    final ContentValues groupValues = new ContentValues();
                    DatabaseUtils.cursorRowToContentValues(cursor, groupValues);
                    result.putParcelable(KEY_GROUP_DATA, groupValues);
                } else {
                    // Group doesn't exist.
                    return result;
                }
            } finally {
                cursor.close();
            }

            final Cursor membersCursor = contentResolver.query(
                    Data.CONTENT_URI, new String[] { Data.RAW_CONTACT_ID },
                    Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
                    new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) }, null);
            final long[] memberIds = new long[membersCursor.getCount()];
            int i = 0;
            while (membersCursor.moveToNext()) {
                memberIds[i++] = membersCursor.getLong(0);
            }
            result.putLongArray(KEY_GROUP_MEMBERS, memberIds);
            return result;
        }

        public Uri undoDeletion(Bundle deletedGroupData) {
            final ContentValues groupData = deletedGroupData.getParcelable(KEY_GROUP_DATA);
            if (groupData == null) {
                return null;
            }
            final Uri groupUri = contentResolver.insert(Groups.CONTENT_URI, groupData);
            final long groupId = ContentUris.parseId(groupUri);

            final long[] memberIds = deletedGroupData.getLongArray(KEY_GROUP_MEMBERS);
            if (memberIds == null) {
                return groupUri;
            }
            final ContentValues[] memberInsertions = new ContentValues[memberIds.length];
            for (int i = 0; i < memberIds.length; i++) {
                memberInsertions[i] = new ContentValues();
                memberInsertions[i].put(Data.RAW_CONTACT_ID, memberIds[i]);
                memberInsertions[i].put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
                memberInsertions[i].put(GroupMembership.GROUP_ROW_ID, groupId);
            }
            final int inserted = contentResolver.bulkInsert(Data.CONTENT_URI, memberInsertions);
            if (inserted != memberIds.length) {
                Log.e(TAG, "Could not recover some members for group deletion undo");
            }

            return groupUri;
        }

        public Uri create(String title, AccountWithDataSet account) {
            final ContentValues values = new ContentValues();
            values.put(Groups.TITLE, title);
            values.put(Groups.ACCOUNT_NAME, account.name);
            values.put(Groups.ACCOUNT_TYPE, account.type);
            values.put(Groups.DATA_SET, account.dataSet);
            return contentResolver.insert(Groups.CONTENT_URI, values);
        }

        public int delete(Uri groupUri) {
            return contentResolver.delete(groupUri, null, null);
        }
    }
}
+5 −4
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import android.widget.Toast;
import com.android.contacts.ContactSaveService;
import com.android.contacts.ContactsDrawerActivity;
import com.android.contacts.R;
import com.android.contacts.common.GroupMetaData;
import com.android.contacts.common.logging.ListEvent;
import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.logging.ScreenEvent.ScreenType;
@@ -416,13 +417,13 @@ public class GroupMembersActivity extends ContactsDrawerActivity implements

    private void deleteGroup() {
        if (mMembersFragment.getMemberCount() == 0) {
            final Intent intent = ContactSaveService.createGroupDeletionIntent(
                    this, mGroupMetadata.groupId,
                    GroupMembersActivity.class, ACTION_DELETE_GROUP);
            final Intent intent = ContactSaveService.createGroupDeletionIntent(this,
                    mGroupMetadata.groupId);
            startService(intent);
            finish();
        } else {
            GroupDeletionDialogFragment.show(getFragmentManager(), mGroupMetadata.groupId,
                    mGroupMetadata.groupName, /* endActivity */ false, ACTION_DELETE_GROUP);
                    mGroupMetadata.groupName);
        }
    }

+58 −1
Original line number Diff line number Diff line
@@ -21,8 +21,11 @@ import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Rect;
@@ -33,12 +36,17 @@ import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.ProviderStatus;
import android.provider.ContactsContract.QuickContact;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
@@ -82,13 +90,13 @@ import com.android.contacts.list.ContactsIntentResolver;
import com.android.contacts.list.ContactsRequest;
import com.android.contacts.list.ContactsUnavailableFragment;
import com.android.contacts.list.DefaultContactBrowseListFragment;
import com.android.contacts.list.DefaultContactBrowseListFragment.FeatureHighlightCallback;
import com.android.contacts.list.MultiSelectContactsListFragment.OnCheckBoxListActionListener;
import com.android.contacts.list.OnContactBrowserActionListener;
import com.android.contacts.list.OnContactsUnavailableActionListener;
import com.android.contacts.quickcontact.QuickContactActivity;
import com.android.contacts.util.DialogManager;
import com.android.contacts.util.SharedPreferenceUtil;
import com.android.contacts.widget.FloatingActionButtonBehavior;
import com.google.android.libraries.material.featurehighlight.FeatureHighlight;

import java.util.List;
@@ -129,8 +137,12 @@ public class PeopleActivity extends ContactsDrawerActivity implements
    private ProviderStatusWatcher mProviderStatusWatcher;
    private Integer mProviderStatus;

    private BroadcastReceiver mSaveServiceListener;

    private boolean mOptionsMenuContactsAvailable;

    private CoordinatorLayout mLayoutRoot;

    /**
     * Showing a list of Contacts. Also used for showing search results in search mode.
     */
@@ -384,6 +396,17 @@ public class PeopleActivity extends ContactsDrawerActivity implements
        initializeFabVisibility();

        invalidateOptionsMenuIfNeeded();

        mLayoutRoot = (CoordinatorLayout) findViewById(R.id.root);

        // Setup the FAB to animate upwards when a snackbar is shown in this activity.
        // Normally the layout_behavior attribute could be used for this but for some reason it
        // throws a ClassNotFoundException so  the layout parameters are set programmatically.
        final CoordinatorLayout.LayoutParams fabParams = new CoordinatorLayout.LayoutParams(
                (ViewGroup.MarginLayoutParams) mFloatingActionButtonContainer.getLayoutParams());
        fabParams.setBehavior(new FloatingActionButtonBehavior());
        fabParams.gravity = Gravity.BOTTOM | Gravity.END;
        mFloatingActionButtonContainer.setLayoutParams(fabParams);
    }

    @Override
@@ -414,7 +437,11 @@ public class PeopleActivity extends ContactsDrawerActivity implements
    protected void onPause() {
        mOptionsMenuContactsAvailable = false;
        mProviderStatusWatcher.stop();

        LocalBroadcastManager.getInstance(this).unregisterReceiver(mSaveServiceListener);

        super.onPause();

    }

    @Override
@@ -435,6 +462,10 @@ public class PeopleActivity extends ContactsDrawerActivity implements
        // the actual contents match the tab.
        updateFragmentsVisibility();
        maybeShowHamburgerFeatureHighlight();

        mSaveServiceListener = new SaveServiceListener();
        LocalBroadcastManager.getInstance(this).registerReceiver(mSaveServiceListener,
                new IntentFilter(ContactSaveService.BROADCAST_ACTION_GROUP_DELETED));
    }

    @Override
@@ -1506,4 +1537,30 @@ public class PeopleActivity extends ContactsDrawerActivity implements
    public void onLoadFinishedCallback() {
        maybeShowHamburgerFeatureHighlight();
    }

    private void onGroupDeleted(Intent intent) {
        if (!ContactSaveService.canUndo(intent)) {
            return;
        }
        Snackbar.make(mLayoutRoot, getString(R.string.groupDeletedToast), Snackbar.LENGTH_LONG)
                .setAction(R.string.undo, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        ContactSaveService.startService(PeopleActivity.this,
                                ContactSaveService.createUndoIntent(PeopleActivity.this, intent));
                    }
                }).show();
    }


    private class SaveServiceListener extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()) {
                case ContactSaveService.BROADCAST_ACTION_GROUP_DELETED:
                    onGroupDeleted(intent);
                    break;
            }
        }
    }
}
Loading