Merge "Show snackbar with "undo" action when deleting group." into ub-contactsdialer-g-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a40bce1..fbedf58 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -675,6 +675,9 @@
and return to the editor [CHAR LIMIT=30] -->
<string name="cancel_confirmation_dialog_keep_editing_button">Cancel</string>
+ <!-- Contents of the alert dialog when the user hits the Cancel button in the customize screen [CHAR LIMIT=128] -->
+ <string name="leave_customize_confirmation_dialog_message">Discard customizations?</string>
+
<!-- Description of a call log entry, made of a call type and a date -->
<string name="call_type_and_date">
<xliff:g id="call_type" example="Friends">%1$s</xliff:g> <xliff:g id="call_short_date" example="Friends">%2$s</xliff:g>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 9ff882d..33fa11e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -486,11 +486,6 @@
<item name="android:overScrollMode">always</item>
</style>
- <style name="ContactListFilterTheme" parent="@android:Theme.Holo.Light">
- <item name="android:listViewStyle">@style/ListViewStyle</item>
- <item name="android:actionButtonStyle">@style/FilterActionButtonStyle</item>
- </style>
-
<!-- Adding padding to action button doesn't move it to left, we increase the button width to
make margin between the button and screen edge 16dp -->
<style name="FilterActionButtonStyle" parent="@android:Widget.ActionButton">
diff --git a/src/com/android/contacts/common/list/AccountFilterActivity.java b/src/com/android/contacts/common/list/AccountFilterActivity.java
index bed6977..3908d18 100644
--- a/src/com/android/contacts/common/list/AccountFilterActivity.java
+++ b/src/com/android/contacts/common/list/AccountFilterActivity.java
@@ -90,8 +90,9 @@
if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
mCustomFilterView = listFilterView;
mIsCustomFilterViewSelected = listFilterView.isChecked();
- final Intent intent = new Intent(this,
- CustomContactListFilterActivity.class);
+ final Intent intent = new Intent(this, CustomContactListFilterActivity.class)
+ .putExtra(CustomContactListFilterActivity.EXTRA_CURRENT_LIST_FILTER_TYPE,
+ mCurrentFilterType);
listFilterView.setActivated(true);
// Switching activity has the highest priority. So when we open another activity, the
// announcement that indicates an account is checked will be interrupted. This is the
diff --git a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
index 04337b8..74e8f84 100644
--- a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
+++ b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
@@ -19,6 +19,8 @@
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.app.ProgressDialog;
import android.content.AsyncTaskLoader;
@@ -30,14 +32,12 @@
import android.content.Intent;
import android.content.Loader;
import android.content.OperationApplicationException;
-import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
-import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Settings;
@@ -81,13 +81,13 @@
LoaderCallbacks<CustomContactListFilterActivity.AccountSet> {
private static final String TAG = "CustomContactListFilterActivity";
+ public static final String EXTRA_CURRENT_LIST_FILTER_TYPE = "currentListFilterType";
+
private static final int ACCOUNT_SET_LOADER_ID = 1;
private ExpandableListView mList;
private DisplayAdapter mAdapter;
- private SharedPreferences mPrefs;
-
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -111,7 +111,6 @@
}
});
- mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mAdapter = new DisplayAdapter(this);
mList.setOnCreateContextMenuListener(this);
@@ -837,6 +836,20 @@
}
}
+ private boolean hasUnsavedChanges() {
+ if (mAdapter == null || mAdapter.mAccounts == null) {
+ return false;
+ }
+ if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) {
+ return true;
+ }
+ final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
+ if (diff.isEmpty()) {
+ return false;
+ }
+ return true;
+ }
+
@SuppressWarnings("unchecked")
private void doSaveAction() {
if (mAdapter == null || mAdapter.mAccounts == null) {
@@ -933,9 +946,7 @@
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
- // Pretend cancel.
- setResult(Activity.RESULT_CANCELED);
- finish();
+ confirmFinish();
return true;
case R.id.menu_save:
this.doSaveAction();
@@ -945,4 +956,47 @@
}
return super.onOptionsItemSelected(item);
}
+
+ @Override
+ public void onBackPressed() {
+ confirmFinish();
+ }
+
+ private void confirmFinish() {
+ // Prompt the user whether they want to discard there customizations unless
+ // nothing will be changed.
+ if (hasUnsavedChanges()) {
+ new ConfirmNavigationDialogFragment().show(getFragmentManager(),
+ "ConfirmNavigationDialog");
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ private int getCurrentListFilterType() {
+ return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE,
+ ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
+ }
+
+ public static class ConfirmNavigationDialogFragment
+ extends DialogFragment implements DialogInterface.OnClickListener {
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity(), getTheme())
+ .setMessage(R.string.leave_customize_confirmation_dialog_message)
+ .setNegativeButton(android.R.string.no, null)
+ .setPositiveButton(android.R.string.yes, this)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (i == DialogInterface.BUTTON_POSITIVE) {
+ getActivity().setResult(RESULT_CANCELED);
+ getActivity().finish();
+ }
+ }
+ }
}
diff --git a/src/com/android/contacts/group/GroupNameEditDialogFragment.java b/src/com/android/contacts/group/GroupNameEditDialogFragment.java
index b2bfd0b..36a4710 100644
--- a/src/com/android/contacts/group/GroupNameEditDialogFragment.java
+++ b/src/com/android/contacts/group/GroupNameEditDialogFragment.java
@@ -25,9 +25,8 @@
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.Groups;
import android.support.design.widget.TextInputLayout;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
@@ -273,16 +272,27 @@
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// Only a single loader so id is ignored.
- return new CursorLoader(getActivity(), GroupNameQuery.URI,
- GroupNameQuery.PROJECTION, GroupNameQuery.getSelection(mAccount),
- GroupNameQuery.getSelectionArgs(mAccount), null);
+ return new CursorLoader(getActivity(), Groups.CONTENT_SUMMARY_URI,
+ new String[] { Groups.TITLE, Groups.SYSTEM_ID, Groups.ACCOUNT_TYPE,
+ Groups.SUMMARY_COUNT, Groups.GROUP_IS_READ_ONLY},
+ getSelection(), getSelectionArgs(), null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mExistingGroups = new HashSet<>();
+ final GroupUtil.GroupsProjection projection = new GroupUtil.GroupsProjection(data);
while (data.moveToNext()) {
- mExistingGroups.add(data.getString(GroupNameQuery.TITLE));
+ final String title = projection.getTitle(data);
+ // Empty system groups aren't shown in the nav drawer so it would be confusing to tell
+ // the user that they already exist. Instead we allow them to create a duplicate
+ // group in this case. This is how the web handles this case as well (it creates a
+ // new non-system group if a new group with a title that matches a system group is
+ // create).
+ if (projection.isEmptyFFCGroup(data)) {
+ continue;
+ }
+ mExistingGroups.add(title);
}
}
@@ -290,38 +300,6 @@
public void onLoaderReset(Loader<Cursor> loader) {
}
- /**
- * Defines the structure of the query performed by the CursorLoader created by
- * GroupNameEditDialogFragment
- */
- private static class GroupNameQuery {
-
- public static final int TITLE = 0;
- public static final Uri URI = ContactsContract.Groups.CONTENT_URI;
- public static final String[] PROJECTION = new String[] { ContactsContract.Groups.TITLE };
-
- public static String getSelection(AccountWithDataSet account) {
- final StringBuilder builder = new StringBuilder();
- builder.append(ContactsContract.Groups.ACCOUNT_NAME).append("=? AND ")
- .append(ContactsContract.Groups.ACCOUNT_TYPE).append("=?");
- if (account.dataSet != null) {
- builder.append(" AND ").append(ContactsContract.Groups.DATA_SET).append("=?");
- }
- return builder.toString();
- }
-
- public static String[] getSelectionArgs(AccountWithDataSet account) {
- final int len = account.dataSet == null ? 2 : 3;
- final String[] args = new String[len];
- args[0] = account.name;
- args[1] = account.type;
- if (account.dataSet != null) {
- args[2] = account.dataSet;
- }
- return args;
- }
- }
-
private void showInputMethod(View view) {
final InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
Context.INPUT_METHOD_SERVICE);
@@ -352,4 +330,27 @@
return mGroupNameEditText == null || mGroupNameEditText.getText() == null
? null : mGroupNameEditText.getText().toString();
}
+
+ private String getSelection() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append(Groups.ACCOUNT_NAME).append("=? AND ")
+ .append(Groups.ACCOUNT_TYPE).append("=? AND ")
+ .append(Groups.DELETED).append("=?");
+ if (mAccount.dataSet != null) {
+ builder.append(" AND ").append(Groups.DATA_SET).append("=?");
+ }
+ return builder.toString();
+ }
+
+ private String[] getSelectionArgs() {
+ final int len = mAccount.dataSet == null ? 3 : 4;
+ final String[] args = new String[len];
+ args[0] = mAccount.name;
+ args[1] = mAccount.type;
+ args[2] = "0"; // Not deleted
+ if (mAccount.dataSet != null) {
+ args[3] = mAccount.dataSet;
+ }
+ return args;
+ }
}
diff --git a/src/com/android/contacts/group/GroupUtil.java b/src/com/android/contacts/group/GroupUtil.java
index fd8c03d..eae0217 100644
--- a/src/com/android/contacts/group/GroupUtil.java
+++ b/src/com/android/contacts/group/GroupUtil.java
@@ -219,4 +219,77 @@
}
return array;
}
+
+ /**
+ * Stores column ordering for the projection of a query of ContactsContract.Groups
+ */
+ public static final class GroupsProjection {
+ public final int groupId;
+ public final int title;
+ public final int summaryCount;
+ public final int systemId;
+ public final int accountName;
+ public final int accountType;
+ public final int dataSet;
+ public final int autoAdd;
+ public final int favorites;
+ public final int isReadOnly;
+ public final int deleted;
+
+ public GroupsProjection(Cursor cursor) {
+ groupId = cursor.getColumnIndex(Groups._ID);
+ title = cursor.getColumnIndex(Groups.TITLE);
+ summaryCount = cursor.getColumnIndex(Groups.SUMMARY_COUNT);
+ systemId = cursor.getColumnIndex(Groups.SYSTEM_ID);
+ accountName = cursor.getColumnIndex(Groups.ACCOUNT_NAME);
+ accountType = cursor.getColumnIndex(Groups.ACCOUNT_TYPE);
+ dataSet = cursor.getColumnIndex(Groups.DATA_SET);
+ autoAdd = cursor.getColumnIndex(Groups.AUTO_ADD);
+ favorites = cursor.getColumnIndex(Groups.FAVORITES);
+ isReadOnly = cursor.getColumnIndex(Groups.GROUP_IS_READ_ONLY);
+ deleted = cursor.getColumnIndex(Groups.DELETED);
+ }
+
+ public GroupsProjection(String[] projection) {
+ List<String> list = Arrays.asList(projection);
+ groupId = list.indexOf(Groups._ID);
+ title = list.indexOf(Groups.TITLE);
+ summaryCount = list.indexOf(Groups.SUMMARY_COUNT);
+ systemId = list.indexOf(Groups.SYSTEM_ID);
+ accountName = list.indexOf(Groups.ACCOUNT_NAME);
+ accountType = list.indexOf(Groups.ACCOUNT_TYPE);
+ dataSet = list.indexOf(Groups.DATA_SET);
+ autoAdd = list.indexOf(Groups.AUTO_ADD);
+ favorites = list.indexOf(Groups.FAVORITES);
+ isReadOnly = list.indexOf(Groups.GROUP_IS_READ_ONLY);
+ deleted = list.indexOf(Groups.DELETED);
+ }
+
+ public String getTitle(Cursor cursor) {
+ return cursor.getString(title);
+ }
+
+ public long getId(Cursor cursor) {
+ return cursor.getLong(groupId);
+ }
+
+ public String getSystemId(Cursor cursor) {
+ return cursor.getString(systemId);
+ }
+
+ public int getSummaryCount(Cursor cursor) {
+ return cursor.getInt(summaryCount);
+ }
+
+ public boolean isEmptyFFCGroup(Cursor cursor) {
+ if (accountType == -1 || isReadOnly == -1 ||
+ systemId == -1 || summaryCount == -1) {
+ throw new IllegalArgumentException("Projection is missing required columns");
+ }
+ return GoogleAccountType.ACCOUNT_TYPE.equals(cursor.getString(accountType))
+ && cursor.getInt(isReadOnly) != 0
+ && isSystemIdFFC(cursor.getString(systemId))
+ && cursor.getInt(summaryCount) <= 0;
+ }
+ }
}
\ No newline at end of file