diff options
Diffstat (limited to 'core/java/android/content')
37 files changed, 3629 insertions, 933 deletions
diff --git a/core/java/android/content/AbstractCursorEntityIterator.java b/core/java/android/content/AbstractCursorEntityIterator.java new file mode 100644 index 0000000..bf3c4de --- /dev/null +++ b/core/java/android/content/AbstractCursorEntityIterator.java @@ -0,0 +1,112 @@ +package android.content; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; + +/** + * An abstract class that makes it easy to implement an EntityIterator over a cursor. + * The user must implement {@link #newEntityFromCursorLocked}, which runs inside of a + * database transaction. + */ +public abstract class AbstractCursorEntityIterator implements EntityIterator { + private final Cursor mEntityCursor; + private final SQLiteDatabase mDb; + private volatile Entity mNextEntity; + private volatile boolean mIsClosed; + + public AbstractCursorEntityIterator(SQLiteDatabase db, Cursor entityCursor) { + mEntityCursor = entityCursor; + mDb = db; + mNextEntity = null; + mIsClosed = false; + } + + /** + * If there are entries left in the cursor then advance the cursor and use the new row to + * populate mNextEntity. If the cursor is at the end or if advancing it causes the cursor + * to become at the end then set mEntityCursor to null. If newEntityFromCursor returns null + * then continue advancing until it either returns a non-null Entity or the cursor reaches + * the end. + */ + private void fillEntityIfAvailable() { + while (mNextEntity == null) { + if (!mEntityCursor.moveToNext()) { + // the cursor is at then end, bail out + return; + } + // This may return null if newEntityFromCursor is not able to create an entity + // from the current cursor position. In that case this method will loop and try + // the next cursor position + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + } + mDb.beginTransaction(); + try { + int position = mEntityCursor.getPosition(); + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + int newPosition = mEntityCursor.getPosition(); + if (newPosition != position) { + throw new IllegalStateException("the cursor position changed during the call to" + + "newEntityFromCursorLocked, from " + position + " to " + newPosition); + } + } finally { + mDb.endTransaction(); + } + } + + /** + * Checks if there are more Entities accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return true if the call to next() will return an Entity. + */ + public boolean hasNext() { + if (mIsClosed) { + throw new IllegalStateException("calling hasNext() when the iterator is closed"); + } + fillEntityIfAvailable(); + return mNextEntity != null; + } + + /** + * Returns the next Entity that is accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return the next Entity that is accessible via this iterator + */ + public Entity next() { + if (mIsClosed) { + throw new IllegalStateException("calling next() when the iterator is closed"); + } + if (!hasNext()) { + throw new IllegalStateException("you may only call next() if hasNext() is true"); + } + + try { + return mNextEntity; + } finally { + mNextEntity = null; + } + } + + /** + * Closes this iterator making it invalid. If is invalid for the user to call any public + * method on the iterator once it has been closed. + */ + public void close() { + if (mIsClosed) { + throw new IllegalStateException("closing when already closed"); + } + mIsClosed = true; + mEntityCursor.close(); + } + + /** + * Returns a new Entity from the current cursor position. This is called from within a + * database transaction. If a new entity cannot be created from this cursor position (e.g. + * if the row that is referred to no longer exists) then this may return null. The cursor + * is guaranteed to be pointing to a valid row when this call is made. The implementation + * of newEntityFromCursorLocked is not allowed to change the position of the cursor. + * @param cursor from where to read the data for the Entity + * @return an Entity that corresponds to the current cursor position or null + */ + public abstract Entity newEntityFromCursorLocked(Cursor cursor); +} diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index 249d9ba..db73dd5 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -4,8 +4,9 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase; import android.database.Cursor; import android.net.Uri; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.OnAccountsUpdatedListener; +import android.accounts.Account; +import android.accounts.AccountManager; import android.provider.SyncConstValue; import android.util.Config; import android.util.Log; @@ -14,9 +15,12 @@ import android.text.TextUtils; import java.util.Collections; import java.util.Map; -import java.util.HashMap; import java.util.Vector; import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; + +import com.google.android.collect.Maps; /** * A specialization of the ContentProvider that centralizes functionality @@ -32,26 +36,30 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro private final String mDatabaseName; private final int mDatabaseVersion; private final Uri mContentUri; - private AccountMonitor mAccountMonitor; /** the account set in the last call to onSyncStart() */ - private String mSyncingAccount; + private Account mSyncingAccount; private SyncStateContentProviderHelper mSyncState = null; - private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT}; + private static final String[] sAccountProjection = + new String[] {SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT_TYPE}; private boolean mIsTemporary; private AbstractTableMerger mCurrentMerger = null; private boolean mIsMergeCancelled = false; - private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?"; + private static final String SYNC_ACCOUNT_WHERE_CLAUSE = + SyncConstValue._SYNC_ACCOUNT + "=? AND " + SyncConstValue._SYNC_ACCOUNT_TYPE + "=?"; protected boolean isTemporary() { return mIsTemporary; } + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private final ThreadLocal<Set<Uri>> mPendingBatchNotifications = new ThreadLocal<Set<Uri>>(); + /** * Indicates whether or not this ContentProvider contains a full * set of data or just diffs. This knowledge comes in handy when @@ -133,7 +141,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (!upgradeDatabase(db, oldVersion, newVersion)) { mSyncState.discardSyncData(db, null /* all accounts */); - getContext().getContentResolver().startSync(mContentUri, new Bundle()); + ContentResolver.requestSync(null /* all accounts */, + mContentUri.getAuthority(), new Bundle()); } } @@ -150,19 +159,19 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName); mSyncState = new SyncStateContentProviderHelper(mOpenHelper); - - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - // Some providers override onAccountsChanged(); give them a database to work with. - mDb = mOpenHelper.getWritableDatabase(); - onAccountsChanged(accounts); - TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter(); - if (syncAdapter != null) { - syncAdapter.onAccountsChanged(accounts); - } - } - }; - mAccountMonitor = new AccountMonitor(getContext(), listener); + AccountManager.get(getContext()).addOnAccountsUpdatedListener( + new OnAccountsUpdatedListener() { + public void onAccountsUpdated(Account[] accounts) { + // Some providers override onAccountsChanged(); give them a database to + // work with. + mDb = mOpenHelper.getWritableDatabase(); + onAccountsChanged(accounts); + TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); + if (syncAdapter != null) { + syncAdapter.onAccountsChanged(accounts); + } + } + }, null /* handler */, true /* updateImmediately */); return true; } @@ -236,147 +245,117 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro return Collections.emptyList(); } - /** - * <p> - * Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction(). - * {@link #endTransaction} MUST be called after calling this method. - * Those methods should be used like this: - * </p> - * - * <pre class="prettyprint"> - * boolean successful = false; - * beginTransaction(); - * try { - * // Do something related to mDb - * successful = true; - * return ret; - * } finally { - * endTransaction(successful); - * } - * </pre> - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void beginTransaction() { + @Override + public final int update(final Uri url, final ContentValues values, + final String selection, final String[] selectionArgs) { mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); - } - - /** - * <p> - * Call mDb.endTransaction(). If successful is true, try to call - * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). - * This method MUST be used with {@link #beginTransaction()}. - * </p> - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void endTransaction(boolean successful) { + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - if (successful) { - // setTransactionSuccessful() must be called just once during opening the - // transaction. - mDb.setTransactionSuccessful(); + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().update( + url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; } - } finally { - mDb.endTransaction(); - } - } - @Override - public final int update(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); - try { - int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs); - successful = true; - return ret; + int result = updateInternal(url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); - } - } - - /** - * @hide - */ - public final int nonTransactionalUpdate(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().update( - uri, values, selection, selectionArgs); - return numRows; - } - - int result = updateInternal(uri, values, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); + if (notApplyingBatch) { + mDb.endTransaction(); + } } - - return result; } @Override - public final int delete(final Uri uri, final String selection, + public final int delete(final Uri url, final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - int ret = nonTransactionalDelete(uri, selection, selectionArgs); - successful = true; - return ret; + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; + } + int result = deleteInternal(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); + if (notApplyingBatch) { + mDb.endTransaction(); + } } } - /** - * @hide - */ - public final int nonTransactionalDelete(final Uri uri, final String selection, - final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs); - return numRows; - } - int result = deleteInternal(uri, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); } @Override - public final Uri insert(final Uri uri, final ContentValues values) { - boolean successful = false; - beginTransaction(); - try { - Uri ret = nonTransactionalInsert(uri, values); - successful = true; - return ret; - } finally { - endTransaction(successful); + public final Uri insert(final Uri url, final ContentValues values) { + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); } - } - - /** - * @hide - */ - public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) { - if (isTemporary() && mSyncState.matches(uri)) { - Uri result = mSyncState.asContentProvider().insert(uri, values); + try { + if (isTemporary() && mSyncState.matches(url)) { + Uri result = mSyncState.asContentProvider().insert(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return result; + } + Uri result = insertInternal(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result != null) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } return result; + } finally { + if (notApplyingBatch) { + mDb.endTransaction(); + } } - Uri result = insertInternal(uri, values); - if (!isTemporary() && result != null) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; } @Override @@ -411,6 +390,92 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro } /** + * <p> + * Start batch transaction. {@link #endTransaction} MUST be called after + * calling this method. Those methods should be used like this: + * </p> + * + * <pre class="prettyprint"> + * boolean successful = false; + * beginBatch() + * try { + * // Do something related to mDb + * successful = true; + * return ret; + * } finally { + * endBatch(successful); + * } + * </pre> + * + * @hide This method should be used only when {@link ContentProvider#applyBatch} is not enough and must be + * used with {@link #endBatch}. + * e.g. If returned value has to be used during one transaction, this method might be useful. + */ + public final void beginBatch() { + // initialize if this is the first time this thread has applied a batch + if (mApplyingBatch.get() == null) { + mApplyingBatch.set(false); + mPendingBatchNotifications.set(new HashSet<Uri>()); + } + + if (applyingBatch()) { + throw new IllegalStateException( + "applyBatch is not reentrant but mApplyingBatch is already set"); + } + SQLiteDatabase db = getDatabase(); + db.beginTransaction(); + boolean successful = false; + try { + mApplyingBatch.set(true); + successful = true; + } finally { + if (!successful) { + // Something unexpected happened. We must call endTransaction() at least. + db.endTransaction(); + } + } + } + + /** + * <p> + * Finish batch transaction. If "successful" is true, try to call + * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). + * This method MUST be used with {@link #beginBatch()}. + * </p> + * + * @hide This method must be used with {@link #beginTransaction} + */ + public final void endBatch(boolean successful) { + try { + if (successful) { + // setTransactionSuccessful() must be called just once during opening the + // transaction. + mDb.setTransactionSuccessful(); + } + } finally { + mApplyingBatch.set(false); + getDatabase().endTransaction(); + for (Uri url : mPendingBatchNotifications.get()) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + } + } + + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + boolean successful = false; + beginBatch(); + try { + ContentProviderResult[] results = super.applyBatch(operations); + successful = true; + return results; + } finally { + endBatch(successful); + } + } + + /** * Check if changes to this URI can be syncable changes. * @param uri the URI of the resource that was changed * @return true if changes to this URI can be syncable changes, false otherwise @@ -437,8 +502,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * @param context the sync context for the operation * @param account */ - public void onSyncStart(SyncContext context, String account) { - if (TextUtils.isEmpty(account)) { + public void onSyncStart(SyncContext context, Account account) { + if (account == null) { throw new IllegalArgumentException("you passed in an empty account"); } mSyncingAccount = account; @@ -457,7 +522,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * The account of the most recent call to onSyncStart() * @return the account */ - public String getSyncingAccount() { + public Account getSyncingAccount() { return mSyncingAccount; } @@ -568,12 +633,11 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected void onAccountsChanged(String[] accountsArray) { - Map<String, Boolean> accounts = new HashMap<String, Boolean>(); - for (String account : accountsArray) { + protected void onAccountsChanged(Account[] accountsArray) { + Map<Account, Boolean> accounts = Maps.newHashMap(); + for (Account account : accountsArray) { accounts.put(account, false); } - accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map<String, String> tableMap = db.getSyncedTables(); @@ -585,8 +649,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro try { mSyncState.onAccountsChanged(accountsArray); for (String table : tables) { - deleteRowsForRemovedAccounts(accounts, table, - SyncConstValue._SYNC_ACCOUNT); + deleteRowsForRemovedAccounts(accounts, table); } db.setTransactionSuccessful(); } finally { @@ -601,23 +664,23 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts, - String table, String accountColumnName) { + protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(table, sAccountProjection, null, null, - accountColumnName, null, null); + "_sync_account, _sync_account_type", null, null); try { while (c.moveToNext()) { - String account = c.getString(0); - if (TextUtils.isEmpty(account)) { + String accountName = c.getString(0); + String accountType = c.getString(1); + if (TextUtils.isEmpty(accountName)) { continue; } + Account account = new Account(accountName, accountType); if (!accounts.containsKey(account)) { int numDeleted; - numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account}); + numDeleted = db.delete(table, "_sync_account=? AND _sync_account_type=?", + new String[]{account.mName, account.mType}); if (Config.LOGV) { Log.v(TAG, "deleted " + numDeleted + " records from table " + table @@ -634,7 +697,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public void wipeAccount(String account) { + public void wipeAccount(Account account) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map<String, String> tableMap = db.getSyncedTables(); ArrayList<String> tables = new ArrayList<String>(); @@ -649,7 +712,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro // remove the data in the synced tables for (String table : tables) { - db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account}); + db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, + new String[]{account.mName, account.mType}); } db.setTransactionSuccessful(); } finally { @@ -660,14 +724,14 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(String account) { + public byte[] readSyncDataBytes(Account account) { return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account); } /** * Sets the SyncData bytes for the given account. The byte array may be null. */ - public void writeSyncDataBytes(String account, byte[] data) { + public void writeSyncDataBytes(Account account, byte[] data) { mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); } } diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java index 9c760d9..3266c07 100644 --- a/core/java/android/content/AbstractTableMerger.java +++ b/core/java/android/content/AbstractTableMerger.java @@ -25,6 +25,7 @@ import android.provider.BaseColumns; import static android.provider.SyncConstValue.*; import android.text.TextUtils; import android.util.Log; +import android.accounts.Account; /** * @hide @@ -55,15 +56,17 @@ public abstract class AbstractTableMerger private volatile boolean mIsMergeCancelled; - private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?"; + private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = - _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?"; + _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; private static final String SELECT_UNSYNCED = - "(" + _SYNC_ACCOUNT + " IS NULL OR " + _SYNC_ACCOUNT + "=?) AND " - + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 AND " + "(" + _SYNC_ACCOUNT + " IS NULL OR (" + + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?)) and " + + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 and " + _SYNC_VERSION + " IS NOT NULL))"; public AbstractTableMerger(SQLiteDatabase database, @@ -134,7 +137,7 @@ public abstract class AbstractTableMerger * construct a temporary instance to hold them. */ public void merge(final SyncContext context, - final String account, + final Account account, final SyncableContentProvider serverDiffs, TempProviderSyncResult result, SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { @@ -157,7 +160,7 @@ public abstract class AbstractTableMerger * @hide this is public for testing purposes only */ public void mergeServerDiffs(SyncContext context, - String account, SyncableContentProvider serverDiffs, SyncResult syncResult) { + Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) { boolean diffsArePartial = serverDiffs.getContainsDiffs(); // mark the current rows so that we can distinguish these from new // inserts that occur during the merge @@ -166,339 +169,337 @@ public abstract class AbstractTableMerger mDb.update(mDeletedTable, mSyncMarkValues, null, null); } - // load the local database entries, so we can merge them with the server - final String[] accountSelectionArgs = new String[]{account}; - Cursor localCursor = mDb.query(mTable, syncDirtyProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mTable + "." + _SYNC_ID); - Cursor deletedCursor; - if (mDeletedTable != null) { - deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + Cursor localCursor = null; + Cursor deletedCursor = null; + Cursor diffsCursor = null; + try { + // load the local database entries, so we can merge them with the server + final String[] accountSelectionArgs = new String[]{account.mName, account.mType}; + localCursor = mDb.query(mTable, syncDirtyProjection, SELECT_MARKED, accountSelectionArgs, null, null, - mDeletedTable + "." + _SYNC_ID); - } else { - deletedCursor = - mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); - } - - // Apply updates and insertions from the server - Cursor diffsCursor = serverDiffs.query(mTableURL, - null, null, null, mTable + "." + _SYNC_ID); - int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); - int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - - String lastSyncId = null; - int diffsCount = 0; - int localCount = 0; - localCursor.moveToFirst(); - deletedCursor.moveToFirst(); - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - localCursor.close(); - deletedCursor.close(); - diffsCursor.close(); - return; - } - mDb.yieldIfContended(); - String serverSyncId = diffsCursor.getString(serverSyncIDColumn); - String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); - long localRowId = 0; - String localSyncVersion = null; - - diffsCount++; - context.setStatusText("Processing " + diffsCount + "/" - + diffsCursor.getCount()); - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + - diffsCount + ", " + serverSyncId); - - if (TRACE) { - if (diffsCount == 10) { - Debug.startMethodTracing("atmtrace"); - } - if (diffsCount == 20) { - Debug.stopMethodTracing(); - } + mTable + "." + _SYNC_ID); + if (mDeletedTable != null) { + deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + SELECT_MARKED, accountSelectionArgs, null, null, + mDeletedTable + "." + _SYNC_ID); + } else { + deletedCursor = + mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); } - boolean conflict = false; - boolean update = false; - boolean insert = false; + // Apply updates and insertions from the server + diffsCursor = serverDiffs.query(mTableURL, + null, null, null, mTable + "." + _SYNC_ID); + int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); + int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); + int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found event with serverSyncID " + serverSyncId); - } - if (TextUtils.isEmpty(serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.e(TAG, "server entry doesn't have a serverSyncID"); + String lastSyncId = null; + int diffsCount = 0; + int localCount = 0; + localCursor.moveToFirst(); + deletedCursor.moveToFirst(); + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; } - continue; - } - - // It is possible that the sync adapter wrote the same record multiple times, - // e.g. if the same record came via multiple feeds. If this happens just ignore - // the duplicate records. - if (serverSyncId.equals(lastSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); + mDb.yieldIfContended(); + String serverSyncId = diffsCursor.getString(serverSyncIDColumn); + String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); + long localRowId = 0; + String localSyncVersion = null; + + diffsCount++; + context.setStatusText("Processing " + diffsCount + "/" + + diffsCursor.getCount()); + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + + diffsCount + ", " + serverSyncId); + + if (TRACE) { + if (diffsCount == 10) { + Debug.startMethodTracing("atmtrace"); + } + if (diffsCount == 20) { + Debug.stopMethodTracing(); + } } - continue; - } - lastSyncId = serverSyncId; - String localSyncID = null; - boolean localSyncDirty = false; + boolean conflict = false; + boolean update = false; + boolean insert = false; - while (!localCursor.isAfterLast()) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "found event with serverSyncID " + serverSyncId); + } + if (TextUtils.isEmpty(serverSyncId)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.e(TAG, "server entry doesn't have a serverSyncID"); + } + continue; } - localCount++; - localSyncID = localCursor.getString(2); - // If the local record doesn't have a _sync_id then - // it is new. Ignore it for now, we will send an insert - // the the server later. - if (TextUtils.isEmpty(localSyncID)) { + // It is possible that the sync adapter wrote the same record multiple times, + // e.g. if the same record came via multiple feeds. If this happens just ignore + // the duplicate records. + if (serverSyncId.equals(lastSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has no _sync_id, ignoring"); + Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); } - localCursor.moveToNext(); - localSyncID = null; continue; } + lastSyncId = serverSyncId; - int comp = serverSyncId.compareTo(localSyncID); + String localSyncID = null; + boolean localSyncDirty = false; - // the local DB has a record that the server doesn't have - if (comp > 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is < server _sync_id " + serverSyncId); + while (!localCursor.isAfterLast()) { + if (mIsMergeCancelled) { + return; } - if (diffsArePartial) { - localCursor.moveToNext(); - } else { - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + localCount++; + localSyncID = localCursor.getString(2); + + // If the local record doesn't have a _sync_id then + // it is new. Ignore it for now, we will send an insert + // the the server later. + if (TextUtils.isEmpty(localSyncID)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has no _sync_id, ignoring"); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); + localCursor.moveToNext(); + localSyncID = null; + continue; } - localSyncID = null; - continue; - } - // the server has a record that the local DB doesn't have - if (comp < 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is > server _sync_id " + serverSyncId); + int comp = serverSyncId.compareTo(localSyncID); + + // the local DB has a record that the server doesn't have + if (comp > 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is < server _sync_id " + serverSyncId); + } + if (diffsArePartial) { + localCursor.moveToNext(); + } else { + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); + } + localSyncID = null; + continue; } - localSyncID = null; - } - // the server and the local DB both have this record - if (comp == 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that matches the server _sync_id"); + // the server has a record that the local DB doesn't have + if (comp < 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is > server _sync_id " + serverSyncId); + } + localSyncID = null; } - localSyncDirty = localCursor.getInt(0) != 0; - localRowId = localCursor.getLong(1); - localSyncVersion = localCursor.getString(3); - localCursor.moveToNext(); - } - break; - } + // the server and the local DB both have this record + if (comp == 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that matches the server _sync_id"); + } + localSyncDirty = localCursor.getInt(0) != 0; + localRowId = localCursor.getLong(1); + localSyncVersion = localCursor.getString(3); + localCursor.moveToNext(); + } - // If this record is in the deleted table then update the server version - // in the deleted table, if necessary, and then ignore it here. - // We will send a deletion indication to the server down a - // little further. - if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); + break; } - final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); - if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + + // If this record is in the deleted table then update the server version + // in the deleted table, if necessary, and then ignore it here. + // We will send a deletion indication to the server down a + // little further. + if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " - + serverSyncVersion); + Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); } - ContentValues values = new ContentValues(); - values.put(_SYNC_VERSION, serverSyncVersion); - mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); + if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " + + serverSyncVersion); + } + ContentValues values = new ContentValues(); + values.put(_SYNC_VERSION, serverSyncVersion); + mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + } + continue; } - continue; - } - // If the _sync_local_id is present in the diffsCursor - // then this record corresponds to a local record that was just - // inserted into the server and the _sync_local_id is the row id - // of the local record. Set these fields so that the next check - // treats this record as an update, which will allow the - // merger to update the record with the server's sync id - if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { - localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the remote record with sync id " + serverSyncId - + " has a local sync id, " + localRowId); + // If the _sync_local_id is present in the diffsCursor + // then this record corresponds to a local record that was just + // inserted into the server and the _sync_local_id is the row id + // of the local record. Set these fields so that the next check + // treats this record as an update, which will allow the + // merger to update the record with the server's sync id + if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { + localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the remote record with sync id " + serverSyncId + + " has a local sync id, " + localRowId); + } + localSyncID = serverSyncId; + localSyncDirty = false; + localSyncVersion = null; } - localSyncID = serverSyncId; - localSyncDirty = false; - localSyncVersion = null; - } - if (!TextUtils.isEmpty(localSyncID)) { - // An existing server item has changed - // If serverSyncVersion is null, there is no edit URL; - // server won't let this change be written. - // Just hold onto it, I guess, in case the server permissions - // change later. - if (serverSyncVersion != null) { - boolean recordChanged = (localSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); - } - conflict = true; - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); + if (!TextUtils.isEmpty(localSyncID)) { + // An existing server item has changed + // If serverSyncVersion is null, there is no edit URL; + // server won't let this change be written. + // Just hold onto it, I guess, in case the server permissions + // change later. + if (serverSyncVersion != null) { + boolean recordChanged = (localSyncVersion == null) || + !serverSyncVersion.equals(localSyncVersion); + if (recordChanged) { + if (localSyncDirty) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + + " conflicts with local _sync_id " + localSyncID + + ", local _id " + localRowId); + } + conflict = true; + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "remote record " + + serverSyncId + + " updates local _sync_id " + + localSyncID + ", local _id " + + localRowId); + } + update = true; } - update = true; } } + } else { + // the local db doesn't know about this record so add it + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + } + insert = true; } - } else { - // the local db doesn't know about this record so add it - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + + if (update) { + updateRow(localRowId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (conflict) { + resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (insert) { + insertRow(serverDiffs, diffsCursor); + syncResult.stats.numInserts++; } - insert = true; } - if (update) { - updateRow(localRowId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (conflict) { - resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (insert) { - insertRow(serverDiffs, diffsCursor); - syncResult.stats.numInserts++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "processed " + diffsCount + " server entries"); } - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "processed " + diffsCount + " server entries"); - } - - // If tombstones aren't in use delete any remaining local rows that - // don't have corresponding server rows. Keep the rows that don't - // have a sync id since those were created locally and haven't been - // synced to the server yet. - if (!diffsArePartial) { - while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; - } - localCount++; - final String localSyncId = localCursor.getString(2); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "deleting local record " + - localCursor.getLong(1) + - " _sync_id " + localSyncId); - } - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + // If tombstones aren't in use delete any remaining local rows that + // don't have corresponding server rows. Keep the rows that don't + // have a sync id since those were created locally and haven't been + // synced to the server yet. + if (!diffsArePartial) { + while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { + if (mIsMergeCancelled) { + return; + } + localCount++; + final String localSyncId = localCursor.getString(2); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "deleting local record " + + localCursor.getLong(1) + + " _sync_id " + localSyncId); + } + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); } + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + + " local entries"); + } finally { + if (diffsCursor != null) diffsCursor.close(); + if (localCursor != null) localCursor.close(); + if (deletedCursor != null) deletedCursor.close(); } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + - " local entries"); - diffsCursor.deactivate(); - localCursor.deactivate(); - deletedCursor.deactivate(); if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server"); // Apply deletions from the server if (mDeletedTableURL != null) { diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null); - - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - diffsCursor.deactivate(); - return; + try { + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + // delete all rows that match each element in the diffsCursor + fullyDeleteMatchingRows(diffsCursor, account, syncResult); + mDb.yieldIfContended(); } - // delete all rows that match each element in the diffsCursor - fullyDeleteMatchingRows(diffsCursor, account, syncResult); - mDb.yieldIfContended(); + } finally { + diffsCursor.close(); } - diffsCursor.deactivate(); } } - private void fullyDeleteMatchingRows(Cursor diffsCursor, String account, + private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account, SyncResult syncResult) { int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn); // delete the rows explicitly so that the delete operation can be overridden - final Cursor c; final String[] selectionArgs; - if (deleteBySyncId) { - selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), account}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, - selectionArgs, null, null, null); - } else { - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, - null, null, null); - } + Cursor c = null; try { + if (deleteBySyncId) { + selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), + account.mName, account.mType}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, + selectionArgs, null, null, null); + } else { + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); + selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, + null, null, null); + } c.moveToFirst(); while (!c.isAfterLast()) { deleteRow(c); // advances the cursor syncResult.stats.numDeletes++; } } finally { - c.deactivate(); + if (c != null) c.close(); } if (deleteBySyncId && mDeletedTable != null) { mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); @@ -516,43 +517,46 @@ public abstract class AbstractTableMerger * Finds local changes, placing the results in the given result object. * @param temporaryInstanceFactory As an optimization for the case * where there are no client-side diffs, mergeResult may initially - * have no {@link android.content.TempProviderSyncResult#tempContentProvider}. If this is + * have no {@link TempProviderSyncResult#tempContentProvider}. If this is * the first in the sequence of AbstractTableMergers to find * client-side diffs, it will use the given ContentProvider to * create a temporary instance and store its {@link - * ContentProvider} in the mergeResult. + * android.content.ContentProvider} in the mergeResult. * @param account * @param syncResult */ private void findLocalChanges(TempProviderSyncResult mergeResult, - SyncableContentProvider temporaryInstanceFactory, String account, + SyncableContentProvider temporaryInstanceFactory, Account account, SyncResult syncResult) { SyncableContentProvider clientDiffs = mergeResult.tempContentProvider; if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates"); - final String[] accountSelectionArgs = new String[]{account}; + final String[] accountSelectionArgs = new String[]{account.mName, account.mType}; // Generate the client updates and insertions // Create a cursor for dirty records + long numInsertsOrUpdates = 0; Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs, null, null, null); - long numInsertsOrUpdates = localChangesCursor.getCount(); - while (localChangesCursor.moveToNext()) { - if (mIsMergeCancelled) { - localChangesCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numInsertsOrUpdates = localChangesCursor.getCount(); + while (localChangesCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + cursorRowToContentValues(localChangesCursor, mValues); + mValues.remove("_id"); + DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, + _SYNC_LOCAL_ID); + clientDiffs.insert(mTableURL, mValues); } - mValues.clear(); - cursorRowToContentValues(localChangesCursor, mValues); - mValues.remove("_id"); - DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, - _SYNC_LOCAL_ID); - clientDiffs.insert(mTableURL, mValues); + } finally { + localChangesCursor.close(); } - localChangesCursor.close(); // Generate the client deletions if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); @@ -561,23 +565,25 @@ public abstract class AbstractTableMerger if (mDeletedTable != null) { Cursor deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, - _SYNC_ACCOUNT + "=? AND " + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, + _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND " + + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, null, null, mDeletedTable + "." + _SYNC_ID); - - numDeletedEntries = deletedCursor.getCount(); - while (deletedCursor.moveToNext()) { - if (mIsMergeCancelled) { - deletedCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numDeletedEntries = deletedCursor.getCount(); + while (deletedCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); + clientDiffs.insert(mDeletedTableURL, mValues); } - mValues.clear(); - DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); - clientDiffs.insert(mDeletedTableURL, mValues); + } finally { + deletedCursor.close(); } - deletedCursor.close(); } if (clientDiffs != null) { diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java new file mode 100644 index 0000000..f15a902 --- /dev/null +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.accounts.Account; +import android.os.Bundle; +import android.os.Process; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An abstract implementation of a SyncAdapter that spawns a thread to invoke a sync operation. + * If a sync operation is already in progress when a startSync() request is received then an error + * will be returned to the new request and the existing request will be allowed to continue. + * When a startSync() is received and there is no sync operation in progress then a thread + * will be started to run the operation and {@link #performSync} will be invoked on that thread. + * If a cancelSync() is received that matches an existing sync operation then the thread + * that is running that sync operation will be interrupted, which will indicate to the thread + * that the sync has been canceled. + * + * @hide + */ +public abstract class AbstractThreadedSyncAdapter { + private final Context mContext; + private final AtomicInteger mNumSyncStarts; + private final ISyncAdapterImpl mISyncAdapterImpl; + + // all accesses to this member variable must be synchronized on "this" + private SyncThread mSyncThread; + + /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ + public static final int LOG_SYNC_DETAILS = 2743; + + /** + * Creates an {@link AbstractThreadedSyncAdapter}. + * @param context the {@link Context} that this is running within. + */ + public AbstractThreadedSyncAdapter(Context context) { + mContext = context; + mISyncAdapterImpl = new ISyncAdapterImpl(); + mNumSyncStarts = new AtomicInteger(0); + mSyncThread = null; + } + + class ISyncAdapterImpl extends ISyncAdapter.Stub { + public void startSync(ISyncContext syncContext, String authority, Account account, + Bundle extras) { + final SyncContext syncContextClient = new SyncContext(syncContext); + + boolean alreadyInProgress; + // synchronize to make sure that mSyncThread doesn't change between when we + // check it and when we use it + synchronized (this) { + if (mSyncThread == null) { + mSyncThread = new SyncThread( + "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), + syncContextClient, authority, account, extras); + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + mSyncThread.start(); + alreadyInProgress = false; + } else { + alreadyInProgress = true; + } + } + + // do this outside since we don't want to call back into the syncContext while + // holding the synchronization lock + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + public void cancelSync(ISyncContext syncContext) { + // synchronize to make sure that mSyncThread doesn't change between when we + // check it and when we use it + synchronized (this) { + if (mSyncThread != null + && mSyncThread.mSyncContext.getISyncContext() == syncContext) { + mSyncThread.interrupt(); + } + } + } + } + + /** + * The thread that invokes performSync(). It also acquires the provider for this sync + * before calling performSync and releases it afterwards. Cancel this thread in order to + * cancel the sync. + */ + private class SyncThread extends Thread { + private final SyncContext mSyncContext; + private final String mAuthority; + private final Account mAccount; + private final Bundle mExtras; + + private SyncThread(String name, SyncContext syncContext, String authority, + Account account, Bundle extras) { + super(name); + mSyncContext = syncContext; + mAuthority = authority; + mAccount = account; + mExtras = extras; + } + + public void run() { + if (isCanceled()) { + return; + } + + SyncResult syncResult = new SyncResult(); + ContentProviderClient provider = null; + try { + provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + if (provider != null) { + AbstractThreadedSyncAdapter.this.performSync(mAccount, mExtras, + mAuthority, provider, syncResult); + } else { + // TODO(fredq) update the syncResults to indicate that we were unable to + // find the provider. maybe with a ProviderError? + } + } finally { + if (provider != null) { + provider.release(); + } + if (!isCanceled()) { + mSyncContext.onFinished(syncResult); + } + // synchronize so that the assignment will be seen by other threads + // that also synchronize accesses to mSyncThread + synchronized (this) { + mSyncThread = null; + } + } + } + + private boolean isCanceled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * @return a reference to the ISyncAdapter interface into this SyncAdapter implementation. + */ + public final ISyncAdapter getISyncAdapter() { + return mISyncAdapterImpl; + } + + /** + * Perform a sync for this account. SyncAdapter-specific parameters may + * be specified in extras, which is guaranteed to not be null. Invocations + * of this method are guaranteed to be serialized. + * + * @param account the account that should be synced + * @param extras SyncAdapter-specific parameters + * @param authority the authority of this sync request + * @param provider a ContentProviderClient that points to the ContentProvider for this + * authority + * @param syncResult SyncAdapter-specific parameters + */ + public abstract void performSync(Account account, Bundle extras, + String authority, ContentProviderClient provider, SyncResult syncResult); +}
\ No newline at end of file diff --git a/core/java/android/content/ActiveSyncInfo.java b/core/java/android/content/ActiveSyncInfo.java index 63be8d1..209dffa 100644 --- a/core/java/android/content/ActiveSyncInfo.java +++ b/core/java/android/content/ActiveSyncInfo.java @@ -16,17 +16,18 @@ package android.content; +import android.accounts.Account; import android.os.Parcel; import android.os.Parcelable.Creator; /** @hide */ public class ActiveSyncInfo { public final int authorityId; - public final String account; + public final Account account; public final String authority; public final long startTime; - ActiveSyncInfo(int authorityId, String account, String authority, + ActiveSyncInfo(int authorityId, Account account, String authority, long startTime) { this.authorityId = authorityId; this.account = account; @@ -40,14 +41,14 @@ public class ActiveSyncInfo { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - parcel.writeString(account); + account.writeToParcel(parcel, 0); parcel.writeString(authority); parcel.writeLong(startTime); } ActiveSyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = parcel.readString(); + account = new Account(parcel); authority = parcel.readString(); startTime = parcel.readLong(); } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 6b50405..5b29b97 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -34,6 +34,7 @@ import android.os.Process; import java.io.File; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * Content providers are one of the primary building blocks of Android applications, providing @@ -130,6 +131,12 @@ public abstract class ContentProvider implements ComponentCallbacks { selectionArgs, sortOrder); } + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + enforceReadPermission(uri); + return ContentProvider.this.queryEntities(uri, selection, selectionArgs, sortOrder); + } + public String getType(Uri uri) { return ContentProvider.this.getType(uri); } @@ -145,6 +152,25 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.bulkInsert(uri, initialValues); } + public Uri insertEntity(Uri uri, Entity entities) { + enforceWritePermission(uri); + return ContentProvider.this.insertEntity(uri, entities); + } + + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + for (ContentProviderOperation operation : operations) { + if (operation.isReadOperation()) { + enforceReadPermission(operation.getUri()); + } + + if (operation.isWriteOperation()) { + enforceWritePermission(operation.getUri()); + } + } + return ContentProvider.this.applyBatch(operations); + } + public int delete(Uri uri, String selection, String[] selectionArgs) { enforceWritePermission(uri); return ContentProvider.this.delete(uri, selection, selectionArgs); @@ -156,6 +182,11 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.update(uri, values, selection, selectionArgs); } + public int updateEntity(Uri uri, Entity entity) { + enforceWritePermission(uri); + return ContentProvider.this.updateEntity(uri, entity); + } + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) enforceWritePermission(uri); @@ -170,12 +201,6 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.openAssetFile(uri, mode); } - public ISyncAdapter getSyncAdapter() { - enforceWritePermission(null); - SyncAdapter sa = ContentProvider.this.getSyncAdapter(); - return sa != null ? sa.getISyncAdapter() : null; - } - private void enforceReadPermission(Uri uri) { final int uid = Binder.getCallingUid(); if (uid == mMyUid) { @@ -377,9 +402,10 @@ public abstract class ContentProvider implements ComponentCallbacks { * Example client call:<p> * <pre>// Request a specific record. * Cursor managedCursor = managedQuery( - Contacts.People.CONTENT_URI.addId(2), + ContentUris.withAppendedId(Contacts.People.CONTENT_URI, 2), projection, // Which columns to return. null, // WHERE clause. + null, // WHERE clause value substitution People.NAME + " ASC"); // Sort order.</pre> * Example implementation:<p> * <pre>// SQLiteQueryBuilder is a helper class that creates the @@ -408,20 +434,28 @@ public abstract class ContentProvider implements ComponentCallbacks { return c;</pre> * * @param uri The URI to query. This will be the full URI sent by the client; - * if the client is requesting a specific record, the URI will end in a record number - * that the implementation should parse and add to a WHERE or HAVING clause, specifying - * that _id value. + * if the client is requesting a specific record, the URI will end in a record number + * that the implementation should parse and add to a WHERE or HAVING clause, specifying + * that _id value. * @param projection The list of columns to put into the cursor. If * null all columns are included. * @param selection A selection criteria to apply when filtering rows. * If null then all rows are included. + * @param selectionArgs You may include ?s in selection, which will be replaced by + * the values from selectionArgs, in order that they appear in the selection. + * The values will be bound as Strings. * @param sortOrder How the rows in the cursor should be sorted. - * If null then the provider is free to define the sort order. + * If null then the provider is free to define the sort order. * @return a Cursor or null. */ public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + /** * Return the MIME type of the data at the given URI. This should start with * <code>vnd.android.cursor.item</code> for a single record, @@ -472,6 +506,10 @@ public abstract class ContentProvider implements ComponentCallbacks { return numValues; } + public Uri insertEntity(Uri uri, Entity entity) { + throw new UnsupportedOperationException(); + } + /** * A request to delete one or more rows. The selection clause is applied when performing * the deletion, allowing the operation to affect multiple rows in a @@ -516,6 +554,10 @@ public abstract class ContentProvider implements ComponentCallbacks { public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs); + public int updateEntity(Uri uri, Entity entity) { + throw new UnsupportedOperationException(); + } + /** * Open a file blob associated with a content URI. * This method can be called from multiple @@ -639,23 +681,6 @@ public abstract class ContentProvider implements ComponentCallbacks { } /** - * Get the sync adapter that is to be used by this content provider. - * This is intended for use by the sync system. If null then this - * content provider is considered not syncable. - * This method can be called from multiple - * threads, as described in - * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: - * Processes and Threads</a>. - * - * @return the SyncAdapter that is to be used by this ContentProvider, or null - * if this ContentProvider is not syncable - * @hide - */ - public SyncAdapter getSyncAdapter() { - return null; - } - - /** * Returns true if this instance is a temporary content provider. * @return true if this instance is a temporary content provider */ @@ -697,4 +722,27 @@ public abstract class ContentProvider implements ComponentCallbacks { ContentProvider.this.onCreate(); } } -} + + /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + */ + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + results[i] = operations.get(i).apply(this, results, i); + } + return results; + } +}
\ No newline at end of file diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java new file mode 100644 index 0000000..452653e --- /dev/null +++ b/core/java/android/content/ContentProviderClient.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.os.ParcelFileDescriptor; +import android.content.res.AssetFileDescriptor; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +/** + * The public interface object used to interact with a {@link ContentProvider}. This is obtained by + * calling {@link ContentResolver#acquireContentProviderClient}. This object must be released + * using {@link #release} in order to indicate to the system that the {@link ContentProvider} is + * no longer needed and can be killed to free up resources. + */ +public class ContentProviderClient { + private final IContentProvider mContentProvider; + private final ContentResolver mContentResolver; + + /** + * @hide + */ + ContentProviderClient(ContentResolver contentResolver, IContentProvider contentProvider) { + mContentProvider = contentProvider; + mContentResolver = contentResolver; + } + + /** see {@link ContentProvider#query} */ + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder) throws RemoteException { + return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#getType} */ + public String getType(Uri url) throws RemoteException { + return mContentProvider.getType(url); + } + + /** see {@link ContentProvider#insert} */ + public Uri insert(Uri url, ContentValues initialValues) + throws RemoteException { + return mContentProvider.insert(url, initialValues); + } + + /** see {@link ContentProvider#bulkInsert} */ + public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException { + return mContentProvider.bulkInsert(url, initialValues); + } + + /** see {@link ContentProvider#delete} */ + public int delete(Uri url, String selection, String[] selectionArgs) + throws RemoteException { + return mContentProvider.delete(url, selection, selectionArgs); + } + + /** see {@link ContentProvider#update} */ + public int update(Uri url, ContentValues values, String selection, + String[] selectionArgs) throws RemoteException { + return mContentProvider.update(url, values, selection, selectionArgs); + } + + /** see {@link ContentProvider#openFile} */ + public ParcelFileDescriptor openFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openFile(url, mode); + } + + /** see {@link ContentProvider#openAssetFile} */ + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openAssetFile(url, mode); + } + + /** see {@link ContentProvider#queryEntities} */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) throws RemoteException { + return mContentProvider.queryEntities(uri, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#insertEntity} */ + public Uri insertEntity(Uri uri, Entity entity) throws RemoteException { + return mContentProvider.insertEntity(uri, entity); + } + + /** see {@link ContentProvider#updateEntity} */ + public int updateEntity(Uri uri, Entity entity) throws RemoteException { + return mContentProvider.updateEntity(uri, entity); + } + + /** see {@link ContentProvider#applyBatch} */ + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + return mContentProvider.applyBatch(operations); + } + + /** + * Call this to indicate to the system that the associated {@link ContentProvider} is no + * longer needed by this {@link ContentProviderClient}. + * @return true if this was release, false if it was already released + */ + public boolean release() { + return mContentResolver.releaseProvider(mContentProvider); + } + + /** + * Get a reference to the {@link ContentProvider} that is associated with this + * client. If the {@link ContentProvider} is running in a different process then + * null will be returned. This can be used if you know you are running in the same + * process as a provider, and want to get direct access to its implementation details. + * + * @return If the associated {@link ContentProvider} is local, returns it. + * Otherwise returns null. + */ + public ContentProvider getLocalContentProvider() { + return ContentProvider.coerceToLocalContentProvider(mContentProvider); + } +} diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index e5e3f74..a4c217b 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -33,6 +33,7 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * {@hide} @@ -105,6 +106,20 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case QUERY_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String selection = data.readString(); + String[] selectionArgs = data.readStringArray(); + String sortOrder = data.readString(); + EntityIterator entityIterator = queryEntities(url, selection, selectionArgs, + sortOrder); + reply.writeNoException(); + reply.writeStrongBinder(new IEntityIteratorImpl(entityIterator).asBinder()); + return true; + } + case GET_TYPE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -140,6 +155,43 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case INSERT_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri uri = Uri.CREATOR.createFromParcel(data); + Entity entity = (Entity) data.readParcelable(null); + Uri newUri = insertEntity(uri, entity); + reply.writeNoException(); + Uri.writeToParcel(reply, newUri); + return true; + } + + case UPDATE_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri uri = Uri.CREATOR.createFromParcel(data); + Entity entity = (Entity) data.readParcelable(null); + int count = updateEntity(uri, entity); + reply.writeNoException(); + reply.writeInt(count); + return true; + } + + case APPLY_BATCH_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + final int numOperations = data.readInt(); + final ArrayList<ContentProviderOperation> operations = + new ArrayList<ContentProviderOperation>(numOperations); + for (int i = 0; i < numOperations; i++) { + operations.add(i, ContentProviderOperation.CREATOR.createFromParcel(data)); + } + final ContentProviderResult[] results = applyBatch(operations); + reply.writeNoException(); + reply.writeTypedArray(results, 0); + return true; + } + case DELETE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -206,15 +258,6 @@ abstract public class ContentProviderNative extends Binder implements IContentPr } return true; } - - case GET_SYNC_ADAPTER_TRANSACTION: - { - data.enforceInterface(IContentProvider.descriptor); - ISyncAdapter sa = getSyncAdapter(); - reply.writeNoException(); - reply.writeStrongBinder(sa != null ? sa.asBinder() : null); - return true; - } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -224,6 +267,25 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return super.onTransact(code, data, reply, flags); } + private class IEntityIteratorImpl extends IEntityIterator.Stub { + private final EntityIterator mEntityIterator; + + IEntityIteratorImpl(EntityIterator iterator) { + mEntityIterator = iterator; + } + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void close() throws RemoteException { + mEntityIterator.close(); + } + } + public IBinder asBinder() { return this; @@ -297,7 +359,7 @@ final class ContentProviderProxy implements IContentProvider BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); IBulkCursor bulkCursor = bulkQuery(url, projection, selection, selectionArgs, sortOrder, adaptor.getObserver(), window); - + if (bulkCursor == null) { return null; } @@ -305,6 +367,54 @@ final class ContentProviderProxy implements IContentProvider return adaptor; } + public EntityIterator queryEntities(Uri url, String selection, String[] selectionArgs, + String sortOrder) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(selection); + data.writeStringArray(selectionArgs); + data.writeString(sortOrder); + + mRemote.transact(IContentProvider.QUERY_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + + IBinder entityIteratorBinder = reply.readStrongBinder(); + + data.recycle(); + reply.recycle(); + + return new RemoteEntityIterator(IEntityIterator.Stub.asInterface(entityIteratorBinder)); + } + + static class RemoteEntityIterator implements EntityIterator { + private final IEntityIterator mEntityIterator; + RemoteEntityIterator(IEntityIterator entityIterator) { + mEntityIterator = entityIterator; + } + + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void close() { + try { + mEntityIterator.close(); + } catch (RemoteException e) { + // doesn't matter + } + } + } + public String getType(Uri url) throws RemoteException { Parcel data = Parcel.obtain(); @@ -366,6 +476,66 @@ final class ContentProviderProxy implements IContentProvider return count; } + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + data.writeInt(operations.size()); + for (ContentProviderOperation operation : operations) { + operation.writeToParcel(data, 0); + } + mRemote.transact(IContentProvider.APPLY_BATCH_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithOperationApplicationExceptionFromParcel(reply); + final ContentProviderResult[] results = + reply.createTypedArray(ContentProviderResult.CREATOR); + + data.recycle(); + reply.recycle(); + + return results; + } + + public Uri insertEntity(Uri uri, Entity entity) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + try { + data.writeInterfaceToken(IContentProvider.descriptor); + uri.writeToParcel(data, 0); + data.writeParcelable(entity, 0); + + mRemote.transact(IContentProvider.INSERT_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + return Uri.CREATOR.createFromParcel(reply); + } finally { + data.recycle(); + reply.recycle(); + } + } + + public int updateEntity(Uri uri, Entity entity) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + try { + data.writeInterfaceToken(IContentProvider.descriptor); + uri.writeToParcel(data, 0); + data.writeParcelable(entity, 0); + + mRemote.transact(IContentProvider.UPDATE_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + return reply.readInt(); + } finally { + data.recycle(); + reply.recycle(); + } + } + public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); @@ -456,23 +626,6 @@ final class ContentProviderProxy implements IContentProvider return fd; } - public ISyncAdapter getSyncAdapter() throws RemoteException { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IContentProvider.descriptor); - - mRemote.transact(IContentProvider.GET_SYNC_ADAPTER_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - ISyncAdapter syncAdapter = ISyncAdapter.Stub.asInterface(reply.readStrongBinder()); - - data.recycle(); - reply.recycle(); - - return syncAdapter; - } - private IBinder mRemote; } diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java new file mode 100644 index 0000000..8b0b6ab --- /dev/null +++ b/core/java/android/content/ContentProviderOperation.java @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.net.Uri; +import android.database.Cursor; +import android.os.Parcelable; +import android.os.Parcel; +import android.os.Debug; + +import java.util.Map; +import java.util.HashMap; + +public class ContentProviderOperation implements Parcelable { + private final static int TYPE_INSERT = 1; + private final static int TYPE_UPDATE = 2; + private final static int TYPE_DELETE = 3; + private final static int TYPE_COUNT = 4; + + private final int mType; + private final Uri mUri; + private final String mSelection; + private final String[] mSelectionArgs; + private final ContentValues mValues; + private final Integer mExpectedCount; + private final ContentValues mValuesBackReferences; + private final Map<Integer, Integer> mSelectionArgsBackReferences; + + private static final String[] COUNT_COLUMNS = new String[]{"count(*)"}; + + /** + * Creates a {@link ContentProviderOperation} by copying the contents of a + * {@link Builder}. + */ + private ContentProviderOperation(Builder builder) { + mType = builder.mType; + mUri = builder.mUri; + mValues = builder.mValues; + mSelection = builder.mSelection; + mSelectionArgs = builder.mSelectionArgs; + mExpectedCount = builder.mExpectedCount; + mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences; + mValuesBackReferences = builder.mValuesBackReferences; + } + + private ContentProviderOperation(Parcel source) { + mType = source.readInt(); + mUri = Uri.CREATOR.createFromParcel(source); + mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null; + mSelection = source.readInt() != 0 ? source.readString() : null; + mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null; + mExpectedCount = source.readInt() != 0 ? source.readInt() : null; + mValuesBackReferences = source.readInt() != 0 + + ? ContentValues.CREATOR.createFromParcel(source) + : null; + mSelectionArgsBackReferences = source.readInt() != 0 + ? new HashMap<Integer, Integer>() + : null; + if (mSelectionArgsBackReferences != null) { + final int count = source.readInt(); + for (int i = 0; i < count; i++) { + mSelectionArgsBackReferences.put(source.readInt(), source.readInt()); + } + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + Uri.writeToParcel(dest, mUri); + if (mValues != null) { + dest.writeInt(1); + mValues.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelection != null) { + dest.writeInt(1); + dest.writeString(mSelection); + } else { + dest.writeInt(0); + } + if (mSelectionArgs != null) { + dest.writeInt(1); + dest.writeStringArray(mSelectionArgs); + } else { + dest.writeInt(0); + } + if (mExpectedCount != null) { + dest.writeInt(1); + dest.writeInt(mExpectedCount); + } else { + dest.writeInt(0); + } + if (mValuesBackReferences != null) { + dest.writeInt(1); + mValuesBackReferences.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelectionArgsBackReferences != null) { + dest.writeInt(1); + dest.writeInt(mSelectionArgsBackReferences.size()); + for (Map.Entry<Integer, Integer> entry : mSelectionArgsBackReferences.entrySet()) { + dest.writeInt(entry.getKey()); + dest.writeInt(entry.getValue()); + } + } else { + dest.writeInt(0); + } + } + + /** + * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the insert. + * @return a {@link Builder} + */ + public static Builder newInsert(Uri uri) { + return new Builder(TYPE_INSERT, uri); + } + + /** + * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the update. + * @return a {@link Builder} + */ + public static Builder newUpdate(Uri uri) { + return new Builder(TYPE_UPDATE, uri); + } + + /** + * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the delete. + * @return a {@link Builder} + */ + public static Builder newDelete(Uri uri) { + return new Builder(TYPE_DELETE, uri); + } + + /** + * Create a {@link Builder} suitable for building a count query. When used in conjunction + * with {@link Builder#withExpectedCount(int)} this is useful for checking that the + * uri/selection has the expected number of rows. + * {@link ContentProviderOperation}. + * @param uri The {@link Uri} to query. + * @return a {@link Builder} + */ + public static Builder newCountQuery(Uri uri) { + return new Builder(TYPE_COUNT, uri); + } + + public Uri getUri() { + return mUri; + } + + public boolean isWriteOperation() { + return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; + } + + public boolean isReadOperation() { + return mType == TYPE_COUNT; + } + + /** + * Applies this operation using the given provider. The backRefs array is used to resolve any + * back references that were requested using + * {@link Builder#withValueBackReferences(ContentValues)} and + * {@link Builder#withSelectionBackReference}. + * @param provider the {@link ContentProvider} on which this batch is applied + * @param backRefs a {@link ContentProviderResult} array that will be consulted + * to resolve any requested back references. + * @param numBackRefs the number of valid results on the backRefs array. + * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted + * row if this was an insert otherwise the number of rows affected. + * @throws OperationApplicationException thrown if either the insert fails or + * if the number of rows affected didn't match the expected count + */ + public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs, + int numBackRefs) throws OperationApplicationException { + ContentValues values = resolveValueBackReferences(backRefs, numBackRefs); + String[] selectionArgs = + resolveSelectionArgsBackReferences(backRefs, numBackRefs); + + if (mType == TYPE_INSERT) { + Uri newUri = provider.insert(mUri, values); + if (newUri == null) { + throw new OperationApplicationException("insert failed"); + } + return new ContentProviderResult(newUri); + } + + int numRows; + if (mType == TYPE_DELETE) { + numRows = provider.delete(mUri, mSelection, selectionArgs); + } else if (mType == TYPE_UPDATE) { + numRows = provider.update(mUri, values, mSelection, selectionArgs); + } else if (mType == TYPE_COUNT) { + Cursor cursor = provider.query(mUri, COUNT_COLUMNS, mSelection, selectionArgs, null); + try { + if (!cursor.moveToNext()) { + throw new RuntimeException("since we are doing a count query we should always " + + "be able to move to the first row"); + } + if (cursor.getCount() != 1) { + throw new RuntimeException("since we are doing a count query there should " + + "always be exacly row, found " + cursor.getCount()); + } + numRows = cursor.getInt(0); + } finally { + cursor.close(); + } + } else { + throw new IllegalStateException("bad type, " + mType); + } + + if (mExpectedCount != null && mExpectedCount != numRows) { + throw new OperationApplicationException("wrong number of rows: " + numRows); + } + + return new ContentProviderResult(numRows); + } + + /** + * The ContentValues back references are represented as a ContentValues object where the + * key refers to a column and the value is an index of the back reference whose + * valued should be associated with the column. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public ContentValues resolveValueBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mValuesBackReferences == null) { + return mValues; + } + final ContentValues values; + if (mValues == null) { + values = new ContentValues(); + } else { + values = new ContentValues(mValues); + } + for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) { + String key = entry.getKey(); + Integer backRefIndex = mValuesBackReferences.getAsInteger(key); + if (backRefIndex == null) { + throw new IllegalArgumentException("values backref " + key + " is not an integer"); + } + values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex)); + } + return values; + } + + /** + * The Selection Arguments back references are represented as a Map of Integer->Integer where + * the key is an index into the selection argument array (see {@link Builder#withSelection}) + * and the value is the index of the previous result that should be used for that selection + * argument array slot. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public String[] resolveSelectionArgsBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mSelectionArgsBackReferences == null) { + return mSelectionArgs; + } + String[] newArgs = new String[mSelectionArgs.length]; + System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length); + for (Map.Entry<Integer, Integer> selectionArgBackRef + : mSelectionArgsBackReferences.entrySet()) { + final Integer selectionArgIndex = selectionArgBackRef.getKey(); + final int backRefIndex = selectionArgBackRef.getValue(); + newArgs[selectionArgIndex] = backRefToValue(backRefs, numBackRefs, backRefIndex); + } + return newArgs; + } + + /** + * Return the string representation of the requested back reference. + * @param backRefs an array of results + * @param numBackRefs the number of items in the backRefs array that are valid + * @param backRefIndex which backRef to be used + * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than + * the numBackRefs + * @return the string representation of the requested back reference. + */ + private static String backRefToValue(ContentProviderResult[] backRefs, int numBackRefs, + Integer backRefIndex) { + if (backRefIndex >= numBackRefs) { + throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex + + " but there are only " + numBackRefs + " back refs"); + } + ContentProviderResult backRef = backRefs[backRefIndex]; + String backRefValue; + if (backRef.uri != null) { + backRefValue = backRef.uri.getLastPathSegment(); + } else { + backRefValue = String.valueOf(backRef.count); + } + return backRefValue; + } + + public int describeContents() { + return 0; + } + + public static final Creator<ContentProviderOperation> CREATOR = + new Creator<ContentProviderOperation>() { + public ContentProviderOperation createFromParcel(Parcel source) { + return new ContentProviderOperation(source); + } + + public ContentProviderOperation[] newArray(int size) { + return new ContentProviderOperation[size]; + } + }; + + + /** + * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is + * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)}, + * {@link ContentProviderOperation#newUpdate(android.net.Uri)}, + * {@link ContentProviderOperation#newDelete(android.net.Uri)} or + * {@link ContentProviderOperation#newCountQuery(android.net.Uri)}. The withXXX methods + * can then be used to add parameters to the builder. See the specific methods to find for + * which {@link Builder} type each is allowed. Call {@link #build} to create the + * {@link ContentProviderOperation} once all the parameters have been supplied. + */ + public static class Builder { + private final int mType; + private final Uri mUri; + private String mSelection; + private String[] mSelectionArgs; + private ContentValues mValues; + private Integer mExpectedCount; + private ContentValues mValuesBackReferences; + private Map<Integer, Integer> mSelectionArgsBackReferences; + + /** Create a {@link Builder} of a given type. The uri must not be null. */ + private Builder(int type, Uri uri) { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + mType = type; + mUri = uri; + } + + /** Create a ContentProviderOperation from this {@link Builder}. */ + public ContentProviderOperation build() { + if (mType == TYPE_UPDATE) { + if ((mValues == null || mValues.size() == 0) + && (mValuesBackReferences == null || mValuesBackReferences.size() == 0)) { + throw new IllegalArgumentException("Empty values"); + } + } + return new ContentProviderOperation(this); + } + + /** + * Add a {@link ContentValues} of back references. The key is the name of the column + * and the value is an integer that is the index of the previous result whose + * value should be used for the column. The value is added as a {@link String}. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReferences(ContentValues backReferences) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException( + "only inserts and updates can have value back-references"); + } + mValuesBackReferences = backReferences; + return this; + } + + /** + * Add a ContentValues back reference. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReference(String key, int previousResult) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException( + "only inserts and updates can have value back-references"); + } + if (mValuesBackReferences == null) { + mValuesBackReferences = new ContentValues(); + } + mValuesBackReferences.put(key, previousResult); + return this; + } + + /** + * Add a back references as a selection arg. Any value at that index of the selection arg + * that was specified by {@link #withSelection} will be overwritten. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) { + if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have selection back-references"); + } + if (mSelectionArgsBackReferences == null) { + mSelectionArgsBackReferences = new HashMap<Integer, Integer>(); + } + mSelectionArgsBackReferences.put(selectionArgIndex, previousResult); + return this; + } + + /** + * The ContentValues to use. This may be null. These values may be overwritten by + * the corresponding value specified by {@link #withValueBackReference} or by + * future calls to {@link #withValues} or {@link #withValue}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValues(ContentValues values) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException("only inserts and updates can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + mValues.putAll(values); + return this; + } + + /** + * A value to insert or update. This value may be overwritten by + * the corresponding value specified by {@link #withValueBackReference}. + * This can only be used with builders of type insert or update. + * @param key the name of this value + * @param value the value itself. the type must be acceptable for insertion by + * {@link ContentValues#put} + * @return this builder, to allow for chaining. + */ + public Builder withValue(String key, Object value) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException("only inserts and updates can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + if (value == null) { + mValues.putNull(key); + } else if (value instanceof String) { + mValues.put(key, (String) value); + } else if (value instanceof Byte) { + mValues.put(key, (Byte) value); + } else if (value instanceof Short) { + mValues.put(key, (Short) value); + } else if (value instanceof Integer) { + mValues.put(key, (Integer) value); + } else if (value instanceof Long) { + mValues.put(key, (Long) value); + } else if (value instanceof Float) { + mValues.put(key, (Float) value); + } else if (value instanceof Double) { + mValues.put(key, (Double) value); + } else if (value instanceof Boolean) { + mValues.put(key, (Boolean) value); + } else if (value instanceof byte[]) { + mValues.put(key, (byte[]) value); + } else { + throw new IllegalArgumentException("bad value type: " + value.getClass().getName()); + } + return this; + } + + /** + * The selection and arguments to use. An occurrence of '?' in the selection will be + * replaced with the corresponding occurence of the selection argument. Any of the + * selection arguments may be overwritten by a selection argument back reference as + * specified by {@link #withSelectionBackReference}. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withSelection(String selection, String[] selectionArgs) { + if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have selections"); + } + mSelection = selection; + mSelectionArgs = selectionArgs; + return this; + } + + /** + * If set then if the number of rows affected by this operation do not match + * this count {@link OperationApplicationException} will be throw. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withExpectedCount(int count) { + if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have expected counts"); + } + mExpectedCount = count; + return this; + } + } +} diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java new file mode 100644 index 0000000..5d188ef --- /dev/null +++ b/core/java/android/content/ContentProviderResult.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.net.Uri; +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Contains the result of the application of a {@link ContentProviderOperation}. It is guaranteed + * to have exactly one of {@link #uri} or {@link #count} set. + */ +public class ContentProviderResult implements Parcelable { + public final Uri uri; + public final Integer count; + + public ContentProviderResult(Uri uri) { + if (uri == null) throw new IllegalArgumentException("uri must not be null"); + this.uri = uri; + this.count = null; + } + + public ContentProviderResult(int count) { + this.count = count; + this.uri = null; + } + + public ContentProviderResult(Parcel source) { + int type = source.readInt(); + if (type == 1) { + count = source.readInt(); + uri = null; + } else { + count = null; + uri = Uri.CREATOR.createFromParcel(source); + } + } + + public void writeToParcel(Parcel dest, int flags) { + if (uri == null) { + dest.writeInt(1); + dest.writeInt(count); + } else { + dest.writeInt(2); + uri.writeToParcel(dest, 0); + } + } + + public int describeContents() { + return 0; + } + + public static final Creator<ContentProviderResult> CREATOR = + new Creator<ContentProviderResult>() { + public ContentProviderResult createFromParcel(Parcel source) { + return new ContentProviderResult(source); + } + + public ContentProviderResult[] newArray(int size) { + return new ContentProviderResult[size]; + } + }; + + public String toString() { + if (uri != null) { + return "ContentProviderResult(uri=" + uri.toString() + ")"; + } + return "ContentProviderResult(count=" + count + ")"; + } +}
\ No newline at end of file diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 74144fc..98ed098 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -30,6 +30,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; +import android.accounts.Account; import android.util.Config; import android.util.Log; @@ -40,15 +41,25 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.ArrayList; /** * This class provides applications access to the content model. */ public abstract class ContentResolver { - public final static String SYNC_EXTRAS_ACCOUNT = "account"; + /** + * @deprecated instead use + * {@link #requestSync(android.accounts.Account, String, android.os.Bundle)} + */ + public static final String SYNC_EXTRAS_ACCOUNT = "account"; public static final String SYNC_EXTRAS_EXPEDITED = "expedited"; + /** + * @deprecated instead use + * {@link #SYNC_EXTRAS_MANUAL} + */ public static final String SYNC_EXTRAS_FORCE = "force"; + public static final String SYNC_EXTRAS_MANUAL = "force"; public static final String SYNC_EXTRAS_UPLOAD = "upload"; public static final String SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS = "deletions_override"; public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions"; @@ -88,7 +99,35 @@ public abstract class ContentResolver { * in the cursor is the same. */ public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; - + + /** @hide */ + public static final int SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS = 1; + /** @hide */ + public static final int SYNC_ERROR_AUTHENTICATION = 2; + /** @hide */ + public static final int SYNC_ERROR_IO = 3; + /** @hide */ + public static final int SYNC_ERROR_PARSE = 4; + /** @hide */ + public static final int SYNC_ERROR_CONFLICT = 5; + /** @hide */ + public static final int SYNC_ERROR_TOO_MANY_DELETIONS = 6; + /** @hide */ + public static final int SYNC_ERROR_TOO_MANY_RETRIES = 7; + /** @hide */ + public static final int SYNC_ERROR_INTERNAL = 8; + + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_SETTINGS = 1<<0; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_PENDING = 1<<1; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_ACTIVE = 1<<2; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_STATUS = 1<<3; + /** @hide */ + public static final int SYNC_OBSERVER_TYPE_ALL = 0x7fffffff; + public ContentResolver(Context context) { mContext = context; } @@ -166,6 +205,87 @@ public abstract class ContentResolver { } /** + * EntityIterator wrapper that releases the associated ContentProviderClient when the + * iterator is closed. + */ + private class EntityIteratorWrapper implements EntityIterator { + private final EntityIterator mInner; + private final ContentProviderClient mClient; + private volatile boolean mClientReleased; + + EntityIteratorWrapper(EntityIterator inner, ContentProviderClient client) { + mInner = inner; + mClient = client; + mClientReleased = false; + } + + public boolean hasNext() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.hasNext(); + } + + public Entity next() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.next(); + } + + public void close() { + mClient.release(); + mInner.close(); + mClientReleased = true; + } + + protected void finalize() throws Throwable { + if (!mClientReleased) { + mClient.release(); + } + super.finalize(); + } + } + + /** + * Query the given URI, returning an {@link EntityIterator} over the result set. + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that they + * appear in the selection. The values will be bound as Strings. + * @param sortOrder How to order the rows, formatted as an SQL ORDER BY + * clause (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @return An EntityIterator object + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + * @throws IllegalArgumentException thrown if there is no provider that matches the uri + */ + public final EntityIterator queryEntities(Uri uri, + String selection, String[] selectionArgs, String sortOrder) throws RemoteException { + ContentProviderClient provider = acquireContentProviderClient(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + uri); + } + try { + EntityIterator entityIterator = + provider.queryEntities(uri, selection, selectionArgs, sortOrder); + return new EntityIteratorWrapper(entityIterator, provider); + } catch(RuntimeException e) { + provider.release(); + throw e; + } catch(RemoteException e) { + provider.release(); + throw e; + } + } + + /** * Open a stream on to the content associated with a content URI. If there * is no data associated with the URI, FileNotFoundException is thrown. * @@ -485,6 +605,36 @@ public abstract class ContentResolver { } /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param authority the authority of the ContentProvider to which this batch should be applied + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + */ + public ContentProviderResult[] applyBatch(String authority, + ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + ContentProviderClient provider = acquireContentProviderClient(authority); + if (provider == null) { + throw new IllegalArgumentException("Unknown authority " + authority); + } + try { + return provider.applyBatch(operations); + } finally { + provider.release(); + } + } + + /** * Inserts multiple rows into a table at the given URL. * * This function make no guarantees about the atomicity of the insertions. @@ -592,6 +742,46 @@ public abstract class ContentResolver { } /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param uri specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(Uri uri) { + IContentProvider provider = acquireProvider(uri); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + + /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param name specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(String name) { + IContentProvider provider = acquireProvider(name); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + + /** * Register an observer class that gets callbacks when data identified by a * given content URI changes. * @@ -676,11 +866,42 @@ public abstract class ContentResolver { * * @param uri the uri of the provider to sync or null to sync all providers. * @param extras any extras to pass to the SyncAdapter. + * @deprecated instead use + * {@link #requestSync(android.accounts.Account, String, android.os.Bundle)} */ public void startSync(Uri uri, Bundle extras) { + Account account = null; + if (extras != null) { + String accountName = extras.getString(SYNC_EXTRAS_ACCOUNT); + if (!TextUtils.isEmpty(accountName)) { + account = new Account(accountName, "com.google.GAIA"); + } + extras.remove(SYNC_EXTRAS_ACCOUNT); + } + requestSync(account, uri != null ? uri.getAuthority() : null, extras); + } + + /** + * Start an asynchronous sync operation. If you want to monitor the progress + * of the sync you may register a SyncObserver. Only values of the following + * types may be used in the extras bundle: + * <ul> + * <li>Integer</li> + * <li>Long</li> + * <li>Boolean</li> + * <li>Float</li> + * <li>Double</li> + * <li>String</li> + * </ul> + * + * @param account which account should be synced + * @param authority which authority should be synced + * @param extras any extras to pass to the SyncAdapter. + */ + public static void requestSync(Account account, String authority, Bundle extras) { validateSyncExtrasBundle(extras); try { - getContentService().startSync(uri, extras); + getContentService().requestSync(account, authority, extras); } catch (RemoteException e) { } } @@ -694,6 +915,7 @@ public abstract class ContentResolver { * <li>Float</li> * <li>Double</li> * <li>String</li> + * <li>Account</li> * <li>null</li> * </ul> * @param extras the Bundle to check @@ -709,6 +931,7 @@ public abstract class ContentResolver { if (value instanceof Float) continue; if (value instanceof Double) continue; if (value instanceof String) continue; + if (value instanceof Account) continue; throw new IllegalArgumentException("unexpected value type: " + value.getClass().getName()); } @@ -719,13 +942,186 @@ public abstract class ContentResolver { } } + /** + * Cancel any active or pending syncs that match the Uri. If the uri is null then + * all syncs will be canceled. + * + * @param uri the uri of the provider to sync or null to sync all providers. + * @deprecated instead use {@link #cancelSync(android.accounts.Account, String)} + */ public void cancelSync(Uri uri) { + cancelSync(null /* all accounts */, uri != null ? uri.getAuthority() : null); + } + + /** + * Cancel any active or pending syncs that match account and authority. The account and + * authority can each independently be set to null, which means that syncs with any account + * or authority, respectively, will match. + * + * @param account filters the syncs that match by this account + * @param authority filters the syncs that match by this authority + */ + public static void cancelSync(Account account, String authority) { + try { + getContentService().cancelSync(account, authority); + } catch (RemoteException e) { + } + } + + /** + * Get information about the SyncAdapters that are known to the system. + * @return an array of SyncAdapters that have registered with the system + */ + public static SyncAdapterType[] getSyncAdapterTypes() { + try { + return getContentService().getSyncAdapterTypes(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Check if the provider should be synced when a network tickle is received + * + * @param account the account whose setting we are querying + * @param authority the provider whose setting we are querying + * @return true if the provider should be synced when a network tickle is received + */ + public static boolean getSyncAutomatically(Account account, String authority) { + try { + return getContentService().getSyncAutomatically(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Set whether or not the provider is synced when it receives a network tickle. + * + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + public static void setSyncAutomatically(Account account, String authority, boolean sync) { + try { + getContentService().setSyncAutomatically(account, authority, sync); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Gets the master auto-sync setting that applies to all the providers and accounts. + * If this is false then the per-provider auto-sync setting is ignored. + * + * @return the master auto-sync setting that applies to all the providers and accounts + */ + public static boolean getMasterSyncAutomatically() { + try { + return getContentService().getMasterSyncAutomatically(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Sets the master auto-sync setting that applies to all the providers and accounts. + * If this is false then the per-provider auto-sync setting is ignored. + * + * @param sync the master auto-sync setting that applies to all the providers and accounts + */ + public static void setMasterSyncAutomatically(boolean sync) { try { - getContentService().cancelSync(uri); + getContentService().setMasterSyncAutomatically(sync); } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted } } + /** + * Returns true if there is currently a sync operation for the given + * account or authority in the pending list, or actively being processed. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return true if a sync is active for the given account or authority. + */ + public static boolean isSyncActive(Account account, String authority) { + try { + return getContentService().isSyncActive(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * If a sync is active returns the information about it, otherwise returns false. + * @return the ActiveSyncInfo for the currently active sync or null if one is not active. + * @hide + */ + public static ActiveSyncInfo getActiveSync() { + try { + return getContentService().getActiveSync(); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Returns the status that matches the authority. If there are multiples accounts for + * the authority, the one with the latest "lastSuccessTime" status is returned. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return the SyncStatusInfo for the authority, or null if none exists + * @hide + */ + public static SyncStatusInfo getSyncStatus(Account account, String authority) { + try { + return getContentService().getSyncStatus(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Return true if the pending status is true of any matching authorities. + * @param account the account whose setting we are querying + * @param authority the provider whose behavior is being queried + * @return true if there is a pending sync with the matching account and authority + */ + public static boolean isSyncPending(Account account, String authority) { + try { + return getContentService().isSyncPending(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static Object addStatusChangeListener(int mask, final SyncStatusObserver callback) { + try { + ISyncStatusObserver.Stub observer = new ISyncStatusObserver.Stub() { + public void onStatusChanged(int which) throws RemoteException { + callback.onStatusChanged(which); + } + }; + getContentService().addStatusChangeListener(mask, observer); + return observer; + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static void removeStatusChangeListener(Object handle) { + try { + getContentService().removeStatusChangeListener((ISyncStatusObserver.Stub) handle); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + private final class CursorWrapperInner extends CursorWrapper { private IContentProvider mContentProvider; public static final String TAG="CursorWrapperInner"; diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 6cd2c54..7a1ad2b 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.database.IContentObserver; import android.database.sqlite.SQLiteException; import android.net.Uri; @@ -160,7 +161,9 @@ public final class ContentService extends IContentService.Stub { } if (syncToNetwork) { SyncManager syncManager = getSyncManager(); - if (syncManager != null) syncManager.scheduleLocalSync(uri); + if (syncManager != null) { + syncManager.scheduleLocalSync(null /* all accounts */, uri.getAuthority()); + } } } finally { restoreCallingIdentity(identityToken); @@ -186,14 +189,16 @@ public final class ContentService extends IContentService.Stub { } } - public void startSync(Uri url, Bundle extras) { + public void requestSync(Account account, String authority, Bundle extras) { ContentResolver.validateSyncExtrasBundle(extras); // This makes it so that future permission checks will be in the context of this // process rather than the caller's process. We will restore this before returning. long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); - if (syncManager != null) syncManager.startSync(url, extras); + if (syncManager != null) { + syncManager.scheduleSync(account, authority, extras, 0 /* no delay */); + } } finally { restoreCallingIdentity(identityToken); } @@ -201,34 +206,50 @@ public final class ContentService extends IContentService.Stub { /** * Clear all scheduled sync operations that match the uri and cancel the active sync - * if it matches the uri. If the uri is null, clear all scheduled syncs and cancel - * the active one, if there is one. - * @param uri Filter on the sync operations to cancel, or all if null. + * if they match the authority and account, if they are present. + * @param account filter the pending and active syncs to cancel using this account + * @param authority filter the pending and active syncs to cancel using this authority */ - public void cancelSync(Uri uri) { + public void cancelSync(Account account, String authority) { // This makes it so that future permission checks will be in the context of this // process rather than the caller's process. We will restore this before returning. long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.clearScheduledSyncOperations(uri); - syncManager.cancelActiveSync(uri); + syncManager.clearScheduledSyncOperations(account, authority); + syncManager.cancelActiveSync(account, authority); } } finally { restoreCallingIdentity(identityToken); } } - public boolean getSyncProviderAutomatically(String providerName) { + /** + * Get information about the SyncAdapters that are known to the system. + * @return an array of SyncAdapters that have registered with the system + */ + public SyncAdapterType[] getSyncAdapterTypes() { + // This makes it so that future permission checks will be in the context of this + // process rather than the caller's process. We will restore this before returning. + long identityToken = clearCallingIdentity(); + try { + SyncManager syncManager = getSyncManager(); + return syncManager.getSyncAdapterTypes(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean getSyncAutomatically(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getSyncProviderAutomatically( - null, providerName); + return syncManager.getSyncStorageEngine().getSyncAutomatically( + account, providerName); } } finally { restoreCallingIdentity(identityToken); @@ -236,29 +257,29 @@ public final class ContentService extends IContentService.Stub { return false; } - public void setSyncProviderAutomatically(String providerName, boolean sync) { + public void setSyncAutomatically(Account account, String providerName, boolean sync) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().setSyncProviderAutomatically( - null, providerName, sync); + syncManager.getSyncStorageEngine().setSyncAutomatically( + account, providerName, sync); } } finally { restoreCallingIdentity(identityToken); } } - public boolean getListenForNetworkTickles() { + public boolean getMasterSyncAutomatically() { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getListenForNetworkTickles(); + return syncManager.getSyncStorageEngine().getMasterSyncAutomatically(); } } finally { restoreCallingIdentity(identityToken); @@ -266,21 +287,21 @@ public final class ContentService extends IContentService.Stub { return false; } - public void setListenForNetworkTickles(boolean flag) { + public void setMasterSyncAutomatically(boolean flag) { mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, "no permission to write the sync settings"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().setListenForNetworkTickles(flag); + syncManager.getSyncStorageEngine().setMasterSyncAutomatically(flag); } } finally { restoreCallingIdentity(identityToken); } } - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); @@ -311,7 +332,7 @@ public final class ContentService extends IContentService.Stub { return null; } - public SyncStatusInfo getStatusByAuthority(String authority) { + public SyncStatusInfo getSyncStatus(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); @@ -327,15 +348,14 @@ public final class ContentService extends IContentService.Stub { return null; } - public boolean isAuthorityPending(String account, String authority) { + public boolean isSyncPending(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().isAuthorityPending( - account, authority); + return syncManager.getSyncStorageEngine().isSyncPending(account, authority); } } finally { restoreCallingIdentity(identityToken); @@ -348,8 +368,7 @@ public final class ContentService extends IContentService.Stub { try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().addStatusChangeListener( - mask, callback); + syncManager.getSyncStorageEngine().addStatusChangeListener(mask, callback); } } finally { restoreCallingIdentity(identityToken); @@ -361,8 +380,7 @@ public final class ContentService extends IContentService.Stub { try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().removeStatusChangeListener( - callback); + syncManager.getSyncStorageEngine().removeStatusChangeListener(callback); } } finally { restoreCallingIdentity(identityToken); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 9e37ae4..c6c9835 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1110,6 +1110,16 @@ public abstract class Context { public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; /** * Use with {@link #getSystemService} to retrieve a + * {@link android.accounts.AccountManager} for receiving intents at a + * time of your choosing. + * TODO STOPSHIP perform a final review of the the account apis before shipping + * + * @see #getSystemService + * @see android.accounts.AccountManager + */ + public static final String ACCOUNT_SERVICE = "account"; + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.ActivityManager} for interacting with the global * system state. * diff --git a/core/java/android/content/Entity.aidl b/core/java/android/content/Entity.aidl new file mode 100644 index 0000000..fb201f3 --- /dev/null +++ b/core/java/android/content/Entity.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/content/Entity.aidl +** +** Copyright 2007, The Android Open Source 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. +*/ + +package android.content; + +parcelable Entity; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java new file mode 100644 index 0000000..325dce5 --- /dev/null +++ b/core/java/android/content/Entity.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.os.Parcelable; +import android.os.Parcel; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Objects that pass through the ContentProvider and ContentResolver's methods that deal with + * Entities must implement this abstract base class and thus themselves be Parcelable. + */ +public final class Entity implements Parcelable { + final private ContentValues mValues; + final private ArrayList<NamedContentValues> mSubValues; + + public Entity(ContentValues values) { + mValues = values; + mSubValues = new ArrayList<NamedContentValues>(); + } + + public ContentValues getEntityValues() { + return mValues; + } + + public ArrayList<NamedContentValues> getSubValues() { + return mSubValues; + } + + public void addSubValue(Uri uri, ContentValues values) { + mSubValues.add(new Entity.NamedContentValues(uri, values)); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + mValues.writeToParcel(dest, 0); + dest.writeInt(mSubValues.size()); + for (NamedContentValues value : mSubValues) { + value.uri.writeToParcel(dest, 0); + value.values.writeToParcel(dest, 0); + } + } + + private Entity(Parcel source) { + mValues = ContentValues.CREATOR.createFromParcel(source); + final int numValues = source.readInt(); + mSubValues = new ArrayList<NamedContentValues>(numValues); + for (int i = 0; i < numValues; i++) { + final Uri uri = Uri.CREATOR.createFromParcel(source); + final ContentValues values = ContentValues.CREATOR.createFromParcel(source); + mSubValues.add(new NamedContentValues(uri, values)); + } + } + + public static final Creator<Entity> CREATOR = new Creator<Entity>() { + public Entity createFromParcel(Parcel source) { + return new Entity(source); + } + + public Entity[] newArray(int size) { + return new Entity[size]; + } + }; + + public static class NamedContentValues { + public final Uri uri; + public final ContentValues values; + + public NamedContentValues(Uri uri, ContentValues values) { + this.uri = uri; + this.values = values; + } + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Entity: ").append(getEntityValues()); + for (Entity.NamedContentValues namedValue : getSubValues()) { + sb.append("\n ").append(namedValue.uri); + sb.append("\n -> ").append(namedValue.values); + } + return sb.toString(); + } +} diff --git a/core/java/android/content/EntityIterator.java b/core/java/android/content/EntityIterator.java new file mode 100644 index 0000000..5e5f14c --- /dev/null +++ b/core/java/android/content/EntityIterator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.os.RemoteException; + +public interface EntityIterator { + /** + * Returns whether there are more elements to iterate, i.e. whether the + * iterator is positioned in front of an element. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + * @see #next + * @since Android 1.0 + */ + public boolean hasNext() throws RemoteException; + + /** + * Returns the next object in the iteration, i.e. returns the element in + * front of the iterator and advances the iterator by one position. + * + * @return the next object. + * @throws java.util.NoSuchElementException + * if there are no more elements. + * @see #hasNext + * @since Android 1.0 + */ + public Entity next() throws RemoteException; + + /** + * Indicates that this iterator is no longer needed and that any associated resources + * may be released (such as a SQLite cursor). + */ + public void close(); +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 0606956..7e5aba5 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -28,6 +28,7 @@ import android.os.IInterface; import android.os.ParcelFileDescriptor; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * The ipc interface to talk to a content provider. @@ -43,19 +44,25 @@ public interface IContentProvider extends IInterface { CursorWindow window) throws RemoteException; public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException; + public EntityIterator queryEntities(Uri url, String selection, + String[] selectionArgs, String sortOrder) + throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException; + public Uri insertEntity(Uri uri, Entity entities) throws RemoteException; public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException; public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) throws RemoteException; + public int updateEntity(Uri uri, Entity entity) throws RemoteException; public ParcelFileDescriptor openFile(Uri url, String mode) throws RemoteException, FileNotFoundException; public AssetFileDescriptor openAssetFile(Uri url, String mode) throws RemoteException, FileNotFoundException; - public ISyncAdapter getSyncAdapter() throws RemoteException; + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException; /* IPC constants */ static final String descriptor = "android.content.IContentProvider"; @@ -65,8 +72,11 @@ public interface IContentProvider extends IInterface { static final int INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; static final int DELETE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; static final int UPDATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9; - static final int GET_SYNC_ADAPTER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10; static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12; static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13; static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; + static final int INSERT_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 16; + static final int UPDATE_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 17; + static final int QUERY_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 18; + static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19; } diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 8617d949..658a5bc 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -16,8 +16,10 @@ package android.content; +import android.accounts.Account; import android.content.ActiveSyncInfo; import android.content.ISyncStatusObserver; +import android.content.SyncAdapterType; import android.content.SyncStatusInfo; import android.net.Uri; import android.os.Bundle; @@ -34,15 +36,15 @@ interface IContentService { void notifyChange(in Uri uri, IContentObserver observer, boolean observerWantsSelfNotifications, boolean syncToNetwork); - void startSync(in Uri url, in Bundle extras); - void cancelSync(in Uri uri); + void requestSync(in Account account, String authority, in Bundle extras); + void cancelSync(in Account account, String authority); /** * Check if the provider should be synced when a network tickle is received * @param providerName the provider whose setting we are querying * @return true of the provider should be synced when a network tickle is received */ - boolean getSyncProviderAutomatically(String providerName); + boolean getSyncAutomatically(in Account account, String providerName); /** * Set whether or not the provider is synced when it receives a network tickle. @@ -50,32 +52,38 @@ interface IContentService { * @param providerName the provider whose behavior is being controlled * @param sync true if the provider should be synced when tickles are received for it */ - void setSyncProviderAutomatically(String providerName, boolean sync); + void setSyncAutomatically(in Account account, String providerName, boolean sync); - void setListenForNetworkTickles(boolean flag); + void setMasterSyncAutomatically(boolean flag); - boolean getListenForNetworkTickles(); + boolean getMasterSyncAutomatically(); /** * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - boolean isSyncActive(String account, String authority); + boolean isSyncActive(in Account account, String authority); ActiveSyncInfo getActiveSync(); /** + * Returns the types of the SyncAdapters that are registered with the system. + * @return Returns the types of the SyncAdapters that are registered with the system. + */ + SyncAdapterType[] getSyncAdapterTypes(); + + /** * Returns the status that matches the authority. If there are multiples accounts for * the authority, the one with the latest "lastSuccessTime" status is returned. * @param authority the authority whose row should be selected * @return the SyncStatusInfo for the authority, or null if none exists */ - SyncStatusInfo getStatusByAuthority(String authority); + SyncStatusInfo getSyncStatus(in Account account, String authority); /** * Return true if the pending status is true of any matching authorities. */ - boolean isAuthorityPending(String account, String authority); + boolean isSyncPending(in Account account, String authority); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IEntityIterator.java b/core/java/android/content/IEntityIterator.java new file mode 100644 index 0000000..1c478b3 --- /dev/null +++ b/core/java/android/content/IEntityIterator.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Log; + +/** + * ICPC interface methods for an iterator over Entity objects. + * @hide + */ +public interface IEntityIterator extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IEntityIterator { + private static final String TAG = "IEntityIterator"; + private static final java.lang.String DESCRIPTOR = "android.content.IEntityIterator"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + /** + * Cast an IBinder object into an IEntityIterator interface, + * generating a proxy if needed. + */ + public static IEntityIterator asInterface(IBinder obj) { + if ((obj==null)) { + return null; + } + IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof IEntityIterator))) { + return ((IEntityIterator)iin); + } + return new IEntityIterator.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: + { + reply.writeString(DESCRIPTOR); + return true; + } + + case TRANSACTION_hasNext: + { + data.enforceInterface(DESCRIPTOR); + boolean _result; + try { + _result = this.hasNext(); + } catch (Exception e) { + Log.e(TAG, "caught exception in hasNext()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + reply.writeInt(((_result)?(1):(0))); + return true; + } + + case TRANSACTION_next: + { + data.enforceInterface(DESCRIPTOR); + Entity entity; + try { + entity = this.next(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in next()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + entity.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + return true; + } + + case TRANSACTION_close: + { + data.enforceInterface(DESCRIPTOR); + try { + this.close(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in close()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IEntityIterator { + private IBinder mRemote; + Proxy(IBinder remote) { + mRemote = remote; + } + public IBinder asBinder() { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + public boolean hasNext() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_hasNext, _data, _reply, 0); + _reply.readException(); + _result = (0!=_reply.readInt()); + } + finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public Entity next() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_next, _data, _reply, 0); + _reply.readException(); + return Entity.CREATOR.createFromParcel(_reply); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void close() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_close, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_hasNext = (IBinder.FIRST_CALL_TRANSACTION + 0); + static final int TRANSACTION_next = (IBinder.FIRST_CALL_TRANSACTION + 1); + static final int TRANSACTION_close = (IBinder.FIRST_CALL_TRANSACTION + 2); + } + public boolean hasNext() throws RemoteException; + public Entity next() throws RemoteException; + public void close() throws RemoteException; +} diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl index 671188c..4660527 100644 --- a/core/java/android/content/ISyncAdapter.aidl +++ b/core/java/android/content/ISyncAdapter.aidl @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.os.Bundle; import android.content.ISyncContext; @@ -30,14 +31,17 @@ oneway interface ISyncAdapter { * * @param syncContext the ISyncContext used to indicate the progress of the sync. When * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. + * @param authority the authority that should be synced * @param account the account that should be synced * @param extras SyncAdapter-specific parameters */ - void startSync(ISyncContext syncContext, String account, in Bundle extras); + void startSync(ISyncContext syncContext, String authority, + in Account account, in Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive * after the ISyncContext.onFinished() for that sync was called. + * @param syncContext the ISyncContext that was passed to {@link #startSync} */ - void cancelSync(); + void cancelSync(ISyncContext syncContext); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 263f927..ca4bea8 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1325,7 +1325,7 @@ public class Intent implements Parcelable { * that wait until power is available to trigger. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED"; + public static final String ACTION_POWER_CONNECTED = "android.intent.action.POWER_CONNECTED"; /** * Broadcast Action: External power has been removed from the device. * This is intended for applications that wish to register specifically to this notification. @@ -1334,7 +1334,8 @@ public class Intent implements Parcelable { * that wait until power is available to trigger. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED"; + public static final String ACTION_POWER_DISCONNECTED = + "android.intent.action.POWER_DISCONNECTED"; /** * Broadcast Action: Device is shutting down. * This is broadcast when the device is being shut down (completely turned @@ -1607,6 +1608,20 @@ public class Intent implements Parcelable { "android.intent.action.REBOOT"; /** + * Broadcast Action: a remote intent is to be broadcasted. + * + * A remote intent is used for remote RPC between devices. The remote intent + * is serialized and sent from one device to another device. The receiving + * device parses the remote intent and broadcasts it. Note that anyone can + * broadcast a remote intent. However, if the intent receiver of the remote intent + * does not trust intent broadcasts from arbitrary intent senders, it should require + * the sender to hold certain permissions so only trusted sender's broadcast will be + * let through. + */ + public static final String ACTION_REMOTE_INTENT = + "android.intent.action.REMOTE_INTENT"; + + /** * @hide * TODO: This will be unhidden in a later CL. * Broadcast Action: The TextToSpeech synthesizer has completed processing @@ -1874,6 +1889,13 @@ public class Intent implements Parcelable { public static final String EXTRA_INSTALLER_PACKAGE_NAME = "android.intent.extra.INSTALLER_PACKAGE_NAME"; + /** + * Used in the extra field in the remote intent. It's astring token passed with the + * remote intent. + */ + public static final String EXTRA_REMOTE_INTENT_TOKEN = + "android.intent.extra.remote_intent_token"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). diff --git a/core/java/android/content/OperationApplicationException.java b/core/java/android/content/OperationApplicationException.java new file mode 100644 index 0000000..d4101bf --- /dev/null +++ b/core/java/android/content/OperationApplicationException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +/** + * Thrown when an application of a {@link ContentProviderOperation} fails due the specified + * constraints. + */ +public class OperationApplicationException extends Exception { + public OperationApplicationException() { + super(); + } + public OperationApplicationException(String message) { + super(message); + } + public OperationApplicationException(String message, Throwable cause) { + super(message, cause); + } + public OperationApplicationException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 7826e50..1d5ade1 100644 --- a/core/java/android/content/SyncAdapter.java +++ b/core/java/android/content/SyncAdapter.java @@ -18,6 +18,7 @@ package android.content; import android.os.Bundle; import android.os.RemoteException; +import android.accounts.Account; /** * @hide @@ -29,12 +30,12 @@ public abstract class SyncAdapter { public static final int LOG_SYNC_DETAILS = 2743; class Transport extends ISyncAdapter.Stub { - public void startSync(ISyncContext syncContext, String account, + public void startSync(ISyncContext syncContext, String authority, Account account, Bundle extras) throws RemoteException { SyncAdapter.this.startSync(new SyncContext(syncContext), account, extras); } - public void cancelSync() throws RemoteException { + public void cancelSync(ISyncContext syncContext) throws RemoteException { SyncAdapter.this.cancelSync(); } } @@ -42,9 +43,9 @@ public abstract class SyncAdapter { Transport mTransport = new Transport(); /** - * Get the Transport object. (note this is package private). + * Get the Transport object. */ - final ISyncAdapter getISyncAdapter() + public final ISyncAdapter getISyncAdapter() { return mTransport; } @@ -59,7 +60,7 @@ public abstract class SyncAdapter { * @param account the account that should be synced * @param extras SyncAdapter-specific parameters */ - public abstract void startSync(SyncContext syncContext, String account, Bundle extras); + public abstract void startSync(SyncContext syncContext, Account account, Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive diff --git a/core/java/android/content/SyncAdapterType.aidl b/core/java/android/content/SyncAdapterType.aidl new file mode 100644 index 0000000..e67841f --- /dev/null +++ b/core/java/android/content/SyncAdapterType.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +parcelable SyncAdapterType; + diff --git a/core/java/android/content/SyncAdapterType.java b/core/java/android/content/SyncAdapterType.java new file mode 100644 index 0000000..5a96003 --- /dev/null +++ b/core/java/android/content/SyncAdapterType.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.text.TextUtils; +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Value type that represents a SyncAdapterType. This object overrides {@link #equals} and + * {@link #hashCode}, making it suitable for use as the key of a {@link java.util.Map} + */ +public class SyncAdapterType implements Parcelable { + public final String authority; + public final String accountType; + + public SyncAdapterType(String authority, String accountType) { + if (TextUtils.isEmpty(authority)) { + throw new IllegalArgumentException("the authority must not be empty: " + authority); + } + if (TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("the accountType must not be empty: " + accountType); + } + this.authority = authority; + this.accountType = accountType; + } + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof SyncAdapterType)) return false; + final SyncAdapterType other = (SyncAdapterType)o; + return authority.equals(other.authority) && accountType.equals(other.accountType); + } + + public int hashCode() { + int result = 17; + result = 31 * result + authority.hashCode(); + result = 31 * result + accountType.hashCode(); + return result; + } + + public String toString() { + return "SyncAdapterType {name=" + authority + ", type=" + accountType + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(authority); + dest.writeString(accountType); + } + + public SyncAdapterType(Parcel source) { + this(source.readString(), source.readString()); + } + + public static final Creator<SyncAdapterType> CREATOR = new Creator<SyncAdapterType>() { + public SyncAdapterType createFromParcel(Parcel source) { + return new SyncAdapterType(source); + } + + public SyncAdapterType[] newArray(int size) { + return new SyncAdapterType[size]; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java new file mode 100644 index 0000000..ce47d76 --- /dev/null +++ b/core/java/android/content/SyncAdaptersCache.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +import android.content.pm.RegisteredServicesCache; +import android.content.res.TypedArray; +import android.content.Context; +import android.util.AttributeSet; + +/** + * A cache of services that export the {@link android.content.ISyncAdapter} interface. + * @hide + */ +/* package private */ class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType> { + private static final String TAG = "Account"; + + private static final String SERVICE_INTERFACE = "android.content.SyncAdapter"; + private static final String SERVICE_META_DATA = "android.content.SyncAdapter"; + private static final String ATTRIBUTES_NAME = "sync-adapter"; + + SyncAdaptersCache(Context context) { + super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME); + } + + public SyncAdapterType parseServiceAttributes(String packageName, AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.SyncAdapter); + try { + final String authority = + sa.getString(com.android.internal.R.styleable.SyncAdapter_contentAuthority); + final String accountType = + sa.getString(com.android.internal.R.styleable.SyncAdapter_accountType); + if (authority == null || accountType == null) { + return null; + } + return new SyncAdapterType(authority, accountType); + } finally { + sa.recycle(); + } + } +}
\ No newline at end of file diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 4d2cce8..f73b394 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -21,8 +21,9 @@ import com.google.android.collect.Maps; import com.android.internal.R; import com.android.internal.util.ArrayUtils; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdatedListener; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,11 +31,10 @@ import android.app.PendingIntent; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; +import android.content.pm.RegisteredServicesCache; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; @@ -48,7 +48,6 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; -import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; @@ -72,11 +71,12 @@ import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; +import java.util.Collection; /** * @hide */ -class SyncManager { +class SyncManager implements OnAccountsUpdatedListener { private static final String TAG = "SyncManager"; // used during dumping of the Sync history @@ -117,14 +117,11 @@ class SyncManager { private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; - private ContentResolver mContentResolver; private String mStatusText = ""; private long mHeartbeatTime = 0; - private AccountMonitor mAccountMonitor; - - private volatile String[] mAccounts = null; + private volatile Account[] mAccounts = null; volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; @@ -151,17 +148,18 @@ class SyncManager { private final PendingIntent mSyncAlarmIntent; private final PendingIntent mSyncPollAlarmIntent; + private final SyncAdaptersCache mSyncAdapters; + private BroadcastReceiver mStorageIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { - ensureContentResolver(); String action = intent.getAction(); if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is low."); } mStorageIsLow = true; - cancelActiveSync(null /* no url */); + cancelActiveSync(null /* any account */, null /* any authority */); } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Internal storage is ok."); @@ -172,6 +170,43 @@ class SyncManager { } }; + private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (!mFactoryTest) { + AccountManager.get(mContext).addOnAccountsUpdatedListener(SyncManager.this, + mSyncHandler, true /* updateImmediately */); + } + } + }; + + public void onAccountsUpdated(Account[] accounts) { + final boolean hadAccountsAlready = mAccounts != null; + mAccounts = accounts; + + // if a sync is in progress yet it is no longer in the accounts list, + // cancel it + ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (activeSyncContext != null) { + if (!ArrayUtils.contains(accounts, activeSyncContext.mSyncOperation.account)) { + Log.d(TAG, "canceling sync since the account has been removed"); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + // we must do this since we don't bother scheduling alarms when + // the accounts are not set yet + sendCheckAlarmsMessage(); + + mSyncStorageEngine.doDatabaseCleanup(accounts); + + if (hadAccountsAlready && accounts.length > 0) { + // request a sync so that if the password was changed we will + // retry any sync that failed when it was wrong + scheduleSync(null, null, null, 0 /* no delay */); + } + } + private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { @@ -229,7 +264,11 @@ class SyncManager { private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; + private final boolean mFactoryTest; + public SyncManager(Context context, boolean factoryTest) { + mFactoryTest = factoryTest; + // Initialize the SyncStorageEngine first, before registering observers // and creating threads and so on; it may fail if the disk is full. SyncStorageEngine.init(context); @@ -244,6 +283,8 @@ class SyncManager { mPackageManager = null; + mSyncAdapters = new SyncAdaptersCache(mContext); + mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); @@ -253,6 +294,9 @@ class SyncManager { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mBootCompletedReceiver, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); @@ -282,48 +326,12 @@ class SyncManager { mHandleAlarmWakeLock.setReferenceCounted(false); mSyncStorageEngine.addStatusChangeListener( - SyncStorageEngine.CHANGE_SETTINGS, new ISyncStatusObserver.Stub() { + ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new ISyncStatusObserver.Stub() { public void onStatusChanged(int which) { // force the sync loop to run if the settings change sendCheckAlarmsMessage(); } }); - - if (!factoryTest) { - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - final boolean hadAccountsAlready = mAccounts != null; - // copy the accounts into a new array and change mAccounts to point to it - String[] newAccounts = new String[accounts.length]; - System.arraycopy(accounts, 0, newAccounts, 0, accounts.length); - mAccounts = newAccounts; - - // if a sync is in progress yet it is no longer in the accounts list, cancel it - ActiveSyncContext activeSyncContext = mActiveSyncContext; - if (activeSyncContext != null) { - if (!ArrayUtils.contains(newAccounts, - activeSyncContext.mSyncOperation.account)) { - Log.d(TAG, "canceling sync since the account has been removed"); - sendSyncFinishedOrCanceledMessage(activeSyncContext, - null /* no result since this is a cancel */); - } - } - - // we must do this since we don't bother scheduling alarms when - // the accounts are not set yet - sendCheckAlarmsMessage(); - - mSyncStorageEngine.doDatabaseCleanup(accounts); - - if (hadAccountsAlready && mAccounts.length > 0) { - // request a sync so that if the password was changed we will retry any sync - // that failed when it was wrong - startSync(null /* all providers */, null /* no extras */); - } - } - }; - mAccountMonitor = new AccountMonitor(context, listener); - } } private synchronized void initializeSyncPoll() { @@ -397,7 +405,8 @@ class SyncManager { scheduleSyncPollAlarm(nextRelativePollTimeMs); // perform a poll - scheduleSync(null /* sync all syncable providers */, new Bundle(), 0 /* no delay */); + scheduleSync(null /* sync all syncable accounts */, null /* sync all syncable providers */, + new Bundle(), 0 /* no delay */); } private void writeSyncPollTime(long when) { @@ -452,19 +461,13 @@ class SyncManager { return mSyncStorageEngine; } - private void ensureContentResolver() { - if (mContentResolver == null) { - mContentResolver = mContext.getContentResolver(); - } - } - private void ensureAlarmService() { if (mAlarmService == null) { mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); } } - public String getSyncingAccount() { + public Account getSyncingAccount() { ActiveSyncContext activeSyncContext = mActiveSyncContext; return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } @@ -499,20 +502,21 @@ class SyncManager { * * <p>You'll start getting callbacks after this. * - * @param url The Uri of a specific provider to be synced, or - * null to sync all providers. + * @param requestedAccount the account to sync, may be null to signify all accounts + * @param requestedAuthority the authority to sync, may be null to indicate all authorities * @param extras a Map of SyncAdapter-specific information to control * syncs of a specific provider. Can be null. Is ignored * if the url is null. * @param delay how many milliseconds in the future to wait before performing this - * sync. -1 means to make this the next sync to perform. */ - public void scheduleSync(Uri url, Bundle extras, long delay) { + public void scheduleSync(Account requestedAccount, String requestedAuthority, + Bundle extras, long delay) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) { Log.v(TAG, "scheduleSync:" + " delay " + delay - + ", url " + ((url == null) ? "(null)" : url) + + ", account " + requestedAccount + + ", authority " + requestedAuthority + ", extras " + ((extras == null) ? "(null)" : extras)); } @@ -535,10 +539,9 @@ class SyncManager { delay = -1; // this means schedule at the front of the queue } - String[] accounts; - String accountFromExtras = extras.getString(ContentResolver.SYNC_EXTRAS_ACCOUNT); - if (!TextUtils.isEmpty(accountFromExtras)) { - accounts = new String[]{accountFromExtras}; + Account[] accounts; + if (requestedAccount != null) { + accounts = new Account[]{requestedAccount}; } else { // if the accounts aren't configured yet then we can't support an account-less // sync request @@ -560,14 +563,14 @@ class SyncManager { } final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); - final boolean force = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); int source; if (uploadOnly) { source = SyncStorageEngine.SOURCE_LOCAL; - } else if (force) { + } else if (manualSync) { source = SyncStorageEngine.SOURCE_USER; - } else if (url == null) { + } else if (requestedAuthority == null) { source = SyncStorageEngine.SOURCE_POLL; } else { // this isn't strictly server, since arbitrary callers can (and do) request @@ -575,20 +578,33 @@ class SyncManager { source = SyncStorageEngine.SOURCE_SERVER; } - List<String> names = new ArrayList<String>(); - List<ProviderInfo> providers = new ArrayList<ProviderInfo>(); - populateProvidersList(url, names, providers); + // Compile a list of authorities that have sync adapters. + // For each authority sync each account that matches a sync adapter. + final HashSet<String> syncableAuthorities = new HashSet<String>(); + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter : + mSyncAdapters.getAllServices()) { + syncableAuthorities.add(syncAdapter.type.authority); + } - final int numProviders = providers.size(); - for (int i = 0; i < numProviders; i++) { - if (!providers.get(i).isSyncable) continue; - final String name = names.get(i); - for (String account : accounts) { - scheduleSyncOperation(new SyncOperation(account, source, name, extras, delay)); - // TODO: remove this when Calendar supports multiple accounts. Until then - // pretend that only the first account exists when syncing calendar. - if ("calendar".equals(name)) { - break; + // if the url was specified then replace the list of authorities with just this authority + // or clear it if this authority isn't syncable + if (requestedAuthority != null) { + final boolean isSyncable = syncableAuthorities.contains(requestedAuthority); + syncableAuthorities.clear(); + if (isSyncable) syncableAuthorities.add(requestedAuthority); + } + + for (String authority : syncableAuthorities) { + for (Account account : accounts) { + if (mSyncAdapters.getServiceInfo(new SyncAdapterType(authority, account.mType)) + != null) { + scheduleSyncOperation( + new SyncOperation(account, source, authority, extras, delay)); + // TODO: remove this when Calendar supports multiple accounts. Until then + // pretend that only the first account exists when syncing calendar. + if ("calendar".equals(authority)) { + break; + } } } } @@ -598,36 +614,10 @@ class SyncManager { mStatusText = message; } - private void populateProvidersList(Uri url, List<String> names, List<ProviderInfo> providers) { - try { - final IPackageManager packageManager = getPackageManager(); - if (url == null) { - packageManager.querySyncProviders(names, providers); - } else { - final String authority = url.getAuthority(); - ProviderInfo info = packageManager.resolveContentProvider(url.getAuthority(), 0); - if (info != null) { - // only set this provider if the requested authority is the primary authority - String[] providerNames = info.authority.split(";"); - if (url.getAuthority().equals(providerNames[0])) { - names.add(authority); - providers.add(info); - } - } - } - } catch (RemoteException ex) { - // we should really never get this, but if we do then clear the lists, which - // will result in the dropping of the sync request - Log.e(TAG, "error trying to get the ProviderInfo for " + url, ex); - names.clear(); - providers.clear(); - } - } - - public void scheduleLocalSync(Uri url) { + public void scheduleLocalSync(Account account, String authority) { final Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); - scheduleSync(url, extras, LOCAL_SYNC_DELAY); + scheduleSync(account, authority, extras, LOCAL_SYNC_DELAY); } private IPackageManager getPackageManager() { @@ -641,18 +631,16 @@ class SyncManager { return mPackageManager; } - /** - * Initiate a sync for this given URL, or pass null for a full sync. - * - * <p>You'll start getting callbacks after this. - * - * @param url The Uri of a specific provider to be synced, or - * null to sync all providers. - * @param extras a Map of SyncAdapter specific information to control - * syncs of a specific provider. Can be null. Is ignored - */ - public void startSync(Uri url, Bundle extras) { - scheduleSync(url, extras, 0 /* no delay */); + public SyncAdapterType[] getSyncAdapterTypes() { + final Collection<RegisteredServicesCache.ServiceInfo<SyncAdapterType>> serviceInfos = + mSyncAdapters.getAllServices(); + SyncAdapterType[] types = new SyncAdapterType[serviceInfos.size()]; + int i = 0; + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> serviceInfo : serviceInfos) { + types[i] = serviceInfo.type; + ++i; + } + return types; } public void updateHeartbeatTime() { @@ -721,8 +709,7 @@ class SyncManager { } // Cap the delay - ensureContentResolver(); - long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver, + long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContext.getContentResolver(), Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS, DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { @@ -736,17 +723,22 @@ class SyncManager { } /** - * Cancel the active sync if it matches the uri. The uri corresponds to the one passed - * in to startSync(). - * @param uri If non-null, the active sync is only canceled if it matches the uri. - * If null, any active sync is canceled. + * Cancel the active sync if it matches the authority and account. + * @param account limit the cancelations to syncs with this account, if non-null + * @param authority limit the cancelations to syncs with this authority, if non-null */ - public void cancelActiveSync(Uri uri) { + public void cancelActiveSync(Account account, String authority) { ActiveSyncContext activeSyncContext = mActiveSyncContext; if (activeSyncContext != null) { - // if a Uri was specified then only cancel the sync if it matches the the uri - if (uri != null) { - if (!uri.getAuthority().equals(activeSyncContext.mSyncOperation.authority)) { + // if an authority was specified then only cancel the sync if it matches + if (account != null) { + if (!account.equals(activeSyncContext.mSyncOperation.account)) { + return; + } + } + // if an account was specified then only cancel the sync if it matches + if (authority != null) { + if (!authority.equals(activeSyncContext.mSyncOperation.authority)) { return; } } @@ -798,14 +790,13 @@ class SyncManager { } /** - * Remove any scheduled sync operations that match uri. The uri corresponds to the one passed - * in to startSync(). - * @param uri If non-null, only operations that match the uri are cleared. - * If null, all operations are cleared. + * Remove scheduled sync operations. + * @param account limit the removals to operations with this account, if non-null + * @param authority limit the removals to operations with this authority, if non-null */ - public void clearScheduledSyncOperations(Uri uri) { + public void clearScheduledSyncOperations(Account account, String authority) { synchronized (mSyncQueue) { - mSyncQueue.clear(null, uri != null ? uri.getAuthority() : null); + mSyncQueue.clear(account, authority); } } @@ -857,7 +848,7 @@ class SyncManager { * Value type that represents a sync operation. */ static class SyncOperation implements Comparable { - final String account; + final Account account; int syncSource; String authority; Bundle extras; @@ -866,7 +857,7 @@ class SyncManager { long delay; SyncStorageEngine.PendingOperation pendingOperation; - SyncOperation(String account, int source, String authority, Bundle extras, long delay) { + SyncOperation(Account account, int source, String authority, Bundle extras, long delay) { this.account = account; this.syncSource = source; this.authority = authority; @@ -937,21 +928,19 @@ class SyncManager { /** * @hide */ - class ActiveSyncContext extends ISyncContext.Stub { + class ActiveSyncContext extends ISyncContext.Stub implements ServiceConnection { final SyncOperation mSyncOperation; final long mHistoryRowId; - final IContentProvider mContentProvider; - final ISyncAdapter mSyncAdapter; + ISyncAdapter mSyncAdapter; final long mStartTime; long mTimeoutStartTime; - public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider, - ISyncAdapter syncAdapter, long historyRowId) { + public ActiveSyncContext(SyncOperation syncOperation, + long historyRowId) { super(); mSyncOperation = syncOperation; mHistoryRowId = historyRowId; - mContentProvider = contentProvider; - mSyncAdapter = syncAdapter; + mSyncAdapter = null; mStartTime = SystemClock.elapsedRealtime(); mTimeoutStartTime = mStartTime; } @@ -977,6 +966,37 @@ class SyncManager { .append(", syncOperation ").append(mSyncOperation); } + public void onServiceConnected(ComponentName name, IBinder service) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_CONNECTED; + msg.obj = new ServiceConnectionData(this, ISyncAdapter.Stub.asInterface(service)); + mSyncHandler.sendMessage(msg); + } + + public void onServiceDisconnected(ComponentName name) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_DISCONNECTED; + msg.obj = new ServiceConnectionData(this, null); + mSyncHandler.sendMessage(msg); + } + + boolean bindToSyncAdapter(RegisteredServicesCache.ServiceInfo info) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "bindToSyncAdapter: " + info.componentName + ", connection " + this); + } + Intent intent = new Intent(); + intent.setAction("android.content.SyncAdapter"); + intent.setComponent(info.componentName); + return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + void unBindFromSyncAdapter() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "unBindFromSyncAdapter: connection " + this); + } + mContext.unbindService(this); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -991,6 +1011,12 @@ class SyncManager { if (isSyncEnabled()) { dumpSyncHistory(pw, sb); } + + pw.println(); + pw.println("SyncAdapters:"); + for (RegisteredServicesCache.ServiceInfo info : mSyncAdapters.getAllServices()) { + pw.println(" " + info); + } } static String formatTime(long time) { @@ -1004,7 +1030,7 @@ class SyncManager { pw.print("data connected: "); pw.println(mDataConnectionIsConnected); pw.print("memory low: "); pw.println(mStorageIsLow); - final String[] accounts = mAccounts; + final Account[] accounts = mAccounts; pw.print("accounts: "); if (accounts != null) { pw.println(accounts.length); @@ -1068,7 +1094,8 @@ class SyncManager { for (int i=0; i<N; i++) { SyncStorageEngine.PendingOperation op = ops.get(i); pw.print(" #"); pw.print(i); pw.print(": account="); - pw.print(op.account); pw.print(" authority="); + pw.print(op.account.mName); pw.print(":"); + pw.print(op.account.mType); pw.print(" authority="); pw.println(op.authority); if (op.extras != null && op.extras.size() > 0) { sb.setLength(0); @@ -1078,7 +1105,7 @@ class SyncManager { } } - HashSet<String> processedAccounts = new HashSet<String>(); + HashSet<Account> processedAccounts = new HashSet<Account>(); ArrayList<SyncStatusInfo> statuses = mSyncStorageEngine.getSyncStatus(); if (statuses != null && statuses.size() > 0) { @@ -1090,7 +1117,7 @@ class SyncManager { SyncStorageEngine.AuthorityInfo authority = mSyncStorageEngine.getAuthority(status.authorityId); if (authority != null) { - String curAccount = authority.account; + Account curAccount = authority.account; if (processedAccounts.contains(curAccount)) { continue; @@ -1098,8 +1125,9 @@ class SyncManager { processedAccounts.add(curAccount); - pw.print(" Account "); pw.print(authority.account); - pw.println(":"); + pw.print(" Account "); pw.print(authority.account.mName); + pw.print(" "); pw.print(authority.account.mType); + pw.println(":"); for (int j=i; j<N; j++) { status = statuses.get(j); authority = mSyncStorageEngine.getAuthority(status.authorityId); @@ -1219,9 +1247,15 @@ class SyncManager { SyncStorageEngine.AuthorityInfo authority = mSyncStorageEngine.getAuthority(item.authorityId); pw.print(" #"); pw.print(i+1); pw.print(": "); - pw.print(authority != null ? authority.account : "<no account>"); - pw.print(" "); - pw.print(authority != null ? authority.authority : "<no account>"); + if (authority != null) { + pw.print(authority.account.mName); + pw.print(":"); + pw.print(authority.account.mType); + pw.print(" "); + pw.print(authority.authority); + } else { + pw.print("<no account>"); + } Time time = new Time(); time.set(item.eventTime); pw.print(" "); pw.print(SyncStorageEngine.SOURCES[item.source]); @@ -1278,6 +1312,15 @@ class SyncManager { } } + class ServiceConnectionData { + public final ActiveSyncContext activeSyncContext; + public final ISyncAdapter syncAdapter; + ServiceConnectionData(ActiveSyncContext activeSyncContext, ISyncAdapter syncAdapter) { + this.activeSyncContext = activeSyncContext; + this.syncAdapter = syncAdapter; + } + } + /** * Handles SyncOperation Messages that are posted to the associated * HandlerThread. @@ -1287,6 +1330,8 @@ class SyncManager { private static final int MESSAGE_SYNC_FINISHED = 1; private static final int MESSAGE_SYNC_ALARM = 2; private static final int MESSAGE_CHECK_ALARMS = 3; + private static final int MESSAGE_SERVICE_CONNECTED = 4; + private static final int MESSAGE_SERVICE_DISCONNECTED = 5; public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; @@ -1301,7 +1346,7 @@ class SyncManager { */ class SyncNotificationInfo { // only valid if isActive is true - public String account; + public Account account; // only valid if isActive is true public String authority; @@ -1358,6 +1403,53 @@ class SyncManager { runStateIdle(); break; + case SyncHandler.MESSAGE_SERVICE_CONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + runBoundToSyncAdapter(msgData.syncAdapter); + } + break; + } + + case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + // cancel the sync if we have a syncadapter, which means one is + // outstanding + if (mActiveSyncContext.mSyncAdapter != null) { + try { + mActiveSyncContext.mSyncAdapter.cancelSync(mActiveSyncContext); + } catch (RemoteException e) { + // we don't need to retry this in this case + } + } + + // pretend that the sync failed with an IOException, + // which is a soft error + SyncResult syncResult = new SyncResult(); + syncResult.stats.numIoExceptions++; + runSyncFinishedOrCanceled(syncResult); + + // since we are no longer syncing, check if it is time to start a new + // sync + runStateIdle(); + } + + break; + } + case SyncHandler.MESSAGE_SYNC_ALARM: { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) { @@ -1456,7 +1548,7 @@ class SyncManager { // If the accounts aren't known yet then we aren't ready to run. We will be kicked // when the account lookup request does complete. - String[] accounts = mAccounts; + Account[] accounts = mAccounts; if (accounts == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: accounts not known, skipping"); @@ -1468,14 +1560,14 @@ class SyncManager { // Otherwise consume SyncOperations from the head of the SyncQueue until one is // found that is runnable (not disabled, etc). If that one is ready to run then // start it, otherwise just get out. - SyncOperation syncOperation; + SyncOperation op; final ConnectivityManager connManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - final boolean backgroundDataSetting = connManager.getBackgroundDataSetting(); + final boolean backgroundDataUsageAllowed = connManager.getBackgroundDataSetting(); synchronized (mSyncQueue) { while (true) { - syncOperation = mSyncQueue.head(); - if (syncOperation == null) { + op = mSyncQueue.head(); + if (op == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: no more sync operations, returning"); } @@ -1485,39 +1577,40 @@ class SyncManager { // Sync is disabled, drop this operation. if (!isSyncEnabled()) { if (isLoggable) { - Log.v(TAG, "runStateIdle: sync disabled, dropping " + syncOperation); + Log.v(TAG, "runStateIdle: sync disabled, dropping " + op); } mSyncQueue.popHead(); continue; } - // skip the sync if it isn't a force and the settings are off for this provider - final boolean force = syncOperation.extras.getBoolean( - ContentResolver.SYNC_EXTRAS_FORCE, false); - if (!force && (!backgroundDataSetting - || !mSyncStorageEngine.getListenForNetworkTickles() - || !mSyncStorageEngine.getSyncProviderAutomatically( - null, syncOperation.authority))) { + // skip the sync if it isn't manual and auto sync is disabled + final boolean manualSync = op.extras.getBoolean( + ContentResolver.SYNC_EXTRAS_MANUAL, false); + final boolean syncAutomatically = + mSyncStorageEngine.getSyncAutomatically(op.account, op.authority) + || mSyncStorageEngine.getMasterSyncAutomatically(); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + if (!syncAllowed) { if (isLoggable) { - Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation); + Log.v(TAG, "runStateIdle: sync off, dropping " + op); } mSyncQueue.popHead(); continue; } // skip the sync if the account of this operation no longer exists - if (!ArrayUtils.contains(accounts, syncOperation.account)) { + if (!ArrayUtils.contains(accounts, op.account)) { mSyncQueue.popHead(); if (isLoggable) { - Log.v(TAG, "runStateIdle: account not present, dropping " - + syncOperation); + Log.v(TAG, "runStateIdle: account not present, dropping " + op); } continue; } // go ahead and try to sync this syncOperation if (isLoggable) { - Log.v(TAG, "runStateIdle: found sync candidate: " + syncOperation); + Log.v(TAG, "runStateIdle: found sync candidate: " + op); } break; } @@ -1525,11 +1618,10 @@ class SyncManager { // If the first SyncOperation isn't ready to run schedule a wakeup and // get out. final long now = SystemClock.elapsedRealtime(); - if (syncOperation.earliestRunTime > now) { + if (op.earliestRunTime > now) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "runStateIdle: the time is " + now + " yet the next " - + "sync operation is for " + syncOperation.earliestRunTime - + ": " + syncOperation); + + "sync operation is for " + op.earliestRunTime + ": " + op); } return; } @@ -1537,72 +1629,73 @@ class SyncManager { // We will do this sync. Remove it from the queue and run it outside of the // synchronized block. if (isLoggable) { - Log.v(TAG, "runStateIdle: we are going to sync " + syncOperation); + Log.v(TAG, "runStateIdle: we are going to sync " + op); } mSyncQueue.popHead(); } - String providerName = syncOperation.authority; - ensureContentResolver(); - IContentProvider contentProvider; - - // acquire the provider and update the sync history - try { - contentProvider = mContentResolver.acquireProvider(providerName); - if (contentProvider == null) { - Log.e(TAG, "Provider " + providerName + " doesn't exist"); - return; - } - if (contentProvider.getSyncAdapter() == null) { - Log.e(TAG, "Provider " + providerName + " isn't syncable, " + contentProvider); - return; + // connect to the sync adapter + SyncAdapterType syncAdapterType = new SyncAdapterType(op.authority, + op.account.mType); + RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo = + mSyncAdapters.getServiceInfo(syncAdapterType); + if (syncAdapterInfo == null) { + if (Config.LOGD) { + Log.d(TAG, "can't find a sync adapter for " + syncAdapterType); } - } catch (RemoteException remoteExc) { - Log.e(TAG, "Caught a RemoteException while preparing for sync, rescheduling " - + syncOperation, remoteExc); - rescheduleWithDelay(syncOperation); + runStateIdle(); return; - } catch (RuntimeException exc) { - Log.e(TAG, "Caught a RuntimeException while validating sync of " + providerName, - exc); + } + + ActiveSyncContext activeSyncContext = + new ActiveSyncContext(op, insertStartSyncEvent(op)); + mActiveSyncContext = activeSyncContext; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runStateIdle: setting mActiveSyncContext to " + mActiveSyncContext); + } + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo)) { + Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo); + mActiveSyncContext = null; + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + runStateIdle(); return; } - final long historyRowId = insertStartSyncEvent(syncOperation); + mSyncWakeLock.acquire(); + // no need to schedule an alarm, as that will be done by our caller. + + // the next step will occur when we get either a timeout or a + // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message + } + private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) { + mActiveSyncContext.mSyncAdapter = syncAdapter; + final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; try { - ISyncAdapter syncAdapter = contentProvider.getSyncAdapter(); - ActiveSyncContext activeSyncContext = new ActiveSyncContext(syncOperation, - contentProvider, syncAdapter, historyRowId); - mSyncWakeLock.acquire(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "starting sync of " + syncOperation); - } - syncAdapter.startSync(activeSyncContext, syncOperation.account, - syncOperation.extras); - mActiveSyncContext = activeSyncContext; - mSyncStorageEngine.setActiveSync(mActiveSyncContext); + syncAdapter.startSync(mActiveSyncContext, syncOperation.authority, + syncOperation.account, syncOperation.extras); } catch (RemoteException remoteExc) { if (Config.LOGD) { Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc); } + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); rescheduleWithDelay(syncOperation); } catch (RuntimeException exc) { + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); Log.e(TAG, "Caught a RuntimeException while starting the sync " + syncOperation, exc); } - - // no need to schedule an alarm, as that will be done by our caller. } private void runSyncFinishedOrCanceled(SyncResult syncResult) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) Log.v(TAG, "runSyncFinishedOrCanceled"); - ActiveSyncContext activeSyncContext = mActiveSyncContext; + final ActiveSyncContext activeSyncContext = mActiveSyncContext; mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); @@ -1642,10 +1735,12 @@ class SyncManager { Log.v(TAG, "runSyncFinishedOrCanceled: is a cancel: operation " + syncOperation); } - try { - activeSyncContext.mSyncAdapter.cancelSync(); - } catch (RemoteException e) { - // we don't need to retry this in this case + if (activeSyncContext.mSyncAdapter != null) { + try { + activeSyncContext.mSyncAdapter.cancelSync(activeSyncContext); + } catch (RemoteException e) { + // we don't need to retry this in this case + } } historyMessage = SyncStorageEngine.MESG_CANCELED; downstreamActivity = 0; @@ -1655,7 +1750,7 @@ class SyncManager { stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage, upstreamActivity, downstreamActivity, elapsedTime); - mContentResolver.releaseProvider(activeSyncContext.mContentProvider); + activeSyncContext.unBindFromSyncAdapter(); if (syncResult != null && syncResult.tooManyDeletions) { installHandleTooManyDeletesNotification(syncOperation.account, @@ -1683,21 +1778,21 @@ class SyncManager { */ private int syncResultToErrorNumber(SyncResult syncResult) { if (syncResult.syncAlreadyInProgress) - return SyncStorageEngine.ERROR_SYNC_ALREADY_IN_PROGRESS; + return ContentResolver.SYNC_ERROR_SYNC_ALREADY_IN_PROGRESS; if (syncResult.stats.numAuthExceptions > 0) - return SyncStorageEngine.ERROR_AUTHENTICATION; + return ContentResolver.SYNC_ERROR_AUTHENTICATION; if (syncResult.stats.numIoExceptions > 0) - return SyncStorageEngine.ERROR_IO; + return ContentResolver.SYNC_ERROR_IO; if (syncResult.stats.numParseExceptions > 0) - return SyncStorageEngine.ERROR_PARSE; + return ContentResolver.SYNC_ERROR_PARSE; if (syncResult.stats.numConflictDetectedExceptions > 0) - return SyncStorageEngine.ERROR_CONFLICT; + return ContentResolver.SYNC_ERROR_CONFLICT; if (syncResult.tooManyDeletions) - return SyncStorageEngine.ERROR_TOO_MANY_DELETIONS; + return ContentResolver.SYNC_ERROR_TOO_MANY_DELETIONS; if (syncResult.tooManyRetries) - return SyncStorageEngine.ERROR_TOO_MANY_RETRIES; + return ContentResolver.SYNC_ERROR_TOO_MANY_RETRIES; if (syncResult.databaseError) - return SyncStorageEngine.ERROR_INTERNAL; + return ContentResolver.SYNC_ERROR_INTERNAL; throw new IllegalStateException("we are not in an error state, " + syncResult); } @@ -1738,9 +1833,10 @@ class SyncManager { } else { final boolean timeToShowNotification = now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY; - final boolean syncIsForced = syncOperation.extras - .getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); - shouldInstall = timeToShowNotification || syncIsForced; + // show the notification immediately if this is a manual sync + final boolean manualSync = syncOperation.extras + .getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); + shouldInstall = timeToShowNotification || manualSync; } } @@ -1860,7 +1956,7 @@ class SyncManager { mContext.sendBroadcast(syncStateIntent); } - private void installHandleTooManyDeletesNotification(String account, String authority, + private void installHandleTooManyDeletesNotification(Account account, String authority, long numDeletes) { if (mNotificationMgr == null) return; Intent clickIntent = new Intent(); @@ -1995,9 +2091,9 @@ class SyncManager { SyncOperation existingOperation = mOpsByKey.get(operationKey); // if this operation matches an existing operation that is being retried (delay > 0) - // and this operation isn't forced, ignore this operation + // and this isn't a manual sync operation, ignore this operation if (existingOperation != null && existingOperation.delay > 0) { - if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)) { + if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) { return false; } } @@ -2071,7 +2167,7 @@ class SyncManager { if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } - public void clear(String account, String authority) { + public void clear(Account account, String authority) { Iterator<Map.Entry<String, SyncOperation>> entries = mOpsByKey.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, SyncOperation> entry = entries.next(); diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java index f503e6f..dc728ec 100644 --- a/core/java/android/content/SyncStateContentProviderHelper.java +++ b/core/java/android/content/SyncStateContentProviderHelper.java @@ -23,6 +23,7 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.accounts.Account; /** * Extends the schema of a ContentProvider to include the _sync_state table @@ -43,14 +44,15 @@ public class SyncStateContentProviderHelper { private static final Uri CONTENT_URI = Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); - private static final String ACCOUNT_WHERE = "_sync_account = ?"; + private static final String ACCOUNT_WHERE = "_sync_account = ? AND _sync_account_type = ?"; private final Provider mInternalProviderInterface; private static final String SYNC_STATE_TABLE = "_sync_state"; - private static long DB_VERSION = 2; + private static long DB_VERSION = 3; - private static final String[] ACCOUNT_PROJECTION = new String[]{"_sync_account"}; + private static final String[] ACCOUNT_PROJECTION = + new String[]{"_sync_account", "_sync_account_type"}; static { sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); @@ -70,8 +72,9 @@ public class SyncStateContentProviderHelper { db.execSQL("CREATE TABLE _sync_state (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + + "_sync_account_type TEXT," + "data TEXT," + - "UNIQUE(_sync_account)" + + "UNIQUE(_sync_account, _sync_account_type)" + ");"); db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); @@ -168,15 +171,17 @@ public class SyncStateContentProviderHelper { * @param account the account of the row that should be copied over. */ public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest, - String account) { - final String[] whereArgs = new String[]{account}; - Cursor c = dbSrc.query(SYNC_STATE_TABLE, new String[]{"_sync_account", "data"}, + Account account) { + final String[] whereArgs = new String[]{account.mName, account.mType}; + Cursor c = dbSrc.query(SYNC_STATE_TABLE, + new String[]{"_sync_account", "_sync_account_type", "data"}, ACCOUNT_WHERE, whereArgs, null, null, null); try { if (c.moveToNext()) { ContentValues values = new ContentValues(); values.put("_sync_account", c.getString(0)); - values.put("data", c.getBlob(1)); + values.put("_sync_account_type", c.getString(1)); + values.put("data", c.getBlob(2)); dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); } } finally { @@ -184,14 +189,17 @@ public class SyncStateContentProviderHelper { } } - public void onAccountsChanged(String[] accounts) { + public void onAccountsChanged(Account[] accounts) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null); try { while (c.moveToNext()) { - final String account = c.getString(0); + final String accountName = c.getString(0); + final String accountType = c.getString(1); + Account account = new Account(accountName, accountType); if (!ArrayUtils.contains(accounts, account)) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, + new String[]{accountName, accountType}); } } } finally { @@ -199,9 +207,9 @@ public class SyncStateContentProviderHelper { } } - public void discardSyncData(SQLiteDatabase db, String account) { + public void discardSyncData(SQLiteDatabase db, Account account) { if (account != null) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account.mName, account.mType}); } else { db.delete(SYNC_STATE_TABLE, null, null); } @@ -210,9 +218,9 @@ public class SyncStateContentProviderHelper { /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(SQLiteDatabase db, String account) { + public byte[] readSyncDataBytes(SQLiteDatabase db, Account account) { Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, - new String[]{account}, null, null, null); + new String[]{account.mName, account.mType}, null, null, null); try { if (c.moveToFirst()) { return c.getBlob(c.getColumnIndexOrThrow("data")); @@ -226,9 +234,10 @@ public class SyncStateContentProviderHelper { /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public void writeSyncDataBytes(SQLiteDatabase db, String account, byte[] data) { + public void writeSyncDataBytes(SQLiteDatabase db, Account account, byte[] data) { ContentValues values = new ContentValues(); values.put("data", data); - db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, new String[]{account}); + db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, + new String[]{account.mName, account.mType}); } } diff --git a/core/java/android/content/SyncStatusObserver.java b/core/java/android/content/SyncStatusObserver.java new file mode 100644 index 0000000..663378a --- /dev/null +++ b/core/java/android/content/SyncStatusObserver.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content; + +public interface SyncStatusObserver { + void onStatusChanged(int which); +} diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index f781e0d..13bcdd3 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -24,6 +24,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import android.accounts.Account; import android.backup.IBackupManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -88,6 +89,9 @@ public class SyncStorageEngine extends Handler { /** Enum value for a user-initiated sync. */ public static final int SOURCE_USER = 3; + private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT = + new Intent("com.android.sync.SYNC_CONN_STATUS_CHANGED"); + // TODO: i18n -- grab these out of resources. /** String names for the sync source types. */ public static final String[] SOURCES = { "SERVER", @@ -95,26 +99,10 @@ public class SyncStorageEngine extends Handler { "POLL", "USER" }; - // Error types - public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; - public static final int ERROR_AUTHENTICATION = 2; - public static final int ERROR_IO = 3; - public static final int ERROR_PARSE = 4; - public static final int ERROR_CONFLICT = 5; - public static final int ERROR_TOO_MANY_DELETIONS = 6; - public static final int ERROR_TOO_MANY_RETRIES = 7; - public static final int ERROR_INTERNAL = 8; - // The MESG column will contain one of these or one of the Error types. public static final String MESG_SUCCESS = "success"; public static final String MESG_CANCELED = "canceled"; - public static final int CHANGE_SETTINGS = 1<<0; - public static final int CHANGE_PENDING = 1<<1; - public static final int CHANGE_ACTIVE = 1<<2; - public static final int CHANGE_STATUS = 1<<3; - public static final int CHANGE_ALL = 0x7fffffff; - public static final int MAX_HISTORY = 15; private static final int MSG_WRITE_STATUS = 1; @@ -124,7 +112,7 @@ public class SyncStorageEngine extends Handler { private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour public static class PendingOperation { - final String account; + final Account account; final int syncSource; final String authority; final Bundle extras; // note: read-only. @@ -132,7 +120,7 @@ public class SyncStorageEngine extends Handler { int authorityId; byte[] flatExtras; - PendingOperation(String account, int source, + PendingOperation(Account account, int source, String authority, Bundle extras) { this.account = account; this.syncSource = source; @@ -151,22 +139,22 @@ public class SyncStorageEngine extends Handler { } static class AccountInfo { - final String account; + final Account account; final HashMap<String, AuthorityInfo> authorities = new HashMap<String, AuthorityInfo>(); - AccountInfo(String account) { + AccountInfo(Account account) { this.account = account; } } public static class AuthorityInfo { - final String account; + final Account account; final String authority; final int ident; boolean enabled; - - AuthorityInfo(String account, String authority, int ident) { + + AuthorityInfo(Account account, String authority, int ident) { this.account = account; this.authority = authority; this.ident = ident; @@ -202,8 +190,8 @@ public class SyncStorageEngine extends Handler { private final SparseArray<AuthorityInfo> mAuthorities = new SparseArray<AuthorityInfo>(); - private final HashMap<String, AccountInfo> mAccounts = - new HashMap<String, AccountInfo>(); + private final HashMap<Account, AccountInfo> mAccounts = + new HashMap<Account, AccountInfo>(); private final ArrayList<PendingOperation> mPendingOperations = new ArrayList<PendingOperation>(); @@ -258,7 +246,7 @@ public class SyncStorageEngine extends Handler { private int mNumPendingFinished = 0; private int mNextHistoryId = 0; - private boolean mListenForTickles = true; + private boolean mMasterSyncAutomatically = true; private SyncStorageEngine(Context context) { mContext = context; @@ -365,14 +353,14 @@ public class SyncStorageEngine extends Handler { } } - public boolean getSyncProviderAutomatically(String account, String providerName) { + public boolean getSyncAutomatically(Account account, String providerName) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, - "getSyncProviderAutomatically"); - return authority != null ? authority.enabled : false; + "getSyncAutomatically"); + return authority != null && authority.enabled; } - + int i = mAuthorities.size(); while (i > 0) { i--; @@ -386,45 +374,35 @@ public class SyncStorageEngine extends Handler { } } - public void setSyncProviderAutomatically(String account, String providerName, boolean sync) { + public void setSyncAutomatically(Account account, String providerName, boolean sync) { synchronized (mAuthorities) { - if (account != null) { - AuthorityInfo authority = getAuthorityLocked(account, providerName, - "setSyncProviderAutomatically"); - if (authority != null) { - authority.enabled = sync; - } - } else { - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.get(i); - if (authority.authority.equals(providerName)) { - authority.enabled = sync; - } - } + AuthorityInfo authority = getAuthorityLocked(account, providerName, + "setSyncAutomatically"); + if (authority != null) { + authority.enabled = sync; } writeAccountInfoLocked(); } - reportChange(CHANGE_SETTINGS); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); } - public void setListenForNetworkTickles(boolean flag) { + public void setMasterSyncAutomatically(boolean flag) { synchronized (mAuthorities) { - mListenForTickles = flag; + mMasterSyncAutomatically = flag; writeAccountInfoLocked(); } - reportChange(CHANGE_SETTINGS); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + mContext.sendBroadcast(SYNC_CONNECTION_SETTING_CHANGED_INTENT); } - public boolean getListenForNetworkTickles() { + public boolean getMasterSyncAutomatically() { synchronized (mAuthorities) { - return mListenForTickles; + return mMasterSyncAutomatically; } } - public AuthorityInfo getAuthority(String account, String authority) { + public AuthorityInfo getAuthority(Account account, String authority) { synchronized (mAuthorities) { return getAuthorityLocked(account, authority, null); } @@ -440,7 +418,7 @@ public class SyncStorageEngine extends Handler { * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { synchronized (mAuthorities) { int i = mPendingOperations.size(); while (i > 0) { @@ -489,7 +467,7 @@ public class SyncStorageEngine extends Handler { status.pending = true; } - reportChange(CHANGE_PENDING); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return op; } @@ -535,7 +513,7 @@ public class SyncStorageEngine extends Handler { } } - reportChange(CHANGE_PENDING); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return res; } @@ -551,7 +529,7 @@ public class SyncStorageEngine extends Handler { } writePendingOperationsLocked(); } - reportChange(CHANGE_PENDING); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return num; } @@ -579,7 +557,7 @@ public class SyncStorageEngine extends Handler { * Called when the set of account has changed, given the new array of * active accounts. */ - public void doDatabaseCleanup(String[] accounts) { + public void doDatabaseCleanup(Account[] accounts) { synchronized (mAuthorities) { if (DEBUG) Log.w(TAG, "Updating for new accounts..."); SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>(); @@ -658,20 +636,20 @@ public class SyncStorageEngine extends Handler { } } - reportChange(CHANGE_ACTIVE); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE); } /** * To allow others to send active change reports, to poke clients. */ public void reportActiveChange() { - reportChange(CHANGE_ACTIVE); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE); } /** * Note that sync has started for the given account and authority. */ - public long insertStartSyncEvent(String accountName, String authorityName, + public long insertStartSyncEvent(Account accountName, String authorityName, long now, int source) { long id; synchronized (mAuthorities) { @@ -697,7 +675,7 @@ public class SyncStorageEngine extends Handler { if (DEBUG) Log.v(TAG, "returning historyId " + id); } - reportChange(CHANGE_STATUS); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); return id; } @@ -801,7 +779,7 @@ public class SyncStorageEngine extends Handler { } } - reportChange(CHANGE_STATUS); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); } /** @@ -859,7 +837,7 @@ public class SyncStorageEngine extends Handler { /** * Return true if the pending status is true of any matching authorities. */ - public boolean isAuthorityPending(String account, String authority) { + public boolean isSyncPending(Account account, String authority) { synchronized (mAuthorities) { final int N = mSyncStatus.size(); for (int i=0; i<N; i++) { @@ -915,7 +893,7 @@ public class SyncStorageEngine extends Handler { */ public long getInitialSyncFailureTime() { synchronized (mAuthorities) { - if (!mListenForTickles) { + if (!mMasterSyncAutomatically) { return 0; } @@ -956,7 +934,7 @@ public class SyncStorageEngine extends Handler { * @param tag If non-null, this will be used in a log message if the * requested authority does not exist. */ - private AuthorityInfo getAuthorityLocked(String accountName, String authorityName, + private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName, String tag) { AccountInfo account = mAccounts.get(accountName); if (account == null) { @@ -976,7 +954,7 @@ public class SyncStorageEngine extends Handler { return authority; } - private AuthorityInfo getOrCreateAuthorityLocked(String accountName, + private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, String authorityName, int ident, boolean doWrite) { AccountInfo account = mAccounts.get(accountName); if (account == null) { @@ -1049,7 +1027,7 @@ public class SyncStorageEngine extends Handler { if ("accounts".equals(tagName)) { String listen = parser.getAttributeValue( null, "listen-for-tickles"); - mListenForTickles = listen == null + mMasterSyncAutomatically = listen == null || Boolean.parseBoolean(listen); eventType = parser.next(); do { @@ -1067,6 +1045,11 @@ public class SyncStorageEngine extends Handler { if (id >= 0) { String accountName = parser.getAttributeValue( null, "account"); + String accountType = parser.getAttributeValue( + null, "type"); + if (accountType == null) { + accountType = "com.google.GAIA"; + } String authorityName = parser.getAttributeValue( null, "authority"); String enabled = parser.getAttributeValue( @@ -1078,7 +1061,8 @@ public class SyncStorageEngine extends Handler { if (authority == null) { if (DEBUG_FILE) Log.v(TAG, "Creating entry"); authority = getOrCreateAuthorityLocked( - accountName, authorityName, id, false); + new Account(accountName, accountType), + authorityName, id, false); } if (authority != null) { authority.enabled = enabled == null @@ -1124,7 +1108,7 @@ public class SyncStorageEngine extends Handler { out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, "accounts"); - if (!mListenForTickles) { + if (!mMasterSyncAutomatically) { out.attribute(null, "listen-for-tickles", "false"); } @@ -1133,7 +1117,8 @@ public class SyncStorageEngine extends Handler { AuthorityInfo authority = mAuthorities.get(i); out.startTag(null, "authority"); out.attribute(null, "id", Integer.toString(authority.ident)); - out.attribute(null, "account", authority.account); + out.attribute(null, "account", authority.account.mName); + out.attribute(null, "type", authority.account.mType); out.attribute(null, "authority", authority.authority); if (!authority.enabled) { out.attribute(null, "enabled", "false"); @@ -1182,6 +1167,8 @@ public class SyncStorageEngine extends Handler { } if (db != null) { + final boolean hasType = db.getVersion() >= 11; + // Copy in all of the status information, as well as accounts. if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db"); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); @@ -1189,6 +1176,9 @@ public class SyncStorageEngine extends Handler { HashMap<String,String> map = new HashMap<String,String>(); map.put("_id", "status._id as _id"); map.put("account", "stats.account as account"); + if (hasType) { + map.put("account_type", "stats.account_type as account_type"); + } map.put("authority", "stats.authority as authority"); map.put("totalElapsedTime", "totalElapsedTime"); map.put("numSyncs", "numSyncs"); @@ -1207,9 +1197,15 @@ public class SyncStorageEngine extends Handler { Cursor c = qb.query(db, null, null, null, null, null, null); while (c.moveToNext()) { String accountName = c.getString(c.getColumnIndex("account")); + String accountType = hasType + ? c.getString(c.getColumnIndex("account_type")) : null; + if (accountType == null) { + accountType = "com.google.GAIA"; + } String authorityName = c.getString(c.getColumnIndex("authority")); AuthorityInfo authority = this.getOrCreateAuthorityLocked( - accountName, authorityName, -1, false); + new Account(accountName, accountType), + authorityName, -1, false); if (authority != null) { int i = mSyncStatus.size(); boolean found = false; @@ -1252,13 +1248,18 @@ public class SyncStorageEngine extends Handler { String value = c.getString(c.getColumnIndex("value")); if (name == null) continue; if (name.equals("listen_for_tickles")) { - setListenForNetworkTickles(value == null - || Boolean.parseBoolean(value)); + setMasterSyncAutomatically(value == null || Boolean.parseBoolean(value)); } else if (name.startsWith("sync_provider_")) { String provider = name.substring("sync_provider_".length(), name.length()); - setSyncProviderAutomatically(null, provider, - value == null || Boolean.parseBoolean(value)); + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.get(i); + if (authority.authority.equals(provider)) { + authority.enabled = value == null || Boolean.parseBoolean(value); + } + } } } diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java index e0cd786..ab4e91c 100644 --- a/core/java/android/content/SyncableContentProvider.java +++ b/core/java/android/content/SyncableContentProvider.java @@ -19,6 +19,7 @@ package android.content; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.accounts.Account; import java.util.Map; @@ -32,6 +33,16 @@ import java.util.Map; public abstract class SyncableContentProvider extends ContentProvider { protected abstract boolean isTemporary(); + private volatile TempProviderSyncAdapter mTempProviderSyncAdapter; + + public void setTempProviderSyncAdapter(TempProviderSyncAdapter syncAdapter) { + mTempProviderSyncAdapter = syncAdapter; + } + + public TempProviderSyncAdapter getTempProviderSyncAdapter() { + return mTempProviderSyncAdapter; + } + /** * Close resources that must be closed. You must call this to properly release * the resources used by the SyncableContentProvider. @@ -110,7 +121,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * @param context the sync context for the operation * @param account */ - public abstract void onSyncStart(SyncContext context, String account); + public abstract void onSyncStart(SyncContext context, Account account); /** * Called right after a sync is completed @@ -124,7 +135,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * The account of the most recent call to onSyncStart() * @return the account */ - public abstract String getSyncingAccount(); + public abstract Account getSyncingAccount(); /** * Merge diffs from a sync source with this content provider. @@ -194,7 +205,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected abstract void onAccountsChanged(String[] accountsArray); + protected abstract void onAccountsChanged(Account[] accountsArray); /** * A helper method to delete all rows whose account is not in the accounts @@ -203,26 +214,24 @@ public abstract class SyncableContentProvider extends ContentProvider { * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected abstract void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts, - String table, String accountColumnName); + protected abstract void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, + String table); /** * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public abstract void wipeAccount(String account); + public abstract void wipeAccount(Account account); /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public abstract byte[] readSyncDataBytes(String account); + public abstract byte[] readSyncDataBytes(Account account); /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public abstract void writeSyncDataBytes(String account, byte[] data); + public abstract void writeSyncDataBytes(Account account, byte[] data); } diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java index eb3a5da..fb05fe7 100644 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -12,6 +12,7 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.TimingLogger; +import android.accounts.Account; /** * @hide @@ -62,12 +63,10 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * * @param context allows you to publish status and interact with the * @param account the account to sync - * @param forced if true then the sync was forced + * @param manualSync true if this sync was requested manually by the user * @param result information to track what happened during this sync attempt - * @return true, if the sync was successfully started. One reason it can - * fail to start is if there is no user configured on the device. */ - public abstract void onSyncStarting(SyncContext context, String account, boolean forced, + public abstract void onSyncStarting(SyncContext context, Account account, boolean manualSync, SyncResult result); /** @@ -168,12 +167,12 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * exist. * @param accounts the list of accounts */ - public abstract void onAccountsChanged(String[] accounts); + public abstract void onAccountsChanged(Account[] accounts); private Context mContext; private class SyncThread extends Thread { - private final String mAccount; + private final Account mAccount; private final Bundle mExtras; private final SyncContext mSyncContext; private volatile boolean mIsCanceled = false; @@ -181,7 +180,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { private long mInitialRxBytes; private final SyncResult mResult; - SyncThread(SyncContext syncContext, String account, Bundle extras) { + SyncThread(SyncContext syncContext, Account account, Bundle extras) { super("SyncThread"); mAccount = account; mExtras = extras; @@ -221,19 +220,19 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void sync(SyncContext syncContext, String account, Bundle extras) { + private void sync(SyncContext syncContext, Account account, Bundle extras) { mIsCanceled = false; mProviderSyncStarted = false; mAdapterSyncStarted = false; String message = null; - boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); try { mProvider.onSyncStart(syncContext, account); mProviderSyncStarted = true; - onSyncStarting(syncContext, account, syncForced, mResult); + onSyncStarting(syncContext, account, manualSync, mResult); if (mResult.hasError()) { message = "SyncAdapter failed while trying to start sync"; return; @@ -273,7 +272,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) { + private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) { TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); syncTimer.addSplit("start"); int loopCount = 0; @@ -518,7 +517,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); } - public void startSync(SyncContext syncContext, String account, Bundle extras) { + public void startSync(SyncContext syncContext, Account account, Bundle extras) { if (mSyncThread != null) { syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); return; diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index bf2a895..68f8417 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -119,6 +119,7 @@ interface IPackageManager { * providers that can sync. * @param outInfo Filled in with a list of the ProviderInfo for each * name in 'outNames'. + * @deprecated */ void querySyncProviders(inout List<String> outNames, inout List<ProviderInfo> outInfo); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 941ca9e..3250a87 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1435,8 +1435,6 @@ public abstract class PackageManager { * which market the package came from. * * @param packageName The name of the package to query - * - * @hide */ public abstract String getInstallerPackageName(String packageName); diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index d01460e..d61e95b 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -74,7 +74,11 @@ public final class ProviderInfo extends ComponentInfo * running in the same process. Higher goes first. */ public int initOrder = 0; - /** Whether or not this provider is syncable. */ + /** + * Whether or not this provider is syncable. + * @deprecated This flag is now being ignored. The current way to make a provider + * syncable is to provide a SyncAdapter service for a given provider/account type. + */ public boolean isSyncable = false; public ProviderInfo() { diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java new file mode 100644 index 0000000..bb94372 --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2009 The Android Open Source 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. + */ + +package android.content.pm; + +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ComponentName; +import android.content.res.XmlResourceParser; +import android.util.Log; +import android.util.AttributeSet; +import android.util.Xml; + +import java.util.Map; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.IOException; + +import com.google.android.collect.Maps; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +/** + * A cache of registered services. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The services are referred to by type V and + * are made available via the {@link #getServiceInfo} method. + * @hide + */ +public abstract class RegisteredServicesCache<V> { + private static final String TAG = "PackageManager"; + + public final Context mContext; + private final String mInterfaceName; + private final String mMetaDataName; + private final String mAttributesName; + + // no need to be synchronized since the map is never changed once mService is written + private volatile Map<V, ServiceInfo<V>> mServices; + + // synchronized on "this" + private BroadcastReceiver mReceiver = null; + + public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, + String attributeName) { + mContext = context; + mInterfaceName = interfaceName; + mMetaDataName = metaDataName; + mAttributesName = attributeName; + } + + public void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + getAllServices(); + Map<V, ServiceInfo<V>> services = mServices; + fout.println("RegisteredServicesCache: " + services.size() + " services"); + for (ServiceInfo info : services.values()) { + fout.println(" " + info); + } + } + + private boolean maybeRegisterForPackageChanges() { + synchronized (this) { + if (mReceiver == null) { + synchronized (this) { + mReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + mServices = generateServicesMap(); + } + }; + } + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + mContext.registerReceiver(mReceiver, intentFilter); + return true; + } + return false; + } + } + + private void maybeUnregisterForPackageChanges() { + synchronized (this) { + if (mReceiver != null) { + mContext.unregisterReceiver(mReceiver); + mReceiver = null; + } + } + } + + /** + * Value type that describes a Service. The information within can be used + * to bind to the service. + */ + public static class ServiceInfo<V> { + public final V type; + public final ComponentName componentName; + + private ServiceInfo(V type, ComponentName componentName) { + this.type = type; + this.componentName = componentName; + } + + public String toString() { + return "ServiceInfo: " + type + ", " + componentName; + } + } + + /** + * Accessor for the registered authenticators. + * @param type the account type of the authenticator + * @return the AuthenticatorInfo that matches the account type or null if none is present + */ + public ServiceInfo<V> getServiceInfo(V type) { + if (mServices == null) { + maybeRegisterForPackageChanges(); + mServices = generateServicesMap(); + } + return mServices.get(type); + } + + /** + * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all + * registered authenticators. + */ + public Collection<ServiceInfo<V>> getAllServices() { + if (mServices == null) { + maybeRegisterForPackageChanges(); + mServices = generateServicesMap(); + } + return Collections.unmodifiableCollection(mServices.values()); + } + + /** + * Stops the monitoring of package additions, removals and changes. + */ + public void close() { + maybeUnregisterForPackageChanges(); + } + + protected void finalize() throws Throwable { + synchronized (this) { + if (mReceiver != null) { + Log.e(TAG, "RegisteredServicesCache finalized without being closed"); + } + } + close(); + super.finalize(); + } + + private Map<V, ServiceInfo<V>> generateServicesMap() { + Map<V, ServiceInfo<V>> services = Maps.newHashMap(); + PackageManager pm = mContext.getPackageManager(); + + List<ResolveInfo> resolveInfos = + pm.queryIntentServices(new Intent(mInterfaceName), PackageManager.GET_META_DATA); + + for (ResolveInfo resolveInfo : resolveInfos) { + try { + ServiceInfo<V> info = parseServiceInfo(resolveInfo); + if (info != null) { + services.put(info.type, info); + } else { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString()); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString(), e); + } catch (IOException e) { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString(), e); + } + } + + return services; + } + + private ServiceInfo<V> parseServiceInfo(ResolveInfo service) + throws XmlPullParserException, IOException { + android.content.pm.ServiceInfo si = service.serviceInfo; + ComponentName componentName = new ComponentName(si.packageName, si.name); + + PackageManager pm = mContext.getPackageManager(); + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, mMetaDataName); + if (parser == null) { + throw new XmlPullParserException("No " + mMetaDataName + " meta-data"); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!mAttributesName.equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with " + mAttributesName + " tag"); + } + + V v = parseServiceAttributes(si.packageName, attrs); + if (v == null) { + return null; + } + return new ServiceInfo<V>(v, componentName); + } finally { + if (parser != null) parser.close(); + } + } + + public abstract V parseServiceAttributes(String packageName, AttributeSet attrs); +} diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 577aa60..4928e93 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -60,7 +60,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration /** * The kind of keyboard attached to the device. - * One of: {@link #KEYBOARD_QWERTY}, {@link #KEYBOARD_12KEY}. + * One of: {@link #KEYBOARD_NOKEYS}, {@link #KEYBOARD_QWERTY}, + * {@link #KEYBOARD_12KEY}. */ public int keyboard; @@ -99,8 +100,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration /** * The kind of navigation method available on the device. - * One of: {@link #NAVIGATION_DPAD}, {@link #NAVIGATION_TRACKBALL}, - * {@link #NAVIGATION_WHEEL}. + * One of: {@link #NAVIGATION_NONAV}, {@link #NAVIGATION_DPAD}, + * {@link #NAVIGATION_TRACKBALL}, {@link #NAVIGATION_WHEEL}. */ public int navigation; |