summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJorge Ruesga <jorge@ruesga.com>2015-03-01 22:02:55 +0100
committerJorge Ruesga <jorge@ruesga.com>2015-03-04 00:07:25 +0100
commitcd6ab96730f4d0a3040d4280fc3a55d64c76df51 (patch)
tree6bd36ddc22b94c488039f2c5d8f30c18cd17b17a
parent0812ca4d5763f18d26cbfb1fa87a3787dde35d2b (diff)
downloadandroid_packages_apps_UnifiedEmail-cd6ab96730f4d0a3040d4280fc3a55d64c76df51.tar.gz
android_packages_apps_UnifiedEmail-cd6ab96730f4d0a3040d4280fc3a55d64c76df51.tar.bz2
android_packages_apps_UnifiedEmail-cd6ab96730f4d0a3040d4280fc3a55d64c76df51.zip
unified email: suggested contacts
This change adds support for suggested contacts (email addresses not in the contact provider and received via email). Change-Id: I4010d0b6cf8bbb98308c7fc13f63e9d859a87d43 Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
-rw-r--r--res/values/cm_arrays.xml34
-rw-r--r--res/values/cm_strings.xml33
-rw-r--r--res/xml/general_preferences.xml18
-rw-r--r--src/com/android/mail/compose/RecipientAdapter.java229
-rw-r--r--src/com/android/mail/preferences/MailPrefs.java18
-rw-r--r--src/com/android/mail/ui/settings/GeneralPrefsFragment.java77
6 files changed, 408 insertions, 1 deletions
diff --git a/res/values/cm_arrays.xml b/res/values/cm_arrays.xml
new file mode 100644
index 000000000..3177784d5
--- /dev/null
+++ b/res/values/cm_arrays.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The CyanogenMod Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Suggested contacts mode preference -->
+ <string-array name="suggested_contacts_mode_entries" translatable="false">
+ <item>@string/prefDialogTitle_suggestedContactsMode_none</item>
+ <item>@string/prefDialogTitle_suggestedContactsMode_recents</item>
+ <item>@string/prefDialogTitle_suggestedContactsMode_all</item>
+ </string-array>
+ <string-array name="suggested_contacts_mode_summaries" translatable="false">
+ <item>@string/prefDialogTitle_suggestedContactsMode_summary_none</item>
+ <item>@string/prefDialogTitle_suggestedContactsMode_summary_recents</item>
+ <item>@string/prefDialogTitle_suggestedContactsMode_summary_all</item>
+ </string-array>
+ <string-array name="suggested_contacts_mode_values" translatable="false">
+ <item>none</item>
+ <item>recents</item>
+ <item>all</item>
+ </string-array>
+</resources>
diff --git a/res/values/cm_strings.xml b/res/values/cm_strings.xml
new file mode 100644
index 000000000..fbea162b5
--- /dev/null
+++ b/res/values/cm_strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The CyanogenMod Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- Suggested contacts preferences -->
+ <string name="prefDefault_suggestedContactsMode" translatable="false">none</string>
+ <string name="prefDialogTitle_suggestedContactsMode">Mode</string>
+ <string name="prefDialogTitle_suggestedContactsMode_none">Disabled</string>
+ <string name="prefDialogTitle_suggestedContactsMode_summary_none">Suggested contacts feature is disabled</string>
+ <string name="prefDialogTitle_suggestedContactsMode_recents">Recents</string>
+ <string name="prefDialogTitle_suggestedContactsMode_summary_recents">Only recent suggested contacts will be selectables</string>
+ <string name="prefDialogTitle_suggestedContactsMode_all">All</string>
+ <string name="prefDialogTitle_suggestedContactsMode_summary_all">All suggested contacts will be selectables</string>
+ <string name="preference_header_suggested_contacts">Suggested contacts</string>
+ <string name="preference_suggested_contacts_mode">Suggested contacts mode</string>
+ <string name="preference_suggested_contacts_clear_all">Clear suggested contacts</string>
+ <string name="suggested_contacts_cleared">Suggested contacts cleared.</string>
+ <string name="clear_suggested_contacts_dialog_title">Clear suggested contacts?</string>
+ <string name="clear_suggested_contacts_dialog_message">All the suggested contacts previously stored will be removed.</string>
+</resources>
diff --git a/res/xml/general_preferences.xml b/res/xml/general_preferences.xml
index 991d59881..3581ecc71 100644
--- a/res/xml/general_preferences.xml
+++ b/res/xml/general_preferences.xml
@@ -68,6 +68,24 @@
gm:entrySummaries="@array/prefSummaries_autoAdvance" />
<PreferenceCategory
+ android:title="@string/preference_header_suggested_contacts"
+ android:key="suggested-contacts">
+
+ <com.android.mail.ui.settings.FancySummaryListPreference
+ android:defaultValue="@string/prefDefault_suggestedContactsMode"
+ android:dialogTitle="@string/prefDialogTitle_suggestedContactsMode"
+ android:entries="@array/suggested_contacts_mode_entries"
+ android:entryValues="@array/suggested_contacts_mode_values"
+ android:key="suggested-contacts-mode"
+ android:title="@string/preference_suggested_contacts_mode"
+ gm:entrySummaries="@array/suggested_contacts_mode_summaries" />
+
+ <Preference
+ android:key="suggested-contacts-clear-all"
+ android:title="@string/preference_suggested_contacts_clear_all" />
+ </PreferenceCategory>
+
+ <PreferenceCategory
android:title="@string/preference_header_action_confirmations"
android:key="removal-actions-group">
<CheckBoxPreference
diff --git a/src/com/android/mail/compose/RecipientAdapter.java b/src/com/android/mail/compose/RecipientAdapter.java
index 2f84eea65..d11e88440 100644
--- a/src/com/android/mail/compose/RecipientAdapter.java
+++ b/src/com/android/mail/compose/RecipientAdapter.java
@@ -1,5 +1,6 @@
/**
* Copyright (c) 2007, Google Inc.
+ * Copyright (c) 2015, The CyanogenMod Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,14 +16,240 @@
*/
package com.android.mail.compose;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.SuggestedContact;
import com.android.ex.chips.BaseRecipientAdapter;
+import com.android.ex.chips.RecipientEntry;
+import com.android.mail.preferences.MailPrefs;
import com.android.mail.providers.Account;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+public class RecipientAdapter extends BaseRecipientAdapter
+implements OnSharedPreferenceChangeListener {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "RecipientAdapter";
+
+ private static final int MAX_SUGGESTED_CONTACTS_ENTRIES = 3;
+
+ private static final int MAX_DAYS_FOR_RECENTS_SUGGESTED_CONTACTS = -7;
+
+ private String mSuggestedContactsMode;
+ private long mAccountId;
-public class RecipientAdapter extends BaseRecipientAdapter {
public RecipientAdapter(Context context, Account account) {
super(context);
setAccount(account.getAccountManagerAccount());
+ mAccountId = -1;
+
+ // Load the account id because we needed to access the suggested contacts data
+ // (in async mode because will do i/o writes)
+ loadAccountKey(account);
+
+ mSuggestedContactsMode = MailPrefs.get(getContext()).getSuggestedContactMode();
+ MailPrefs.get(context).registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ MailPrefs.get(getContext()).unregisterOnSharedPreferenceChangeListener(this);
+ super.finalize();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (key.equals(MailPrefs.PreferenceKeys.SUGGESTED_CONTACTS_MODE)) {
+ mSuggestedContactsMode = MailPrefs.get(getContext()).getSuggestedContactMode();
+ }
+ }
+
+ @Override
+ protected Set<SuggestionEntry> loadSuggestedEntries(CharSequence constraint, int maxResults) {
+ Set<SuggestionEntry> entries = new HashSet<>();
+ if (!mSuggestedContactsMode.equals(MailPrefs.SuggestedContactsMode.NONE)) {
+ boolean recentsMode = mSuggestedContactsMode.equals(
+ MailPrefs.SuggestedContactsMode.RECENTS);
+
+ String selection = SuggestedContact.ACCOUNT_KEY + " = ?" +
+ " and LOWER(" + SuggestedContact.DISPLAY_NAME + ") like LOWER(?) ESCAPE '\\' ";
+ String[] args = new String[recentsMode ? 3 : 2];
+ args[0] = String.valueOf(mAccountId);
+ args[1] = "%" + constraint + "%";
+ String sort = SuggestedContact.LAST_SEEN + " DESC LIMIT "
+ + Math.min(MAX_SUGGESTED_CONTACTS_ENTRIES, maxResults);
+ if (recentsMode) {
+ Calendar c = Calendar.getInstance(Locale.getDefault());
+ c.add(Calendar.DAY_OF_YEAR, MAX_DAYS_FOR_RECENTS_SUGGESTED_CONTACTS);
+ selection += SuggestedContact.LAST_SEEN + " >= ? ";
+ args[2] = String.valueOf(c.getTimeInMillis());
+ }
+
+ Cursor cursor = getContext().getContentResolver().query(SuggestedContact.CONTENT_URI,
+ SuggestedContact.PROJECTION, selection, args, sort);
+ try {
+ if (cursor != null) {
+ Set<String> cachedAddresses = new HashSet<>();
+ Map<String, Integer> cachedContacts = new HashMap<>();
+ int contactsIds = -100;
+ while (cursor.moveToNext()) {
+ long suggestionId = cursor.getLong(
+ cursor.getColumnIndexOrThrow(SuggestedContact._ID));
+ String address = cursor.getString(
+ cursor.getColumnIndexOrThrow(SuggestedContact.ADDRESS));
+ String name = cursor.getString(
+ cursor.getColumnIndexOrThrow(SuggestedContact.NAME));
+ String displayName = cursor.getString(
+ cursor.getColumnIndexOrThrow(SuggestedContact.DISPLAY_NAME));
+
+ if (!cachedAddresses.contains(address)) {
+ int contactId = (cachedContacts.containsKey(name)) ?
+ cachedContacts.get(name) : contactsIds++;
+ SuggestionEntry entry = new SuggestionEntry(
+ suggestionId, displayName, address, name, contactId);
+ entries.add(entry);
+ cachedAddresses.add(address);
+
+ cachedContacts.put(name, contactId);
+ }
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed to perform search over suggested contacts table", e);
+
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.i(TAG, "Found " + entries.size() + " entries in suggested contacts");
+ }
+
+ return entries;
+ }
+
+ protected void onAddSuggestion(final RecipientEntry entry, final SuggestionAddCallback cb) {
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... args) {
+ return createSuggestedContact(entry);
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ cb.onSucess();
+ } else {
+ cb.onFailed();
+ }
+ }
+ }.execute();
+ }
+
+ protected void onDeleteSuggestion(final RecipientEntry entry,
+ final SuggestionRemoveCallback cb) {
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... args) {
+ return deleteSuggestedContact(entry);
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ cb.onSucess();
+ } else {
+ cb.onFailed();
+ }
+ }
+ }.execute();
+ }
+
+ private void loadAccountKey(final Account account) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ Cursor c = getContext().getContentResolver().query(
+ com.android.emailcommon.provider.Account.CONTENT_URI,
+ EmailContent.ID_PROJECTION,
+ com.android.emailcommon.provider.Account.AccountColumns.EMAIL_ADDRESS
+ + " = ?",
+ new String[]{account.getAccountId()}, null);
+ try {
+ if (c != null && c.moveToFirst()) {
+ mAccountId = c.getLong(0);
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private boolean createSuggestedContact(RecipientEntry entry) {
+ ContentResolver cr = getContext().getContentResolver();
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ int rawContactInsertIndex = ops.size();
+
+ ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
+ .withValue(RawContacts.ACCOUNT_TYPE, null)
+ .withValue(RawContacts.ACCOUNT_NAME, null)
+ .build());
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
+ .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(StructuredName.DISPLAY_NAME, entry.getDisplayName())
+ .build());
+ ops.add(ContentProviderOperation
+ .newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
+ .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE)
+ .withValue(Email.DATA, entry.getDestination())
+ .withValue(Email.TYPE, Email.TYPE_OTHER)
+ .build());
+
+ try {
+ cr.applyBatch(ContactsContract.AUTHORITY, ops);
+ return true;
+
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to create the suggested contact.", e);
+ }
+ return false;
+ }
+
+ private boolean deleteSuggestedContact(RecipientEntry entry) {
+ ContentResolver cr = getContext().getContentResolver();
+ final Uri uri = ContentUris.withAppendedId(SuggestedContact.CONTENT_URI, entry.getDataId());
+ return cr.delete(uri, null, null) == 1;
}
}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index cb4eb01ae..5035c4698 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -116,6 +116,8 @@ public final class MailPrefs extends VersionedPrefs {
public static final String RECENT_ACCOUNTS = "recent-accounts";
+ public static final String SUGGESTED_CONTACTS_MODE = "suggested-contacts-mode";
+
public static final ImmutableSet<String> BACKUP_KEYS =
new ImmutableSet.Builder<String>()
.add(DEFAULT_REPLY_ALL)
@@ -131,6 +133,7 @@ public final class MailPrefs extends VersionedPrefs {
.add(CONFIRM_SEND)
.add(CONVERSATION_OVERVIEW_MODE)
.add(SNAP_HEADER_MODE)
+ .add(SUGGESTED_CONTACTS_MODE)
.build();
}
@@ -140,6 +143,12 @@ public final class MailPrefs extends VersionedPrefs {
public static final String DISABLED = "disabled";
}
+ public static final class SuggestedContactsMode {
+ public static final String NONE = "none";
+ public static final String RECENTS = "recents";
+ public static final String ALL = "all";
+ }
+
@Retention(RetentionPolicy.SOURCE)
@StringDef({
RemovalActions.ARCHIVE,
@@ -521,4 +530,13 @@ public final class MailPrefs extends VersionedPrefs {
public void setRecentAccounts(Set<String> recentAccounts) {
getEditor().putStringSet(PreferenceKeys.RECENT_ACCOUNTS, recentAccounts).apply();
}
+
+ public String getSuggestedContactMode() {
+ return getSharedPreferences().getString(
+ PreferenceKeys.SUGGESTED_CONTACTS_MODE, SuggestedContactsMode.NONE);
+ }
+
+ public void setSuggestedContactMode(String mode) {
+ getEditor().putString(PreferenceKeys.SUGGESTED_CONTACTS_MODE, mode).apply();
+ }
}
diff --git a/src/com/android/mail/ui/settings/GeneralPrefsFragment.java b/src/com/android/mail/ui/settings/GeneralPrefsFragment.java
index d3b19c3ae..e8eacb3b6 100644
--- a/src/com/android/mail/ui/settings/GeneralPrefsFragment.java
+++ b/src/com/android/mail/ui/settings/GeneralPrefsFragment.java
@@ -21,18 +21,21 @@ import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
+import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
import android.provider.SearchRecentSuggestions;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
+import com.android.emailcommon.provider.SuggestedContact;
import com.android.mail.preferences.MailPrefs;
import com.android.mail.preferences.MailPrefs.PreferenceKeys;
import com.android.mail.providers.SuggestionsProvider;
@@ -49,6 +52,7 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
// Keys used to reference pref widgets which don't map directly to preference entries
static final String AUTO_ADVANCE_WIDGET = "auto-advance-widget";
+ static final String SUGGESTED_CONTACTS_CLEAR_ALL = "suggested-contacts-clear-all";
static final String CALLED_FROM_TEST = "called-from-test";
@@ -58,8 +62,11 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
protected MailPrefs mMailPrefs;
private AlertDialog mClearSearchHistoryDialog;
+ private AlertDialog mClearSuggestedContactsDialog;
private ListPreference mAutoAdvance;
+ private Preference mClearAllSuggestedContacts;
+
private static final int[] AUTO_ADVANCE_VALUES = {
AutoAdvance.NEWER,
AutoAdvance.OLDER,
@@ -83,6 +90,17 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
addPreferencesFromResource(R.xml.general_preferences);
mAutoAdvance = (ListPreference) findPreference(AUTO_ADVANCE_WIDGET);
+
+ mClearAllSuggestedContacts = findPreference(SUGGESTED_CONTACTS_CLEAR_ALL);
+ mClearAllSuggestedContacts.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ clearSuggestedContacts();
+ return true;
+ }
+ });
+ mClearAllSuggestedContacts.setEnabled(false);
+ computeSuggestedContacts();
}
@Override
@@ -157,6 +175,45 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
.show();
}
+ private void clearSuggestedContacts() {
+ mClearSuggestedContactsDialog = new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.clear_suggested_contacts_dialog_message)
+ .setTitle(R.string.clear_suggested_contacts_dialog_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.clear, this)
+ .setNegativeButton(R.string.cancel, this)
+ .show();
+ }
+
+ private void computeSuggestedContacts() {
+ final Context context = getActivity();
+ new AsyncTask<Void, Void, Integer>() {
+ @Override
+ protected Integer doInBackground(Void... params) {
+ Cursor c = context.getContentResolver().query(
+ SuggestedContact.CONTENT_URI,
+ new String[] {"count(*) AS count"},
+ null,
+ null,
+ null);
+ try {
+ if (c != null && c.moveToFirst()) {
+ return c.getInt(0);
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return 0;
+ }
+ @Override
+ protected void onPostExecute(Integer result) {
+ mClearAllSuggestedContacts.setEnabled(result > 0);
+ }
+ }.execute();
+ }
+
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -180,6 +237,23 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
Toast.makeText(getActivity(), R.string.search_history_cleared, Toast.LENGTH_SHORT)
.show();
}
+ } else if (dialog.equals(mClearSuggestedContactsDialog)) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ final Context context = getActivity();
+ // Clear the suggested contacts in the background, as it causes a disk
+ // write.
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ context.getContentResolver().delete(
+ SuggestedContact.CONTENT_URI, null, null);
+ computeSuggestedContacts();
+ return null;
+ }
+ }.execute();
+ Toast.makeText(getActivity(), R.string.suggested_contacts_cleared,
+ Toast.LENGTH_SHORT).show();
+ }
}
}
@@ -189,6 +263,9 @@ public class GeneralPrefsFragment extends MailPreferenceFragment
if (mClearSearchHistoryDialog != null && mClearSearchHistoryDialog.isShowing()) {
mClearSearchHistoryDialog.dismiss();
}
+ if (mClearSuggestedContactsDialog != null && mClearSuggestedContactsDialog.isShowing()) {
+ mClearSuggestedContactsDialog.dismiss();
+ }
}
@Override