diff options
Diffstat (limited to 'core/java/android/content')
62 files changed, 5852 insertions, 1416 deletions
diff --git a/core/java/android/content/AbstractCursorEntityIterator.java b/core/java/android/content/AbstractCursorEntityIterator.java new file mode 100644 index 0000000..a804f3c --- /dev/null +++ b/core/java/android/content/AbstractCursorEntityIterator.java @@ -0,0 +1,121 @@ +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. + * @hide + */ +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; + } + } + + public void reset() throws RemoteException { + if (mIsClosed) { + throw new IllegalStateException("calling reset() when the iterator is closed"); + } + mEntityCursor.moveToPosition(-1); + 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..fbe3548 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.OnAccountsUpdateListener; +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 @@ -127,13 +135,16 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro public void onCreate(SQLiteDatabase db) { bootstrapDatabase(db); mSyncState.createDatabase(db); + ContentResolver.requestSync(null /* all accounts */, + mContentUri.getAuthority(), new Bundle()); } @Override 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,23 +161,36 @@ 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 OnAccountsUpdateListener() { + public void onAccountsUpdated(Account[] accounts) { + // Some providers override onAccountsChanged(); give them a database to + // work with. + mDb = mOpenHelper.getWritableDatabase(); + // Only call onAccountsChanged on GAIA accounts; otherwise, the contacts and + // calendar providers will choke as they try to sync unknown accounts with + // AbstractGDataSyncAdapter, which will put acore into a crash loop + ArrayList<Account> gaiaAccounts = new ArrayList<Account>(); + for (Account acct: accounts) { + if (acct.type.equals("com.google")) { + gaiaAccounts.add(acct); + } + } + accounts = new Account[gaiaAccounts.size()]; + int i = 0; + for (Account acct: gaiaAccounts) { + accounts[i++] = acct; + } + onAccountsChanged(accounts); + TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); + if (syncAdapter != null) { + syncAdapter.onAccountsChanged(accounts); + } + } + }, null /* handler */, true /* updateImmediately */); return true; } - /** * Get a non-persistent instance of this content provider. * You must call {@link #close} on the returned @@ -236,147 +260,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 +405,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 +517,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 +537,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 +648,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 +664,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 +679,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.name, account.type}); if (Config.LOGV) { Log.v(TAG, "deleted " + numDeleted + " records from table " + table @@ -634,7 +712,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 +727,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.name, account.type}); } db.setTransactionSuccessful(); } finally { @@ -660,14 +739,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 9f609a3..9545fd7 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,342 +169,340 @@ 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.name, account.type}; + 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"); + } + 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}); } - 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. - boolean recordChanged = (localSyncVersion == null) || - (serverSyncVersion == 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); + 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. + boolean recordChanged = (localSyncVersion == null) || + (serverSyncVersion == 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; } - conflict = true; } else { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); + "Skipping update: localSyncVersion: " + localSyncVersion + + ", serverSyncVersion: " + serverSyncVersion); } - update = true; } } else { + // the local db doesn't know about this record so add it if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "Skipping update: localSyncVersion: " + localSyncVersion + - ", serverSyncVersion: " + serverSyncVersion); + 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.name, account.type}; + 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); @@ -519,43 +520,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.name, account.type}; // 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"); @@ -564,23 +568,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..fb6091a --- /dev/null +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -0,0 +1,221 @@ +/* + * 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 android.os.NetStat; +import android.os.IBinder; +import android.util.EventLog; + +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 #onPerformSync} 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. + */ +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 mSyncThreadLock + private SyncThread mSyncThread; + private final Object mSyncThreadLock = new Object(); + + /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ + public static final int LOG_SYNC_DETAILS = 2743; + private static final String TAG = "Sync"; + private final boolean mAutoInitialize; + + /** + * Creates an {@link AbstractThreadedSyncAdapter}. + * @param context the {@link android.content.Context} that this is running within. + * @param autoInitialize if true then sync requests that have + * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by + * {@link AbstractThreadedSyncAdapter} by calling + * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it + * is currently set to <0. + */ + public AbstractThreadedSyncAdapter(Context context, boolean autoInitialize) { + mContext = context; + mISyncAdapterImpl = new ISyncAdapterImpl(); + mNumSyncStarts = new AtomicInteger(0); + mSyncThread = null; + mAutoInitialize = autoInitialize; + } + + public Context getContext() { + return mContext; + } + + private 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 (mSyncThreadLock) { + if (mSyncThread == null) { + if (mAutoInitialize + && extras != null + && extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + if (ContentResolver.getIsSyncable(account, authority) < 0) { + ContentResolver.setIsSyncable(account, authority, 1); + } + syncContextClient.onFinished(new SyncResult()); + return; + } + mSyncThread = new SyncThread( + "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), + syncContextClient, authority, account, extras); + 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 (mSyncThreadLock) { + if (mSyncThread != null + && mSyncThread.mSyncContext.getSyncContextBinder() + == syncContext.asBinder()) { + mSyncThread.interrupt(); + } + } + } + } + + /** + * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires + * the provider for this sync before calling onPerformSync 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 long mInitialTxBytes; + private long mInitialRxBytes; + + 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() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + if (isCanceled()) { + return; + } + + SyncResult syncResult = new SyncResult(); + int uid = Process.myUid(); + mInitialTxBytes = NetStat.getUidTxBytes(uid); + mInitialRxBytes = NetStat.getUidRxBytes(uid); + ContentProviderClient provider = null; + try { + provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); + if (provider != null) { + AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras, + mAuthority, provider, syncResult); + } else { + syncResult.databaseError = true; + } + } finally { + if (provider != null) { + provider.release(); + } + if (!isCanceled()) { + mSyncContext.onFinished(syncResult); + } + onLogSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, syncResult); + // synchronize so that the assignment will be seen by other threads + // that also synchronize accesses to mSyncThread + synchronized (mSyncThreadLock) { + mSyncThread = null; + } + } + } + + private boolean isCanceled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * @return a reference to the IBinder of the SyncAdapter service. + */ + public final IBinder getSyncAdapterBinder() { + return mISyncAdapterImpl.asBinder(); + } + + /** + * 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 onPerformSync(Account account, Bundle extras, + String authority, ContentProviderClient provider, SyncResult syncResult); + + /** + * Logs details on the sync. + * Normally this will be overridden by a subclass that will provide + * provider-specific details. + * + * @param bytesSent number of bytes the sync sent over the network + * @param bytesReceived number of bytes the sync received over the network + * @param result The SyncResult object holding info on the sync + * @hide + */ + protected void onLogSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { + EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); + } +} 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/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java index ac851cc..0a4a804 100644 --- a/core/java/android/content/AsyncQueryHandler.java +++ b/core/java/android/content/AsyncQueryHandler.java @@ -38,7 +38,8 @@ public abstract class AsyncQueryHandler extends Handler { private static final int EVENT_ARG_INSERT = 2; private static final int EVENT_ARG_UPDATE = 3; private static final int EVENT_ARG_DELETE = 4; - + private static final int EVENT_ARG_QUERY_ENTITIES = 5; + /* package */ final WeakReference<ContentResolver> mResolver; private static Looper sLooper = null; @@ -85,12 +86,25 @@ public abstract class AsyncQueryHandler extends Handler { cursor.getCount(); } } catch (Exception e) { + Log.w(TAG, e.toString()); cursor = null; } args.result = cursor; break; + case EVENT_ARG_QUERY_ENTITIES: + EntityIterator iterator = null; + try { + iterator = resolver.queryEntities(args.uri, args.selection, + args.selectionArgs, args.orderBy); + } catch (Exception e) { + Log.w(TAG, e.toString()); + } + + args.result = iterator; + break; + case EVENT_ARG_INSERT: args.result = resolver.insert(args.uri, args.values); break; @@ -103,7 +117,6 @@ public abstract class AsyncQueryHandler extends Handler { case EVENT_ARG_DELETE: args.result = resolver.delete(args.uri, args.selection, args.selectionArgs); break; - } // passing the original token value back to the caller @@ -128,7 +141,7 @@ public abstract class AsyncQueryHandler extends Handler { if (sLooper == null) { HandlerThread thread = new HandlerThread("AsyncQueryWorker"); thread.start(); - + sLooper = thread.getLooper(); } } @@ -182,6 +195,45 @@ public abstract class AsyncQueryHandler extends Handler { } /** + * This method begins an asynchronous query for an {@link EntityIterator}. + * When the query is done {@link #onQueryEntitiesComplete} is called. + * + * @param token A token passed into {@link #onQueryComplete} to identify the + * query. + * @param cookie An object that gets passed into {@link #onQueryComplete} + * @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 orderBy 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. + * @hide + */ + public void startQueryEntities(int token, Object cookie, Uri uri, String selection, + String[] selectionArgs, String orderBy) { + // Use the token as what so cancelOperations works properly + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_ARG_QUERY_ENTITIES; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.selection = selection; + args.selectionArgs = selectionArgs; + args.orderBy = orderBy; + args.cookie = cookie; + msg.obj = args; + + mWorkerThreadHandler.sendMessage(msg); + } + + /** * Attempts to cancel operation that has not already started. Note that * there is no guarantee that the operation will be canceled. They still may * result in a call to on[Query/Insert/Update/Delete]Complete after this @@ -279,8 +331,8 @@ public abstract class AsyncQueryHandler extends Handler { * Called when an asynchronous query is completed. * * @param token the token to identify the query, passed in from - * {@link #startQuery}. - * @param cookie the cookie object that's passed in from {@link #startQuery}. + * {@link #startQuery}. + * @param cookie the cookie object passed in from {@link #startQuery}. * @param cursor The cursor holding the results from the query. */ protected void onQueryComplete(int token, Object cookie, Cursor cursor) { @@ -288,6 +340,18 @@ public abstract class AsyncQueryHandler extends Handler { } /** + * Called when an asynchronous query is completed. + * + * @param token The token to identify the query. + * @param cookie The cookie object. + * @param iterator The iterator holding the query results. + * @hide + */ + protected void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) { + // Empty + } + + /** * Called when an asynchronous insert is completed. * * @param token the token to identify the query, passed in from @@ -337,13 +401,17 @@ public abstract class AsyncQueryHandler extends Handler { int token = msg.what; int event = msg.arg1; - + // pass token back to caller on each callback. switch (event) { case EVENT_ARG_QUERY: onQueryComplete(token, args.cookie, (Cursor) args.result); break; + case EVENT_ARG_QUERY_ENTITIES: + onQueryEntitiesComplete(token, args.cookie, (EntityIterator)args.result); + break; + case EVENT_ARG_INSERT: onInsertComplete(token, args.cookie, (Uri) args.result); break; diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java index b391c57..b63d026 100644 --- a/core/java/android/content/BroadcastReceiver.java +++ b/core/java/android/content/BroadcastReceiver.java @@ -384,6 +384,24 @@ public abstract class BroadcastReceiver { } /** + * Returns true if the receiver is currently processing an ordered + * broadcast. + */ + public final boolean isOrderedBroadcast() { + return mOrderedHint; + } + + /** + * Returns true if the receiver is currently processing the initial + * value of a sticky broadcast -- that is, the value that was last + * broadcast and is currently held in the sticky cache, so this is + * not directly the result of a broadcast right now. + */ + public final boolean isInitialStickyBroadcast() { + return mInitialStickyHint; + } + + /** * For internal use, sets the hint about whether this BroadcastReceiver is * running in ordered mode. */ @@ -392,6 +410,14 @@ public abstract class BroadcastReceiver { } /** + * For internal use, sets the hint about whether this BroadcastReceiver is + * receiving the initial sticky broadcast value. @hide + */ + public final void setInitialStickyHint(boolean isInitialSticky) { + mInitialStickyHint = isInitialSticky; + } + + /** * Control inclusion of debugging help for mismatched * calls to {@ Context#registerReceiver(BroadcastReceiver, IntentFilter) * Context.registerReceiver()}. @@ -414,7 +440,10 @@ public abstract class BroadcastReceiver { } void checkSynchronousHint() { - if (mOrderedHint) { + // Note that we don't assert when receiving the initial sticky value, + // since that may have come from an ordered broadcast. We'll catch + // them later when the real broadcast happens again. + if (mOrderedHint || mInitialStickyHint) { return; } RuntimeException e = new RuntimeException( @@ -429,5 +458,6 @@ public abstract class BroadcastReceiver { private boolean mAbortBroadcast; private boolean mDebugUnregister; private boolean mOrderedHint; + private boolean mInitialStickyHint; } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index bffd829..a341c9b 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,15 @@ public abstract class ContentProvider implements ComponentCallbacks { selectionArgs, sortOrder); } + /** + * @hide + */ + 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 +155,20 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.bulkInsert(uri, initialValues); } + 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); @@ -170,12 +194,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) { @@ -427,6 +445,14 @@ public abstract class ContentProvider implements ComponentCallbacks { String selection, String[] selectionArgs, String sortOrder); /** + * @hide + */ + 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, * or <code>vnd.android.cursor.dir/</code> for multiple items. @@ -523,7 +549,7 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Open a file blob associated with a content URI. * This method can be called from multiple - * threads, as described in + * threads, as described inentity * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: * Processes and Threads</a>. * @@ -643,23 +669,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 */ @@ -701,4 +710,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..403c4d8 --- /dev/null +++ b/core/java/android/content/ContentProviderClient.java @@ -0,0 +1,128 @@ +/* + * 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} + * @hide + */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) throws RemoteException { + return mContentProvider.queryEntities(uri, selection, selectionArgs, sortOrder); + } + + /** 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..adc3f60 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,21 @@ abstract public class ContentProviderNative extends Binder implements IContentPr 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 +236,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 +245,32 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return super.onTransact(code, data, reply, flags); } + /** + * @hide + */ + 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 reset() throws RemoteException { + mEntityIterator.reset(); + } + + public void close() throws RemoteException { + mEntityIterator.close(); + } + } + public IBinder asBinder() { return this; @@ -297,7 +344,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 +352,64 @@ final class ContentProviderProxy implements IContentProvider return adaptor; } + /** + * @hide + */ + 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)); + } + + /** + * @hide + */ + 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 reset() throws RemoteException { + mEntityIterator.reset(); + } + + 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 +471,28 @@ 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 int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); @@ -456,23 +583,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..ca36df2 --- /dev/null +++ b/core/java/android/content/ContentProviderOperation.java @@ -0,0 +1,577 @@ +/* + * 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.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public class ContentProviderOperation implements Parcelable { + /** @hide exposed for unit tests */ + public final static int TYPE_INSERT = 1; + /** @hide exposed for unit tests */ + public final static int TYPE_UPDATE = 2; + /** @hide exposed for unit tests */ + public final static int TYPE_DELETE = 3; + /** @hide exposed for unit tests */ + public final static int TYPE_ASSERT = 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 final boolean mYieldAllowed; + + /** + * 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; + mYieldAllowed = builder.mYieldAllowed; + } + + 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()); + } + } + mYieldAllowed = source.readInt() != 0; + } + + 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); + } + dest.writeInt(mYieldAllowed ? 1 : 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 + * {@link ContentProviderOperation} to assert a set of values as provided + * through {@link Builder#withValues(ContentValues)}. + */ + public static Builder newAssertQuery(Uri uri) { + return new Builder(TYPE_ASSERT, uri); + } + + public Uri getUri() { + return mUri; + } + + public boolean isYieldAllowed() { + return mYieldAllowed; + } + + /** @hide exposed for unit tests */ + public int getType() { + return mType; + } + + public boolean isWriteOperation() { + return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; + } + + public boolean isReadOperation() { + return mType == TYPE_ASSERT; + } + + /** + * 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_ASSERT) { + // Assert that all rows match expected values + String[] projection = null; + if (values != null) { + // Build projection map from expected values + final ArrayList<String> projectionList = new ArrayList<String>(); + for (Map.Entry<String, Object> entry : values.valueSet()) { + projectionList.add(entry.getKey()); + } + projection = projectionList.toArray(new String[projectionList.size()]); + } + final Cursor cursor = provider.query(mUri, projection, mSelection, selectionArgs, null); + try { + numRows = cursor.getCount(); + if (projection != null) { + while (cursor.moveToNext()) { + for (int i = 0; i < projection.length; i++) { + final String cursorValue = cursor.getString(i); + final String expectedValue = values.getAsString(projection[i]); + if (!TextUtils.equals(cursorValue, expectedValue)) { + // Throw exception when expected values don't match + throw new OperationApplicationException("Found value " + cursorValue + + " when expected " + expectedValue + " for column " + + projection[i]); + } + } + } + } + } 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] = + String.valueOf(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 long 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]; + long backRefValue; + if (backRef.uri != null) { + backRefValue = ContentUris.parseId(backRef.uri); + } else { + backRefValue = 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#newAssertQuery(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; + private boolean mYieldAllowed; + + /** 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"); + } + } + if (mType == TYPE_ASSERT) { + if ((mValues == null || mValues.size() == 0) + && (mValuesBackReferences == null || mValuesBackReferences.size() == 0) + && (mExpectedCount == null)) { + 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, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReferences(ContentValues backReferences) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts 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, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReference(String key, int previousResult) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts 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 assert. + * @return this builder, to allow for chaining. + */ + public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException("only updates, deletes, and asserts " + + "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, update, or assert. + * @return this builder, to allow for chaining. + */ + public Builder withValues(ContentValues values) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only inserts, updates, and asserts 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, update, or assert. + * @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 && mType != TYPE_ASSERT) { + 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 assert. + * @return this builder, to allow for chaining. + */ + public Builder withSelection(String selection, String[] selectionArgs) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only updates, deletes, and asserts can have selections"); + } + mSelection = selection; + if (selectionArgs == null) { + mSelectionArgs = null; + } else { + mSelectionArgs = new String[selectionArgs.length]; + System.arraycopy(selectionArgs, 0, mSelectionArgs, 0, selectionArgs.length); + } + 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 assert. + * @return this builder, to allow for chaining. + */ + public Builder withExpectedCount(int count) { + if (mType != TYPE_UPDATE && mType != TYPE_DELETE && mType != TYPE_ASSERT) { + throw new IllegalArgumentException( + "only updates, deletes, and asserts can have expected counts"); + } + mExpectedCount = count; + return this; + } + + public Builder withYieldAllowed(boolean yieldAllowed) { + mYieldAllowed = yieldAllowed; + 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..c4b0807 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,19 +41,40 @@ 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)} + */ + @Deprecated + public static final String SYNC_EXTRAS_ACCOUNT = "account"; public static final String SYNC_EXTRAS_EXPEDITED = "expedited"; + /** + * @deprecated instead use + * {@link #SYNC_EXTRAS_MANUAL} + */ + @Deprecated 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"; + /** + * Set by the SyncManager to request that the SyncAdapter initialize itself for + * the given account/authority pair. One required initialization step is to + * ensure that {@link #setIsSyncable(android.accounts.Account, String, int)} has been + * called with a >= 0 value. When this flag is set the SyncAdapter does not need to + * do a full sync, though it is allowed to do so. + */ + public static final String SYNC_EXTRAS_INITIALIZE = "initialize"; + public static final String SCHEME_CONTENT = "content"; public static final String SCHEME_ANDROID_RESOURCE = "android.resource"; public static final String SCHEME_FILE = "file"; @@ -88,7 +110,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 +216,96 @@ public abstract class ContentResolver { } /** + * EntityIterator wrapper that releases the associated ContentProviderClient when the + * iterator is closed. + * @hide + */ + 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 reset() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + mInner.reset(); + } + + 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 + * @hide + */ + 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. * @@ -389,12 +529,22 @@ public abstract class ContentResolver { } } - class OpenResourceIdResult { - Resources r; - int id; + /** + * A resource identified by the {@link Resources} that contains it, and a resource id. + * + * @hide + */ + public class OpenResourceIdResult { + public Resources r; + public int id; } - - OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { + + /** + * Resolves an android.resource URI to a {@link Resources} and a resource id. + * + * @hide + */ + public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { String authority = uri.getAuthority(); Resources r; if (TextUtils.isEmpty(authority)) { @@ -485,6 +635,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 +772,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 +896,43 @@ 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)} */ + @Deprecated 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"); + } + 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 +946,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 +962,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 +973,211 @@ 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)} + */ + @Deprecated 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 { - getContentService().cancelSync(uri); + 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 + } + } + + /** + * Check if this account/provider is syncable. + * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. + */ + public static int getIsSyncable(Account account, String authority) { + try { + return getContentService().getIsSyncable(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Set whether this account/provider is syncable. + * @param syncable >0 denotes syncable, 0 means not syncable, <0 means unknown + */ + public static void setIsSyncable(Account account, String authority, int syncable) { + try { + getContentService().setIsSyncable(account, authority, syncable); + } 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().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. + * @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..974a667 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,17 @@ 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 */, + false /* onlyThoseWithUnkownSyncableState */); + } } finally { restoreCallingIdentity(identityToken); } @@ -201,34 +207,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,51 +258,82 @@ 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 int getIsSyncable(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().getListenForNetworkTickles(); + return syncManager.getSyncStorageEngine().getIsSyncable( + account, providerName); + } + } finally { + restoreCallingIdentity(identityToken); + } + return -1; + } + + public void setIsSyncable(Account account, String providerName, int syncable) { + 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().setIsSyncable( + account, providerName, syncable); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + 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().getMasterSyncAutomatically(); } } finally { restoreCallingIdentity(identityToken); } 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(); @@ -295,7 +348,7 @@ public final class ContentService extends IContentService.Stub { } return false; } - + public ActiveSyncInfo getActiveSync() { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); @@ -310,65 +363,62 @@ 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(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - return syncManager.getSyncStorageEngine().getStatusByAuthority( - authority); + return syncManager.getSyncStorageEngine().getStatusByAccountAndAuthority( + account, authority); } } finally { restoreCallingIdentity(identityToken); } 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); } return false; } - + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().addStatusChangeListener( - mask, callback); + syncManager.getSyncStorageEngine().addStatusChangeListener(mask, callback); } } finally { restoreCallingIdentity(identityToken); } } - + public void removeStatusChangeListener(ISyncStatusObserver callback) { long identityToken = clearCallingIdentity(); try { SyncManager syncManager = getSyncManager(); if (syncManager != null) { - syncManager.getSyncStorageEngine().removeStatusChangeListener( - callback); + syncManager.getSyncStorageEngine().removeStatusChangeListener(callback); } } finally { restoreCallingIdentity(identityToken); } } - + public static IContentService main(Context context, boolean factoryTest) { ContentService service = new ContentService(context, factoryTest); ServiceManager.addService(ContentResolver.CONTENT_SERVICE_NAME, service); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 25b5de3..8f1c671 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -488,90 +488,52 @@ public abstract class Context { public abstract String[] databaseList(); /** - * Like {@link #peekWallpaper}, but always returns a valid Drawable. If - * no wallpaper is set, the system default wallpaper is returned. - * - * @return Returns a Drawable object that will draw the wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#getDrawable + * WallpaperManager.get()} instead. */ + @Deprecated public abstract Drawable getWallpaper(); /** - * Retrieve the current system wallpaper. This is returned as an - * abstract Drawable that you can install in a View to display whatever - * wallpaper the user has currently set. If there is no wallpaper set, - * a null pointer is returned. - * - * @return Returns a Drawable object that will draw the wallpaper or a - * null pointer if these is none. + * @deprecated Use {@link android.app.WallpaperManager#peekDrawable + * WallpaperManager.peek()} instead. */ + @Deprecated public abstract Drawable peekWallpaper(); /** - * Returns the desired minimum width for the wallpaper. Callers of - * {@link #setWallpaper(android.graphics.Bitmap)} or - * {@link #setWallpaper(java.io.InputStream)} should check this value - * beforehand to make sure the supplied wallpaper respects the desired - * minimum width. - * - * If the returned value is <= 0, the caller should use the width of - * the default display instead. - * - * @return The desired minimum width for the wallpaper. This value should - * be honored by applications that set the wallpaper but it is not - * mandatory. + * @deprecated Use {@link android.app.WallpaperManager#getDesiredMinimumWidth() + * WallpaperManager.getDesiredMinimumWidth()} instead. */ + @Deprecated public abstract int getWallpaperDesiredMinimumWidth(); /** - * Returns the desired minimum height for the wallpaper. Callers of - * {@link #setWallpaper(android.graphics.Bitmap)} or - * {@link #setWallpaper(java.io.InputStream)} should check this value - * beforehand to make sure the supplied wallpaper respects the desired - * minimum height. - * - * If the returned value is <= 0, the caller should use the height of - * the default display instead. - * - * @return The desired minimum height for the wallpaper. This value should - * be honored by applications that set the wallpaper but it is not - * mandatory. + * @deprecated Use {@link android.app.WallpaperManager#getDesiredMinimumHeight() + * WallpaperManager.getDesiredMinimumHeight()} instead. */ + @Deprecated public abstract int getWallpaperDesiredMinimumHeight(); /** - * Change the current system wallpaper to a bitmap. The given bitmap is - * converted to a PNG and stored as the wallpaper. On success, the intent - * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast. - * - * @param bitmap The bitmap to save. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#setBitmap(Bitmap) + * WallpaperManager.set()} instead. */ + @Deprecated public abstract void setWallpaper(Bitmap bitmap) throws IOException; /** - * Change the current system wallpaper to a specific byte stream. The - * give InputStream is copied into persistent storage and will now be - * used as the wallpaper. Currently it must be either a JPEG or PNG - * image. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} - * is broadcast. - * - * @param data A stream containing the raw data to install as a wallpaper. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#setStream(InputStream) + * WallpaperManager.set()} instead. */ + @Deprecated public abstract void setWallpaper(InputStream data) throws IOException; /** - * Remove any currently set wallpaper, reverting to the system's default - * wallpaper. On success, the intent {@link Intent#ACTION_WALLPAPER_CHANGED} - * is broadcast. - * - * @throws IOException If an error occurs reverting to the default - * wallpaper. + * @deprecated Use {@link android.app.WallpaperManager#clear + * WallpaperManager.clear()} instead. */ + @Deprecated public abstract void clearWallpaper() throws IOException; /** @@ -597,6 +559,27 @@ public abstract class Context { public abstract void startActivity(Intent intent); /** + * Like {@link #startActivity(Intent)}, but taking a IntentSender + * to start. If the IntentSender is for an activity, that activity will be started + * as if you had called the regular {@link #startActivity(Intent)} + * here; otherwise, its associated action will be executed (such as + * sending a broadcast) as if you had called + * {@link IntentSender#sendIntent IntentSender.sendIntent} on it. + * + * @param intent The IntentSender to launch. + * @param fillInIntent If non-null, this will be provided as the + * intent parameter to {@link IntentSender#sendIntent}. + * @param flagsMask Intent flags in the original IntentSender that you + * would like to change. + * @param flagsValues Desired values for any bits set in + * <var>flagsMask</var> + * @param extraFlags Always set to 0. + */ + public abstract void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException; + + /** * Broadcast the given intent to all interested BroadcastReceivers. This * call is asynchronous; it returns immediately, and you will continue * executing while the receivers are run. No results are propagated from @@ -674,8 +657,7 @@ public abstract class Context { * supplying your own BroadcastReceiver when calling, which will be * treated as a final receiver at the end of the broadcast -- its * {@link BroadcastReceiver#onReceive} method will be called with - * the result values collected from the other receivers. If you use - * an <var>resultReceiver</var> with this method, then the broadcast will + * the result values collected from the other receivers. The broadcast will * be serialized in the same way as calling * {@link #sendOrderedBroadcast(Intent, String)}. * @@ -706,6 +688,7 @@ public abstract class Context { * @see #sendBroadcast(Intent, String) * @see #sendOrderedBroadcast(Intent, String) * @see #sendStickyBroadcast(Intent) + * @see #sendStickyOrderedBroadcast(Intent, BroadcastReceiver, Handler, int, String, Bundle) * @see android.content.BroadcastReceiver * @see #registerReceiver * @see android.app.Activity#RESULT_OK @@ -732,8 +715,55 @@ public abstract class Context { * be re-broadcast to future receivers. * * @see #sendBroadcast(Intent) + * @see #sendStickyOrderedBroadcast(Intent, BroadcastReceiver, Handler, int, String, Bundle) */ public abstract void sendStickyBroadcast(Intent intent); + + /** + * Version of {@link #sendStickyBroadcast} that allows you to + * receive data back from the broadcast. This is accomplished by + * supplying your own BroadcastReceiver when calling, which will be + * treated as a final receiver at the end of the broadcast -- its + * {@link BroadcastReceiver#onReceive} method will be called with + * the result values collected from the other receivers. The broadcast will + * be serialized in the same way as calling + * {@link #sendOrderedBroadcast(Intent, String)}. + * + * <p>Like {@link #sendBroadcast(Intent)}, this method is + * asynchronous; it will return before + * resultReceiver.onReceive() is called. Note that the sticky data + * stored is only the data you initially supply to the broadcast, not + * the result of any changes made by the receivers. + * + * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts. + * + * @param intent The Intent to broadcast; all receivers matching this + * Intent will receive the broadcast. + * @param resultReceiver Your own BroadcastReceiver to treat as the final + * receiver of the broadcast. + * @param scheduler A custom Handler with which to schedule the + * resultReceiver callback; if null it will be + * scheduled in the Context's main thread. + * @param initialCode An initial value for the result code. Often + * Activity.RESULT_OK. + * @param initialData An initial value for the result data. Often + * null. + * @param initialExtras An initial value for the result extras. Often + * null. + * + * @see #sendBroadcast(Intent) + * @see #sendBroadcast(Intent, String) + * @see #sendOrderedBroadcast(Intent, String) + * @see #sendStickyBroadcast(Intent) + * @see android.content.BroadcastReceiver + * @see #registerReceiver + * @see android.app.Activity#RESULT_OK + */ + public abstract void sendStickyOrderedBroadcast(Intent intent, + BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras); + /** * Remove the data previously sent with {@link #sendStickyBroadcast}, @@ -1110,6 +1140,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. * @@ -1178,15 +1218,6 @@ public abstract class Context { */ public static final String SENSOR_SERVICE = "sensor"; /** - * Use with {@link #getSystemService} to retrieve a {@link - * android.bluetooth.BluetoothDevice} for interacting with Bluetooth. - * - * @see #getSystemService - * @see android.bluetooth.BluetoothDevice - * @hide - */ - public static final String BLUETOOTH_SERVICE = "bluetooth"; - /** * Use with {@link #getSystemService} to retrieve a * com.android.server.WallpaperService for accessing wallpapers. * diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 15612ce..1b34320 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -249,6 +249,14 @@ public class ContextWrapper extends Context { } @Override + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + mBase.startIntentSender(intent, fillInIntent, flagsMask, + flagsValues, extraFlags); + } + + @Override public void sendBroadcast(Intent intent) { mBase.sendBroadcast(intent); } @@ -280,6 +288,16 @@ public class ContextWrapper extends Context { } @Override + public void sendStickyOrderedBroadcast( + Intent intent, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + mBase.sendStickyOrderedBroadcast(intent, + resultReceiver, scheduler, initialCode, + initialData, initialExtras); + } + + @Override public void removeStickyBroadcast(Intent intent) { mBase.removeStickyBroadcast(intent); } 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..ee8112e --- /dev/null +++ b/core/java/android/content/Entity.java @@ -0,0 +1,105 @@ +/* + * 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. + * @hide + */ +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..1b73439 --- /dev/null +++ b/core/java/android/content/EntityIterator.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * @hide + */ +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; + + public void reset() 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..0798adf 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,6 +44,12 @@ public interface IContentProvider extends IInterface { CursorWindow window) throws RemoteException; public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException; + /** + * @hide + */ + 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; @@ -55,7 +62,8 @@ public interface IContentProvider extends IInterface { 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 +73,12 @@ 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; + /** + * @hide + */ + 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..b0f14c1 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,50 @@ 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); + /** + * Check if this account/provider is syncable. + * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. + */ + int getIsSyncable(in Account account, String providerName); + + /** + * Set whether this account/provider is syncable. + * @param syncable, >0 denotes syncable, 0 means not syncable, <0 means unknown + */ + void setIsSyncable(in Account account, String providerName, int syncable); - boolean getListenForNetworkTickles(); + void setMasterSyncAutomatically(boolean flag); + + 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..068581e --- /dev/null +++ b/core/java/android/content/IEntityIterator.java @@ -0,0 +1,210 @@ +/* + * 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_reset: + { + data.enforceInterface(DESCRIPTOR); + try { + this.reset(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in next()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + 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 reset() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_reset, _data, _reply, 0); + _reply.readException(); + } 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); + static final int TRANSACTION_reset = (IBinder.FIRST_CALL_TRANSACTION + 3); + } + public boolean hasNext() throws RemoteException; + public Entity next() throws RemoteException; + public void reset() throws RemoteException; + public void close() throws RemoteException; +} diff --git a/core/java/android/content/IIntentReceiver.aidl b/core/java/android/content/IIntentReceiver.aidl index 443db2d..6f2f7c4 100755 --- a/core/java/android/content/IIntentReceiver.aidl +++ b/core/java/android/content/IIntentReceiver.aidl @@ -28,6 +28,6 @@ import android.os.Bundle; */ oneway interface IIntentReceiver { void performReceive(in Intent intent, int resultCode, - String data, in Bundle extras, boolean ordered); + String data, in Bundle extras, boolean ordered, boolean sticky); } 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 dfda09c..b785dbf 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -529,6 +529,8 @@ import java.util.Set; * <li> {@link #CATEGORY_HOME} * <li> {@link #CATEGORY_PREFERENCE} * <li> {@link #CATEGORY_TEST} + * <li> {@link #CATEGORY_CAR_DOCK} + * <li> {@link #CATEGORY_DESK_DOCK} * </ul> * * <h3>Standard Extra Data</h3> @@ -923,6 +925,16 @@ public class Intent implements Parcelable { * get*ArrayListExtra can have either a {@link #EXTRA_TEXT} or {@link * #EXTRA_STREAM} field, containing the data to be sent. * <p> + * Multiple types are supported, and receivers should handle mixed types + * whenever possible. The right way for the receiver to check them is to + * use the content resolver on each URI. The intent sender should try to + * put the most concrete mime type in the intent type, but it can fall + * back to {@literal <type>/*} or {@literal *}/* as needed. + * <p> + * e.g. if you are sending image/jpg and image/jpg, the intent's type can + * be image/jpg, but if you are sending image/jpg and image/png, then the + * intent's type should be image/*. + * <p> * Optional standard extras, which may be interpreted by some recipients as * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC}, * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}. @@ -1267,6 +1279,8 @@ public class Intent implements Parcelable { * enabled or disabled. The data contains the name of the package. * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. + * <li> {@link #EXTRA_CHANGED_COMPONENT_NAME} containing the class name of the changed component. + * <li> {@link #EXTRA_DONT_KILL_APP} containing boolean field to override the default action of restarting the application. * </ul> * * <p class="note">This is a protected intent that can only be sent @@ -1316,7 +1330,7 @@ public class Intent implements Parcelable { public static final String ACTION_UID_REMOVED = "android.intent.action.UID_REMOVED"; /** * Broadcast Action: The current system wallpaper has changed. See - * {@link Context#getWallpaper} for retrieving the new wallpaper. + * {@link android.app.WallpaperManager} for retrieving the new wallpaper. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED"; @@ -1338,14 +1352,20 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED"; /** - * Broadcast Action: The charging state, or charge level of the battery has - * changed. + * Broadcast Action: This is a <em>sticky broadcast</em> containing the + * charging state, level, and other information about the battery. + * See {@link android.os.BatteryManager} for documentation on the + * contents of the Intent. * * <p class="note"> * You can <em>not</em> receive this through components declared * in manifests, only by explicitly registering for it with * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) - * Context.registerReceiver()}. + * Context.registerReceiver()}. See {@link #ACTION_BATTERY_LOW}, + * {@link #ACTION_BATTERY_OKAY}, {@link #ACTION_POWER_CONNECTED}, + * and {@link #ACTION_POWER_DISCONNECTED} for distinct battery-related + * broadcasts that are sent and can be received through manifest + * receivers. * * <p class="note">This is a protected intent that can only be sent * by the system. @@ -1395,13 +1415,13 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_POWER_DISCONNECTED = - "android.intent.action.ACTION_POWER_DISCONNECTED"; + "android.intent.action.ACTION_POWER_DISCONNECTED"; /** * Broadcast Action: Device is shutting down. * This is broadcast when the device is being shut down (completely turned * off, not sleeping). Once the broadcast is complete, the final shutdown * will proceed and all unsaved data lost. Apps will not normally need - * to handle this, since the forground activity will be paused as well. + * to handle this, since the foreground activity will be paused as well. * * <p class="note">This is a protected intent that can only be sent * by the system. @@ -1409,7 +1429,19 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_SHUTDOWN = "android.intent.action.ACTION_SHUTDOWN"; /** - * Broadcast Action: Indicates low memory condition on the device + * Activity Action: Start this activity to request system shutdown. + * The optional boolean extra field {@link #EXTRA_KEY_CONFIRM} can be set to true + * to request confirmation from the user before shutting down. + * + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * {@hide} + */ + public static final String ACTION_REQUEST_SHUTDOWN = "android.intent.action.ACTION_REQUEST_SHUTDOWN"; + /** + * Broadcast Action: A sticky broadcast that indicates low memory + * condition on the device * * <p class="note">This is a protected intent that can only be sent * by the system. @@ -1685,6 +1717,44 @@ public class Intent implements Parcelable { public static final String ACTION_REBOOT = "android.intent.action.REBOOT"; + /** + * Broadcast Action: A sticky broadcast indicating the phone was docked + * or undocked. Includes the extra + * field {@link #EXTRA_DOCK_STATE}, containing the current dock state. + * This is intended for monitoring the current dock state. + * To launch an activity from a dock state change, use {@link #CATEGORY_CAR_DOCK} + * or {@link #CATEGORY_DESK_DOCK} instead. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DOCK_EVENT = + "android.intent.action.DOCK_EVENT"; + + /** + * 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. + * @hide + */ + public static final String ACTION_REMOTE_INTENT = + "android.intent.action.REMOTE_INTENT"; + + /** + * Broadcast Action: hook for permforming cleanup after a system update. + * + * The broadcast is sent when the system is booting, before the + * BOOT_COMPLETED broadcast. It is only sent to receivers in the system + * image. A receiver for this should do its work and then disable itself + * so that it does not get run again at the next boot. + * @hide + */ + public static final String ACTION_PRE_BOOT_COMPLETED = + "android.intent.action.PRE_BOOT_COMPLETED"; // --------------------------------------------------------------------- // --------------------------------------------------------------------- @@ -1813,6 +1883,21 @@ public class Intent implements Parcelable { */ public static final String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST = "android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"; + /** + * An activity to run when device is inserted into a car dock. + * Used with {@link #ACTION_MAIN} to launch an activity. + * To monitor dock state, use {@link #ACTION_DOCK_EVENT} instead. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_CAR_DOCK = "android.intent.category.CAR_DOCK"; + /** + * An activity to run when device is inserted into a car dock. + * Used with {@link #ACTION_MAIN} to launch an activity. + * To monitor dock state, use {@link #ACTION_DOCK_EVENT} instead. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_DESK_DOCK = "android.intent.category.DESK_DOCK"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard extra data keys. @@ -1873,12 +1958,29 @@ public class Intent implements Parcelable { public static final String EXTRA_TITLE = "android.intent.extra.TITLE"; /** + * A Parcelable[] of {@link Intent} or + * {@link android.content.pm.LabeledIntent} objects as set with + * {@link #putExtra(String, Parcelable[])} of additional activities to place + * a the front of the list of choices, when shown to the user with a + * {@link #ACTION_CHOOSER}. + */ + public static final String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS"; + + /** * A {@link android.view.KeyEvent} object containing the event that * triggered the creation of the Intent it is in. */ public static final String EXTRA_KEY_EVENT = "android.intent.extra.KEY_EVENT"; /** + * Set to true in {@link #ACTION_REQUEST_SHUTDOWN} to request confirmation from the user + * before shutting down. + * + * {@hide} + */ + public static final String EXTRA_KEY_CONFIRM = "android.intent.extra.KEY_CONFIRM"; + + /** * Used as an boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} or * {@link android.content.Intent#ACTION_PACKAGE_CHANGED} intents to override the default action * of restarting the application. @@ -1927,6 +2029,39 @@ public class Intent implements Parcelable { public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT"; /** + * Used as an int extra field in {@link android.content.Intent#ACTION_DOCK_EVENT} + * intents to request the dock state. Possible values are + * {@link android.content.Intent#EXTRA_DOCK_STATE_UNDOCKED}, + * {@link android.content.Intent#EXTRA_DOCK_STATE_DESK}, or + * {@link android.content.Intent#EXTRA_DOCK_STATE_CAR}. + */ + public static final String EXTRA_DOCK_STATE = "android.intent.extra.DOCK_STATE"; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is not in any dock. + */ + public static final int EXTRA_DOCK_STATE_UNDOCKED = 0; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is in a desk dock. + */ + public static final int EXTRA_DOCK_STATE_DESK = 1; + + /** + * Used as an int value for {@link android.content.Intent#EXTRA_DOCK_STATE} + * to represent that the phone is in a car dock. + */ + public static final int EXTRA_DOCK_STATE_CAR = 2; + + /** + * Boolean that can be supplied as meta-data with a dock activity, to + * indicate that the dock should take over the home key when it is active. + */ + public static final String METADATA_DOCK_HOME = "android.dock_home"; + + /** * Used as a parcelable extra field in {@link #ACTION_APP_ERROR}, containing * the bug report. * @@ -1944,6 +2079,39 @@ 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"; + + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_PACKAGE_CHANGED} + * intent to supply the name of the component that changed. + * + */ + public static final String EXTRA_CHANGED_COMPONENT_NAME = + "android.intent.extra.changed_component_name"; + + /** + * @hide + * Magic extra system code can use when binding, to give a label for + * who it is that has bound to a service. This is an integer giving + * a framework string resource that can be displayed to the user. + */ + public static final String EXTRA_CLIENT_LABEL = + "android.intent.extra.client_label"; + + /** + * @hide + * Magic extra system code can use when binding, to give a PendingIntent object + * that can be launched for the user to disable the system's use of this + * service. + */ + public static final String EXTRA_CLIENT_INTENT = + "android.intent.extra.client_intent"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). @@ -2038,12 +2206,14 @@ public class Intent implements Parcelable { * of activity B, then C and D will be finished and B receive the given * Intent, resulting in the stack now being: A, B. * - * <p>The currently running instance of task B in the above example will + * <p>The currently running instance of activity B in the above example will * either receive the new intent you are starting here in its * onNewIntent() method, or be itself finished and restarted with the * new intent. If it has declared its launch mode to be "multiple" (the - * default) it will be finished and re-created; for all other launch modes - * it will receive the Intent in the current instance. + * default) and you have not set {@link #FLAG_ACTIVITY_SINGLE_TOP} in + * the same intent, then it will be finished and re-created; for all other + * launch modes or if {@link #FLAG_ACTIVITY_SINGLE_TOP} is set then this + * Intent will be delivered to the current instance's onNewIntent(). * * <p>This launch mode can also be used to good effect in conjunction with * {@link #FLAG_ACTIVITY_NEW_TASK}: if used to start the root activity @@ -2154,6 +2324,18 @@ public class Intent implements Parcelable { */ public static final int FLAG_ACTIVITY_REORDER_TO_FRONT = 0X00020000; /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will prevent the system from applying an activity transition + * animation to go to the next activity state. This doesn't mean an + * animation will never run -- if another activity change happens that doesn't + * specify this flag before the activity started here is displayed, then + * that transition will be used. This this flag can be put to good use + * when you are going to do a series of activity operations but the + * animation seen by the user shouldn't be driven by the first activity + * change but rather a later one. + */ + public static final int FLAG_ACTIVITY_NO_ANIMATION = 0X00010000; + /** * If set, when sending a broadcast only registered receivers will be * called -- no BroadcastReceiver components will be launched. */ @@ -2172,7 +2354,21 @@ public class Intent implements Parcelable { * @hide */ public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x20000000; + /** + * Set when this broadcast is for a boot upgrade, a special mode that + * allows the broadcast to be sent before the system is ready and launches + * the app process with no providers running in it. + * @hide + */ + public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x10000000; + /** + * @hide Flags that can't be changed with PendingIntent. + */ + public static final int IMMUTABLE_FLAGS = + FLAG_GRANT_READ_URI_PERMISSION + | FLAG_GRANT_WRITE_URI_PERMISSION; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // toUri() and parseUri() options. @@ -2353,6 +2549,7 @@ public class Intent implements Parcelable { * * @param uri The URI to turn into an Intent. * @param flags Additional processing flags. Either 0 or + * {@link #URI_INTENT_SCHEME}. * * @return Intent The newly created Intent object. * @@ -2486,24 +2683,24 @@ public class Intent implements Parcelable { int i = uri.lastIndexOf('#'); if (i >= 0) { - Uri data = null; String action = null; - if (i > 0) { - data = Uri.parse(uri.substring(0, i)); - } + final int intentFragmentStart = i; + boolean isIntentFragment = false; i++; if (uri.regionMatches(i, "action(", 0, 7)) { + isIntentFragment = true; i += 7; int j = uri.indexOf(')', i); action = uri.substring(i, j); i = j + 1; } - intent = new Intent(action, data); + intent = new Intent(action); if (uri.regionMatches(i, "categories(", 0, 11)) { + isIntentFragment = true; i += 11; int j = uri.indexOf(')', i); while (i < j) { @@ -2518,6 +2715,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "type(", 0, 5)) { + isIntentFragment = true; i += 5; int j = uri.indexOf(')', i); intent.mType = uri.substring(i, j); @@ -2525,6 +2723,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "launchFlags(", 0, 12)) { + isIntentFragment = true; i += 12; int j = uri.indexOf(')', i); intent.mFlags = Integer.decode(uri.substring(i, j)).intValue(); @@ -2532,6 +2731,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "component(", 0, 10)) { + isIntentFragment = true; i += 10; int j = uri.indexOf(')', i); int sep = uri.indexOf('!', i); @@ -2544,6 +2744,7 @@ public class Intent implements Parcelable { } if (uri.regionMatches(i, "extras(", 0, 7)) { + isIntentFragment = true; i += 7; final int closeParen = uri.indexOf(')', i); @@ -2615,6 +2816,12 @@ public class Intent implements Parcelable { } } + if (isIntentFragment) { + intent.mData = Uri.parse(uri.substring(0, intentFragmentStart)); + } else { + intent.mData = Uri.parse(uri); + } + if (intent.mAction == null) { // By default, if no action is specified, then use VIEW. intent.mAction = ACTION_VIEW; @@ -3404,7 +3611,7 @@ public class Intent implements Parcelable { } } else { ResolveInfo info = pm.resolveActivity( - this, PackageManager.MATCH_DEFAULT_ONLY); + this, PackageManager.MATCH_DEFAULT_ONLY | flags); if (info != null) { ai = info.activityInfo; } @@ -4930,7 +5137,8 @@ public class Intent implements Parcelable { } }; - private Intent(Parcel in) { + /** @hide */ + protected Intent(Parcel in) { readFromParcel(in); } diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java index 0e4d984..e182021 100644 --- a/core/java/android/content/IntentSender.java +++ b/core/java/android/content/IntentSender.java @@ -113,7 +113,7 @@ public class IntentSender implements Parcelable { mHandler = handler; } public void performReceive(Intent intent, int resultCode, - String data, Bundle extras, boolean serialized) { + String data, Bundle extras, boolean serialized, boolean sticky) { mIntent = intent; mResultCode = resultCode; mResultData = data; @@ -249,6 +249,11 @@ public class IntentSender implements Parcelable { } /** @hide */ + public IIntentSender getTarget() { + return mTarget; + } + + /** @hide */ public IntentSender(IIntentSender target) { mTarget = target; } diff --git a/core/java/android/content/OperationApplicationException.java b/core/java/android/content/OperationApplicationException.java new file mode 100644 index 0000000..2fc19bb --- /dev/null +++ b/core/java/android/content/OperationApplicationException.java @@ -0,0 +1,54 @@ +/* + * 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 { + private final int mNumSuccessfulYieldPoints; + + public OperationApplicationException() { + super(); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(String message) { + super(message); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(String message, Throwable cause) { + super(message, cause); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(Throwable cause) { + super(cause); + mNumSuccessfulYieldPoints = 0; + } + public OperationApplicationException(int numSuccessfulYieldPoints) { + super(); + mNumSuccessfulYieldPoints = numSuccessfulYieldPoints; + } + public OperationApplicationException(String message, int numSuccessfulYieldPoints) { + super(message); + mNumSuccessfulYieldPoints = numSuccessfulYieldPoints; + } + + public int getNumSuccessfulYieldPoints() { + return mNumSuccessfulYieldPoints; + } +} diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 7826e50..88dc332 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); + SyncAdapter.this.startSync(new SyncContext(syncContext), account, authority, 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; } @@ -57,9 +58,11 @@ public abstract class SyncAdapter { * @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 account the account that should be synced + * @param authority the authority if the sync request * @param extras SyncAdapter-specific parameters */ - public abstract void startSync(SyncContext syncContext, String account, Bundle extras); + public abstract void startSync(SyncContext syncContext, Account account, String authority, + 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..25cbdb1 --- /dev/null +++ b/core/java/android/content/SyncAdapterType.java @@ -0,0 +1,145 @@ +/* + * 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 final boolean isKey; + private final boolean userVisible; + private final boolean supportsUploading; + + public SyncAdapterType(String authority, String accountType, boolean userVisible, + boolean supportsUploading) { + 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; + this.userVisible = userVisible; + this.supportsUploading = supportsUploading; + this.isKey = false; + } + + private 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; + this.userVisible = true; + this.supportsUploading = true; + this.isKey = true; + } + + public boolean supportsUploading() { + if (isKey) { + throw new IllegalStateException( + "this method is not allowed to be called when this is a key"); + } + return supportsUploading; + } + + public boolean isUserVisible() { + if (isKey) { + throw new IllegalStateException( + "this method is not allowed to be called when this is a key"); + } + return userVisible; + } + + public static SyncAdapterType newKey(String authority, String accountType) { + return new SyncAdapterType(authority, accountType); + } + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof SyncAdapterType)) return false; + final SyncAdapterType other = (SyncAdapterType)o; + // don't include userVisible or supportsUploading in the equality check + 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(); + // don't include userVisible or supportsUploading the hash + return result; + } + + public String toString() { + if (isKey) { + return "SyncAdapterType Key {name=" + authority + + ", type=" + accountType + + "}"; + } else { + return "SyncAdapterType {name=" + authority + + ", type=" + accountType + + ", userVisible=" + userVisible + + ", supportsUploading=" + supportsUploading + + "}"; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + if (isKey) { + throw new IllegalStateException("keys aren't parcelable"); + } + + dest.writeString(authority); + dest.writeString(accountType); + dest.writeInt(userVisible ? 1 : 0); + dest.writeInt(supportsUploading ? 1 : 0); + } + + public SyncAdapterType(Parcel source) { + this( + source.readString(), + source.readString(), + source.readInt() != 0, + source.readInt() != 0); + } + + 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..6ade837 --- /dev/null +++ b/core/java/android/content/SyncAdaptersCache.java @@ -0,0 +1,80 @@ +/* + * 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.pm.XmlSerializerAndParser; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * 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"; + private static final MySerializer sSerializer = new MySerializer(); + + SyncAdaptersCache(Context context) { + super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, sSerializer); + } + + 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; + } + final boolean userVisible = + sa.getBoolean(com.android.internal.R.styleable.SyncAdapter_userVisible, true); + final boolean supportsUploading = + sa.getBoolean(com.android.internal.R.styleable.SyncAdapter_supportsUploading, + true); + return new SyncAdapterType(authority, accountType, userVisible, supportsUploading); + } finally { + sa.recycle(); + } + } + + static class MySerializer implements XmlSerializerAndParser<SyncAdapterType> { + public void writeAsXml(SyncAdapterType item, XmlSerializer out) throws IOException { + out.attribute(null, "authority", item.authority); + out.attribute(null, "accountType", item.accountType); + } + + public SyncAdapterType createFromXml(XmlPullParser parser) + throws IOException, XmlPullParserException { + final String authority = parser.getAttributeValue(null, "authority"); + final String accountType = parser.getAttributeValue(null, "accountType"); + return SyncAdapterType.newKey(authority, accountType); + } + } +}
\ No newline at end of file diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java index f4faa04..587586d 100644 --- a/core/java/android/content/SyncContext.java +++ b/core/java/android/content/SyncContext.java @@ -18,16 +18,17 @@ package android.content; import android.os.RemoteException; import android.os.SystemClock; +import android.os.IBinder; -/** - * @hide - */ public class SyncContext { private ISyncContext mSyncContext; private long mLastHeartbeatSendTime; private static final long HEARTBEAT_SEND_INTERVAL_IN_MS = 1000; + /** + * @hide + */ public SyncContext(ISyncContext syncContextInterface) { mSyncContext = syncContextInterface; mLastHeartbeatSendTime = 0; @@ -38,6 +39,8 @@ public class SyncContext { * {@link #updateHeartbeat}, so it also takes the place of a call to that. * * @param message the current status message for this sync + * + * @hide */ public void setStatusText(String message) { updateHeartbeat(); @@ -48,7 +51,7 @@ public class SyncContext { * downloads or sends records to/from the server, this may be called after each record * is downloaded or uploaded. */ - public void updateHeartbeat() { + private void updateHeartbeat() { final long now = SystemClock.elapsedRealtime(); if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return; try { @@ -67,7 +70,7 @@ public class SyncContext { } } - public ISyncContext getISyncContext() { - return mSyncContext; + public IBinder getSyncContextBinder() { + return mSyncContext.asBinder(); } } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 4d2cce8..ba18615 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.OnAccountsUpdateListener; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,11 +31,11 @@ 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.content.pm.ProviderInfo; 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 +49,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 +72,13 @@ import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; /** * @hide */ -class SyncManager { +class SyncManager implements OnAccountsUpdateListener { private static final String TAG = "SyncManager"; // used during dumping of the Sync history @@ -86,13 +88,37 @@ class SyncManager { private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4; /** Delay a sync due to local changes this long. In milliseconds */ - private static final long LOCAL_SYNC_DELAY = 30 * 1000; // 30 seconds + private static final long LOCAL_SYNC_DELAY; /** * If a sync takes longer than this and the sync queue is not empty then we will * cancel it and add it back to the end of the sync queue. In milliseconds. */ - private static final long MAX_TIME_PER_SYNC = 5 * 60 * 1000; // 5 minutes + private static final long MAX_TIME_PER_SYNC; + + static { + String localSyncDelayString = SystemProperties.get("sync.local_sync_delay"); + long localSyncDelay = 30 * 1000; // 30 seconds + if (localSyncDelayString != null) { + try { + localSyncDelay = Long.parseLong(localSyncDelayString); + } catch (NumberFormatException nfe) { + // ignore, use default + } + } + LOCAL_SYNC_DELAY = localSyncDelay; + + String maxTimePerSyncString = SystemProperties.get("sync.max_time_per_sync"); + long maxTimePerSync = 5 * 60 * 1000; // 5 minutes + if (maxTimePerSyncString != null) { + try { + maxTimePerSync = Long.parseLong(maxTimePerSyncString); + } catch (NumberFormatException nfe) { + // ignore, use default + } + } + MAX_TIME_PER_SYNC = maxTimePerSync; + } private static final long SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds @@ -117,14 +143,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; @@ -150,18 +173,22 @@ class SyncManager { private volatile boolean mSyncPollInitialized; private final PendingIntent mSyncAlarmIntent; private final PendingIntent mSyncPollAlarmIntent; + // Synchronized on "this". Instead of using this directly one should instead call + // its accessor, getConnManager(). + private ConnectivityManager mConnManagerDoNotUseDirectly; + + 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 +199,62 @@ class SyncManager { } }; + private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + mSyncHandler.onBootCompleted(); + } + }; + + private BroadcastReceiver mBackgroundDataSettingChanged = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (getConnectivityManager().getBackgroundDataSetting()) { + scheduleSync(null /* account */, null /* authority */, new Bundle(), 0 /* delay */, + false /* onlyThoseWithUnknownSyncableState */); + } + } + }; + + public void onAccountsUpdated(Account[] accounts) { + // remember if this was the first time this was called after an update + final boolean justBootedUp = 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 (accounts.length > 0) { + // If this is the first time this was called after a bootup then + // the accounts haven't really changed, instead they were just loaded + // from the AccountManager. Otherwise at least one of the accounts + // has a change. + // + // If there was a real account change then force a sync of all accounts. + // This is a bit of overkill, but at least it will end up retrying syncs + // that failed due to an authentication failure and thus will recover if the + // account change was a password update. + // + // If this was the bootup case then don't sync everything, instead only + // sync those that have an unknown syncable state, which will give them + // a chance to set their syncable state. + boolean onlyThoseWithUnkownSyncableState = justBootedUp; + scheduleSync(null, null, null, 0 /* no delay */, onlyThoseWithUnkownSyncableState); + } + } + private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { @@ -229,7 +312,23 @@ class SyncManager { private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; + private final boolean mFactoryTest; + + private volatile boolean mBootCompleted = false; + + private ConnectivityManager getConnectivityManager() { + synchronized (this) { + if (mConnManagerDoNotUseDirectly == null) { + mConnManagerDoNotUseDirectly = (ConnectivityManager)mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + } + return mConnManagerDoNotUseDirectly; + } + } + 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 +343,8 @@ class SyncManager { mPackageManager = null; + mSyncAdapters = new SyncAdaptersCache(mContext); + mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); @@ -253,6 +354,14 @@ class SyncManager { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); + if (!factoryTest) { + intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mBootCompletedReceiver, intentFilter); + } + + intentFilter = new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED); + context.registerReceiver(mBackgroundDataSettingChanged, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); @@ -282,47 +391,18 @@ 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); + if (!factoryTest) { + AccountManager.get(mContext).addOnAccountsUpdatedListener(SyncManager.this, + mSyncHandler, false /* updateImmediately */); + // do this synchronously to ensure we have the accounts before this call returns + onAccountsUpdated(AccountManager.get(mContext).getAccounts()); } } @@ -397,7 +477,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 */, false /* onlyThoseWithUnkownSyncableState */); } private void writeSyncPollTime(long when) { @@ -451,12 +532,6 @@ class SyncManager { public SyncStorageEngine getSyncStorageEngine() { return mSyncStorageEngine; } - - private void ensureContentResolver() { - if (mContentResolver == null) { - mContentResolver = mContext.getContentResolver(); - } - } private void ensureAlarmService() { if (mAlarmService == null) { @@ -464,7 +539,7 @@ class SyncManager { } } - public String getSyncingAccount() { + public Account getSyncingAccount() { ActiveSyncContext activeSyncContext = mActiveSyncContext; return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } @@ -499,21 +574,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. + * @param onlyThoseWithUnkownSyncableState */ - public void scheduleSync(Uri url, Bundle extras, long delay) { + public void scheduleSync(Account requestedAccount, String requestedAuthority, + Bundle extras, long delay, boolean onlyThoseWithUnkownSyncableState) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - if (isLoggable) { - Log.v(TAG, "scheduleSync:" - + " delay " + delay - + ", url " + ((url == null) ? "(null)" : url) - + ", extras " + ((extras == null) ? "(null)" : extras)); + + if (mAccounts == null) { + Log.e(TAG, "scheduleSync: the accounts aren't known yet, this should never happen"); + return; } if (!isSyncEnabled()) { @@ -524,7 +599,9 @@ class SyncManager { return; } - if (mAccounts == null) setStatusText("The accounts aren't known yet."); + final boolean backgroundDataUsageAllowed = !mBootCompleted || + getConnectivityManager().getBackgroundDataSetting(); + if (!mDataConnectionIsConnected) setStatusText("No data connection"); if (mStorageIsLow) setStatusText("Memory low"); @@ -535,10 +612,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 +636,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,59 +651,85 @@ 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 hasSyncAdapter = syncableAuthorities.contains(requestedAuthority); + syncableAuthorities.clear(); + if (hasSyncAdapter) syncableAuthorities.add(requestedAuthority); } - } - private void setStatusText(String message) { - mStatusText = message; - } + final boolean masterSyncAutomatically = mSyncStorageEngine.getMasterSyncAutomatically(); - 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); + for (String authority : syncableAuthorities) { + for (Account account : accounts) { + int isSyncable = mSyncStorageEngine.getIsSyncable(account, authority); + if (isSyncable == 0) { + continue; + } + if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) { + continue; + } + final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo = + mSyncAdapters.getServiceInfo( + SyncAdapterType.newKey(authority, account.type)); + if (syncAdapterInfo != null) { + if (!syncAdapterInfo.type.supportsUploading() && uploadOnly) { + continue; + } + + // make this an initialization sync if the isSyncable state is unknown + Bundle extrasCopy = extras; + long delayCopy = delay; + if (isSyncable < 0) { + extrasCopy = new Bundle(extras); + extrasCopy.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + delayCopy = -1; // expedite this + } else { + final boolean syncAutomatically = masterSyncAutomatically + && mSyncStorageEngine.getSyncAutomatically(account, authority); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + if (!syncAllowed) { + if (isLoggable) { + Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority + + " is not allowed, dropping request"); + } + continue; + } + } + if (isLoggable) { + Log.v(TAG, "scheduleSync:" + + " delay " + delayCopy + + ", source " + source + + ", account " + account + + ", authority " + authority + + ", extras " + extrasCopy); } + scheduleSyncOperation( + new SyncOperation(account, source, authority, extrasCopy, delayCopy)); } } - } 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) { + private void setStatusText(String message) { + mStatusText = message; + } + + 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, + false /* onlyThoseWithUnkownSyncableState */); } private IPackageManager getPackageManager() { @@ -641,18 +743,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() { @@ -711,7 +811,7 @@ class SyncManager { private long rescheduleWithDelay(SyncOperation syncOperation) { long newDelayInMs; - if (syncOperation.delay == 0) { + if (syncOperation.delay <= 0) { // The initial delay is the jitterized INITIAL_SYNC_RETRY_TIME_IN_MS newDelayInMs = jitterize(INITIAL_SYNC_RETRY_TIME_IN_MS, (long)(INITIAL_SYNC_RETRY_TIME_IN_MS * 1.1)); @@ -721,8 +821,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 +835,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 +902,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 +960,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 +969,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 +1040,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 +1078,41 @@ 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); + intent.putExtra(Intent.EXTRA_CLIENT_LABEL, + com.android.internal.R.string.sync_binding_label); + intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( + mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0)); + 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 +1127,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) { @@ -998,13 +1140,13 @@ class SyncManager { tobj.set(time); return tobj.format("%Y-%m-%d %H:%M:%S"); } - + protected void dumpSyncState(PrintWriter pw, StringBuilder sb) { pw.print("sync enabled: "); pw.println(isSyncEnabled()); 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 +1210,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.name); pw.print(":"); + pw.print(op.account.type); pw.print(" authority="); pw.println(op.authority); if (op.extras != null && op.extras.size() > 0) { sb.setLength(0); @@ -1078,7 +1221,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,16 +1233,17 @@ 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; } - + processedAccounts.add(curAccount); - - pw.print(" Account "); pw.print(authority.account); - pw.println(":"); + + pw.print(" Account "); pw.print(authority.account.name); + pw.print(" "); pw.print(authority.account.type); + pw.println(":"); for (int j=i; j<N; j++) { status = statuses.get(j); authority = mSyncStorageEngine.getAuthority(status.authorityId); @@ -1142,7 +1286,7 @@ class SyncManager { pw.print(time/1000); pw.print('.'); pw.print((time/100)%10); pw.print('s'); } - + private void dumpDayStatistic(PrintWriter pw, SyncStorageEngine.DayStats ds) { pw.print("Success ("); pw.print(ds.successCount); if (ds.successCount > 0) { @@ -1156,7 +1300,7 @@ class SyncManager { } pw.println(")"); } - + protected void dumpSyncHistory(PrintWriter pw, StringBuilder sb) { SyncStorageEngine.DayStats dses[] = mSyncStorageEngine.getDayStatistics(); if (dses != null && dses[0] != null) { @@ -1166,18 +1310,18 @@ class SyncManager { int today = dses[0].day; int i; SyncStorageEngine.DayStats ds; - + // Print each day in the current week. for (i=1; i<=6 && i < dses.length; i++) { ds = dses[i]; if (ds == null) break; int delta = today-ds.day; if (delta > 6) break; - + pw.print(" Day-"); pw.print(delta); pw.print(": "); dumpDayStatistic(pw, ds); } - + // Aggregate all following days into weeks and print totals. int weekDay = today; while (i < dses.length) { @@ -1192,7 +1336,7 @@ class SyncManager { int delta = weekDay-ds.day; if (delta > 6) break; i++; - + if (aggr == null) { aggr = new SyncStorageEngine.DayStats(weekDay); } @@ -1207,7 +1351,7 @@ class SyncManager { } } } - + ArrayList<SyncStorageEngine.SyncHistoryItem> items = mSyncStorageEngine.getSyncHistory(); if (items != null && items.size() > 0) { @@ -1219,9 +1363,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.name); + pw.print(":"); + pw.print(authority.account.type); + 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 +1428,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 +1446,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; @@ -1295,13 +1456,35 @@ class SyncManager { // used to track if we have installed the error notification so that we don't reinstall // it if sync is still failing private boolean mErrorNotificationInstalled = false; + private volatile CountDownLatch mReadyToRunLatch = new CountDownLatch(1); + + public void onBootCompleted() { + mBootCompleted = true; + if (mReadyToRunLatch != null) { + mReadyToRunLatch.countDown(); + } + } + private void waitUntilReadyToRun() { + CountDownLatch latch = mReadyToRunLatch; + if (latch != null) { + while (true) { + try { + latch.await(); + mReadyToRunLatch = null; + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } /** * Used to keep track of whether a sync notification is active and who it is for. */ class SyncNotificationInfo { // only valid if isActive is true - public String account; + public Account account; // only valid if isActive is true public String authority; @@ -1333,11 +1516,8 @@ class SyncManager { } public void handleMessage(Message msg) { - handleSyncHandlerMessage(msg); - } - - private void handleSyncHandlerMessage(Message msg) { try { + waitUntilReadyToRun(); switch (msg.what) { case SyncHandler.MESSAGE_SYNC_FINISHED: if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -1358,6 +1538,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 +1683,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 +1695,13 @@ 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; - final ConnectivityManager connManager = (ConnectivityManager) - mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - final boolean backgroundDataSetting = connManager.getBackgroundDataSetting(); + SyncOperation op; + final boolean backgroundDataUsageAllowed = + getConnectivityManager().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 +1711,49 @@ 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); + int isSyncable = mSyncStorageEngine.getIsSyncable(op.account, op.authority); + if (isSyncable == 0) { + // if not syncable, don't allow + syncAllowed = false; + } else if (isSyncable < 0) { + // if the syncable state is unknown, only allow initialization syncs + syncAllowed = + op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false); + } + 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 +1761,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 +1772,72 @@ 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 = SyncAdapterType.newKey(op.authority, op.account.type); + 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 +1877,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 +1892,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 +1920,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 +1975,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; } } @@ -1855,19 +2093,29 @@ class SyncManager { private void sendSyncStateIntent() { Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED); + syncStateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); syncStateIntent.putExtra("active", mNeedSyncActiveNotification); syncStateIntent.putExtra("failing", mNeedSyncErrorNotification); mContext.sendBroadcast(syncStateIntent); } - private void installHandleTooManyDeletesNotification(String account, String authority, + private void installHandleTooManyDeletesNotification(Account account, String authority, long numDeletes) { if (mNotificationMgr == null) return; + + final ProviderInfo providerInfo = mContext.getPackageManager().resolveContentProvider( + authority, 0 /* flags */); + if (providerInfo == null) { + return; + } + CharSequence authorityName = providerInfo.loadLabel(mContext.getPackageManager()); + Intent clickIntent = new Intent(); clickIntent.setClassName("com.android.providers.subscribedfeeds", "com.android.settings.SyncActivityTooManyDeletes"); clickIntent.putExtra("account", account); - clickIntent.putExtra("provider", authority); + clickIntent.putExtra("authority", authority); + clickIntent.putExtra("provider", authorityName.toString()); clickIntent.putExtra("numDeletes", numDeletes); if (!isActivityAvailable(clickIntent)) { @@ -1881,14 +2129,13 @@ class SyncManager { CharSequence tooManyDeletesDescFormat = mContext.getResources().getText( R.string.contentServiceTooManyDeletesNotificationDesc); - String[] authorities = authority.split(";"); Notification notification = new Notification(R.drawable.stat_notify_sync_error, mContext.getString(R.string.contentServiceSync), System.currentTimeMillis()); notification.setLatestEventInfo(mContext, mContext.getString(R.string.contentServiceSyncNotificationTitle), - String.format(tooManyDeletesDescFormat.toString(), authorities[0]), + String.format(tooManyDeletesDescFormat.toString(), authorityName), pendingIntent); notification.flags |= Notification.FLAG_ONGOING_EVENT; mNotificationMgr.notify(account.hashCode() ^ authority.hashCode(), notification); @@ -1920,7 +2167,8 @@ class SyncManager { final long now = System.currentTimeMillis(); EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_START, source); + SyncStorageEngine.EVENT_START, source, + syncOperation.account.name.hashCode()); return mSyncStorageEngine.insertStartSyncEvent( syncOperation.account, syncOperation.authority, now, source); @@ -1929,7 +2177,8 @@ class SyncManager { public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage, int upstreamActivity, int downstreamActivity, long elapsedTime) { EventLog.writeEvent(2720, syncOperation.authority, - SyncStorageEngine.EVENT_STOP, syncOperation.syncSource); + SyncStorageEngine.EVENT_STOP, syncOperation.syncSource, + syncOperation.account.name.hashCode()); mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage, downstreamActivity, upstreamActivity); @@ -1961,7 +2210,7 @@ class SyncManager { syncOperation.pendingOperation = op; add(syncOperation, op); } - + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } @@ -1995,9 +2244,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; } } @@ -2045,8 +2294,8 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " - + operationToRemove); + final String errorMessage = "unable to find pending row for " + operationToRemove; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); @@ -2065,13 +2314,14 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(operation.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " + operation); + final String errorMessage = "unable to find pending row for " + operation; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } 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(); @@ -2087,8 +2337,8 @@ class SyncManager { } if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) { - throw new IllegalStateException("unable to find pending row for " - + syncOperation); + final String errorMessage = "unable to find pending row for " + syncOperation; + Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); } if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java index f3260f3..57161b6 100644 --- a/core/java/android/content/SyncResult.java +++ b/core/java/android/content/SyncResult.java @@ -5,8 +5,6 @@ import android.os.Parcelable; /** * This class is used to store information about the result of a sync - * - * @hide */ public final class SyncResult implements Parcelable { public final boolean syncAlreadyInProgress; @@ -113,14 +111,19 @@ public final class SyncResult implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(" syncAlreadyInProgress: ").append(syncAlreadyInProgress); - sb.append(" tooManyDeletions: ").append(tooManyDeletions); - sb.append(" tooManyRetries: ").append(tooManyRetries); - sb.append(" databaseError: ").append(databaseError); - sb.append(" fullSyncRequested: ").append(fullSyncRequested); - sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable); - sb.append(" moreRecordsToGet: ").append(moreRecordsToGet); - sb.append(" stats: ").append(stats); + sb.append("SyncResult:"); + if (syncAlreadyInProgress) { + sb.append(" syncAlreadyInProgress: ").append(syncAlreadyInProgress); + } + if (tooManyDeletions) sb.append(" tooManyDeletions: ").append(tooManyDeletions); + if (tooManyRetries) sb.append(" tooManyRetries: ").append(tooManyRetries); + if (databaseError) sb.append(" databaseError: ").append(databaseError); + if (fullSyncRequested) sb.append(" fullSyncRequested: ").append(fullSyncRequested); + if (partialSyncUnavailable) { + sb.append(" partialSyncUnavailable: ").append(partialSyncUnavailable); + } + if (moreRecordsToGet) sb.append(" moreRecordsToGet: ").append(moreRecordsToGet); + sb.append(stats); return sb.toString(); } diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java index f503e6f..64bbe25 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.name, account.type}; + 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.name, account.type}); } 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.name, account.type}, 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.name, account.type}); } } diff --git a/core/java/android/content/SyncStats.java b/core/java/android/content/SyncStats.java index b561b05..cc544c0 100644 --- a/core/java/android/content/SyncStats.java +++ b/core/java/android/content/SyncStats.java @@ -60,15 +60,18 @@ public class SyncStats implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("numAuthExceptions: ").append(numAuthExceptions); - sb.append(" numIoExceptions: ").append(numIoExceptions); - sb.append(" numParseExceptions: ").append(numParseExceptions); - sb.append(" numConflictDetectedExceptions: ").append(numConflictDetectedExceptions); - sb.append(" numInserts: ").append(numInserts); - sb.append(" numUpdates: ").append(numUpdates); - sb.append(" numDeletes: ").append(numDeletes); - sb.append(" numEntries: ").append(numEntries); - sb.append(" numSkippedEntries: ").append(numSkippedEntries); + sb.append(" stats ["); + if (numAuthExceptions > 0) sb.append(" numAuthExceptions: ").append(numAuthExceptions); + if (numIoExceptions > 0) sb.append(" numIoExceptions: ").append(numIoExceptions); + if (numParseExceptions > 0) sb.append(" numParseExceptions: ").append(numParseExceptions); + if (numConflictDetectedExceptions > 0) + sb.append(" numConflictDetectedExceptions: ").append(numConflictDetectedExceptions); + if (numInserts > 0) sb.append(" numInserts: ").append(numInserts); + if (numUpdates > 0) sb.append(" numUpdates: ").append(numUpdates); + if (numDeletes > 0) sb.append(" numDeletes: ").append(numDeletes); + if (numEntries > 0) sb.append(" numEntries: ").append(numEntries); + if (numSkippedEntries > 0) sb.append(" numSkippedEntries: ").append(numSkippedEntries); + sb.append("]"); return sb.toString(); } diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java index 6687fcb..b8fda03 100644 --- a/core/java/android/content/SyncStatusInfo.java +++ b/core/java/android/content/SyncStatusInfo.java @@ -38,6 +38,7 @@ public class SyncStatusInfo implements Parcelable { public String lastFailureMesg; public long initialFailureTime; public boolean pending; + public boolean initialize; SyncStatusInfo(int authorityId) { this.authorityId = authorityId; @@ -73,6 +74,7 @@ public class SyncStatusInfo implements Parcelable { parcel.writeString(lastFailureMesg); parcel.writeLong(initialFailureTime); parcel.writeInt(pending ? 1 : 0); + parcel.writeInt(initialize ? 1 : 0); } SyncStatusInfo(Parcel parcel) { @@ -94,6 +96,7 @@ public class SyncStatusInfo implements Parcelable { lastFailureMesg = parcel.readString(); initialFailureTime = parcel.readLong(); pending = parcel.readInt() != 0; + initialize = parcel.readInt() != 0; } public static final Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() { diff --git a/core/java/android/content/SyncUIContext.java b/core/java/android/content/SyncStatusObserver.java index 6dde004..663378a 100644 --- a/core/java/android/content/SyncUIContext.java +++ b/core/java/android/content/SyncStatusObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * 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. @@ -16,22 +16,6 @@ package android.content; -/** - * Class with callback methods for SyncAdapters and ContentProviders - * that are called in response to the calls on SyncContext. This class - * is really only meant to be used by the Sync UI activities. - * - * <p>All of the onXXX callback methods here are called from a handler - * on the thread this object was created in. - * - * <p>This interface is unused. It should be removed. - * - * @hide - */ -@Deprecated -public interface SyncUIContext { - - void setStatusText(String text); - - Context context(); +public interface SyncStatusObserver { + void onStatusChanged(int which); } diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index 756f35c..be70909 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; @@ -53,14 +54,14 @@ import java.util.TimeZone; /** * Singleton that tracks the sync data and overall sync * history on the device. - * + * * @hide */ public class SyncStorageEngine extends Handler { private static final String TAG = "SyncManager"; private static final boolean DEBUG = false; private static final boolean DEBUG_FILE = false; - + // @VisibleForTesting static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; @@ -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,44 +99,30 @@ 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; - + public static final int MAX_HISTORY = 100; + private static final int MSG_WRITE_STATUS = 1; private static final long WRITE_STATUS_DELAY = 1000*60*10; // 10 minutes - + private static final int MSG_WRITE_STATISTICS = 2; private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour - + + private static final boolean SYNC_ENABLED_DEFAULT = false; + public static class PendingOperation { - final String account; + final Account account; final int syncSource; final String authority; final Bundle extras; // note: read-only. - + int authorityId; byte[] flatExtras; - - PendingOperation(String account, int source, + + PendingOperation(Account account, int source, String authority, Bundle extras) { this.account = account; this.syncSource = source; @@ -149,31 +139,33 @@ public class SyncStorageEngine extends Handler { this.authorityId = other.authorityId; } } - + 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) { + int syncable; + + AuthorityInfo(Account account, String authority, int ident) { this.account = account; this.authority = authority; this.ident = ident; - enabled = true; + enabled = SYNC_ENABLED_DEFAULT; + syncable = -1; // default to "unknown" } } - + public static class SyncHistoryItem { int authorityId; int historyId; @@ -185,69 +177,69 @@ public class SyncStorageEngine extends Handler { long downstreamActivity; String mesg; } - + public static class DayStats { public final int day; public int successCount; public long successTime; public int failureCount; public long failureTime; - + public DayStats(int day) { this.day = day; } } - + // Primary list of all syncable authorities. Also our global lock. 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>(); - + private ActiveSyncInfo mActiveSync; - + private final SparseArray<SyncStatusInfo> mSyncStatus = new SparseArray<SyncStatusInfo>(); - + private final ArrayList<SyncHistoryItem> mSyncHistory = new ArrayList<SyncHistoryItem>(); - + private final RemoteCallbackList<ISyncStatusObserver> mChangeListeners = new RemoteCallbackList<ISyncStatusObserver>(); - + // We keep 4 weeks of stats. private final DayStats[] mDayStats = new DayStats[7*4]; private final Calendar mCal; private int mYear; private int mYearInDays; - + private final Context mContext; private static volatile SyncStorageEngine sSyncStorageEngine = null; - + /** * This file contains the core engine state: all accounts and the * settings for them. It must never be lost, and should be changed * infrequently, so it is stored as an XML file. */ private final AtomicFile mAccountInfoFile; - + /** * This file contains the current sync status. We would like to retain * it across boots, but its loss is not the end of the world, so we store * this information as binary data. */ private final AtomicFile mStatusFile; - + /** * This file contains sync statistics. This is purely debugging information * so is written infrequently and can be thrown away at any time. */ private final AtomicFile mStatisticsFile; - + /** * This file contains the pending sync operations. It is a binary file, * which must be updated every time an operation is added or removed, @@ -256,16 +248,16 @@ public class SyncStorageEngine extends Handler { private final AtomicFile mPendingFile; private static final int PENDING_FINISH_TO_WRITE = 4; private int mNumPendingFinished = 0; - + private int mNextHistoryId = 0; - private boolean mListenForTickles = true; - + private boolean mMasterSyncAutomatically = true; + private SyncStorageEngine(Context context) { mContext = context; sSyncStorageEngine = this; - + mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - + File dataDir = Environment.getDataDirectory(); File systemDir = new File(dataDir, "system"); File syncDir = new File(systemDir, "sync"); @@ -273,7 +265,7 @@ public class SyncStorageEngine extends Handler { mStatusFile = new AtomicFile(new File(syncDir, "status.bin")); mPendingFile = new AtomicFile(new File(syncDir, "pending.bin")); mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin")); - + readAccountInfoLocked(); readStatusLocked(); readPendingOperationsLocked(); @@ -310,19 +302,19 @@ public class SyncStorageEngine extends Handler { } } } - + public void addStatusChangeListener(int mask, ISyncStatusObserver callback) { synchronized (mAuthorities) { mChangeListeners.register(callback, mask); } } - + public void removeStatusChangeListener(ISyncStatusObserver callback) { synchronized (mAuthorities) { mChangeListeners.unregister(callback); } } - + private void reportChange(int which) { ArrayList<ISyncStatusObserver> reports = null; synchronized (mAuthorities) { @@ -340,9 +332,9 @@ public class SyncStorageEngine extends Handler { } mChangeListeners.finishBroadcast(); } - + if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports); - + if (reports != null) { int i = reports.size(); while (i > 0) { @@ -354,30 +346,20 @@ public class SyncStorageEngine extends Handler { } } } - // Inform the backup manager about a data change - IBackupManager ibm = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); - if (ibm != null) { - try { - ibm.dataChanged("com.android.providers.settings"); - } catch (RemoteException e) { - // Try again later - } - } } - 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--; - AuthorityInfo authority = mAuthorities.get(i); + AuthorityInfo authority = mAuthorities.valueAt(i); if (authority.authority.equals(providerName) && authority.enabled) { return true; @@ -387,61 +369,102 @@ public class SyncStorageEngine extends Handler { } } - public void setSyncProviderAutomatically(String account, String providerName, boolean sync) { + public void setSyncAutomatically(Account account, String providerName, boolean sync) { + boolean wasEnabled; + synchronized (mAuthorities) { + AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + wasEnabled = authority.enabled; + authority.enabled = sync; + writeAccountInfoLocked(); + } + + if (!wasEnabled && sync) { + mContext.getContentResolver().requestSync(account, providerName, new Bundle()); + } + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + } + + public int getIsSyncable(Account account, String providerName) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, - "setSyncProviderAutomatically"); - if (authority != null) { - authority.enabled = sync; + "getIsSyncable"); + if (authority == null) { + return -1; } - } else { - int i = mAuthorities.size(); - while (i > 0) { - i--; - AuthorityInfo authority = mAuthorities.get(i); - if (authority.authority.equals(providerName)) { - authority.enabled = sync; - } + return authority.syncable; + } + + int i = mAuthorities.size(); + while (i > 0) { + i--; + AuthorityInfo authority = mAuthorities.valueAt(i); + if (authority.authority.equals(providerName)) { + return authority.syncable; } } + return -1; + } + } + + public void setIsSyncable(Account account, String providerName, int syncable) { + int oldState; + if (syncable > 1) { + syncable = 1; + } else if (syncable < -1) { + syncable = -1; + } + Log.d(TAG, "setIsSyncable: " + account + ", provider " + providerName + " -> " + syncable); + synchronized (mAuthorities) { + AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + oldState = authority.syncable; + authority.syncable = syncable; writeAccountInfoLocked(); } - - reportChange(CHANGE_SETTINGS); + + if (oldState <= 0 && syncable > 0) { + mContext.getContentResolver().requestSync(account, providerName, new Bundle()); + } + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); } - public void setListenForNetworkTickles(boolean flag) { + public void setMasterSyncAutomatically(boolean flag) { + boolean old; synchronized (mAuthorities) { - mListenForTickles = flag; + old = mMasterSyncAutomatically; + mMasterSyncAutomatically = flag; writeAccountInfoLocked(); } - reportChange(CHANGE_SETTINGS); + if (!old && flag) { + mContext.getContentResolver().requestSync(null, null, new Bundle()); + } + 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); } } - + public AuthorityInfo getAuthority(int authorityId) { synchronized (mAuthorities) { return mAuthorities.get(authorityId); } } - + /** * 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) { @@ -453,7 +476,7 @@ public class SyncStorageEngine extends Handler { return true; } } - + if (mActiveSync != null) { AuthorityInfo ainfo = getAuthority(mActiveSync.authorityId); if (ainfo != null && ainfo.account.equals(account) @@ -462,17 +485,17 @@ public class SyncStorageEngine extends Handler { } } } - + return false; } - + public PendingOperation insertIntoPending(PendingOperation op) { synchronized (mAuthorities) { if (DEBUG) Log.v(TAG, "insertIntoPending: account=" + op.account + " auth=" + op.authority + " src=" + op.syncSource + " extras=" + op.extras); - + AuthorityInfo authority = getOrCreateAuthorityLocked(op.account, op.authority, -1 /* desired identifier */, @@ -480,17 +503,20 @@ public class SyncStorageEngine extends Handler { if (authority == null) { return null; } - + op = new PendingOperation(op); op.authorityId = authority.ident; mPendingOperations.add(op); appendPendingOperationLocked(op); - + SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = true; + status.initialize = op.extras != null && + op.extras.containsKey(ContentResolver.SYNC_EXTRAS_INITIALIZE) && + op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE); } - - reportChange(CHANGE_PENDING); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return op; } @@ -509,7 +535,7 @@ public class SyncStorageEngine extends Handler { } else { mNumPendingFinished++; } - + AuthorityInfo authority = getAuthorityLocked(op.account, op.authority, "deleteFromPending"); if (authority != null) { @@ -524,19 +550,19 @@ public class SyncStorageEngine extends Handler { break; } } - + if (!morePending) { if (DEBUG) Log.v(TAG, "no more pending!"); SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = false; } } - + res = true; } } - - reportChange(CHANGE_PENDING); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return res; } @@ -548,11 +574,11 @@ public class SyncStorageEngine extends Handler { mPendingOperations.clear(); final int N = mSyncStatus.size(); for (int i=0; i<N; i++) { - mSyncStatus.get(i).pending = false; + mSyncStatus.valueAt(i).pending = false; } writePendingOperationsLocked(); } - reportChange(CHANGE_PENDING); + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_PENDING); return num; } @@ -566,7 +592,7 @@ public class SyncStorageEngine extends Handler { return new ArrayList<PendingOperation>(mPendingOperations); } } - + /** * Return the number of currently pending operations. */ @@ -575,12 +601,12 @@ public class SyncStorageEngine extends Handler { return mPendingOperations.size(); } } - + /** * 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>(); @@ -596,7 +622,7 @@ public class SyncStorageEngine extends Handler { accIt.remove(); } } - + // Clean out all data structures. int i = removing.size(); if (i > 0) { @@ -658,21 +684,21 @@ public class SyncStorageEngine extends Handler { mActiveSync = null; } } - - 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,8 +723,8 @@ public class SyncStorageEngine extends Handler { id = item.historyId; if (DEBUG) Log.v(TAG, "returning historyId " + id); } - - reportChange(CHANGE_STATUS); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); return id; } @@ -716,20 +742,20 @@ public class SyncStorageEngine extends Handler { } item = null; } - + if (item == null) { Log.w(TAG, "stopSyncEvent: no history for id " + historyId); return; } - + item.elapsedTime = elapsedTime; item.event = EVENT_STOP; item.mesg = resultMessage; item.downstreamActivity = downstreamActivity; item.upstreamActivity = upstreamActivity; - + SyncStatusInfo status = getOrCreateSyncStatusLocked(item.authorityId); - + status.numSyncs++; status.totalElapsedTime += elapsedTime; switch (item.source) { @@ -746,7 +772,7 @@ public class SyncStorageEngine extends Handler { status.numSourceServer++; break; } - + boolean writeStatisticsNow = false; int day = getCurrentDayLocked(); if (mDayStats[0] == null) { @@ -758,7 +784,7 @@ public class SyncStorageEngine extends Handler { } else if (mDayStats[0] == null) { } final DayStats ds = mDayStats[0]; - + final long lastSyncTime = (item.eventTime + elapsedTime); boolean writeStatusNow = false; if (MESG_SUCCESS.equals(resultMessage)) { @@ -787,7 +813,7 @@ public class SyncStorageEngine extends Handler { ds.failureCount++; ds.failureTime += elapsedTime; } - + if (writeStatusNow) { writeStatusLocked(); } else if (!hasMessages(MSG_WRITE_STATUS)) { @@ -799,10 +825,10 @@ public class SyncStorageEngine extends Handler { } else if (!hasMessages(MSG_WRITE_STATISTICS)) { sendMessageDelayed(obtainMessage(MSG_WRITE_STATISTICS), WRITE_STATISTICS_DELAY); - } + } } - - reportChange(CHANGE_STATUS); + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_STATUS); } /** @@ -815,7 +841,7 @@ public class SyncStorageEngine extends Handler { return mActiveSync; } } - + /** * Return an array of the current sync status for all authorities. Note * that the objects inside the array are the real, live status objects, @@ -831,40 +857,41 @@ public class SyncStorageEngine extends Handler { return ops; } } - + /** - * Returns the status that matches the authority. If there are multiples accounts for - * the authority, the one with the latest "lastSuccessTime" status is returned. + * Returns the status that matches the authority and account. + * + * @param account the account we want to check * @param authority the authority whose row should be selected * @return the SyncStatusInfo for the authority, or null if none exists */ - public SyncStatusInfo getStatusByAuthority(String authority) { + public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) { + if (account == null || authority == null) { + throw new IllegalArgumentException(); + } synchronized (mAuthorities) { - SyncStatusInfo best = null; final int N = mSyncStatus.size(); for (int i=0; i<N; i++) { - SyncStatusInfo cur = mSyncStatus.get(i); + SyncStatusInfo cur = mSyncStatus.valueAt(i); AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); - if (ainfo != null && ainfo.authority.equals(authority)) { - if (best == null) { - best = cur; - } else if (best.lastSuccessTime > cur.lastSuccessTime) { - best = cur; - } + + if (ainfo != null && ainfo.authority.equals(authority) && + account.equals(ainfo.account)) { + return cur; } } - return best; + return null; } } - + /** * 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++) { - SyncStatusInfo cur = mSyncStatus.get(i); + SyncStatusInfo cur = mSyncStatus.valueAt(i); AuthorityInfo ainfo = mAuthorities.get(cur.authorityId); if (ainfo == null) { continue; @@ -895,7 +922,7 @@ public class SyncStorageEngine extends Handler { return items; } } - + /** * Return an array of the current per-day statistics. Note * that the objects inside the array are the real, live status objects, @@ -908,7 +935,7 @@ public class SyncStorageEngine extends Handler { return ds; } } - + /** * If sync is failing for any of the provider/accounts then determine the time at which it * started failing and return the earliest time over all the provider/accounts. If none are @@ -916,10 +943,10 @@ public class SyncStorageEngine extends Handler { */ public long getInitialSyncFailureTime() { synchronized (mAuthorities) { - if (!mListenForTickles) { + if (!mMasterSyncAutomatically) { return 0; } - + long oldest = 0; int i = mSyncStatus.size(); while (i > 0) { @@ -932,11 +959,11 @@ public class SyncStorageEngine extends Handler { } } } - + return oldest; } } - + private int getCurrentDayLocked() { mCal.setTimeInMillis(System.currentTimeMillis()); final int dayOfYear = mCal.get(Calendar.DAY_OF_YEAR); @@ -948,36 +975,40 @@ public class SyncStorageEngine extends Handler { } return dayOfYear + mYearInDays; } - + /** * Retrieve an authority, returning null if one does not exist. - * + * * @param accountName The name of the account for the authority. * @param authorityName The name of the authority itself. * @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) { if (tag != null) { - Log.w(TAG, tag + ": unknown account " + accountName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, tag + ": unknown account " + accountName); + } } return null; } AuthorityInfo authority = account.authorities.get(authorityName); if (authority == null) { if (tag != null) { - Log.w(TAG, tag + ": unknown authority " + authorityName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, tag + ": unknown authority " + authorityName); + } } return null; } - + 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) { @@ -997,6 +1028,8 @@ public class SyncStorageEngine extends Handler { ident++; } } + if (DEBUG) Log.v(TAG, "created a new AuthorityInfo for " + accountName + + ", provider " + authorityName); authority = new AuthorityInfo(accountName, authorityName, ident); account.authorities.put(authorityName, authority); mAuthorities.put(ident, authority); @@ -1004,10 +1037,10 @@ public class SyncStorageEngine extends Handler { writeAccountInfoLocked(); } } - + return authority; } - + private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) { SyncStatusInfo status = mSyncStatus.get(authorityId); if (status == null) { @@ -1016,22 +1049,22 @@ public class SyncStorageEngine extends Handler { } return status; } - + public void writeAllState() { synchronized (mAuthorities) { // Account info is always written so no need to do it here. - + if (mNumPendingFinished > 0) { // Only write these if they are out of date. writePendingOperationsLocked(); } - + // Just always write these... they are likely out of date. writeStatusLocked(); writeStatisticsLocked(); } } - + /** * Read all account information back in to the initial engine state. */ @@ -1050,7 +1083,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 { @@ -1068,26 +1101,43 @@ 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"; + } String authorityName = parser.getAttributeValue( null, "authority"); String enabled = parser.getAttributeValue( null, "enabled"); - AuthorityInfo authority = mAuthorities.get(id); + String syncable = parser.getAttributeValue(null, "syncable"); + AuthorityInfo authority = mAuthorities.get(id); if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" + accountName + " auth=" + authorityName - + " enabled=" + enabled); + + " enabled=" + enabled + + " syncable=" + syncable); 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 || Boolean.parseBoolean(enabled); + if ("unknown".equals(syncable)) { + authority.syncable = -1; + } else { + authority.syncable = + (syncable == null || Boolean.parseBoolean(enabled)) + ? 1 + : 0; + } } else { Log.w(TAG, "Failure adding authority: account=" + accountName + " auth=" + authorityName - + " enabled=" + enabled); + + " enabled=" + enabled + + " syncable=" + syncable); } } } @@ -1109,43 +1159,49 @@ public class SyncStorageEngine extends Handler { } } } - + /** * Write all account information to the account file. */ private void writeAccountInfoLocked() { if (DEBUG_FILE) Log.v(TAG, "Writing new " + mAccountInfoFile.getBaseFile()); FileOutputStream fos = null; - + try { fos = mAccountInfoFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, "utf-8"); out.startDocument(null, true); 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"); } - + final int N = mAuthorities.size(); for (int i=0; i<N; i++) { - AuthorityInfo authority = mAuthorities.get(i); + AuthorityInfo authority = mAuthorities.valueAt(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.name); + out.attribute(null, "type", authority.account.type); out.attribute(null, "authority", authority.authority); if (!authority.enabled) { out.attribute(null, "enabled", "false"); } + if (authority.syncable < 0) { + out.attribute(null, "syncable", "unknown"); + } else if (authority.syncable == 0) { + out.attribute(null, "syncable", "false"); + } out.endTag(null, "authority"); } - + out.endTag(null, "accounts"); - + out.endDocument(); - + mAccountInfoFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing accounts", e1); @@ -1154,15 +1210,15 @@ public class SyncStorageEngine extends Handler { } } } - + static int getIntColumn(Cursor c, String name) { return c.getInt(c.getColumnIndex(name)); } - + static long getLongColumn(Cursor c, String name) { return c.getLong(c.getColumnIndex(name)); } - + /** * Load sync engine state from the old syncmanager database, and then * erase it. Note that we don't deal with pending operations, active @@ -1181,8 +1237,10 @@ public class SyncStorageEngine extends Handler { SQLiteDatabase.OPEN_READONLY); } catch (SQLiteException e) { } - + 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(); @@ -1190,6 +1248,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"); @@ -1208,16 +1269,22 @@ 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"; + } 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; SyncStatusInfo st = null; while (i > 0) { i--; - st = mSyncStatus.get(i); + st = mSyncStatus.valueAt(i); if (st.authorityId == authority.ident) { found = true; break; @@ -1241,9 +1308,9 @@ public class SyncStorageEngine extends Handler { st.pending = getIntColumn(c, "pending") != 0; } } - + c.close(); - + // Retrieve the settings. qb = new SQLiteQueryBuilder(); qb.setTables("settings"); @@ -1253,29 +1320,35 @@ 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.valueAt(i); + if (authority.authority.equals(provider)) { + authority.enabled = value == null || Boolean.parseBoolean(value); + authority.syncable = 1; + } + } } } - + c.close(); - + db.close(); - + writeAccountInfoLocked(); writeStatusLocked(); (new File(path)).delete(); } } - + public static final int STATUS_FILE_END = 0; public static final int STATUS_FILE_ITEM = 100; - + /** * Read all sync status back in to the initial engine state. */ @@ -1306,17 +1379,17 @@ public class SyncStorageEngine extends Handler { Log.i(TAG, "No initial status"); } } - + /** * Write all sync status to the sync status file. */ private void writeStatusLocked() { if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatusFile.getBaseFile()); - + // The file is being written, so we don't need to have a scheduled // write until the next change. removeMessages(MSG_WRITE_STATUS); - + FileOutputStream fos = null; try { fos = mStatusFile.startWrite(); @@ -1330,7 +1403,7 @@ public class SyncStorageEngine extends Handler { out.writeInt(STATUS_FILE_END); fos.write(out.marshall()); out.recycle(); - + mStatusFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing status", e1); @@ -1339,9 +1412,9 @@ public class SyncStorageEngine extends Handler { } } } - + public static final int PENDING_OPERATION_VERSION = 1; - + /** * Read all pending operations back in to the initial engine state. */ @@ -1385,7 +1458,7 @@ public class SyncStorageEngine extends Handler { Log.i(TAG, "No initial pending operations"); } } - + private void writePendingOperationLocked(PendingOperation op, Parcel out) { out.writeInt(PENDING_OPERATION_VERSION); out.writeInt(op.authorityId); @@ -1395,7 +1468,7 @@ public class SyncStorageEngine extends Handler { } out.writeByteArray(op.flatExtras); } - + /** * Write all currently pending ops to the pending ops file. */ @@ -1408,10 +1481,10 @@ public class SyncStorageEngine extends Handler { mPendingFile.truncate(); return; } - + if (DEBUG_FILE) Log.v(TAG, "Writing new " + mPendingFile.getBaseFile()); fos = mPendingFile.startWrite(); - + Parcel out = Parcel.obtain(); for (int i=0; i<N; i++) { PendingOperation op = mPendingOperations.get(i); @@ -1419,7 +1492,7 @@ public class SyncStorageEngine extends Handler { } fos.write(out.marshall()); out.recycle(); - + mPendingFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing pending operations", e1); @@ -1428,7 +1501,7 @@ public class SyncStorageEngine extends Handler { } } } - + /** * Append the given operation to the pending ops file; if unable to, * write all pending ops. @@ -1443,7 +1516,7 @@ public class SyncStorageEngine extends Handler { writePendingOperationsLocked(); return; } - + try { Parcel out = Parcel.obtain(); writePendingOperationLocked(op, out); @@ -1458,7 +1531,7 @@ public class SyncStorageEngine extends Handler { } } } - + static private byte[] flattenBundle(Bundle bundle) { byte[] flatData = null; Parcel parcel = Parcel.obtain(); @@ -1470,7 +1543,7 @@ public class SyncStorageEngine extends Handler { } return flatData; } - + static private Bundle unflattenBundle(byte[] flatData) { Bundle bundle; Parcel parcel = Parcel.obtain(); @@ -1487,11 +1560,11 @@ public class SyncStorageEngine extends Handler { } return bundle; } - + public static final int STATISTICS_FILE_END = 0; public static final int STATISTICS_FILE_ITEM_OLD = 100; public static final int STATISTICS_FILE_ITEM = 101; - + /** * Read all sync statistics back in to the initial engine state. */ @@ -1529,17 +1602,17 @@ public class SyncStorageEngine extends Handler { Log.i(TAG, "No initial statistics"); } } - + /** * Write all sync statistics to the sync status file. */ private void writeStatisticsLocked() { if (DEBUG_FILE) Log.v(TAG, "Writing new " + mStatisticsFile.getBaseFile()); - + // The file is being written, so we don't need to have a scheduled // write until the next change. removeMessages(MSG_WRITE_STATISTICS); - + FileOutputStream fos = null; try { fos = mStatisticsFile.startWrite(); @@ -1560,7 +1633,7 @@ public class SyncStorageEngine extends Handler { out.writeInt(STATISTICS_FILE_END); fos.write(out.marshall()); out.recycle(); - + mStatisticsFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing stats", e1); 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..b46c545 100644 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -12,6 +12,11 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.TimingLogger; +import android.accounts.Account; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; + +import java.io.IOException; /** * @hide @@ -62,12 +67,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); /** @@ -85,6 +88,9 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { */ public abstract boolean isReadOnly(); + public abstract boolean getIsSyncable(Account account) + throws IOException, AuthenticatorException, OperationCanceledException; + /** * Get diffs from the server since the last completed sync and put them * into a temporary provider. @@ -168,12 +174,13 @@ 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 String mAuthority; private final Bundle mExtras; private final SyncContext mSyncContext; private volatile boolean mIsCanceled = false; @@ -181,9 +188,10 @@ 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, String authority, Bundle extras) { super("SyncThread"); mAccount = account; + mAuthority = authority; mExtras = extras; mSyncContext = syncContext; mResult = new SyncResult(); @@ -207,7 +215,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { mInitialTxBytes = NetStat.getUidTxBytes(uid); mInitialRxBytes = NetStat.getUidRxBytes(uid); try { - sync(mSyncContext, mAccount, mExtras); + sync(mSyncContext, mAccount, mAuthority, mExtras); } catch (SQLException e) { Log.e(TAG, "Sync failed", e); mResult.databaseError = true; @@ -221,19 +229,45 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void sync(SyncContext syncContext, String account, Bundle extras) { + private void sync(SyncContext syncContext, Account account, String authority, + Bundle extras) { mIsCanceled = false; mProviderSyncStarted = false; mAdapterSyncStarted = false; String message = null; - boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + // always attempt to initialize if the isSyncable state isn't set yet + int isSyncable = ContentResolver.getIsSyncable(account, authority); + if (isSyncable < 0) { + try { + isSyncable = (getIsSyncable(account)) ? 1 : 0; + ContentResolver.setIsSyncable(account, authority, isSyncable); + } catch (IOException e) { + ++mResult.stats.numIoExceptions; + } catch (AuthenticatorException e) { + ++mResult.stats.numParseExceptions; + } catch (OperationCanceledException e) { + // do nothing + } + } + + // if this is an initialization request then our work is done here + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + return; + } + + // if we aren't syncable then get out + if (isSyncable <= 0) { + return; + } + + 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 +307,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,13 +552,14 @@ 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, String authority, + Bundle extras) { if (mSyncThread != null) { syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); return; } - mSyncThread = new SyncThread(syncContext, account, extras); + mSyncThread = new SyncThread(syncContext, account, authority, extras); mSyncThread.start(); } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 27783ef..87da55f 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -127,12 +127,20 @@ public class ActivityInfo extends ComponentInfo */ public static final int FLAG_NO_HISTORY = 0x0080; /** + * Bit in {@link #flags} indicating that, when a request to close system + * windows happens, this activity is finished. + * Set from the + * {@link android.R.attr#finishOnCloseSystemDialogs} attribute. + */ + public static final int FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS = 0x0100; + /** * Options that have been set in the activity declaration in the * manifest: {@link #FLAG_MULTIPROCESS}, * {@link #FLAG_FINISH_ON_TASK_LAUNCH}, {@link #FLAG_CLEAR_TASK_ON_LAUNCH}, * {@link #FLAG_ALWAYS_RETAIN_TASK_STATE}, * {@link #FLAG_STATE_NOT_NEEDED}, {@link #FLAG_EXCLUDE_FROM_RECENTS}, - * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}. + * {@link #FLAG_ALLOW_TASK_REPARENTING}, {@link #FLAG_NO_HISTORY}, + * {@link #FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS}. */ public int flags; @@ -217,7 +225,9 @@ public class ActivityInfo extends ComponentInfo public static final int CONFIG_KEYBOARD = 0x0010; /** * Bit in {@link #configChanges} that indicates that the activity - * can itself handle changes to the keyboard being hidden/exposed. + * can itself handle changes to the keyboard or navigation being hidden/exposed. + * Note that inspite of the name, this applies to the changes to any + * hidden states: keyboard or navigation. * Set from the {@link android.R.attr#configChanges} attribute. */ public static final int CONFIG_KEYBOARD_HIDDEN = 0x0020; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 0a42a6f..7a65af8 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -184,7 +184,29 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@hide} */ public static final int FLAG_ALLOW_BACKUP = 1<<14; - + + /** + * Value for {@link #flags}: this is false if the application has set + * its android:killAfterRestore to false, true otherwise. + * + * <p>If android:allowBackup is set to false or no android:backupAgent + * is specified, this flag will be ignored. + * + * {@hide} + */ + public static final int FLAG_KILL_AFTER_RESTORE = 1<<15; + + /** + * Value for {@link #flags}: this is true if the application has set + * its android:restoreNeedsApplication to true, false otherwise. + * + * <p>If android:allowBackup is set to false or no android:backupAgent + * is specified, this flag will be ignored. + * + * {@hide} + */ + public static final int FLAG_RESTORE_NEEDS_APPLICATION = 1<<16; + /** * Flags associated with the application. Any combination of * {@link #FLAG_SYSTEM}, {@link #FLAG_DEBUGGABLE}, {@link #FLAG_HAS_CODE}, @@ -193,7 +215,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@link #FLAG_ALLOW_CLEAR_USER_DATA}, {@link #FLAG_UPDATED_SYSTEM_APP}, * {@link #FLAG_TEST_ONLY}, {@link #FLAG_SUPPORTS_SMALL_SCREENS}, * {@link #FLAG_SUPPORTS_NORMAL_SCREENS}, - * {@link #FLAG_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_RESIZEABLE_FOR_SCREENS}. + * {@link #FLAG_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_RESIZEABLE_FOR_SCREENS}, + * {@link #FLAG_SUPPORTS_SCREEN_DENSITIES}. */ public int flags = 0; @@ -230,7 +253,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public int uid; /** - * The minimum SDK version this application targets. It may run on earilier + * The minimum SDK version this application targets. It may run on earlier * versions, but it knows how to work with any new behavior added at this * version. Will be {@link android.os.Build.VERSION_CODES#CUR_DEVELOPMENT} * if this is a development build and the app is targeting that. You should diff --git a/core/java/android/content/pm/ConfigurationInfo.java b/core/java/android/content/pm/ConfigurationInfo.java index fb7a47f..8edd436 100755 --- a/core/java/android/content/pm/ConfigurationInfo.java +++ b/core/java/android/content/pm/ConfigurationInfo.java @@ -22,9 +22,9 @@ import android.os.Parcelable; /** * Information you can retrieve about hardware configuration preferences * declared by an application. This corresponds to information collected from the - * AndroidManifest.xml's <uses-configuration> and the <uses-feature>tags. + * AndroidManifest.xml's <uses-configuration> and <uses-feature> tags. */ -public class ConfigurationInfo implements Parcelable { +public class ConfigurationInfo implements Parcelable { /** * The kind of touch screen attached to the device. * One of: {@link android.content.res.Configuration#TOUCHSCREEN_NOTOUCH}, @@ -92,13 +92,13 @@ public class ConfigurationInfo implements Parcelable { } public String toString() { - return "ApplicationHardwarePreferences{" + return "ConfigurationInfo{" + Integer.toHexString(System.identityHashCode(this)) - + ", touchscreen = " + reqTouchScreen + "}" - + ", inputMethod = " + reqKeyboardType + "}" - + ", navigation = " + reqNavigation + "}" - + ", reqInputFeatures = " + reqInputFeatures + "}" - + ", reqGlEsVersion = " + reqGlEsVersion + "}"; + + " touchscreen = " + reqTouchScreen + + " inputMethod = " + reqKeyboardType + + " navigation = " + reqNavigation + + " reqInputFeatures = " + reqInputFeatures + + " reqGlEsVersion = " + reqGlEsVersion + "}"; } public int describeContents() { diff --git a/core/java/android/content/pm/FeatureInfo.aidl b/core/java/android/content/pm/FeatureInfo.aidl new file mode 100755 index 0000000..d84a84c --- /dev/null +++ b/core/java/android/content/pm/FeatureInfo.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 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; + +parcelable FeatureInfo; diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java new file mode 100644 index 0000000..57d61fd --- /dev/null +++ b/core/java/android/content/pm/FeatureInfo.java @@ -0,0 +1,101 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Parcelable.Creator; + +/** + * A single feature that can be requested by an application. This corresponds + * to information collected from the + * AndroidManifest.xml's <uses-feature> tag. + */ +public class FeatureInfo implements Parcelable { + /** + * The name of this feature, for example "android.hardware.camera". If + * this is null, then this is an OpenGL ES version feature as described + * in {@link #reqGlEsVersion}. + */ + public String name; + + /** + * Default value for {@link #reqGlEsVersion}; + */ + public static final int GL_ES_VERSION_UNDEFINED = 0; + + /** + * The GLES version used by an application. The upper order 16 bits represent the + * major version and the lower order 16 bits the minor version. Only valid + * if {@link #name} is null. + */ + public int reqGlEsVersion; + + /** + * Set on {@link #flags} if this feature has been required by the application. + */ + public static final int FLAG_REQUIRED = 0x0001; + + /** + * Additional flags. May be zero or more of {@link #FLAG_REQUIRED}. + */ + public int flags; + + public FeatureInfo() { + } + + public FeatureInfo(FeatureInfo orig) { + name = orig.name; + reqGlEsVersion = orig.reqGlEsVersion; + flags = orig.flags; + } + + public String toString() { + if (name != null) { + return "FeatureInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + " fl=0x" + Integer.toHexString(flags) + "}"; + } else { + return "FeatureInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " glEsVers=" + getGlEsVersion() + + " fl=0x" + Integer.toHexString(flags) + "}"; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeString(name); + dest.writeInt(reqGlEsVersion); + dest.writeInt(flags); + } + + public static final Creator<FeatureInfo> CREATOR = + new Creator<FeatureInfo>() { + public FeatureInfo createFromParcel(Parcel source) { + return new FeatureInfo(source); + } + public FeatureInfo[] newArray(int size) { + return new FeatureInfo[size]; + } + }; + + private FeatureInfo(Parcel source) { + name = source.readString(); + reqGlEsVersion = source.readInt(); + flags = source.readInt(); + } + + /** + * This method extracts the major and minor version of reqGLEsVersion attribute + * and returns it as a string. Say reqGlEsVersion value of 0x00010002 is returned + * as 1.2 + * @return String representation of the reqGlEsVersion attribute + */ + public String getGlEsVersion() { + int major = ((reqGlEsVersion & 0xffff0000) >> 16); + int minor = reqGlEsVersion & 0x0000ffff; + return String.valueOf(major)+"."+String.valueOf(minor); + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index e587ca7..fc6538f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.FeatureInfo; import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageDataObserver; @@ -75,6 +76,8 @@ interface IPackageManager { int checkSignatures(String pkg1, String pkg2); + int checkUidSignatures(int uid1, int uid2); + String[] getPackagesForUid(int uid); String getNameForUid(int uid); @@ -274,6 +277,14 @@ interface IPackageManager { */ String[] getSystemSharedLibraryNames(); + /** + * Get a list of features that are available on the + * system. + */ + FeatureInfo[] getSystemAvailableFeatures(); + + boolean hasSystemFeature(String name); + void enterSafeMode(); boolean isSafeMode(); void systemReady(); diff --git a/core/java/android/content/pm/LabeledIntent.java b/core/java/android/content/pm/LabeledIntent.java new file mode 100644 index 0000000..d70a698 --- /dev/null +++ b/core/java/android/content/pm/LabeledIntent.java @@ -0,0 +1,177 @@ +package android.content.pm; + +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.text.TextUtils; + +/** + * A special subclass of Intent that can have a custom label/icon + * associated with it. Primarily for use with {@link Intent#ACTION_CHOOSER}. + */ +public class LabeledIntent extends Intent { + private String mSourcePackage; + private int mLabelRes; + private CharSequence mNonLocalizedLabel; + private int mIcon; + + /** + * Create a labeled intent from the given intent, supplying the label + * and icon resources for it. + * + * @param origIntent The original Intent to copy. + * @param sourcePackage The package in which the label and icon live. + * @param labelRes Resource containing the label, or 0 if none. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(Intent origIntent, String sourcePackage, + int labelRes, int icon) { + super(origIntent); + mSourcePackage = sourcePackage; + mLabelRes = labelRes; + mNonLocalizedLabel = null; + mIcon = icon; + } + + /** + * Create a labeled intent from the given intent, supplying a textual + * label and icon resource for it. + * + * @param origIntent The original Intent to copy. + * @param sourcePackage The package in which the label and icon live. + * @param nonLocalizedLabel Concrete text to use for the label. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(Intent origIntent, String sourcePackage, + CharSequence nonLocalizedLabel, int icon) { + super(origIntent); + mSourcePackage = sourcePackage; + mLabelRes = 0; + mNonLocalizedLabel = nonLocalizedLabel; + mIcon = icon; + } + + /** + * Create a labeled intent with no intent data but supplying the label + * and icon resources for it. + * + * @param sourcePackage The package in which the label and icon live. + * @param labelRes Resource containing the label, or 0 if none. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(String sourcePackage, int labelRes, int icon) { + mSourcePackage = sourcePackage; + mLabelRes = labelRes; + mNonLocalizedLabel = null; + mIcon = icon; + } + + /** + * Create a labeled intent with no intent data but supplying a textual + * label and icon resource for it. + * + * @param sourcePackage The package in which the label and icon live. + * @param nonLocalizedLabel Concrete text to use for the label. + * @param icon Resource containing the icon, or 0 if none. + */ + public LabeledIntent(String sourcePackage, + CharSequence nonLocalizedLabel, int icon) { + mSourcePackage = sourcePackage; + mLabelRes = 0; + mNonLocalizedLabel = nonLocalizedLabel; + mIcon = icon; + } + + /** + * Return the name of the package holding label and icon resources. + */ + public String getSourcePackage() { + return mSourcePackage; + } + + /** + * Return any resource identifier that has been given for the label text. + */ + public int getLabelResource() { + return mLabelRes; + } + + /** + * Return any concrete text that has been given for the label text. + */ + public CharSequence getNonLocalizedLabel() { + return mNonLocalizedLabel; + } + + /** + * Return any resource identifier that has been given for the label icon. + */ + public int getIconResource() { + return mIcon; + } + + /** + * Retrieve the label associated with this object. If the object does + * not have a label, null will be returned, in which case you will probably + * want to load the label from the underlying resolved info for the Intent. + */ + public CharSequence loadLabel(PackageManager pm) { + if (mNonLocalizedLabel != null) { + return mNonLocalizedLabel; + } + if (mLabelRes != 0 && mSourcePackage != null) { + CharSequence label = pm.getText(mSourcePackage, mLabelRes, null); + if (label != null) { + return label; + } + } + return null; + } + + /** + * Retrieve the icon associated with this object. If the object does + * not have a icon, null will be returned, in which case you will probably + * want to load the icon from the underlying resolved info for the Intent. + */ + public Drawable loadIcon(PackageManager pm) { + if (mIcon != 0 && mSourcePackage != null) { + Drawable icon = pm.getDrawable(mSourcePackage, mIcon, null); + if (icon != null) { + return icon; + } + } + return null; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(mSourcePackage); + dest.writeInt(mLabelRes); + TextUtils.writeToParcel(mNonLocalizedLabel, dest, parcelableFlags); + dest.writeInt(mIcon); + } + + /** @hide */ + protected LabeledIntent(Parcel in) { + readFromParcel(in); + } + + public void readFromParcel(Parcel in) { + super.readFromParcel(in); + mSourcePackage = in.readString(); + mLabelRes = in.readInt(); + mNonLocalizedLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readInt(); + } + + public static final Creator<LabeledIntent> CREATOR + = new Creator<LabeledIntent>() { + public LabeledIntent createFromParcel(Parcel source) { + return new LabeledIntent(source); + } + public LabeledIntent[] newArray(int size) { + return new LabeledIntent[size]; + } + }; + +} diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index d9326f2..a8ce889 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -127,6 +127,11 @@ public class PackageInfo implements Parcelable { */ public ConfigurationInfo[] configPreferences; + /** + * The features that this application has said it requires. + */ + public FeatureInfo[] reqFeatures; + public PackageInfo() { } @@ -162,6 +167,7 @@ public class PackageInfo implements Parcelable { dest.writeStringArray(requestedPermissions); dest.writeTypedArray(signatures, parcelableFlags); dest.writeTypedArray(configPreferences, parcelableFlags); + dest.writeTypedArray(reqFeatures, parcelableFlags); } public static final Parcelable.Creator<PackageInfo> CREATOR @@ -195,5 +201,6 @@ public class PackageInfo implements Parcelable { requestedPermissions = source.createStringArray(); signatures = source.createTypedArray(Signature.CREATOR); configPreferences = source.createTypedArray(ConfigurationInfo.CREATOR); + reqFeatures = source.createTypedArray(FeatureInfo.CREATOR); } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 67bd1ac..cd48dcb 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -159,8 +159,10 @@ public abstract class PackageManager { /** * {@link PackageInfo} flag: return information about - * hardware preferences - * {@link PackageInfo#configPreferences} + * hardware preferences in + * {@link PackageInfo#configPreferences PackageInfo.configPreferences} and + * requested features in {@link PackageInfo#reqFeatures + * PackageInfo.reqFeatures}. */ public static final int GET_CONFIGURATIONS = 0x00004000; @@ -400,6 +402,14 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a feature that is not available. + * @hide + */ + public static final int INSTALL_FAILED_MISSING_FEATURE = -17; + + /** * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} * if the parser was given a path that is not a file, or does not end with the expected @@ -865,6 +875,7 @@ public abstract class PackageManager { * {@link #SIGNATURE_SECOND_NOT_SIGNED}, {@link #SIGNATURE_NO_MATCH}, * or {@link #SIGNATURE_UNKNOWN_PACKAGE}. * + * @see #checkSignatures(int, int) * @see #SIGNATURE_MATCH * @see #SIGNATURE_NEITHER_SIGNED * @see #SIGNATURE_FIRST_NOT_SIGNED @@ -875,6 +886,34 @@ public abstract class PackageManager { public abstract int checkSignatures(String pkg1, String pkg2); /** + * Like {@link #checkSignatures(String, String)}, but takes UIDs of + * the two packages to be checked. This can be useful, for example, + * when doing the check in an IPC, where the UID is the only identity + * available. It is functionally identical to determining the package + * associated with the UIDs and checking their signatures. + * + * @param uid1 First UID whose signature will be compared. + * @param uid2 Second UID whose signature will be compared. + * @return Returns an integer indicating whether there is a matching + * signature: the value is >= 0 if there is a match (or neither package + * is signed), or < 0 if there is not a match. The match result can be + * further distinguished with the success (>= 0) constants + * {@link #SIGNATURE_MATCH}, {@link #SIGNATURE_NEITHER_SIGNED}; or + * failure (< 0) constants {@link #SIGNATURE_FIRST_NOT_SIGNED}, + * {@link #SIGNATURE_SECOND_NOT_SIGNED}, {@link #SIGNATURE_NO_MATCH}, + * or {@link #SIGNATURE_UNKNOWN_PACKAGE}. + * + * @see #checkSignatures(int, int) + * @see #SIGNATURE_MATCH + * @see #SIGNATURE_NEITHER_SIGNED + * @see #SIGNATURE_FIRST_NOT_SIGNED + * @see #SIGNATURE_SECOND_NOT_SIGNED + * @see #SIGNATURE_NO_MATCH + * @see #SIGNATURE_UNKNOWN_PACKAGE + */ + public abstract int checkSignatures(int uid1, int uid2); + + /** * Retrieve the names of all packages that are associated with a particular * user id. In most cases, this will be a single package name, the package * that has been assigned that user id. Where there are multiple packages @@ -951,6 +990,24 @@ public abstract class PackageManager { public abstract String[] getSystemSharedLibraryNames(); /** + * Get a list of features that are available on the + * system. + * + * @return An array of FeatureInfo classes describing the features + * that are available on the system, or null if there are none(!!). + */ + public abstract FeatureInfo[] getSystemAvailableFeatures(); + + /** + * Check whether the given feature name is one of the available + * features as returned by {@link #getSystemAvailableFeatures()}. + * + * @return Returns true if the devices supports the feature, else + * false. + */ + public abstract boolean hasSystemFeature(String name); + + /** * Determine the best action to perform for a given Intent. This is how * {@link Intent#resolveActivity} finds an activity if a class has not * been explicitly specified. @@ -1429,8 +1486,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/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 33f4b52..b798bde 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -27,6 +27,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; import android.util.AttributeSet; @@ -84,8 +85,9 @@ public class PackageParser { private String mArchiveSourcePath; private String[] mSeparateProcesses; - private int mSdkVersion; - private String mSdkCodename; + private static final int SDK_VERSION = Build.VERSION.SDK_INT; + private static final String SDK_CODENAME = "REL".equals(Build.VERSION.CODENAME) + ? null : Build.VERSION.CODENAME; private int mParseError = PackageManager.INSTALL_SUCCEEDED; @@ -152,11 +154,6 @@ public class PackageParser { mSeparateProcesses = procs; } - public void setSdkVersion(int sdkVersion, String codename) { - mSdkVersion = sdkVersion; - mSdkCodename = codename; - } - private static final boolean isPackageFilename(String name) { return name.endsWith(".apk"); } @@ -184,20 +181,31 @@ public class PackageParser { int N = p.configPreferences.size(); if (N > 0) { pi.configPreferences = new ConfigurationInfo[N]; - for (int i=0; i<N; i++) { - pi.configPreferences[i] = p.configPreferences.get(i); - } + p.configPreferences.toArray(pi.configPreferences); + } + N = p.reqFeatures != null ? p.reqFeatures.size() : 0; + if (N > 0) { + pi.reqFeatures = new FeatureInfo[N]; + p.reqFeatures.toArray(pi.reqFeatures); } } if ((flags&PackageManager.GET_ACTIVITIES) != 0) { int N = p.activities.size(); if (N > 0) { - pi.activities = new ActivityInfo[N]; - for (int i=0; i<N; i++) { + if ((flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { + pi.activities = new ActivityInfo[N]; + } else { + int num = 0; + for (int i=0; i<N; i++) { + if (p.activities.get(i).info.enabled) num++; + } + pi.activities = new ActivityInfo[num]; + } + for (int i=0, j=0; i<N; i++) { final Activity activity = p.activities.get(i); if (activity.info.enabled || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { - pi.activities[i] = generateActivityInfo(p.activities.get(i), flags); + pi.activities[j++] = generateActivityInfo(p.activities.get(i), flags); } } } @@ -205,12 +213,20 @@ public class PackageParser { if ((flags&PackageManager.GET_RECEIVERS) != 0) { int N = p.receivers.size(); if (N > 0) { - pi.receivers = new ActivityInfo[N]; - for (int i=0; i<N; i++) { + if ((flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { + pi.receivers = new ActivityInfo[N]; + } else { + int num = 0; + for (int i=0; i<N; i++) { + if (p.receivers.get(i).info.enabled) num++; + } + pi.receivers = new ActivityInfo[num]; + } + for (int i=0, j=0; i<N; i++) { final Activity activity = p.receivers.get(i); if (activity.info.enabled || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { - pi.receivers[i] = generateActivityInfo(p.receivers.get(i), flags); + pi.receivers[j++] = generateActivityInfo(p.receivers.get(i), flags); } } } @@ -218,12 +234,20 @@ public class PackageParser { if ((flags&PackageManager.GET_SERVICES) != 0) { int N = p.services.size(); if (N > 0) { - pi.services = new ServiceInfo[N]; - for (int i=0; i<N; i++) { + if ((flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { + pi.services = new ServiceInfo[N]; + } else { + int num = 0; + for (int i=0; i<N; i++) { + if (p.services.get(i).info.enabled) num++; + } + pi.services = new ServiceInfo[num]; + } + for (int i=0, j=0; i<N; i++) { final Service service = p.services.get(i); if (service.info.enabled || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { - pi.services[i] = generateServiceInfo(p.services.get(i), flags); + pi.services[j++] = generateServiceInfo(p.services.get(i), flags); } } } @@ -231,12 +255,20 @@ public class PackageParser { if ((flags&PackageManager.GET_PROVIDERS) != 0) { int N = p.providers.size(); if (N > 0) { - pi.providers = new ProviderInfo[N]; - for (int i=0; i<N; i++) { + if ((flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { + pi.providers = new ProviderInfo[N]; + } else { + int num = 0; + for (int i=0; i<N; i++) { + if (p.providers.get(i).info.enabled) num++; + } + pi.providers = new ProviderInfo[num]; + } + for (int i=0, j=0; i<N; i++) { final Provider provider = p.providers.get(i); if (provider.info.enabled || (flags&PackageManager.GET_DISABLED_COMPONENTS) != 0) { - pi.providers[i] = generateProviderInfo(p.providers.get(i), flags); + pi.providers[j++] = generateProviderInfo(p.providers.get(i), flags); } } } @@ -268,8 +300,8 @@ public class PackageParser { } } if ((flags&PackageManager.GET_SIGNATURES) != 0) { - int N = p.mSignatures.length; - if (N > 0) { + int N = (p.mSignatures != null) ? p.mSignatures.length : 0; + if (N > 0) { pi.signatures = new Signature[N]; System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N); } @@ -758,19 +790,37 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("uses-feature")) { - ConfigurationInfo cPref = new ConfigurationInfo(); + FeatureInfo fi = new FeatureInfo(); sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestUsesFeature); - cPref.reqGlEsVersion = sa.getInt( - com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, - ConfigurationInfo.GL_ES_VERSION_UNDEFINED); + fi.name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesFeature_name); + if (fi.name == null) { + fi.reqGlEsVersion = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, + FeatureInfo.GL_ES_VERSION_UNDEFINED); + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesFeature_required, + true)) { + fi.flags |= FeatureInfo.FLAG_REQUIRED; + } sa.recycle(); - pkg.configPreferences.add(cPref); + if (pkg.reqFeatures == null) { + pkg.reqFeatures = new ArrayList<FeatureInfo>(); + } + pkg.reqFeatures.add(fi); + + if (fi.name == null) { + ConfigurationInfo cPref = new ConfigurationInfo(); + cPref.reqGlEsVersion = fi.reqGlEsVersion; + pkg.configPreferences.add(cPref); + } XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("uses-sdk")) { - if (mSdkVersion > 0) { + if (SDK_VERSION > 0) { sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestUsesSdk); @@ -803,15 +853,15 @@ public class PackageParser { int maxVers = sa.getInt( com.android.internal.R.styleable.AndroidManifestUsesSdk_maxSdkVersion, - mSdkVersion); + SDK_VERSION); sa.recycle(); if (minCode != null) { - if (!minCode.equals(mSdkCodename)) { - if (mSdkCodename != null) { + if (!minCode.equals(SDK_CODENAME)) { + if (SDK_CODENAME != null) { outError[0] = "Requires development platform " + minCode - + " (current platform is " + mSdkCodename + ")"; + + " (current platform is " + SDK_CODENAME + ")"; } else { outError[0] = "Requires development platform " + minCode + " but this is a release platform."; @@ -819,18 +869,18 @@ public class PackageParser { mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } - } else if (minVers > mSdkVersion) { + } else if (minVers > SDK_VERSION) { outError[0] = "Requires newer sdk version #" + minVers - + " (current version is #" + mSdkVersion + ")"; + + " (current version is #" + SDK_VERSION + ")"; mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } if (targetCode != null) { - if (!targetCode.equals(mSdkCodename)) { - if (mSdkCodename != null) { + if (!targetCode.equals(SDK_CODENAME)) { + if (SDK_CODENAME != null) { outError[0] = "Requires development platform " + targetCode - + " (current platform is " + mSdkCodename + ")"; + + " (current platform is " + SDK_CODENAME + ")"; } else { outError[0] = "Requires development platform " + targetCode + " but this is a release platform."; @@ -845,9 +895,9 @@ public class PackageParser { pkg.applicationInfo.targetSdkVersion = targetVers; } - if (maxVers < mSdkVersion) { + if (maxVers < SDK_VERSION) { outError[0] = "Requires older sdk version #" + maxVers - + " (current version is #" + mSdkVersion + ")"; + + " (current version is #" + SDK_VERSION + ")"; mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } @@ -918,8 +968,9 @@ public class PackageParser { return null; } else { - Log.w(TAG, "Bad element under <manifest>: " - + parser.getName()); + Log.w(TAG, "Unknown element under <manifest>: " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -931,6 +982,7 @@ public class PackageParser { } final int NP = PackageParser.NEW_PERMISSIONS.length; + StringBuilder implicitPerms = null; for (int ip=0; ip<NP; ip++) { final PackageParser.NewPermissionInfo npi = PackageParser.NEW_PERMISSIONS[ip]; @@ -938,15 +990,19 @@ public class PackageParser { break; } if (!pkg.requestedPermissions.contains(npi.name)) { - Log.i(TAG, "Impliciting adding " + npi.name + " to old pkg " - + pkg.packageName); + if (implicitPerms == null) { + implicitPerms = new StringBuilder(128); + implicitPerms.append(pkg.packageName); + implicitPerms.append(": compat added "); + } else { + implicitPerms.append(' '); + } + implicitPerms.append(npi.name); pkg.requestedPermissions.add(npi.name); } } - - if (pkg.usesLibraries.size() > 0) { - pkg.usesLibraryFiles = new String[pkg.usesLibraries.size()]; - pkg.usesLibraries.toArray(pkg.usesLibraryFiles); + if (implicitPerms != null) { + Log.i(TAG, implicitPerms.toString()); } if (supportsSmallScreens < 0 || (supportsSmallScreens > 0 @@ -1278,12 +1334,28 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestApplication_allowBackup, true); if (allowBackup) { ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; + + // backupAgent, killAfterRestore, and restoreNeedsApplication are only relevant + // if backup is possible for the given application. String backupAgent = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestApplication_backupAgent); if (backupAgent != null) { ai.backupAgentName = buildClassName(pkgName, backupAgent, outError); - Log.v(TAG, "android:backupAgent = " + ai.backupAgentName - + " from " + pkgName + "+" + backupAgent); + if (false) { + Log.v(TAG, "android:backupAgent = " + ai.backupAgentName + + " from " + pkgName + "+" + backupAgent); + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_killAfterRestore, + true)) { + ai.flags |= ApplicationInfo.FLAG_KILL_AFTER_RESTORE; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_restoreNeedsApplication, + false)) { + ai.flags |= ApplicationInfo.FLAG_RESTORE_NEEDS_APPLICATION; + } } } @@ -1434,19 +1506,37 @@ public class PackageParser { String lname = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + boolean req = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_required, + true); sa.recycle(); - if (lname != null && !owner.usesLibraries.contains(lname)) { - owner.usesLibraries.add(lname.intern()); + if (lname != null) { + if (req) { + if (owner.usesLibraries == null) { + owner.usesLibraries = new ArrayList<String>(); + } + if (!owner.usesLibraries.contains(lname)) { + owner.usesLibraries.add(lname.intern()); + } + } else { + if (owner.usesOptionalLibraries == null) { + owner.usesOptionalLibraries = new ArrayList<String>(); + } + if (!owner.usesOptionalLibraries.contains(lname)) { + owner.usesOptionalLibraries.add(lname.intern()); + } + } } XmlUtils.skipCurrentTag(parser); } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under <application>: " + tagName); + Log.w(TAG, "Unknown element under <application>: " + tagName + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } else { @@ -1491,25 +1581,6 @@ public class PackageParser { return true; } - private boolean parseComponentInfo(Package owner, int flags, - ComponentInfo outInfo, String[] outError, String tag, TypedArray sa, - int nameRes, int labelRes, int iconRes, int processRes, - int enabledRes) { - if (!parsePackageItemInfo(owner, outInfo, outError, tag, sa, - nameRes, labelRes, iconRes)) { - return false; - } - - if (processRes != 0) { - outInfo.processName = buildProcessName(owner.applicationInfo.packageName, - owner.applicationInfo.processName, sa.getNonResourceString(processRes), - flags, mSeparateProcesses, outError); - } - outInfo.enabled = sa.getBoolean(enabledRes, true); - - return outError[0] == null; - } - private Activity parseActivity(Package owner, Resources res, XmlPullParser parser, AttributeSet attrs, int flags, String[] outError, boolean receiver) throws XmlPullParserException, IOException { @@ -1609,6 +1680,12 @@ public class PackageParser { a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING; } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_finishOnCloseSystemDialogs, + false)) { + a.info.flags |= ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS; + } + if (!receiver) { a.info.launchMode = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_launchMode, @@ -1648,8 +1725,9 @@ public class PackageParser { return null; } if (intent.countActions() == 0) { - Log.w(TAG, "Intent filter for activity " + intent - + " defines no actions"); + Log.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { a.intents.add(intent); } @@ -1662,9 +1740,13 @@ public class PackageParser { if (!RIGID_PARSER) { Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); if (receiver) { - Log.w(TAG, "Unknown element under <receiver>: " + parser.getName()); + Log.w(TAG, "Unknown element under <receiver>: " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { - Log.w(TAG, "Unknown element under <activity>: " + parser.getName()); + Log.w(TAG, "Unknown element under <activity>: " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } XmlUtils.skipCurrentTag(parser); continue; @@ -1792,8 +1874,9 @@ public class PackageParser { return null; } if (intent.countActions() == 0) { - Log.w(TAG, "Intent filter for activity alias " + intent - + " defines no actions"); + Log.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { a.intents.add(intent); } @@ -1804,8 +1887,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under <activity-alias>: " + parser.getName()); + Log.w(TAG, "Unknown element under <activity-alias>: " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -1968,8 +2052,9 @@ public class PackageParser { outInfo.info.grantUriPermissions = true; } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No path, pathPrefix, or pathPattern for <path-permission>"); + Log.w(TAG, "Unknown element under <path-permission>: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2009,8 +2094,9 @@ public class PackageParser { if (!havePerm) { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No readPermission or writePermssion for <path-permission>"); + Log.w(TAG, "No readPermission or writePermssion for <path-permission>: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2054,8 +2140,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "No path, pathPrefix, or pathPattern for <path-permission>"); + Log.w(TAG, "No path, pathPrefix, or pathPattern for <path-permission>: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2066,9 +2153,9 @@ public class PackageParser { } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under <provider>: " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2146,9 +2233,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under <service>: " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2185,9 +2272,9 @@ public class PackageParser { } } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); Log.w(TAG, "Unknown element under " + tag + ": " - + parser.getName()); + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } @@ -2243,8 +2330,9 @@ public class PackageParser { data.putFloat(name, v.getFloat()); } else { if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "<meta-data> only supports string, integer, float, color, boolean, and resource reference types"); + Log.w(TAG, "<meta-data> only supports string, integer, float, color, boolean, and resource reference types: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); } else { outError[0] = "<meta-data> only supports string, integer, float, color, boolean, and resource reference types"; data = null; @@ -2278,6 +2366,7 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestIntentFilter_priority, 0); if (priority > 0 && isActivity && (flags&PARSE_IS_SYSTEM) == 0) { Log.w(TAG, "Activity with priority > 0, forcing to 0 at " + + mArchiveSourcePath + " " + parser.getPositionDescription()); priority = 0; } @@ -2375,8 +2464,9 @@ public class PackageParser { sa.recycle(); XmlUtils.skipCurrentTag(parser); } else if (!RIGID_PARSER) { - Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); - Log.w(TAG, "Unknown element under <intent-filter>: " + parser.getName()); + Log.w(TAG, "Unknown element under <intent-filter>: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); } else { outError[0] = "Bad element under <intent-filter>: " + parser.getName(); @@ -2416,7 +2506,8 @@ public class PackageParser { public ArrayList<String> protectedBroadcasts; - public final ArrayList<String> usesLibraries = new ArrayList<String>(); + public ArrayList<String> usesLibraries = null; + public ArrayList<String> usesOptionalLibraries = null; public String[] usesLibraryFiles = null; // We store the application meta-data independently to avoid multiple unwanted references @@ -2464,6 +2555,11 @@ public class PackageParser { public final ArrayList<ConfigurationInfo> configPreferences = new ArrayList<ConfigurationInfo>(); + /* + * Applications requested features + */ + public ArrayList<FeatureInfo> reqFeatures = null; + public Package(String _name) { packageName = _name; applicationInfo.packageName = _name; diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index d01460e..ec01775 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -74,7 +74,12 @@ 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. + */ + @Deprecated 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..b39a67d --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -0,0 +1,498 @@ +/* + * 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.os.Environment; +import android.os.Handler; +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.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.IOException; +import java.io.FileInputStream; + +import com.android.internal.os.AtomicFile; +import com.android.internal.util.FastXmlSerializer; + +import com.google.android.collect.Maps; +import com.google.android.collect.Lists; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlSerializer; + +/** + * 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; + private final XmlSerializerAndParser<V> mSerializerAndParser; + private final AtomicReference<BroadcastReceiver> mReceiver; + + private final Object mServicesLock = new Object(); + // synchronized on mServicesLock + private HashMap<V, Integer> mPersistentServices; + // synchronized on mServicesLock + private Map<V, ServiceInfo<V>> mServices; + // synchronized on mServicesLock + private boolean mPersistentServicesFileDidNotExist; + + /** + * This file contains the list of known services. We would like to maintain this forever + * so we store it as an XML file. + */ + private final AtomicFile mPersistentServicesFile; + + // the listener and handler are synchronized on "this" and must be updated together + private RegisteredServicesCacheListener<V> mListener; + private Handler mHandler; + + public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, + String attributeName, XmlSerializerAndParser<V> serializerAndParser) { + mContext = context; + mInterfaceName = interfaceName; + mMetaDataName = metaDataName; + mAttributesName = attributeName; + mSerializerAndParser = serializerAndParser; + + File dataDir = Environment.getDataDirectory(); + File systemDir = new File(dataDir, "system"); + File syncDir = new File(systemDir, "registered_services"); + mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml")); + + generateServicesMap(); + + final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context1, Intent intent) { + generateServicesMap(); + } + }; + mReceiver = new AtomicReference<BroadcastReceiver>(receiver); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + mContext.registerReceiver(receiver, intentFilter); + } + + public void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + Map<V, ServiceInfo<V>> services; + synchronized (mServicesLock) { + services = mServices; + } + fout.println("RegisteredServicesCache: " + services.size() + " services"); + for (ServiceInfo info : services.values()) { + fout.println(" " + info); + } + } + + public RegisteredServicesCacheListener<V> getListener() { + synchronized (this) { + return mListener; + } + } + + public void setListener(RegisteredServicesCacheListener<V> listener, Handler handler) { + if (handler == null) { + handler = new Handler(mContext.getMainLooper()); + } + synchronized (this) { + mHandler = handler; + mListener = listener; + } + } + + private void notifyListener(final V type, final boolean removed) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "notifyListener: " + type + " is " + (removed ? "removed" : "added")); + } + RegisteredServicesCacheListener<V> listener; + Handler handler; + synchronized (this) { + listener = mListener; + handler = mHandler; + } + if (listener == null) { + return; + } + + final RegisteredServicesCacheListener<V> listener2 = listener; + handler.post(new Runnable() { + public void run() { + listener2.onServiceChanged(type, removed); + } + }); + } + + /** + * 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; + public final int uid; + + private ServiceInfo(V type, ComponentName componentName, int uid) { + this.type = type; + this.componentName = componentName; + this.uid = uid; + } + + @Override + public String toString() { + return "ServiceInfo: " + type + ", " + componentName + ", uid " + uid; + } + } + + /** + * 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) { + synchronized (mServicesLock) { + return mServices.get(type); + } + } + + /** + * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all + * registered authenticators. + */ + public Collection<ServiceInfo<V>> getAllServices() { + synchronized (mServicesLock) { + return Collections.unmodifiableCollection(mServices.values()); + } + } + + /** + * Stops the monitoring of package additions, removals and changes. + */ + public void close() { + final BroadcastReceiver receiver = mReceiver.getAndSet(null); + if (receiver != null) { + mContext.unregisterReceiver(receiver); + } + } + + @Override + protected void finalize() throws Throwable { + if (mReceiver.get() != null) { + Log.e(TAG, "RegisteredServicesCache finalized without being closed"); + } + close(); + super.finalize(); + } + + private boolean inSystemImage(int callerUid) { + String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); + for (String name : packages) { + try { + PackageInfo packageInfo = + mContext.getPackageManager().getPackageInfo(name, 0 /* flags */); + if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + return false; + } + + void generateServicesMap() { + PackageManager pm = mContext.getPackageManager(); + ArrayList<ServiceInfo<V>> serviceInfos = new ArrayList<ServiceInfo<V>>(); + 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) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString()); + continue; + } + serviceInfos.add(info); + } catch (XmlPullParserException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); + } catch (IOException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); + } + } + + synchronized (mServicesLock) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "generateServicesMap: " + mInterfaceName); + } + if (mPersistentServices == null) { + readPersistentServicesLocked(); + } + mServices = Maps.newHashMap(); + boolean changed = false; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "found " + serviceInfos.size() + " services"); + } + for (ServiceInfo<V> info : serviceInfos) { + // four cases: + // - doesn't exist yet + // - add, notify user that it was added + // - exists and the UID is the same + // - replace, don't notify user + // - exists, the UID is different, and the new one is not a system package + // - ignore + // - exists, the UID is different, and the new one is a system package + // - add, notify user that it was added + Integer previousUid = mPersistentServices.get(info.type); + if (previousUid == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered new type: " + info); + } + changed = true; + mServices.put(info.type, info); + mPersistentServices.put(info.type, info.uid); + if (!mPersistentServicesFileDidNotExist) { + notifyListener(info.type, false /* removed */); + } + } else if (previousUid == info.uid) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered existing type with the same uid: " + info); + } + mServices.put(info.type, info); + } else if (inSystemImage(info.uid) + || !containsTypeAndUid(serviceInfos, info.type, previousUid)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + if (inSystemImage(info.uid)) { + Log.d(TAG, "encountered existing type with a new uid but from" + + " the system: " + info); + } else { + Log.d(TAG, "encountered existing type with a new uid but existing was" + + " removed: " + info); + } + } + changed = true; + mServices.put(info.type, info); + mPersistentServices.put(info.type, info.uid); + notifyListener(info.type, false /* removed */); + } else { + // ignore + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "encountered existing type with a new uid, ignoring: " + info); + } + } + } + + ArrayList<V> toBeRemoved = Lists.newArrayList(); + for (V v1 : mPersistentServices.keySet()) { + if (!containsType(serviceInfos, v1)) { + toBeRemoved.add(v1); + } + } + for (V v1 : toBeRemoved) { + mPersistentServices.remove(v1); + changed = true; + notifyListener(v1, true /* removed */); + } + if (changed) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "writing updated list of persistent services"); + } + writePersistentServicesLocked(); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "persistent services did not change, so not writing anything"); + } + } + mPersistentServicesFileDidNotExist = false; + } + } + + private boolean containsType(ArrayList<ServiceInfo<V>> serviceInfos, V type) { + for (int i = 0, N = serviceInfos.size(); i < N; i++) { + if (serviceInfos.get(i).type.equals(type)) { + return true; + } + } + + return false; + } + + private boolean containsTypeAndUid(ArrayList<ServiceInfo<V>> serviceInfos, V type, int uid) { + for (int i = 0, N = serviceInfos.size(); i < N; i++) { + final ServiceInfo<V> serviceInfo = serviceInfos.get(i); + if (serviceInfo.type.equals(type) && serviceInfo.uid == uid) { + return true; + } + } + + return false; + } + + 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; + } + final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo; + final ApplicationInfo applicationInfo = serviceInfo.applicationInfo; + final int uid = applicationInfo.uid; + return new ServiceInfo<V>(v, componentName, uid); + } finally { + if (parser != null) parser.close(); + } + } + + /** + * Read all sync status back in to the initial engine state. + */ + private void readPersistentServicesLocked() { + mPersistentServices = Maps.newHashMap(); + if (mSerializerAndParser == null) { + return; + } + FileInputStream fis = null; + try { + mPersistentServicesFileDidNotExist = !mPersistentServicesFile.getBaseFile().exists(); + if (mPersistentServicesFileDidNotExist) { + return; + } + fis = mPersistentServicesFile.openRead(); + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(fis, null); + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.START_TAG) { + eventType = parser.next(); + } + String tagName = parser.getName(); + if ("services".equals(tagName)) { + eventType = parser.next(); + do { + if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) { + tagName = parser.getName(); + if ("service".equals(tagName)) { + V service = mSerializerAndParser.createFromXml(parser); + if (service == null) { + break; + } + String uidString = parser.getAttributeValue(null, "uid"); + int uid = Integer.parseInt(uidString); + mPersistentServices.put(service, uid); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + } + } catch (Exception e) { + Log.w(TAG, "Error reading persistent services, starting from scratch", e); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (java.io.IOException e1) { + } + } + } + } + + /** + * Write all sync status to the sync status file. + */ + private void writePersistentServicesLocked() { + if (mSerializerAndParser == null) { + return; + } + FileOutputStream fos = null; + try { + fos = mPersistentServicesFile.startWrite(); + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(fos, "utf-8"); + out.startDocument(null, true); + out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + out.startTag(null, "services"); + for (Map.Entry<V, Integer> service : mPersistentServices.entrySet()) { + out.startTag(null, "service"); + out.attribute(null, "uid", Integer.toString(service.getValue())); + mSerializerAndParser.writeAsXml(service.getKey(), out); + out.endTag(null, "service"); + } + out.endTag(null, "services"); + out.endDocument(); + mPersistentServicesFile.finishWrite(fos); + } catch (java.io.IOException e1) { + Log.w(TAG, "Error writing accounts", e1); + if (fos != null) { + mPersistentServicesFile.failWrite(fos); + } + } + } + + public abstract V parseServiceAttributes(String packageName, AttributeSet attrs); +} diff --git a/core/java/android/content/pm/RegisteredServicesCacheListener.java b/core/java/android/content/pm/RegisteredServicesCacheListener.java new file mode 100644 index 0000000..2bc0942 --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCacheListener.java @@ -0,0 +1,16 @@ +package android.content.pm; + +import android.os.Parcelable; + +/** + * Listener for changes to the set of registered services managed by a RegisteredServicesCache. + * @hide + */ +public interface RegisteredServicesCacheListener<V> { + /** + * Invoked when a service is registered or changed. + * @param type the type of registered service + * @param removed true if the service was removed + */ + void onServiceChanged(V type, boolean removed); +} diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index ee49c02..380db65 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -92,6 +92,13 @@ public class ResolveInfo implements Parcelable { public int icon; /** + * Optional -- if non-null, the {@link #labelRes} and {@link #icon} + * resources will be loaded from this package, rather than the one + * containing the resolved component. + */ + public String resolvePackageName; + + /** * Retrieve the current textual label associated with this resolution. This * will call back on the given PackageManager to load the label from * the application. @@ -106,9 +113,15 @@ public class ResolveInfo implements Parcelable { if (nonLocalizedLabel != null) { return nonLocalizedLabel; } + CharSequence label; + if (resolvePackageName != null && labelRes != 0) { + label = pm.getText(resolvePackageName, labelRes, null); + if (label != null) { + return label; + } + } ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; ApplicationInfo ai = ci.applicationInfo; - CharSequence label; if (labelRes != 0) { label = pm.getText(ci.packageName, labelRes, ai); if (label != null) { @@ -133,6 +146,12 @@ public class ResolveInfo implements Parcelable { ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; ApplicationInfo ai = ci.applicationInfo; Drawable dr; + if (resolvePackageName != null && icon != 0) { + dr = pm.getDrawable(resolvePackageName, icon, null); + if (dr != null) { + return dr; + } + } if (icon != 0) { dr = pm.getDrawable(ci.packageName, icon, ai); if (dr != null) { @@ -160,24 +179,26 @@ public class ResolveInfo implements Parcelable { if (filter != null) { pw.println(prefix + "Filter:"); filter.dump(pw, prefix + " "); - } else { - pw.println(prefix + "Filter: null"); } pw.println(prefix + "priority=" + priority + " preferredOrder=" + preferredOrder + " match=0x" + Integer.toHexString(match) + " specificIndex=" + specificIndex + " isDefault=" + isDefault); - pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes) - + " nonLocalizedLabel=" + nonLocalizedLabel - + " icon=0x" + Integer.toHexString(icon)); + if (resolvePackageName != null) { + pw.println(prefix + "resolvePackageName=" + resolvePackageName); + } + if (labelRes != 0 || nonLocalizedLabel != null || icon != 0) { + pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes) + + " nonLocalizedLabel=" + nonLocalizedLabel + + " icon=0x" + Integer.toHexString(icon)); + } if (activityInfo != null) { pw.println(prefix + "ActivityInfo:"); activityInfo.dump(pw, prefix + " "); } else if (serviceInfo != null) { pw.println(prefix + "ServiceInfo:"); - // TODO - //serviceInfo.dump(pw, prefix + " "); + serviceInfo.dump(pw, prefix + " "); } } @@ -219,6 +240,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(labelRes); TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags); dest.writeInt(icon); + dest.writeString(resolvePackageName); } public static final Creator<ResolveInfo> CREATOR @@ -257,6 +279,7 @@ public class ResolveInfo implements Parcelable { nonLocalizedLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); icon = source.readInt(); + resolvePackageName = source.readString(); } public static class DisplayNameComparator diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index b60650c..51d2a4d 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -2,6 +2,7 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; +import android.util.Printer; /** * Information you can retrieve about a particular application @@ -24,6 +25,11 @@ public class ServiceInfo extends ComponentInfo permission = orig.permission; } + public void dump(Printer pw, String prefix) { + super.dumpFront(pw, prefix); + pw.println(prefix + "permission=" + permission); + } + public String toString() { return "ServiceInfo{" + Integer.toHexString(System.identityHashCode(this)) diff --git a/core/java/android/content/pm/XmlSerializerAndParser.java b/core/java/android/content/pm/XmlSerializerAndParser.java new file mode 100644 index 0000000..33598f0 --- /dev/null +++ b/core/java/android/content/pm/XmlSerializerAndParser.java @@ -0,0 +1,14 @@ +package android.content.pm; + +import org.xmlpull.v1.XmlSerializer; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import android.os.Parcel; + +import java.io.IOException; + +/** @hide */ +public interface XmlSerializerAndParser<T> { + void writeAsXml(T item, XmlSerializer out) throws IOException; + T createFromXml(XmlPullParser parser) throws IOException, XmlPullParserException; +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 0bc8a9d..0d43b2a 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -24,7 +24,6 @@ import android.util.TypedValue; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Locale; /** * Provides access to an application's raw asset files; see {@link Resources} diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java index 50faf57..11c67cc 100644 --- a/core/java/android/content/res/CompatibilityInfo.java +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -274,6 +274,25 @@ public class CompatibilityInfo { * Apply translation to the canvas that is necessary to draw the content. */ public void translateCanvas(Canvas canvas) { + if (applicationScale == 1.5f) { + /* When we scale for compatibility, we can put our stretched + bitmaps and ninepatches on exacty 1/2 pixel boundaries, + which can give us inconsistent drawing due to imperfect + float precision in the graphics engine's inverse matrix. + + As a work-around, we translate by a tiny amount to avoid + landing on exact pixel centers and boundaries, giving us + the slop we need to draw consistently. + + This constant is meant to resolve to 1/255 after it is + scaled by 1.5 (applicationScale). Note, this is just a guess + as to what is small enough not to create its own artifacts, + and big enough to avoid the precision problems. Feel free + to experiment with smaller values as you choose. + */ + final float tinyOffset = 2.0f / (3 * 255); + canvas.translate(tinyOffset, tinyOffset); + } canvas.scale(applicationScale, applicationScale); } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index cbf8410..1fe34b5 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -138,6 +138,18 @@ public final class Configuration implements Parcelable, Comparable<Configuration */ public int navigation; + public static final int NAVIGATIONHIDDEN_UNDEFINED = 0; + public static final int NAVIGATIONHIDDEN_NO = 1; + public static final int NAVIGATIONHIDDEN_YES = 2; + + /** + * A flag indicating whether any 5-way or DPAD navigation available. + * This will be set on a device with a mechanism to hide the navigation + * controls from the user, when that mechanism is closed. One of: + * {@link #NAVIGATIONHIDDEN_NO}, {@link #NAVIGATIONHIDDEN_YES}. + */ + public int navigationHidden; + public static final int ORIENTATION_UNDEFINED = 0; public static final int ORIENTATION_PORTRAIT = 1; public static final int ORIENTATION_LANDSCAPE = 2; @@ -174,6 +186,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration keyboardHidden = o.keyboardHidden; hardKeyboardHidden = o.hardKeyboardHidden; navigation = o.navigation; + navigationHidden = o.navigationHidden; orientation = o.orientation; screenLayout = o.screenLayout; } @@ -198,6 +211,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration sb.append(hardKeyboardHidden); sb.append(" nav="); sb.append(navigation); + sb.append("/"); + sb.append(navigationHidden); sb.append(" orien="); sb.append(orientation); sb.append(" layout="); @@ -219,6 +234,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration keyboardHidden = KEYBOARDHIDDEN_UNDEFINED; hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED; navigation = NAVIGATION_UNDEFINED; + navigationHidden = NAVIGATIONHIDDEN_UNDEFINED; orientation = ORIENTATION_UNDEFINED; screenLayout = SCREENLAYOUT_SIZE_UNDEFINED; } @@ -286,6 +302,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_NAVIGATION; navigation = delta.navigation; } + if (delta.navigationHidden != NAVIGATIONHIDDEN_UNDEFINED + && navigationHidden != delta.navigationHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + navigationHidden = delta.navigationHidden; + } if (delta.orientation != ORIENTATION_UNDEFINED && orientation != delta.orientation) { changed |= ActivityInfo.CONFIG_ORIENTATION; @@ -360,6 +381,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && navigation != delta.navigation) { changed |= ActivityInfo.CONFIG_NAVIGATION; } + if (delta.navigationHidden != NAVIGATIONHIDDEN_UNDEFINED + && navigationHidden != delta.navigationHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + } if (delta.orientation != ORIENTATION_UNDEFINED && orientation != delta.orientation) { changed |= ActivityInfo.CONFIG_ORIENTATION; @@ -416,6 +441,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(keyboardHidden); dest.writeInt(hardKeyboardHidden); dest.writeInt(navigation); + dest.writeInt(navigationHidden); dest.writeInt(orientation); dest.writeInt(screenLayout); } @@ -448,6 +474,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration keyboardHidden = source.readInt(); hardKeyboardHidden = source.readInt(); navigation = source.readInt(); + navigationHidden = source.readInt(); orientation = source.readInt(); screenLayout = source.readInt(); } @@ -478,6 +505,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (n != 0) return n; n = this.navigation - that.navigation; if (n != 0) return n; + n = this.navigationHidden - that.navigationHidden; + if (n != 0) return n; n = this.orientation - that.orientation; if (n != 0) return n; n = this.screenLayout - that.screenLayout; @@ -503,6 +532,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration return ((int)this.fontScale) + this.mcc + this.mnc + this.locale.hashCode() + this.touchscreen + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden - + this.navigation + this.orientation + this.screenLayout; + + this.navigation + this.navigationHidden + + this.orientation + this.screenLayout; } } diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 2354519..1c0ed36 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -22,10 +22,10 @@ import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import android.content.pm.ApplicationInfo; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; +import android.os.Build; import android.os.Bundle; import android.os.SystemProperties; import android.util.AttributeSet; @@ -51,8 +51,10 @@ public class Resources { private static final boolean DEBUG_CONFIG = false; private static final boolean TRACE_FOR_PRELOAD = false; - private static final int sSdkVersion = SystemProperties.getInt( - "ro.build.version.sdk", 0); + // Use the current SDK version code. If we are a development build, + // also allow the previous SDK version + 1. + private static final int sSdkVersion = Build.VERSION.SDK_INT + + ("REL".equals(Build.VERSION.CODENAME) ? 0 : 1); private static final Object mSync = new Object(); private static Resources mSystem = null; @@ -65,8 +67,6 @@ public class Resources { = new SparseArray<ColorStateList>(); private static boolean mPreloaded; - private final LongSparseArray<Drawable.ConstantState> mPreloadedDrawables; - /*package*/ final TypedValue mTmpValue = new TypedValue(); // These are protected by the mTmpValue lock. @@ -157,11 +157,6 @@ public class Resources { } updateConfiguration(config, metrics); assets.ensureStringBlocks(); - if (mCompatibilityInfo.isScalingRequired()) { - mPreloadedDrawables = emptySparseArray(); - } else { - mPreloadedDrawables = sPreloadedDrawables; - } } /** @@ -1668,9 +1663,9 @@ public class Resources { return dr; } - Drawable.ConstantState cs = mPreloadedDrawables.get(key); + Drawable.ConstantState cs = sPreloadedDrawables.get(key); if (cs != null) { - dr = cs.newDrawable(); + dr = cs.newDrawable(this); } else { if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { @@ -1705,9 +1700,10 @@ public class Resources { } else { try { InputStream is = mAssets.openNonAsset( - value.assetCookie, file, AssetManager.ACCESS_BUFFER); + value.assetCookie, file, AssetManager.ACCESS_STREAMING); // System.out.println("Opened file " + file + ": " + is); - dr = Drawable.createFromResourceStream(this, value, is, file); + dr = Drawable.createFromResourceStream(this, value, is, + file, null); is.close(); // System.out.println("Created stream: " + dr); } catch (Exception e) { @@ -1750,7 +1746,7 @@ public class Resources { //Log.i(TAG, "Returning cached drawable @ #" + // Integer.toHexString(((Integer)key).intValue()) // + " in " + this + ": " + entry); - return entry.newDrawable(); + return entry.newDrawable(this); } else { // our entry has been purged mDrawableCache.delete(key); @@ -1974,7 +1970,6 @@ public class Resources { mMetrics.setToDefaults(); updateConfiguration(null, null); mAssets.ensureStringBlocks(); - mPreloadedDrawables = sPreloadedDrawables; mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; } } diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index e684cb8..8fb82be 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -202,7 +202,7 @@ final class StringBlock { sub = subtag(tag, ";size="); if (sub != null) { int size = Integer.parseInt(sub); - buffer.setSpan(new AbsoluteSizeSpan(size), + buffer.setSpan(new AbsoluteSizeSpan(size, true), style[i+1], style[i+2]+1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } @@ -310,7 +310,7 @@ final class StringBlock { * the ascent if possible, or the descent if shrinking the ascent further * will make the text unreadable. */ - private static class Height implements LineHeightSpan { + private static class Height implements LineHeightSpan.WithDensity { private int mSize; private static float sProportion = 0; @@ -321,9 +321,21 @@ final class StringBlock { public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) { - if (fm.bottom - fm.top < mSize) { - fm.top = fm.bottom - mSize; - fm.ascent = fm.ascent - mSize; + // Should not get called, at least not by StaticLayout. + chooseHeight(text, start, end, spanstartv, v, fm, null); + } + + public void chooseHeight(CharSequence text, int start, int end, + int spanstartv, int v, + Paint.FontMetricsInt fm, TextPaint paint) { + int size = mSize; + if (paint != null) { + size *= paint.density; + } + + if (fm.bottom - fm.top < size) { + fm.top = fm.bottom - size; + fm.ascent = fm.ascent - size; } else { if (sProportion == 0) { /* @@ -343,27 +355,27 @@ final class StringBlock { int need = (int) Math.ceil(-fm.top * sProportion); - if (mSize - fm.descent >= need) { + if (size - fm.descent >= need) { /* * It is safe to shrink the ascent this much. */ - fm.top = fm.bottom - mSize; - fm.ascent = fm.descent - mSize; - } else if (mSize >= need) { + fm.top = fm.bottom - size; + fm.ascent = fm.descent - size; + } else if (size >= need) { /* * We can't show all the descent, but we can at least * show all the ascent. */ fm.top = fm.ascent = -need; - fm.bottom = fm.descent = fm.top + mSize; + fm.bottom = fm.descent = fm.top + size; } else { /* * Show as much of the ascent as we can, and no descent. */ - fm.top = fm.ascent = -mSize; + fm.top = fm.ascent = -size; fm.bottom = fm.descent = 0; } } |