diff options
author | Jorge Ruesga <jorge@ruesga.com> | 2015-03-01 22:02:55 +0100 |
---|---|---|
committer | Jorge Ruesga <jorge@ruesga.com> | 2015-03-04 00:07:25 +0100 |
commit | cd6ab96730f4d0a3040d4280fc3a55d64c76df51 (patch) | |
tree | 6bd36ddc22b94c488039f2c5d8f30c18cd17b17a | |
parent | 0812ca4d5763f18d26cbfb1fa87a3787dde35d2b (diff) | |
download | android_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.xml | 34 | ||||
-rw-r--r-- | res/values/cm_strings.xml | 33 | ||||
-rw-r--r-- | res/xml/general_preferences.xml | 18 | ||||
-rw-r--r-- | src/com/android/mail/compose/RecipientAdapter.java | 229 | ||||
-rw-r--r-- | src/com/android/mail/preferences/MailPrefs.java | 18 | ||||
-rw-r--r-- | src/com/android/mail/ui/settings/GeneralPrefsFragment.java | 77 |
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 |