summaryrefslogtreecommitdiffstats
path: root/core/java/android/content
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/content')
-rw-r--r--core/java/android/content/AbstractCursorEntityIterator.java121
-rw-r--r--core/java/android/content/AbstractSyncableContentProvider.java405
-rw-r--r--core/java/android/content/AbstractTableMerger.java606
-rw-r--r--core/java/android/content/AbstractThreadedSyncAdapter.java221
-rw-r--r--core/java/android/content/ActiveSyncInfo.java9
-rw-r--r--core/java/android/content/AsyncQueryHandler.java80
-rw-r--r--core/java/android/content/BroadcastReceiver.java32
-rw-r--r--core/java/android/content/ContentProvider.java82
-rw-r--r--core/java/android/content/ContentProviderClient.java128
-rw-r--r--core/java/android/content/ContentProviderNative.java164
-rw-r--r--core/java/android/content/ContentProviderOperation.java577
-rw-r--r--core/java/android/content/ContentProviderResult.java84
-rw-r--r--core/java/android/content/ContentResolver.java470
-rw-r--r--core/java/android/content/ContentService.java124
-rw-r--r--core/java/android/content/Context.java171
-rw-r--r--core/java/android/content/ContextWrapper.java18
-rw-r--r--core/java/android/content/Entity.aidl20
-rw-r--r--core/java/android/content/Entity.java105
-rw-r--r--core/java/android/content/EntityIterator.java54
-rw-r--r--core/java/android/content/IContentProvider.java16
-rw-r--r--core/java/android/content/IContentService.aidl38
-rw-r--r--core/java/android/content/IEntityIterator.java210
-rwxr-xr-xcore/java/android/content/IIntentReceiver.aidl2
-rw-r--r--core/java/android/content/ISyncAdapter.aidl8
-rw-r--r--core/java/android/content/Intent.java242
-rw-r--r--core/java/android/content/IntentSender.java7
-rw-r--r--core/java/android/content/OperationApplicationException.java54
-rw-r--r--core/java/android/content/SyncAdapter.java15
-rw-r--r--core/java/android/content/SyncAdapterType.aidl20
-rw-r--r--core/java/android/content/SyncAdapterType.java145
-rw-r--r--core/java/android/content/SyncAdaptersCache.java80
-rw-r--r--core/java/android/content/SyncContext.java15
-rw-r--r--core/java/android/content/SyncManager.java788
-rw-r--r--core/java/android/content/SyncResult.java23
-rw-r--r--core/java/android/content/SyncStateContentProviderHelper.java43
-rw-r--r--core/java/android/content/SyncStats.java21
-rw-r--r--core/java/android/content/SyncStatusInfo.java3
-rw-r--r--core/java/android/content/SyncStatusObserver.java (renamed from core/java/android/content/SyncUIContext.java)22
-rw-r--r--core/java/android/content/SyncStorageEngine.java529
-rw-r--r--core/java/android/content/SyncableContentProvider.java29
-rw-r--r--core/java/android/content/TempProviderSyncAdapter.java63
-rw-r--r--core/java/android/content/pm/ActivityInfo.java14
-rw-r--r--core/java/android/content/pm/ApplicationInfo.java29
-rwxr-xr-xcore/java/android/content/pm/ConfigurationInfo.java16
-rwxr-xr-xcore/java/android/content/pm/FeatureInfo.aidl19
-rw-r--r--core/java/android/content/pm/FeatureInfo.java101
-rw-r--r--core/java/android/content/pm/IPackageManager.aidl11
-rw-r--r--core/java/android/content/pm/LabeledIntent.java177
-rw-r--r--core/java/android/content/pm/PackageInfo.java7
-rw-r--r--core/java/android/content/pm/PackageManager.java63
-rw-r--r--core/java/android/content/pm/PackageParser.java294
-rw-r--r--core/java/android/content/pm/ProviderInfo.java7
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java498
-rw-r--r--core/java/android/content/pm/RegisteredServicesCacheListener.java16
-rw-r--r--core/java/android/content/pm/ResolveInfo.java39
-rw-r--r--core/java/android/content/pm/ServiceInfo.java6
-rw-r--r--core/java/android/content/pm/XmlSerializerAndParser.java14
-rw-r--r--core/java/android/content/res/AssetManager.java1
-rw-r--r--core/java/android/content/res/CompatibilityInfo.java19
-rw-r--r--core/java/android/content/res/Configuration.java32
-rw-r--r--core/java/android/content/res/Resources.java27
-rw-r--r--core/java/android/content/res/StringBlock.java34
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 &lt;uses-configuration&gt; and the &lt;uses-feature&gt;tags.
+ * AndroidManifest.xml's &lt;uses-configuration&gt; and &lt;uses-feature&gt; 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 &lt;uses-feature&gt; 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;
}
}