Loading res/layout/people_activity.xml +35 −27 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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> res/values/strings.xml +5 −1 Original line number Diff line number Diff line Loading @@ -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] --> Loading Loading @@ -967,6 +967,10 @@ <!-- The body text for hamburger promo [CHAR LIMIT=200]--> <string name="hamburger_feature_highlight_body">Clean up duplicates & 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] --> Loading src/com/android/contacts/ContactSaveService.java +155 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -168,6 +176,7 @@ public class ContactSaveService extends IntentService { new CopyOnWriteArrayList<Listener>(); private Handler mMainHandler; private GroupsDao mGroupsDao; public ContactSaveService() { super(TAG); Loading @@ -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" Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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 Loading @@ -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(); Loading Loading @@ -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; } Loading @@ -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. Loading Loading @@ -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); } } } src/com/android/contacts/activities/GroupMembersActivity.java +5 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } } Loading src/com/android/contacts/activities/PeopleActivity.java +58 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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. */ Loading Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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
res/layout/people_activity.xml +35 −27 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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>
res/values/strings.xml +5 −1 Original line number Diff line number Diff line Loading @@ -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] --> Loading Loading @@ -967,6 +967,10 @@ <!-- The body text for hamburger promo [CHAR LIMIT=200]--> <string name="hamburger_feature_highlight_body">Clean up duplicates & 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] --> Loading
src/com/android/contacts/ContactSaveService.java +155 −27 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -168,6 +176,7 @@ public class ContactSaveService extends IntentService { new CopyOnWriteArrayList<Listener>(); private Handler mMainHandler; private GroupsDao mGroupsDao; public ContactSaveService() { super(TAG); Loading @@ -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" Loading @@ -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); } Loading Loading @@ -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); } } Loading Loading @@ -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 Loading @@ -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(); Loading Loading @@ -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; } Loading @@ -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. Loading Loading @@ -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); } } }
src/com/android/contacts/activities/GroupMembersActivity.java +5 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); } } Loading
src/com/android/contacts/activities/PeopleActivity.java +58 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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. */ Loading Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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; } } } }