Fix content observing

b/12834957

Change-Id: I00e2fc48e1d78665e0cdcfc3f4fb483f5a047252
This commit is contained in:
Tony Mantler 2014-01-31 10:48:22 -08:00
parent 94456929ff
commit 0f8d16f56a
4 changed files with 301 additions and 138 deletions

View File

@ -226,6 +226,11 @@ public final class Account extends EmailContent implements AccountColumns, Parce
Account.CONTENT_URI, Account.CONTENT_PROJECTION, id, observer);
}
@Override
protected Uri getContentNotificationUri() {
return Account.CONTENT_URI;
}
/**
* Returns {@code true} if the given account ID is a "normal" account. Normal accounts
* always have an ID greater than {@code 0} and not equal to any pseudo account IDs

View File

@ -24,6 +24,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.res.Resources;
import android.database.ContentObservable;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
@ -42,6 +43,7 @@ import com.android.mail.utils.LogUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -112,6 +114,15 @@ public abstract class EmailContent {
// The id of the Content
public long mId = NOT_SAVED;
/**
* Since we close the cursor we use to generate this object, and possibly create the object
* without using any cursor at all (eg: parcel), we need to handle observing provider changes
* ourselves. This content observer uses a weak reference to keep from rooting this object
* in the ContentResolver in case it is not properly disposed of using {@link #close(Context)}
*/
private SelfContentObserver mSelfObserver;
private ContentObservable mObservable;
// Write the Content into a ContentValues container
public abstract ContentValues toContentValues();
// Read the Content from a ContentCursor
@ -211,18 +222,20 @@ public abstract class EmailContent {
return restoreContentWithId(context, klass, contentUri, contentProjection, id, null);
}
public static <T extends EmailContent> T restoreContentWithId(Context context,
Class<T> klass, Uri contentUri, String[] contentProjection, long id, ContentObserver observer) {
public static <T extends EmailContent> T restoreContentWithId(final Context context,
final Class<T> klass, final Uri contentUri, final String[] contentProjection,
final long id, final ContentObserver observer) {
warnIfUiThread();
Uri u = ContentUris.withAppendedId(contentUri, id);
Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null);
final Uri u = ContentUris.withAppendedId(contentUri, id);
final Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null);
if (c == null) throw new ProviderUnavailableException();
try {
if (observer != null) {
c.registerContentObserver(observer);
}
if (c.moveToFirst()) {
return getContent(c, klass);
final T content = getContent(c, klass);
if (observer != null) {
content.registerObserver(context, observer);
}
return content;
} else {
return null;
}
@ -231,6 +244,103 @@ public abstract class EmailContent {
}
}
/**
* Register a content observer to be notified when the data underlying this object changes
* @param observer ContentObserver to register
*/
public synchronized void registerObserver(final Context context, final ContentObserver observer) {
if (mSelfObserver == null) {
mSelfObserver = new SelfContentObserver(this);
context.getContentResolver().registerContentObserver(getContentNotificationUri(),
true, mSelfObserver);
mObservable = new ContentObservable();
}
mObservable.registerObserver(observer);
}
/**
* Unregister a content observer previously registered with
* {@link #registerObserver(Context, ContentObserver)}
* @param observer ContentObserver to unregister
*/
public synchronized void unregisterObserver(final ContentObserver observer) {
if (mObservable == null) {
throw new IllegalStateException("Unregistering with null observable");
}
mObservable.unregisterObserver(observer);
}
/**
* Unregister all content observers previously registered with
* {@link #registerObserver(Context, ContentObserver)}
*/
public synchronized void unregisterAllObservers() {
if (mObservable == null) {
throw new IllegalStateException("Unregistering with null observable");
}
mObservable.unregisterAll();
}
/**
* Unregister all content observers previously registered with
* {@link #registerObserver(Context, ContentObserver)} and release internal resources associated
* with content observing
*/
public synchronized void close(final Context context) {
if (mSelfObserver == null) {
return;
}
unregisterAllObservers();
context.getContentResolver().unregisterContentObserver(mSelfObserver);
mSelfObserver = null;
}
/**
* Returns a Uri for observing the underlying content. Subclasses that wish to implement content
* observing must override this method.
* @return Uri for registering content notifications
*/
protected Uri getContentNotificationUri() {
throw new UnsupportedOperationException(
"Subclasses must override this method for content observation to work");
}
/**
* This method is called when the underlying data has changed, and notifies registered observers
* @param selfChange true if this is a self-change notification
*/
@SuppressWarnings("deprecation")
public synchronized void onChange(final boolean selfChange) {
if (mObservable != null) {
mObservable.dispatchChange(selfChange);
}
}
/**
* A content observer that calls {@link #onChange(boolean)} when triggered
*/
private static class SelfContentObserver extends ContentObserver {
WeakReference<EmailContent> mContent;
public SelfContentObserver(final EmailContent content) {
super(null);
mContent = new WeakReference<EmailContent>(content);
}
@Override
public boolean deliverSelfNotifications() {
return false;
}
@Override
public void onChange(final boolean selfChange) {
EmailContent content = mContent.get();
if (content != null) {
content.onChange(false);
}
}
}
// The Content sub class must have a no-arg constructor
static public <T extends EmailContent> T getContent(Cursor cursor, Class<T> klass) {

View File

@ -144,6 +144,11 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol
Policy.CONTENT_PROJECTION, id, observer);
}
@Override
protected Uri getContentNotificationUri() {
return Policy.CONTENT_URI;
}
public static long getAccountIdWithPolicyKey(Context context, long id) {
return Utility.getFirstRowLong(context, Account.CONTENT_URI, Account.ID_PROJECTION,
AccountColumns.POLICY_KEY + "=?", new String[] {Long.toString(id)}, null,

View File

@ -328,6 +328,7 @@ public class AccountSettingsFragment extends PreferenceFragment
*/
private static class AccountLoader extends MailAsyncTaskLoader<Map<String, Object>> {
public static final String RESULT_KEY_ACCOUNT = "account";
private static final String RESULT_KEY_UIACCOUNT_CURSOR = "uiAccountCursor";
public static final String RESULT_KEY_UIACCOUNT = "uiAccount";
public static final String RESULT_KEY_INBOX = "inbox";
@ -342,38 +343,47 @@ public class AccountSettingsFragment extends PreferenceFragment
@Override
public Map<String, Object> loadInBackground() {
final Map<String, Object> map = new HashMap<String, Object>();
Account account = Account.restoreAccountWithId(getContext(), mAccountId, mObserver);
if (account == null) {
return null;
return map;
}
// We don't monitor these for changes, but they probably won't change in any meaningful way
map.put(RESULT_KEY_ACCOUNT, account);
// We don't monitor these for changes, but they probably won't change in any meaningful
// way
account.getOrCreateHostAuthRecv(getContext());
account.getOrCreateHostAuthSend(getContext());
if (account.mHostAuthRecv == null) {
return null;
return map;
}
account.mPolicy = Policy.restorePolicyWithId(getContext(), account.mPolicyKey, mObserver);
account.mPolicy =
Policy.restorePolicyWithId(getContext(), account.mPolicyKey, mObserver);
final Cursor accountCursor = getContext().getContentResolver().query(
EmailProvider.uiUri("uiaccount", mAccountId), UIProvider.ACCOUNTS_PROJECTION, null, null, null);
final Cursor uiAccountCursor = getContext().getContentResolver().query(
EmailProvider.uiUri("uiaccount", mAccountId), UIProvider.ACCOUNTS_PROJECTION,
null, null, null);
final com.android.mail.providers.Account uiAccount;
try {
if (accountCursor != null && accountCursor.moveToFirst()) {
accountCursor.registerContentObserver(mObserver);
uiAccount = new com.android.mail.providers.Account(accountCursor);
} else {
return null;
}
} finally {
if (accountCursor != null) {
accountCursor.close();
}
if (uiAccountCursor != null) {
map.put(RESULT_KEY_UIACCOUNT_CURSOR, uiAccountCursor);
uiAccountCursor.registerContentObserver(mObserver);
} else {
return map;
}
if (!uiAccountCursor.moveToFirst()) {
return map;
}
final com.android.mail.providers.Account uiAccount =
new com.android.mail.providers.Account(uiAccountCursor);
map.put(RESULT_KEY_UIACCOUNT, uiAccount);
final Cursor folderCursor = getContext().getContentResolver().query(
uiAccount.settings.defaultInbox, UIProvider.FOLDERS_PROJECTION, null, null,
null);
@ -383,7 +393,7 @@ public class AccountSettingsFragment extends PreferenceFragment
if (folderCursor != null && folderCursor.moveToFirst()) {
inbox = new Folder(folderCursor);
} else {
return null;
return map;
}
} finally {
if (folderCursor != null) {
@ -391,18 +401,28 @@ public class AccountSettingsFragment extends PreferenceFragment
}
}
final Map<String, Object> map = new HashMap<String, Object>();
map.put(RESULT_KEY_ACCOUNT, account);
map.put(RESULT_KEY_UIACCOUNT, uiAccount);
map.put(RESULT_KEY_INBOX, inbox);
return map;
}
@Override
protected void onDiscardResult(Map<String, Object> result) {}
protected void onDiscardResult(Map<String, Object> result) {
final Account account = (Account) result.get(RESULT_KEY_ACCOUNT);
if (account != null) {
if (account.mPolicy != null) {
account.mPolicy.close(getContext());
}
account.close(getContext());
}
final Cursor uiAccountCursor = (Cursor) result.get(RESULT_KEY_UIACCOUNT_CURSOR);
if (uiAccountCursor != null) {
uiAccountCursor.close();
}
}
}
private class AccountLoaderCallbacks implements LoaderManager.LoaderCallbacks<Map<String, Object>> {
private class AccountLoaderCallbacks
implements LoaderManager.LoaderCallbacks<Map<String, Object>> {
public static final String ARG_ACCOUNT_ID = "accountId";
@Override
@ -413,13 +433,20 @@ public class AccountSettingsFragment extends PreferenceFragment
return;
}
final boolean firstLoad = mUiAccount == null;
mUiAccount = (com.android.mail.providers.Account) data.get(AccountLoader.RESULT_KEY_UIACCOUNT);
mUiAccount = (com.android.mail.providers.Account)
data.get(AccountLoader.RESULT_KEY_UIACCOUNT);
mAccount = (Account) data.get(AccountLoader.RESULT_KEY_ACCOUNT);
final Folder inbox = (Folder) data.get(AccountLoader.RESULT_KEY_INBOX);
mInboxFolderPreferences = new FolderPreferences(mContext, mUiAccount.getEmailAddress(), inbox, true);
if (firstLoad) {
if (mUiAccount == null || mAccount == null || inbox == null) {
mSaveOnExit = false;
mCallback.abandonEdit();
return;
}
mInboxFolderPreferences =
new FolderPreferences(mContext, mUiAccount.getEmailAddress(), inbox, true);
if (!mSaveOnExit) {
loadSettings();
}
}
@ -496,7 +523,8 @@ public class AccountSettingsFragment extends PreferenceFragment
final String protocol = mAccount.getProtocol(mContext);
final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol);
if (info == null) {
LogUtils.e(Logging.LOG_TAG, "Could not find service info for account %d with protocol %s", mAccount.mId,
LogUtils.e(Logging.LOG_TAG,
"Could not find service info for account %d with protocol %s", mAccount.mId,
protocol);
getActivity().onBackPressed();
// TODO: put up some sort of dialog/toast here to tell the user something went wrong
@ -596,31 +624,36 @@ public class AccountSettingsFragment extends PreferenceFragment
PreferenceCategory folderPrefs =
(PreferenceCategory) findPreference(PREFERENCE_SYSTEM_FOLDERS);
if (info.requiresSetup) {
Preference trashPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_TRASH);
Intent i = new Intent(mContext, FolderPickerActivity.class);
Uri uri = EmailContent.CONTENT_URI.buildUpon().appendQueryParameter(
"account", Long.toString(mAccountId)).build();
i.setData(uri);
i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_TRASH);
trashPreference.setIntent(i);
if (folderPrefs != null) {
if (info.requiresSetup) {
Preference trashPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_TRASH);
Intent i = new Intent(mContext, FolderPickerActivity.class);
Uri uri = EmailContent.CONTENT_URI.buildUpon().appendQueryParameter(
"account", Long.toString(mAccountId)).build();
i.setData(uri);
i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_TRASH);
trashPreference.setIntent(i);
Preference sentPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_SENT);
i = new Intent(mContext, FolderPickerActivity.class);
i.setData(uri);
i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_SENT);
sentPreference.setIntent(i);
} else {
getPreferenceScreen().removePreference(folderPrefs);
Preference sentPreference = findPreference(PREFERENCE_SYSTEM_FOLDERS_SENT);
i = new Intent(mContext, FolderPickerActivity.class);
i.setData(uri);
i.putExtra(FolderPickerActivity.MAILBOX_TYPE_EXTRA, Mailbox.TYPE_SENT);
sentPreference.setIntent(i);
} else {
getPreferenceScreen().removePreference(folderPrefs);
}
}
mAccountBackgroundAttachments = (CheckBoxPreference)
findPreference(PREFERENCE_BACKGROUND_ATTACHMENTS);
if (!info.offerAttachmentPreload) {
dataUsageCategory.removePreference(mAccountBackgroundAttachments);
} else {
mAccountBackgroundAttachments.setChecked(0 != (mAccount.getFlags() & Account.FLAGS_BACKGROUND_ATTACHMENTS));
mAccountBackgroundAttachments.setOnPreferenceChangeListener(this);
if (mAccountBackgroundAttachments != null) {
if (!info.offerAttachmentPreload) {
dataUsageCategory.removePreference(mAccountBackgroundAttachments);
} else {
mAccountBackgroundAttachments.setChecked(
0 != (mAccount.getFlags() & Account.FLAGS_BACKGROUND_ATTACHMENTS));
mAccountBackgroundAttachments.setOnPreferenceChangeListener(this);
}
}
final CheckBoxPreference inboxNotify = (CheckBoxPreference) findPreference(
@ -651,55 +684,61 @@ public class AccountSettingsFragment extends PreferenceFragment
// Set the vibrator value, or hide it on devices w/o a vibrator
mInboxVibrate = (CheckBoxPreference) findPreference(
FolderPreferences.PreferenceKeys.NOTIFICATION_VIBRATE);
mInboxVibrate.setChecked(
mInboxFolderPreferences.isNotificationVibrateEnabled());
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator.hasVibrator()) {
// When the value is changed, update the setting.
mInboxVibrate.setOnPreferenceChangeListener(this);
} else {
// No vibrator present. Remove the preference altogether.
notificationsCategory.removePreference(mInboxVibrate);
if (mInboxVibrate != null) {
mInboxVibrate.setChecked(
mInboxFolderPreferences.isNotificationVibrateEnabled());
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator.hasVibrator()) {
// When the value is changed, update the setting.
mInboxVibrate.setOnPreferenceChangeListener(this);
} else {
// No vibrator present. Remove the preference altogether.
notificationsCategory.removePreference(mInboxVibrate);
}
}
final Preference retryAccount = findPreference(PREFERENCE_POLICIES_RETRY_ACCOUNT);
final PreferenceCategory policiesCategory = (PreferenceCategory) findPreference(
PREFERENCE_CATEGORY_POLICIES);
// TODO: This code for showing policies isn't working. For KLP, just don't even bother
// showing this data; we'll fix this later.
/*
if (policy != null) {
if (policy.mProtocolPoliciesEnforced != null) {
ArrayList<String> policies = getSystemPoliciesList(policy);
setPolicyListSummary(policies, policy.mProtocolPoliciesEnforced,
PREFERENCE_POLICIES_ENFORCED);
}
if (policy.mProtocolPoliciesUnsupported != null) {
ArrayList<String> policies = new ArrayList<String>();
setPolicyListSummary(policies, policy.mProtocolPoliciesUnsupported,
PREFERENCE_POLICIES_UNSUPPORTED);
if (policiesCategory != null) {
// TODO: This code for showing policies isn't working. For KLP, just don't even bother
// showing this data; we'll fix this later.
/*
if (policy != null) {
if (policy.mProtocolPoliciesEnforced != null) {
ArrayList<String> policies = getSystemPoliciesList(policy);
setPolicyListSummary(policies, policy.mProtocolPoliciesEnforced,
PREFERENCE_POLICIES_ENFORCED);
}
if (policy.mProtocolPoliciesUnsupported != null) {
ArrayList<String> policies = new ArrayList<String>();
setPolicyListSummary(policies, policy.mProtocolPoliciesUnsupported,
PREFERENCE_POLICIES_UNSUPPORTED);
} else {
// Don't show "retry" unless we have unsupported policies
policiesCategory.removePreference(retryAccount);
}
} else {
// Don't show "retry" unless we have unsupported policies
policiesCategory.removePreference(retryAccount);
}
} else {
*/
// Remove the category completely if there are no policies
getPreferenceScreen().removePreference(policiesCategory);
*/
// Remove the category completely if there are no policies
getPreferenceScreen().removePreference(policiesCategory);
//}
//}
}
retryAccount.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// Release the account
SecurityPolicy.setAccountHoldFlag(mContext, mAccount, false);
// Remove the preference
policiesCategory.removePreference(retryAccount);
return true;
}
});
if (retryAccount != null) {
retryAccount.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// Release the account
SecurityPolicy.setAccountHoldFlag(mContext, mAccount, false);
// Remove the preference
policiesCategory.removePreference(retryAccount);
return true;
}
});
}
findPreference(PREFERENCE_INCOMING).setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
@ -711,53 +750,57 @@ public class AccountSettingsFragment extends PreferenceFragment
// Hide the outgoing account setup link if it's not activated
Preference prefOutgoing = findPreference(PREFERENCE_OUTGOING);
if (info.usesSmtp && mAccount.mHostAuthSend != null) {
prefOutgoing.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
mCallback.onOutgoingSettings(mAccount);
return true;
}
});
} else {
if (info.usesSmtp) {
// We really ought to have an outgoing host auth but we don't.
// There's nothing we can do at this point, so just log the error.
LogUtils.e(Logging.LOG_TAG, "Account %d has a bad outbound hostauth", mAccountId);
if (prefOutgoing != null) {
if (info.usesSmtp && mAccount.mHostAuthSend != null) {
prefOutgoing.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
mCallback.onOutgoingSettings(mAccount);
return true;
}
});
} else {
if (info.usesSmtp) {
// We really ought to have an outgoing host auth but we don't.
// There's nothing we can do at this point, so just log the error.
LogUtils.e(Logging.LOG_TAG, "Account %d has a bad outbound hostauth", mAccountId);
}
PreferenceCategory serverCategory = (PreferenceCategory) findPreference(
PREFERENCE_CATEGORY_SERVER);
serverCategory.removePreference(prefOutgoing);
}
PreferenceCategory serverCategory = (PreferenceCategory) findPreference(
PREFERENCE_CATEGORY_SERVER);
serverCategory.removePreference(prefOutgoing);
}
mSyncContacts = (CheckBoxPreference) findPreference(PREFERENCE_SYNC_CONTACTS);
mSyncCalendar = (CheckBoxPreference) findPreference(PREFERENCE_SYNC_CALENDAR);
mSyncEmail = (CheckBoxPreference) findPreference(PREFERENCE_SYNC_EMAIL);
if (info.syncContacts || info.syncCalendar) {
if (info.syncContacts) {
mSyncContacts.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, ContactsContract.AUTHORITY));
mSyncContacts.setOnPreferenceChangeListener(this);
if (mSyncContacts != null && mSyncCalendar != null && mSyncEmail != null) {
if (info.syncContacts || info.syncCalendar) {
if (info.syncContacts) {
mSyncContacts.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, ContactsContract.AUTHORITY));
mSyncContacts.setOnPreferenceChangeListener(this);
} else {
mSyncContacts.setChecked(false);
mSyncContacts.setEnabled(false);
}
if (info.syncCalendar) {
mSyncCalendar.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, CalendarContract.AUTHORITY));
mSyncCalendar.setOnPreferenceChangeListener(this);
} else {
mSyncCalendar.setChecked(false);
mSyncCalendar.setEnabled(false);
}
mSyncEmail.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, EmailContent.AUTHORITY));
mSyncEmail.setOnPreferenceChangeListener(this);
} else {
mSyncContacts.setChecked(false);
mSyncContacts.setEnabled(false);
dataUsageCategory.removePreference(mSyncContacts);
dataUsageCategory.removePreference(mSyncCalendar);
dataUsageCategory.removePreference(mSyncEmail);
}
if (info.syncCalendar) {
mSyncCalendar.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, CalendarContract.AUTHORITY));
mSyncCalendar.setOnPreferenceChangeListener(this);
} else {
mSyncCalendar.setChecked(false);
mSyncCalendar.setEnabled(false);
}
mSyncEmail.setChecked(ContentResolver
.getSyncAutomatically(androidAcct, EmailContent.AUTHORITY));
mSyncEmail.setOnPreferenceChangeListener(this);
} else {
dataUsageCategory.removePreference(mSyncContacts);
dataUsageCategory.removePreference(mSyncCalendar);
dataUsageCategory.removePreference(mSyncEmail);
}
}