diff options
Diffstat (limited to 'core/java/android/content')
95 files changed, 32951 insertions, 0 deletions
diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java new file mode 100644 index 0000000..ce6501c --- /dev/null +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -0,0 +1,601 @@ +package android.content; + +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.provider.SyncConstValue; +import android.util.Config; +import android.util.Log; +import android.os.Bundle; +import android.text.TextUtils; + +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; +import java.util.Vector; +import java.util.ArrayList; + +/** + * A specialization of the ContentProvider that centralizes functionality + * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider + * inside of database transactions. + * + * @hide + */ +public abstract class AbstractSyncableContentProvider extends SyncableContentProvider { + private static final String TAG = "SyncableContentProvider"; + protected SQLiteOpenHelper mOpenHelper; + protected SQLiteDatabase mDb; + 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 SyncStateContentProviderHelper mSyncState = null; + + private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT}; + + private boolean mIsTemporary; + + private AbstractTableMerger mCurrentMerger = null; + private boolean mIsMergeCancelled = false; + + private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?"; + + protected boolean isTemporary() { + return mIsTemporary; + } + + /** + * Indicates whether or not this ContentProvider contains a full + * set of data or just diffs. This knowledge comes in handy when + * determining how to incorporate the contents of a temporary + * provider into a real provider. + */ + private boolean mContainsDiffs; + + /** + * Initializes the AbstractSyncableContentProvider + * @param dbName the filename of the database + * @param dbVersion the current version of the database schema + * @param contentUri The base Uri of the syncable content in this provider + */ + public AbstractSyncableContentProvider(String dbName, int dbVersion, Uri contentUri) { + super(); + + mDatabaseName = dbName; + mDatabaseVersion = dbVersion; + mContentUri = contentUri; + mIsTemporary = false; + setContainsDiffs(false); + if (Config.LOGV) { + Log.v(TAG, "created SyncableContentProvider " + this); + } + } + + /** + * Close resources that must be closed. You must call this to properly release + * the resources used by the AbstractSyncableContentProvider. + */ + public void close() { + if (mOpenHelper != null) { + mOpenHelper.close(); // OK to call .close() repeatedly. + } + } + + /** + * Override to create your schema and do anything else you need to do with a new database. + * This is run inside a transaction (so you don't need to use one). + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + */ + protected void bootstrapDatabase(SQLiteDatabase db) {} + + /** + * Override to upgrade your database from an old version to the version you specified. + * Don't set the DB version; this will automatically be done after the method returns. + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + * + * @param oldVersion version of the existing database + * @param newVersion current version to upgrade to + * @return true if the upgrade was lossless, false if it was lossy + */ + protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion); + + /** + * Override to do anything (like cleanups or checks) you need to do after opening a database. + * Does nothing by default. This is run inside a transaction (so you don't need to use one). + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + */ + protected void onDatabaseOpened(SQLiteDatabase db) {} + + private class DatabaseHelper extends SQLiteOpenHelper { + DatabaseHelper(Context context, String name) { + // Note: context and name may be null for temp providers + super(context, name, null, mDatabaseVersion); + } + + @Override + public void onCreate(SQLiteDatabase db) { + bootstrapDatabase(db); + mSyncState.createDatabase(db); + } + + @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()); + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + onDatabaseOpened(db); + mSyncState.onDatabaseOpened(db); + } + } + + @Override + public boolean onCreate() { + if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider"); + 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); + + return true; + } + + /** + * Get a non-persistent instance of this content provider. + * You must call {@link #close} on the returned + * SyncableContentProvider when you are done with it. + * + * @return a non-persistent content provider with the same layout as this + * provider. + */ + public AbstractSyncableContentProvider getTemporaryInstance() { + AbstractSyncableContentProvider temp; + try { + temp = getClass().newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException("unable to instantiate class, " + + "this should never happen", e); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "IllegalAccess while instantiating class, " + + "this should never happen", e); + } + + // Note: onCreate() isn't run for the temp provider, and it has no Context. + temp.mIsTemporary = true; + temp.setContainsDiffs(true); + temp.mOpenHelper = temp.new DatabaseHelper(null, null); + temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper); + if (!isTemporary()) { + mSyncState.copySyncState( + mOpenHelper.getReadableDatabase(), + temp.mOpenHelper.getWritableDatabase(), + getSyncingAccount()); + } + return temp; + } + + public SQLiteDatabase getDatabase() { + if (mDb == null) mDb = mOpenHelper.getWritableDatabase(); + return mDb; + } + + public boolean getContainsDiffs() { + return mContainsDiffs; + } + + public void setContainsDiffs(boolean containsDiffs) { + if (containsDiffs && !isTemporary()) { + throw new IllegalStateException( + "only a temporary provider can contain diffs"); + } + mContainsDiffs = containsDiffs; + } + + /** + * Each subclass of this class should define a subclass of {@link + * android.content.AbstractTableMerger} for each table they wish to merge. It + * should then override this method and return one instance of + * each merger, in sequence. Their {@link + * android.content.AbstractTableMerger#merge merge} methods will be called, one at a + * time, in the order supplied. + * + * <p>The default implementation returns an empty list, so that no + * merging will occur. + * @return A sequence of subclasses of {@link + * android.content.AbstractTableMerger}, one for each table that should be merged. + */ + protected Iterable<? extends AbstractTableMerger> getMergers() { + return Collections.emptyList(); + } + + @Override + public final int update(final Uri url, final ContentValues values, + final String selection, final String[] selectionArgs) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransaction(); + try { + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().update( + url, values, selection, selectionArgs); + mDb.setTransactionSuccessful(); + return numRows; + } + + int result = updateInternal(url, values, selection, selectionArgs); + mDb.setTransactionSuccessful(); + + if (!isTemporary() && result > 0) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + + return result; + } finally { + mDb.endTransaction(); + } + } + + @Override + public final int delete(final Uri url, final String selection, + final String[] selectionArgs) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransaction(); + try { + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); + mDb.setTransactionSuccessful(); + return numRows; + } + int result = deleteInternal(url, selection, selectionArgs); + mDb.setTransactionSuccessful(); + if (!isTemporary() && result > 0) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + return result; + } finally { + mDb.endTransaction(); + } + } + + @Override + public final Uri insert(final Uri url, final ContentValues values) { + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransaction(); + try { + if (isTemporary() && mSyncState.matches(url)) { + Uri result = mSyncState.asContentProvider().insert(url, values); + mDb.setTransactionSuccessful(); + return result; + } + Uri result = insertInternal(url, values); + mDb.setTransactionSuccessful(); + if (!isTemporary() && result != null) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + return result; + } finally { + mDb.endTransaction(); + } + } + + @Override + public final int bulkInsert(final Uri uri, final ContentValues[] values) { + int size = values.length; + int completed = 0; + final boolean isSyncStateUri = mSyncState.matches(uri); + mDb = mOpenHelper.getWritableDatabase(); + mDb.beginTransaction(); + try { + for (int i = 0; i < size; i++) { + Uri result; + if (isTemporary() && isSyncStateUri) { + result = mSyncState.asContentProvider().insert(uri, values[i]); + } else { + result = insertInternal(uri, values[i]); + mDb.yieldIfContended(); + } + if (result != null) { + completed++; + } + } + mDb.setTransactionSuccessful(); + } finally { + mDb.endTransaction(); + } + if (!isTemporary() && completed == size) { + getContext().getContentResolver().notifyChange(uri, null /* observer */, + changeRequiresLocalSync(uri)); + } + return completed; + } + + /** + * 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 + */ + public boolean changeRequiresLocalSync(Uri uri) { + return true; + } + + @Override + public final Cursor query(final Uri url, final String[] projection, + final String selection, final String[] selectionArgs, + final String sortOrder) { + mDb = mOpenHelper.getReadableDatabase(); + if (isTemporary() && mSyncState.matches(url)) { + return mSyncState.asContentProvider().query( + url, projection, selection, selectionArgs, sortOrder); + } + return queryInternal(url, projection, selection, selectionArgs, sortOrder); + } + + /** + * Called right before a sync is started. + * + * @param context the sync context for the operation + * @param account + */ + public void onSyncStart(SyncContext context, String account) { + if (TextUtils.isEmpty(account)) { + throw new IllegalArgumentException("you passed in an empty account"); + } + mSyncingAccount = account; + } + + /** + * Called right after a sync is completed + * + * @param context the sync context for the operation + * @param success true if the sync succeeded, false if an error occurred + */ + public void onSyncStop(SyncContext context, boolean success) { + } + + /** + * The account of the most recent call to onSyncStart() + * @return the account + */ + public String getSyncingAccount() { + return mSyncingAccount; + } + + /** + * Merge diffs from a sync source with this content provider. + * + * @param context the SyncContext within which this merge is taking place + * @param diffs A temporary content provider containing diffs from a sync + * source. + * @param result a MergeResult that contains information about the merge, including + * a temporary content provider with the same layout as this provider containing + * @param syncResult + */ + public void merge(SyncContext context, SyncableContentProvider diffs, + TempProviderSyncResult result, SyncResult syncResult) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + synchronized(this) { + mIsMergeCancelled = false; + } + Iterable<? extends AbstractTableMerger> mergers = getMergers(); + try { + for (AbstractTableMerger merger : mergers) { + synchronized(this) { + if (mIsMergeCancelled) break; + mCurrentMerger = merger; + } + merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this); + } + if (mIsMergeCancelled) return; + if (diffs != null) { + mSyncState.copySyncState( + ((AbstractSyncableContentProvider)diffs).mOpenHelper.getReadableDatabase(), + mOpenHelper.getWritableDatabase(), + getSyncingAccount()); + } + } finally { + synchronized (this) { + mCurrentMerger = null; + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + + /** + * Invoked when the active sync has been canceled. Sets the sync state of this provider and + * its merger to canceled. + */ + public void onSyncCanceled() { + synchronized (this) { + mIsMergeCancelled = true; + if (mCurrentMerger != null) { + mCurrentMerger.onMergeCancelled(); + } + } + } + + + public boolean isMergeCancelled() { + return mIsMergeCancelled; + } + + /** + * Subclasses should override this instead of update(). See update() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract int updateInternal(Uri url, ContentValues values, + String selection, String[] selectionArgs); + + /** + * Subclasses should override this instead of delete(). See delete() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs); + + /** + * Subclasses should override this instead of insert(). See insert() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract Uri insertInternal(Uri url, ContentValues values); + + /** + * Subclasses should override this instead of query(). See query() + * for details. + * + * <p> This method is *not* called within a acquireDbLock()/releaseDbLock() + * block for performance reasons. If an implementation needs atomic access + * to the database the lock can be acquired then. + */ + protected abstract Cursor queryInternal(Uri url, String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + /** + * 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) { + accounts.put(account, false); + } + accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false); + + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Map<String, String> tableMap = db.getSyncedTables(); + Vector<String> tables = new Vector<String>(); + tables.addAll(tableMap.keySet()); + tables.addAll(tableMap.values()); + + db.beginTransaction(); + try { + mSyncState.onAccountsChanged(accountsArray); + for (String table : tables) { + deleteRowsForRemovedAccounts(accounts, table, + SyncConstValue._SYNC_ACCOUNT); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * A helper method to delete all rows whose account is not in the accounts + * map. The accountColumnName is the name of the column that is expected + * to hold the account. If a row has an empty account it is never deleted. + * + * @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) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor c = db.query(table, sAccountProjection, null, null, + accountColumnName, null, null); + try { + while (c.moveToNext()) { + String account = c.getString(0); + if (TextUtils.isEmpty(account)) { + continue; + } + if (!accounts.containsKey(account)) { + int numDeleted; + numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account}); + if (Config.LOGV) { + Log.v(TAG, "deleted " + numDeleted + + " records from table " + table + + " for account " + account); + } + } + } + } finally { + c.close(); + } + } + + /** + * Called when the sync system determines that this provider should no longer + * contain records for the specified account. + */ + public void wipeAccount(String account) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Map<String, String> tableMap = db.getSyncedTables(); + ArrayList<String> tables = new ArrayList<String>(); + tables.addAll(tableMap.keySet()); + tables.addAll(tableMap.values()); + + db.beginTransaction(); + + try { + // remove the SyncState data + mSyncState.discardSyncData(db, account); + + // remove the data in the synced tables + for (String table : tables) { + db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account}); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Retrieves the SyncData bytes for the given account. The byte array returned may be null. + */ + public byte[] readSyncDataBytes(String 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) { + mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); + } +} diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java new file mode 100644 index 0000000..700f1d8 --- /dev/null +++ b/core/java/android/content/AbstractTableMerger.java @@ -0,0 +1,582 @@ +/* + * Copyright (C) 2006 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.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.os.Debug; +import android.provider.BaseColumns; +import static android.provider.SyncConstValue.*; +import android.text.TextUtils; +import android.util.Log; + +/** + * @hide + */ +public abstract class AbstractTableMerger +{ + private ContentValues mValues; + + protected SQLiteDatabase mDb; + protected String mTable; + protected Uri mTableURL; + protected String mDeletedTable; + protected Uri mDeletedTableURL; + static protected ContentValues mSyncMarkValues; + static private boolean TRACE; + + static { + mSyncMarkValues = new ContentValues(); + mSyncMarkValues.put(_SYNC_MARK, 1); + TRACE = false; + } + + private static final String TAG = "AbstractTableMerger"; + private static final String[] syncDirtyProjection = + new String[] {_SYNC_DIRTY, BaseColumns._ID, _SYNC_ID, _SYNC_VERSION}; + private static final String[] syncIdAndVersionProjection = + new String[] {_SYNC_ID, _SYNC_VERSION}; + + private volatile boolean mIsMergeCancelled; + + private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?"; + + private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = + _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?"; + private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; + + private static final String SELECT_UNSYNCED = "" + + _SYNC_DIRTY + " > 0 and (" + _SYNC_ACCOUNT + "=? or " + _SYNC_ACCOUNT + " is null)"; + + public AbstractTableMerger(SQLiteDatabase database, + String table, Uri tableURL, String deletedTable, + Uri deletedTableURL) + { + mDb = database; + mTable = table; + mTableURL = tableURL; + mDeletedTable = deletedTable; + mDeletedTableURL = deletedTableURL; + mValues = new ContentValues(); + } + + public abstract void insertRow(ContentProvider diffs, + Cursor diffsCursor); + public abstract void updateRow(long localPersonID, + ContentProvider diffs, Cursor diffsCursor); + public abstract void resolveRow(long localPersonID, + String syncID, ContentProvider diffs, Cursor diffsCursor); + + /** + * This is called when it is determined that a row should be deleted from the + * ContentProvider. The localCursor is on a table from the local ContentProvider + * and its current position is of the row that should be deleted. The localCursor + * is only guaranteed to contain the BaseColumns.ID column so the implementation + * of deleteRow() must query the database directly if other columns are needed. + * <p> + * It is the responsibility of the implementation of this method to ensure that the cursor + * points to the next row when this method returns, either by calling Cursor.deleteRow() or + * Cursor.next(). + * + * @param localCursor The Cursor into the local table, which points to the row that + * is to be deleted. + */ + public void deleteRow(Cursor localCursor) { + localCursor.deleteRow(); + } + + /** + * After {@link #merge} has completed, this method is called to send + * notifications to {@link android.database.ContentObserver}s of changes + * to the containing {@link ContentProvider}. These notifications likely + * do not want to request a sync back to the network. + */ + protected abstract void notifyChanges(); + + private static boolean findInCursor(Cursor cursor, int column, String id) { + while (!cursor.isAfterLast() && !cursor.isNull(column)) { + int comp = id.compareTo(cursor.getString(column)); + if (comp > 0) { + cursor.moveToNext(); + continue; + } + return comp == 0; + } + return false; + } + + public void onMergeCancelled() { + mIsMergeCancelled = true; + } + + /** + * Carry out a merge of the given diffs, and add the results to + * the given MergeResult. If we are the first merge to find + * client-side diffs, we'll use the given ContentProvider to + * construct a temporary instance to hold them. + */ + public void merge(final SyncContext context, + final String account, + final SyncableContentProvider serverDiffs, + TempProviderSyncResult result, + SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { + mIsMergeCancelled = false; + if (serverDiffs != null) { + if (!mDb.isDbLockedByCurrentThread()) { + throw new IllegalStateException("this must be called from within a DB transaction"); + } + mergeServerDiffs(context, account, serverDiffs, syncResult); + notifyChanges(); + } + + if (result != null) { + findLocalChanges(result, temporaryInstanceFactory, account, syncResult); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete"); + } + + /** + * @hide this is public for testing purposes only + */ + public void mergeServerDiffs(SyncContext context, + String 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 + mDb.update(mTable, mSyncMarkValues, null, null); + if (mDeletedTable != null) { + 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, + 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(); + } + } + + boolean conflict = false; + boolean update = false; + boolean insert = false; + + 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; + } + + // 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); + } + continue; + } + lastSyncId = serverSyncId; + + String localSyncID = null; + boolean localSyncDirty = false; + + while (!localCursor.isAfterLast()) { + if (mIsMergeCancelled) { + localCursor.deactivate(); + deletedCursor.deactivate(); + diffsCursor.deactivate(); + return; + } + 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"); + } + localCursor.moveToNext(); + localSyncID = null; + continue; + } + + 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; + } + + // 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; + } + + // 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(); + } + + break; + } + + // 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"); + } + final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); + if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " + + serverSyncVersion); + } + ContentValues values = new ContentValues(); + values.put(_SYNC_VERSION, serverSyncVersion); + mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + } + continue; + } + + // 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; + } + + if (!TextUtils.isEmpty(localSyncID)) { + // An existing server item has changed + boolean recordChanged = (localSyncVersion == null) || + !serverSyncVersion.equals(localSyncVersion); + if (recordChanged) { + if (localSyncDirty) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + + " conflicts with local _sync_id " + localSyncID + + ", local _id " + localRowId); + } + conflict = true; + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "remote record " + + serverSyncId + + " updates local _sync_id " + + localSyncID + ", local _id " + + localRowId); + } + update = true; + } + } + } else { + // the local db doesn't know about this record so add it + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + } + insert = true; + } + + 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 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}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); + } + } + + 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; + } + // delete all rows that match each element in the diffsCursor + fullyDeleteMatchingRows(diffsCursor, account, syncResult); + mDb.yieldIfContended(); + } + diffsCursor.deactivate(); + } + } + + private void fullyDeleteMatchingRows(Cursor diffsCursor, String 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); + } + try { + c.moveToFirst(); + while (!c.isAfterLast()) { + deleteRow(c); // advances the cursor + syncResult.stats.numDeletes++; + } + } finally { + c.deactivate(); + } + if (deleteBySyncId && mDeletedTable != null) { + mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); + } + } + + /** + * Converts cursor into a Map, using the correct types for the values. + */ + protected void cursorRowToContentValues(Cursor cursor, ContentValues map) { + DatabaseUtils.cursorRowToContentValues(cursor, map); + } + + /** + * 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 + * 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. + * @param account + * @param syncResult + */ + private void findLocalChanges(TempProviderSyncResult mergeResult, + SyncableContentProvider temporaryInstanceFactory, String 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}; + + // Generate the client updates and insertions + // Create a cursor for dirty records + 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(); + } + mValues.clear(); + cursorRowToContentValues(localChangesCursor, mValues); + mValues.remove("_id"); + DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, + _SYNC_LOCAL_ID); + clientDiffs.insert(mTableURL, mValues); + } + localChangesCursor.close(); + + // Generate the client deletions + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); + long numEntries = DatabaseUtils.queryNumEntries(mDb, mTable); + long numDeletedEntries = 0; + if (mDeletedTable != null) { + Cursor deletedCursor = mDb.query(mDeletedTable, + syncIdAndVersionProjection, + _SYNC_ACCOUNT + "=? 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(); + } + mValues.clear(); + DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); + clientDiffs.insert(mDeletedTableURL, mValues); + } + deletedCursor.close(); + } + + if (clientDiffs != null) { + mergeResult.tempContentProvider = clientDiffs; + } + syncResult.stats.numDeletes += numDeletedEntries; + syncResult.stats.numUpdates += numInsertsOrUpdates; + syncResult.stats.numEntries += numEntries; + } +} diff --git a/core/java/android/content/ActivityNotFoundException.java b/core/java/android/content/ActivityNotFoundException.java new file mode 100644 index 0000000..16149bb --- /dev/null +++ b/core/java/android/content/ActivityNotFoundException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2006 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; + +/** + * This exception is thrown when a call to {@link Context#startActivity} or + * one of its variants fails because an Activity can not be found to execute + * the given Intent. + */ +public class ActivityNotFoundException extends RuntimeException +{ + public ActivityNotFoundException() + { + } + + public ActivityNotFoundException(String name) + { + super(name); + } +}; + diff --git a/core/java/android/content/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java new file mode 100644 index 0000000..ac851cc --- /dev/null +++ b/core/java/android/content/AsyncQueryHandler.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 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; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.lang.ref.WeakReference; + +/** + * A helper class to help make handling asynchronous {@link ContentResolver} + * queries easier. + */ +public abstract class AsyncQueryHandler extends Handler { + private static final String TAG = "AsyncQuery"; + private static final boolean localLOGV = false; + + private static final int EVENT_ARG_QUERY = 1; + private static final int EVENT_ARG_INSERT = 2; + private static final int EVENT_ARG_UPDATE = 3; + private static final int EVENT_ARG_DELETE = 4; + + /* package */ final WeakReference<ContentResolver> mResolver; + + private static Looper sLooper = null; + + private Handler mWorkerThreadHandler; + + protected static final class WorkerArgs { + public Uri uri; + public Handler handler; + public String[] projection; + public String selection; + public String[] selectionArgs; + public String orderBy; + public Object result; + public Object cookie; + public ContentValues values; + } + + protected class WorkerHandler extends Handler { + public WorkerHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + final ContentResolver resolver = mResolver.get(); + if (resolver == null) return; + + WorkerArgs args = (WorkerArgs) msg.obj; + + int token = msg.what; + int event = msg.arg1; + + switch (event) { + case EVENT_ARG_QUERY: + Cursor cursor; + try { + cursor = resolver.query(args.uri, args.projection, + args.selection, args.selectionArgs, + args.orderBy); + // Calling getCount() causes the cursor window to be filled, + // which will make the first access on the main thread a lot faster. + if (cursor != null) { + cursor.getCount(); + } + } catch (Exception e) { + cursor = null; + } + + args.result = cursor; + break; + + case EVENT_ARG_INSERT: + args.result = resolver.insert(args.uri, args.values); + break; + + case EVENT_ARG_UPDATE: + args.result = resolver.update(args.uri, args.values, args.selection, + args.selectionArgs); + break; + + case EVENT_ARG_DELETE: + args.result = resolver.delete(args.uri, args.selection, args.selectionArgs); + break; + + } + + // passing the original token value back to the caller + // on top of the event values in arg1. + Message reply = args.handler.obtainMessage(token); + reply.obj = args; + reply.arg1 = msg.arg1; + + if (localLOGV) { + Log.d(TAG, "WorkerHandler.handleMsg: msg.arg1=" + msg.arg1 + + ", reply.what=" + reply.what); + } + + reply.sendToTarget(); + } + } + + public AsyncQueryHandler(ContentResolver cr) { + super(); + mResolver = new WeakReference<ContentResolver>(cr); + synchronized (AsyncQueryHandler.class) { + if (sLooper == null) { + HandlerThread thread = new HandlerThread("AsyncQueryWorker"); + thread.start(); + + sLooper = thread.getLooper(); + } + } + mWorkerThreadHandler = createHandler(sLooper); + } + + protected Handler createHandler(Looper looper) { + return new WorkerHandler(looper); + } + + /** + * This method begins an asynchronous query. When the query is done + * {@link #onQueryComplete} 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 projection A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading data + * from storage that isn't going to be used. + * @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. + */ + public void startQuery(int token, Object cookie, Uri uri, + String[] projection, 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; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.projection = projection; + 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 + * call has completed. + * + * @param token The token representing the operation to be canceled. + * If multiple operations have the same token they will all be canceled. + */ + public final void cancelOperation(int token) { + mWorkerThreadHandler.removeMessages(token); + } + + /** + * This method begins an asynchronous insert. When the insert operation is + * done {@link #onInsertComplete} is called. + * + * @param token A token passed into {@link #onInsertComplete} to identify + * the insert operation. + * @param cookie An object that gets passed into {@link #onInsertComplete} + * @param uri the Uri passed to the insert operation. + * @param initialValues the ContentValues parameter passed to the insert operation. + */ + public final void startInsert(int token, Object cookie, Uri uri, + ContentValues initialValues) { + // Use the token as what so cancelOperations works properly + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_ARG_INSERT; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.cookie = cookie; + args.values = initialValues; + msg.obj = args; + + mWorkerThreadHandler.sendMessage(msg); + } + + /** + * This method begins an asynchronous update. When the update operation is + * done {@link #onUpdateComplete} is called. + * + * @param token A token passed into {@link #onUpdateComplete} to identify + * the update operation. + * @param cookie An object that gets passed into {@link #onUpdateComplete} + * @param uri the Uri passed to the update operation. + * @param values the ContentValues parameter passed to the update operation. + */ + public final void startUpdate(int token, Object cookie, Uri uri, + ContentValues values, String selection, String[] selectionArgs) { + // Use the token as what so cancelOperations works properly + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_ARG_UPDATE; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.cookie = cookie; + args.values = values; + args.selection = selection; + args.selectionArgs = selectionArgs; + msg.obj = args; + + mWorkerThreadHandler.sendMessage(msg); + } + + /** + * This method begins an asynchronous delete. When the delete operation is + * done {@link #onDeleteComplete} is called. + * + * @param token A token passed into {@link #onDeleteComplete} to identify + * the delete operation. + * @param cookie An object that gets passed into {@link #onDeleteComplete} + * @param uri the Uri passed to the delete operation. + * @param selection the where clause. + */ + public final void startDelete(int token, Object cookie, Uri uri, + String selection, String[] selectionArgs) { + // Use the token as what so cancelOperations works properly + Message msg = mWorkerThreadHandler.obtainMessage(token); + msg.arg1 = EVENT_ARG_DELETE; + + WorkerArgs args = new WorkerArgs(); + args.handler = this; + args.uri = uri; + args.cookie = cookie; + args.selection = selection; + args.selectionArgs = selectionArgs; + msg.obj = args; + + mWorkerThreadHandler.sendMessage(msg); + } + + /** + * 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}. + * @param cursor The cursor holding the results from the query. + */ + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + // Empty + } + + /** + * Called when an asynchronous insert is completed. + * + * @param token the token to identify the query, passed in from + * {@link #startInsert}. + * @param cookie the cookie object that's passed in from + * {@link #startInsert}. + * @param uri the uri returned from the insert operation. + */ + protected void onInsertComplete(int token, Object cookie, Uri uri) { + // Empty + } + + /** + * Called when an asynchronous update is completed. + * + * @param token the token to identify the query, passed in from + * {@link #startUpdate}. + * @param cookie the cookie object that's passed in from + * {@link #startUpdate}. + * @param result the result returned from the update operation + */ + protected void onUpdateComplete(int token, Object cookie, int result) { + // Empty + } + + /** + * Called when an asynchronous delete is completed. + * + * @param token the token to identify the query, passed in from + * {@link #startDelete}. + * @param cookie the cookie object that's passed in from + * {@link #startDelete}. + * @param result the result returned from the delete operation + */ + protected void onDeleteComplete(int token, Object cookie, int result) { + // Empty + } + + @Override + public void handleMessage(Message msg) { + WorkerArgs args = (WorkerArgs) msg.obj; + + if (localLOGV) { + Log.d(TAG, "AsyncQueryHandler.handleMessage: msg.what=" + msg.what + + ", msg.arg1=" + msg.arg1); + } + + 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_INSERT: + onInsertComplete(token, args.cookie, (Uri) args.result); + break; + + case EVENT_ARG_UPDATE: + onUpdateComplete(token, args.cookie, (Integer) args.result); + break; + + case EVENT_ARG_DELETE: + onDeleteComplete(token, args.cookie, (Integer) args.result); + break; + } + } +} diff --git a/core/java/android/content/BroadcastReceiver.java b/core/java/android/content/BroadcastReceiver.java new file mode 100644 index 0000000..08f6191 --- /dev/null +++ b/core/java/android/content/BroadcastReceiver.java @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2006 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.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +/** + * Base class for code that will receive intents sent by sendBroadcast(). + * You can either dynamically register an instance of this class with + * {@link Context#registerReceiver Context.registerReceiver()} + * or statically publish an implementation through the + * {@link android.R.styleable#AndroidManifestReceiver <receiver>} + * tag in your <code>AndroidManifest.xml</code>. <em><strong>Note:</strong></em> + * If registering a receiver in your + * {@link android.app.Activity#onResume() Activity.onResume()} + * implementation, you should unregister it in + * {@link android.app.Activity#onPause() Activity.onPause()}. + * (You won't receive intents when paused, + * and this will cut down on unnecessary system overhead). Do not unregister in + * {@link android.app.Activity#onSaveInstanceState(android.os.Bundle) Activity.onSaveInstanceState()}, + * because this won't be called if the user moves back in the history + * stack. + * + * <p>There are two major classes of broadcasts that can be received:</p> + * <ul> + * <li> <b>Normal broadcasts</b> (sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}) are completely asynchronous. All receivers of the + * broadcast are run, in an undefined order, often at the same time. This is + * more efficient, but means that receivers can not use the result or abort + * APIs included here. + * <li> <b>Ordered broadcasts</b> (sent with {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}) are delivered to one receiver at a time. + * As each receiver executes in turn, it can propagate a result to the next + * receiver, or it can completely abort the broadcast so that it won't be passed + * to other receivers. The order receivers runs in can be controlled with the + * {@link android.R.styleable#AndroidManifestIntentFilter_priority + * android:priority} attribute of the matching intent-filter; receivers with + * the same priority will be run in an arbitrary order. + * </ul> + * + * <p>Even in the case of normal broadcasts, the system may in some + * situations revert to delivering the broadcast one receiver at a time. In + * particular, for receivers that may require the creation of a process, only + * one will be run at a time to avoid overloading the system with new processes. + * In this situation, however, the non-ordered semantics hold: these receivers + * can not return results or abort their broadcast.</p> + * + * <p>Note that, although the Intent class is used for sending and receiving + * these broadcasts, the Intent broadcast mechanism here is completely separate + * from Intents that are used to start Activities with + * {@link Context#startActivity Context.startActivity()}. + * There is no way for an BroadcastReceiver + * to see or capture Intents used with startActivity(); likewise, when + * you broadcast an Intent, you will never find or start an Activity. + * These two operations are semantically very different: starting an + * Activity with an Intent is a foreground operation that modifies what the + * user is currently interacting with; broadcasting an Intent is a background + * operation that the user is not normally aware of. + * + * <p>The BroadcastReceiver class (when launched as a component through + * a manifest's {@link android.R.styleable#AndroidManifestReceiver <receiver>} + * tag) is an important part of an + * <a href="{@docRoot}guide/topics/fundamentals.html#lcycles">application's overall lifecycle</a>.</p> + * + * <p>Topics covered here: + * <ol> + * <li><a href="#ReceiverLifecycle">Receiver Lifecycle</a> + * <li><a href="#Permissions">Permissions</a> + * <li><a href="#ProcessLifecycle">Process Lifecycle</a> + * </ol> + * + * <a name="ReceiverLifecycle"></a> + * <h3>Receiver Lifecycle</h3> + * + * <p>A BroadcastReceiver object is only valid for the duration of the call + * to {@link #onReceive}. Once your code returns from this function, + * the system considers the object to be finished and no longer active. + * + * <p>This has important repercussions to what you can do in an + * {@link #onReceive} implementation: anything that requires asynchronous + * operation is not available, because you will need to return from the + * function to handle the asynchronous operation, but at that point the + * BroadcastReceiver is no longer active and thus the system is free to kill + * its process before the asynchronous operation completes. + * + * <p>In particular, you may <i>not</i> show a dialog or bind to a service from + * within an BroadcastReceiver. For the former, you should instead use the + * {@link android.app.NotificationManager} API. For the latter, you can + * use {@link android.content.Context#startService Context.startService()} to + * send a command to the service. + * + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * + * <p>Access permissions can be enforced by either the sender or receiver + * of an Intent. + * + * <p>To enforce a permission when sending, you supply a non-null + * <var>permission</var> argument to + * {@link Context#sendBroadcast(Intent, String)} or + * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)}. + * Only receivers who have been granted this permission + * (by requesting it with the + * {@link android.R.styleable#AndroidManifestUsesPermission <uses-permission>} + * tag in their <code>AndroidManifest.xml</code>) will be able to receive + * the broadcast. + * + * <p>To enforce a permission when receiving, you supply a non-null + * <var>permission</var> when registering your receiver -- either when calling + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)} + * or in the static + * {@link android.R.styleable#AndroidManifestReceiver <receiver>} + * tag in your <code>AndroidManifest.xml</code>. Only broadcasters who have + * been granted this permission (by requesting it with the + * {@link android.R.styleable#AndroidManifestUsesPermission <uses-permission>} + * tag in their <code>AndroidManifest.xml</code>) will be able to send an + * Intent to the receiver. + * + * <p>See the <a href="{@docRoot}guide/topics/security/security.html">Security and Permissions</a> + * document for more information on permissions and security in general. + * + * <a name="ProcessLifecycle"></a> + * <h3>Process Lifecycle</h3> + * + * <p>A process that is currently executing an BroadcastReceiver (that is, + * currently running the code in its {@link #onReceive} method) is + * considered to be a foreground process and will be kept running by the + * system except under cases of extreme memory pressure. + * + * <p>Once you return from onReceive(), the BroadcastReceiver is no longer + * active, and its hosting process is only as important as any other application + * components that are running in it. This is especially important because if + * that process was only hosting the BroadcastReceiver (a common case for + * applications that the user has never or not recently interacted with), then + * upon returning from onReceive() the system will consider its process + * to be empty and aggressively kill it so that resources are available for other + * more important processes. + * + * <p>This means that for longer-running operations you will often use + * a {@link android.app.Service} in conjunction with an BroadcastReceiver to keep + * the containing process active for the entire time of your operation. + */ +public abstract class BroadcastReceiver { + public BroadcastReceiver() { + } + + /** + * This method is called when the BroadcastReceiver is receiving an Intent + * broadcast. During this time you can use the other methods on + * BroadcastReceiver to view/modify the current result values. The function + * is normally called from the main thread of its process, so you should + * never perform long-running operations in it (there is a timeout of + * 10 seconds that the system allows before considering the receiver to + * be blocked and a candidate to be killed). You cannot launch a popup dialog + * in your implementation of onReceive(). + * + * <p><b>If this BroadcastReceiver was launched through a <receiver> tag, + * then the object is no longer alive after returning from this + * function.</b> This means you should not perform any operations that + * return a result to you asynchronously -- in particular, for interacting + * with services, you should use + * {@link Context#startService(Intent)} instead of + * {@link Context#bindService(Intent, ServiceConnection, int)}. If you wish + * to interact with a service that is already running, you can use + * {@link #peekService}. + * + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. + */ + public abstract void onReceive(Context context, Intent intent); + + /** + * Provide a binder to an already-running service. This method is synchronous + * and will not start the target service if it is not present, so it is safe + * to call from {@link #onReceive}. + * + * @param myContext The Context that had been passed to {@link #onReceive(Context, Intent)} + * @param service The Intent indicating the service you wish to use. See {@link + * Context#startService(Intent)} for more information. + */ + public IBinder peekService(Context myContext, Intent service) { + IActivityManager am = ActivityManagerNative.getDefault(); + IBinder binder = null; + try { + binder = am.peekService(service, service.resolveTypeIfNeeded( + myContext.getContentResolver())); + } catch (RemoteException e) { + } + return binder; + } + + /** + * Change the current result code of this broadcast; only works with + * broadcasts sent through + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}. Often uses the + * Activity {@link android.app.Activity#RESULT_CANCELED} and + * {@link android.app.Activity#RESULT_OK} constants, though the + * actual meaning of this value is ultimately up to the broadcaster. + * + * <p><strong>This method does not work with non-ordered broadcasts such + * as those sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}</strong></p> + * + * @param code The new result code. + * + * @see #setResult(int, String, Bundle) + */ + public final void setResultCode(int code) { + checkSynchronousHint(); + mResultCode = code; + } + + /** + * Retrieve the current result code, as set by the previous receiver. + * + * @return int The current result code. + */ + public final int getResultCode() { + return mResultCode; + } + + /** + * Change the current result data of this broadcast; only works with + * broadcasts sent through + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}. This is an arbitrary + * string whose interpretation is up to the broadcaster. + * + * <p><strong>This method does not work with non-ordered broadcasts such + * as those sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}</strong></p> + * + * @param data The new result data; may be null. + * + * @see #setResult(int, String, Bundle) + */ + public final void setResultData(String data) { + checkSynchronousHint(); + mResultData = data; + } + + /** + * Retrieve the current result data, as set by the previous receiver. + * Often this is null. + * + * @return String The current result data; may be null. + */ + public final String getResultData() { + return mResultData; + } + + /** + * Change the current result extras of this broadcast; only works with + * broadcasts sent through + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}. This is a Bundle + * holding arbitrary data, whose interpretation is up to the + * broadcaster. Can be set to null. Calling this method completely + * replaces the current map (if any). + * + * <p><strong>This method does not work with non-ordered broadcasts such + * as those sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}</strong></p> + * + * @param extras The new extra data map; may be null. + * + * @see #setResult(int, String, Bundle) + */ + public final void setResultExtras(Bundle extras) { + checkSynchronousHint(); + mResultExtras = extras; + } + + /** + * Retrieve the current result extra data, as set by the previous receiver. + * Any changes you make to the returned Map will be propagated to the next + * receiver. + * + * @param makeMap If true then a new empty Map will be made for you if the + * current Map is null; if false you should be prepared to + * receive a null Map. + * + * @return Map The current extras map. + */ + public final Bundle getResultExtras(boolean makeMap) { + Bundle e = mResultExtras; + if (!makeMap) return e; + if (e == null) mResultExtras = e = new Bundle(); + return e; + } + + /** + * Change all of the result data returned from this broadcasts; only works + * with broadcasts sent through + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}. All current result data is replaced + * by the value given to this method. + * + * <p><strong>This method does not work with non-ordered broadcasts such + * as those sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}</strong></p> + * + * @param code The new result code. Often uses the + * Activity {@link android.app.Activity#RESULT_CANCELED} and + * {@link android.app.Activity#RESULT_OK} constants, though the + * actual meaning of this value is ultimately up to the broadcaster. + * @param data The new result data. This is an arbitrary + * string whose interpretation is up to the broadcaster; may be null. + * @param extras The new extra data map. This is a Bundle + * holding arbitrary data, whose interpretation is up to the + * broadcaster. Can be set to null. This completely + * replaces the current map (if any). + */ + public final void setResult(int code, String data, Bundle extras) { + checkSynchronousHint(); + mResultCode = code; + mResultData = data; + mResultExtras = extras; + } + + /** + * Returns the flag indicating whether or not this receiver should + * abort the current broadcast. + * + * @return True if the broadcast should be aborted. + */ + public final boolean getAbortBroadcast() { + return mAbortBroadcast; + } + + /** + * Sets the flag indicating that this receiver should abort the + * current broadcast; only works with broadcasts sent through + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast}. This will prevent + * any other broadcast receivers from receiving the broadcast. It will still + * call {@link #onReceive} of the BroadcastReceiver that the caller of + * {@link Context#sendOrderedBroadcast(Intent, String) + * Context.sendOrderedBroadcast} passed in. + * + * <p><strong>This method does not work with non-ordered broadcasts such + * as those sent with {@link Context#sendBroadcast(Intent) + * Context.sendBroadcast}</strong></p> + */ + public final void abortBroadcast() { + checkSynchronousHint(); + mAbortBroadcast = true; + } + + /** + * Clears the flag indicating that this receiver should abort the current + * broadcast. + */ + public final void clearAbortBroadcast() { + mAbortBroadcast = false; + } + + /** + * For internal use, sets the hint about whether this BroadcastReceiver is + * running in ordered mode. + */ + public final void setOrderedHint(boolean isOrdered) { + mOrderedHint = isOrdered; + } + + /** + * Control inclusion of debugging help for mismatched + * calls to {@ Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + * If called with true, before given to registerReceiver(), then the + * callstack of the following {@link Context#unregisterReceiver(BroadcastReceiver) + * Context.unregisterReceiver()} call is retained, to be printed if a later + * incorrect unregister call is made. Note that doing this requires retaining + * information about the BroadcastReceiver for the lifetime of the app, + * resulting in a leak -- this should only be used for debugging. + */ + public final void setDebugUnregister(boolean debug) { + mDebugUnregister = debug; + } + + /** + * Return the last value given to {@link #setDebugUnregister}. + */ + public final boolean getDebugUnregister() { + return mDebugUnregister; + } + + void checkSynchronousHint() { + if (mOrderedHint) { + return; + } + RuntimeException e = new RuntimeException( + "BroadcastReceiver trying to return result during a non-ordered broadcast"); + e.fillInStackTrace(); + Log.e("BroadcastReceiver", e.getMessage(), e); + } + + private int mResultCode; + private String mResultData; + private Bundle mResultExtras; + private boolean mAbortBroadcast; + private boolean mDebugUnregister; + private boolean mOrderedHint; +} + diff --git a/core/java/android/content/ComponentCallbacks.java b/core/java/android/content/ComponentCallbacks.java new file mode 100644 index 0000000..dad60b0 --- /dev/null +++ b/core/java/android/content/ComponentCallbacks.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2006 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.res.Configuration; + +/** + * The set of callback APIs that are common to all application components + * ({@link android.app.Activity}, {@link android.app.Service}, + * {@link ContentProvider}, and {@link android.app.Application}). + */ +public interface ComponentCallbacks { + /** + * Called by the system when the device configuration changes while your + * component is running. Note that, unlike activities, other components + * are never restarted when a configuration changes: they must always deal + * with the results of the change, such as by re-retrieving resources. + * + * <p>At the time that this function has been called, your Resources + * object will have been updated to return resource values matching the + * new configuration. + * + * @param newConfig The new device configuration. + */ + void onConfigurationChanged(Configuration newConfig); + + /** + * This is called when the overall system is running low on memory, and + * would like actively running process to try to tighten their belt. While + * the exact point at which this will be called is not defined, generally + * it will happen around the time all background process have been killed, + * that is before reaching the point of killing processes hosting + * service and foreground UI that we would like to avoid killing. + * + * <p>Applications that want to be nice can implement this method to release + * any caches or other unnecessary resources they may be holding on to. + * The system will perform a gc for you after returning from this method. + */ + void onLowMemory(); +} diff --git a/core/java/android/content/ComponentName.aidl b/core/java/android/content/ComponentName.aidl new file mode 100644 index 0000000..40dc8de --- /dev/null +++ b/core/java/android/content/ComponentName.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 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 ComponentName; diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java new file mode 100644 index 0000000..32c6864 --- /dev/null +++ b/core/java/android/content/ComponentName.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2006 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.Parcel; +import android.os.Parcelable; + +/** + * Identifier for a specific application component + * ({@link android.app.Activity}, {@link android.app.Service}, + * {@link android.content.BroadcastReceiver}, or + * {@link android.content.ContentProvider}) that is available. Two + * pieces of information, encapsulated here, are required to identify + * a component: the package (a String) it exists in, and the class (a String) + * name inside of that package. + * + */ +public final class ComponentName implements Parcelable { + private final String mPackage; + private final String mClass; + + /** + * Create a new component identifier. + * + * @param pkg The name of the package that the component exists in. Can + * not be null. + * @param cls The name of the class inside of <var>pkg</var> that + * implements the component. Can not be null. + */ + public ComponentName(String pkg, String cls) { + if (pkg == null) throw new NullPointerException("package name is null"); + if (cls == null) throw new NullPointerException("class name is null"); + mPackage = pkg; + mClass = cls; + } + + /** + * Create a new component identifier from a Context and class name. + * + * @param pkg A Context for the package implementing the component, + * from which the actual package name will be retrieved. + * @param cls The name of the class inside of <var>pkg</var> that + * implements the component. + */ + public ComponentName(Context pkg, String cls) { + if (cls == null) throw new NullPointerException("class name is null"); + mPackage = pkg.getPackageName(); + mClass = cls; + } + + /** + * Create a new component identifier from a Context and Class object. + * + * @param pkg A Context for the package implementing the component, from + * which the actual package name will be retrieved. + * @param cls The Class object of the desired component, from which the + * actual class name will be retrieved. + */ + public ComponentName(Context pkg, Class<?> cls) { + mPackage = pkg.getPackageName(); + mClass = cls.getName(); + } + + /** + * Return the package name of this component. + */ + public String getPackageName() { + return mPackage; + } + + /** + * Return the class name of this component. + */ + public String getClassName() { + return mClass; + } + + /** + * Return the class name, either fully qualified or in a shortened form + * (with a leading '.') if it is a suffix of the package. + */ + public String getShortClassName() { + if (mClass.startsWith(mPackage)) { + int PN = mPackage.length(); + int CN = mClass.length(); + if (CN > PN && mClass.charAt(PN) == '.') { + return mClass.substring(PN, CN); + } + } + return mClass; + } + + /** + * Return a String that unambiguously describes both the package and + * class names contained in the ComponentName. You can later recover + * the ComponentName from this string through + * {@link #unflattenFromString(String)}. + * + * @return Returns a new String holding the package and class names. This + * is represented as the package name, concatenated with a '/' and then the + * class name. + * + * @see #unflattenFromString(String) + */ + public String flattenToString() { + return mPackage + "/" + mClass; + } + + /** + * The samee as {@link #flattenToString()}, but abbreviates the class + * name if it is a suffix of the package. The result can still be used + * with {@link #unflattenFromString(String)}. + * + * @return Returns a new String holding the package and class names. This + * is represented as the package name, concatenated with a '/' and then the + * class name. + * + * @see #unflattenFromString(String) + */ + public String flattenToShortString() { + return mPackage + "/" + getShortClassName(); + } + + /** + * Recover a ComponentName from a String that was previously created with + * {@link #flattenToString()}. It splits the string at the first '/', + * taking the part before as the package name and the part after as the + * class name. As a special convenience (to use, for example, when + * parsing component names on the command line), if the '/' is immediately + * followed by a '.' then the final class name will be the concatenation + * of the package name with the string following the '/'. Thus + * "com.foo/.Blah" becomes package="com.foo" class="com.foo.Blah". + * + * @param str The String that was returned by flattenToString(). + * @return Returns a new ComponentName containing the package and class + * names that were encoded in <var>str</var> + * + * @see #flattenToString() + */ + public static ComponentName unflattenFromString(String str) { + int sep = str.indexOf('/'); + if (sep < 0 || (sep+1) >= str.length()) { + return null; + } + String pkg = str.substring(0, sep); + String cls = str.substring(sep+1); + if (cls.length() > 0 && cls.charAt(0) == '.') { + cls = pkg + cls; + } + return new ComponentName(pkg, cls); + } + + /** + * Return string representation of this class without the class's name + * as a prefix. + */ + public String toShortString() { + return "{" + mPackage + "/" + mClass + "}"; + } + + @Override + public String toString() { + return "ComponentInfo{" + mPackage + "/" + mClass + "}"; + } + + @Override + public boolean equals(Object obj) { + try { + if (obj != null) { + ComponentName other = (ComponentName)obj; + // Note: no null checks, because mPackage and mClass can + // never be null. + return mPackage.equals(other.mPackage) + && mClass.equals(other.mClass); + } + } catch (ClassCastException e) { + } + return false; + } + + @Override + public int hashCode() { + return mPackage.hashCode() + mClass.hashCode(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mPackage); + out.writeString(mClass); + } + + /** + * Write a ComponentName to a Parcel, handling null pointers. Must be + * read with {@link #readFromParcel(Parcel)}. + * + * @param c The ComponentName to be written. + * @param out The Parcel in which the ComponentName will be placed. + * + * @see #readFromParcel(Parcel) + */ + public static void writeToParcel(ComponentName c, Parcel out) { + if (c != null) { + c.writeToParcel(out, 0); + } else { + out.writeString(null); + } + } + + /** + * Read a ComponentName from a Parcel that was previously written + * with {@link #writeToParcel(ComponentName, Parcel)}, returning either + * a null or new object as appropriate. + * + * @param in The Parcel from which to read the ComponentName + * @return Returns a new ComponentName matching the previously written + * object, or null if a null had been written. + * + * @see #writeToParcel(ComponentName, Parcel) + */ + public static ComponentName readFromParcel(Parcel in) { + String pkg = in.readString(); + return pkg != null ? new ComponentName(pkg, in) : null; + } + + public static final Parcelable.Creator<ComponentName> CREATOR + = new Parcelable.Creator<ComponentName>() { + public ComponentName createFromParcel(Parcel in) { + return new ComponentName(in); + } + + public ComponentName[] newArray(int size) { + return new ComponentName[size]; + } + }; + + /** + * Instantiate a new ComponentName from the data in a Parcel that was + * previously written with {@link #writeToParcel(Parcel, int)}. Note that you + * must not use this with data written by + * {@link #writeToParcel(ComponentName, Parcel)} since it is not possible + * to handle a null ComponentObject here. + * + * @param in The Parcel containing the previously written ComponentName, + * positioned at the location in the buffer where it was written. + */ + public ComponentName(Parcel in) { + mPackage = in.readString(); + if (mPackage == null) throw new NullPointerException( + "package name is null"); + mClass = in.readString(); + if (mClass == null) throw new NullPointerException( + "class name is null"); + } + + private ComponentName(String pkg, Parcel in) { + mPackage = pkg; + mClass = in.readString(); + } +} diff --git a/core/java/android/content/ContentInsertHandler.java b/core/java/android/content/ContentInsertHandler.java new file mode 100644 index 0000000..fbf726e --- /dev/null +++ b/core/java/android/content/ContentInsertHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008 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 org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface to insert data to ContentResolver + * @hide + */ +public interface ContentInsertHandler extends ContentHandler { + /** + * insert data from InputStream to ContentResolver + * @param contentResolver + * @param in InputStream + * @throws IOException + * @throws SAXException + */ + public void insert(ContentResolver contentResolver, InputStream in) + throws IOException, SAXException; + + /** + * insert data from String to ContentResolver + * @param contentResolver + * @param in input string + * @throws SAXException + */ + public void insert(ContentResolver contentResolver, String in) + throws SAXException; + +} diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java new file mode 100644 index 0000000..25544de --- /dev/null +++ b/core/java/android/content/ContentProvider.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2006 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.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.content.res.Configuration; +import android.database.Cursor; +import android.database.CursorToBulkCursorAdaptor; +import android.database.CursorWindow; +import android.database.IBulkCursor; +import android.database.IContentObserver; +import android.database.SQLException; +import android.net.Uri; +import android.os.Binder; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileNotFoundException; + +/** + * Content providers are one of the primary building blocks of Android applications, providing + * content to applications. They encapsulate data and provide it to applications through the single + * {@link ContentResolver} interface. A content provider is only required if you need to share + * data between multiple applications. For example, the contacts data is used by multiple + * applications and must be stored in a content provider. If you don't need to share data amongst + * multiple applications you can use a database directly via + * {@link android.database.sqlite.SQLiteDatabase}. + * + * <p>For more information, read <a href="{@docRoot}guide/topics/providers/content-providers.html">Content + * Providers</a>.</p> + * + * <p>When a request is made via + * a {@link ContentResolver} the system inspects the authority of the given URI and passes the + * request to the content provider registered with the authority. The content provider can interpret + * the rest of the URI however it wants. The {@link UriMatcher} class is helpful for parsing + * URIs.</p> + * + * <p>The primary methods that need to be implemented are: + * <ul> + * <li>{@link #query} which returns data to the caller</li> + * <li>{@link #insert} which inserts new data into the content provider</li> + * <li>{@link #update} which updates existing data in the content provider</li> + * <li>{@link #delete} which deletes data from the content provider</li> + * <li>{@link #getType} which returns the MIME type of data in the content provider</li> + * </ul></p> + * + * <p>This class takes care of cross process calls so subclasses don't have to worry about which + * process a request is coming from.</p> + */ +public abstract class ContentProvider implements ComponentCallbacks { + private Context mContext = null; + private String mReadPermission; + private String mWritePermission; + + private Transport mTransport = new Transport(); + + /** + * Given an IContentProvider, try to coerce it back to the real + * ContentProvider object if it is running in the local process. 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. Most + * clients should not nor have a reason to use it. + * + * @param abstractInterface The ContentProvider interface that is to be + * coerced. + * @return If the IContentProvider is non-null and local, returns its actual + * ContentProvider instance. Otherwise returns null. + * @hide + */ + public static ContentProvider coerceToLocalContentProvider( + IContentProvider abstractInterface) { + if (abstractInterface instanceof Transport) { + return ((Transport)abstractInterface).getContentProvider(); + } + return null; + } + + /** + * Binder object that deals with remoting. + * + * @hide + */ + class Transport extends ContentProviderNative { + ContentProvider getContentProvider() { + return ContentProvider.this; + } + + /** + * Remote version of a query, which returns an IBulkCursor. The bulk + * cursor should be wrapped with BulkCursorToCursorAdaptor before use. + */ + public IBulkCursor bulkQuery(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder, + IContentObserver observer, CursorWindow window) { + checkReadPermission(uri); + Cursor cursor = ContentProvider.this.query(uri, projection, + selection, selectionArgs, sortOrder); + if (cursor == null) { + return null; + } + String wperm = getWritePermission(); + return new CursorToBulkCursorAdaptor(cursor, observer, + ContentProvider.this.getClass().getName(), + wperm == null || + getContext().checkCallingOrSelfPermission(getWritePermission()) + == PackageManager.PERMISSION_GRANTED, + window); + } + + public Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + checkReadPermission(uri); + return ContentProvider.this.query(uri, projection, selection, + selectionArgs, sortOrder); + } + + public String getType(Uri uri) { + return ContentProvider.this.getType(uri); + } + + + public Uri insert(Uri uri, ContentValues initialValues) { + checkWritePermission(uri); + return ContentProvider.this.insert(uri, initialValues); + } + + public int bulkInsert(Uri uri, ContentValues[] initialValues) { + checkWritePermission(uri); + return ContentProvider.this.bulkInsert(uri, initialValues); + } + + public int delete(Uri uri, String selection, String[] selectionArgs) { + checkWritePermission(uri); + return ContentProvider.this.delete(uri, selection, selectionArgs); + } + + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + checkWritePermission(uri); + return ContentProvider.this.update(uri, values, selection, selectionArgs); + } + + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + if (mode != null && mode.startsWith("rw")) checkWritePermission(uri); + else checkReadPermission(uri); + return ContentProvider.this.openFile(uri, mode); + } + + public AssetFileDescriptor openAssetFile(Uri uri, String mode) + throws FileNotFoundException { + if (mode != null && mode.startsWith("rw")) checkWritePermission(uri); + else checkReadPermission(uri); + return ContentProvider.this.openAssetFile(uri, mode); + } + + public ISyncAdapter getSyncAdapter() { + checkWritePermission(null); + return ContentProvider.this.getSyncAdapter().getISyncAdapter(); + } + + private void checkReadPermission(Uri uri) { + final String rperm = getReadPermission(); + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + if (getContext().checkUriPermission(uri, rperm, null, pid, uid, + Intent.FLAG_GRANT_READ_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + return; + } + String msg = "Permission Denial: reading " + + ContentProvider.this.getClass().getName() + + " uri " + uri + " from pid=" + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " requires " + rperm; + throw new SecurityException(msg); + } + + private void checkWritePermission(Uri uri) { + final String wperm = getWritePermission(); + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); + if (getContext().checkUriPermission(uri, null, wperm, pid, uid, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + return; + } + String msg = "Permission Denial: writing " + + ContentProvider.this.getClass().getName() + + " uri " + uri + " from pid=" + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid() + + " requires " + wperm; + throw new SecurityException(msg); + } + } + + + /** + * Retrieve the Context this provider is running in. Only available once + * onCreate(Map icicle) has been called -- this will be null in the + * constructor. + */ + public final Context getContext() { + return mContext; + } + + /** + * Change the permission required to read data from the content + * provider. This is normally set for you from its manifest information + * when the provider is first created. + * + * @param permission Name of the permission required for read-only access. + */ + protected final void setReadPermission(String permission) { + mReadPermission = permission; + } + + /** + * Return the name of the permission required for read-only access to + * this content provider. 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>. + */ + public final String getReadPermission() { + return mReadPermission; + } + + /** + * Change the permission required to read and write data in the content + * provider. This is normally set for you from its manifest information + * when the provider is first created. + * + * @param permission Name of the permission required for read/write access. + */ + protected final void setWritePermission(String permission) { + mWritePermission = permission; + } + + /** + * Return the name of the permission required for read/write access to + * this content provider. 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>. + */ + public final String getWritePermission() { + return mWritePermission; + } + + /** + * Called when the provider is being started. + * + * @return true if the provider was successfully loaded, false otherwise + */ + public abstract boolean onCreate(); + + public void onConfigurationChanged(Configuration newConfig) { + } + + public void onLowMemory() { + } + + /** + * Receives a query request from a client in a local process, and + * returns a Cursor. This is called internally by the {@link ContentResolver}. + * 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>. + * <p> + * Example client call:<p> + * <pre>// Request a specific record. + * Cursor managedCursor = managedQuery( + Contacts.People.CONTENT_URI.addId(2), + projection, // Which columns to return. + null, // WHERE clause. + People.NAME + " ASC"); // Sort order.</pre> + * Example implementation:<p> + * <pre>// SQLiteQueryBuilder is a helper class that creates the + // proper SQL syntax for us. + SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); + + // Set the table we're querying. + qBuilder.setTables(DATABASE_TABLE_NAME); + + // If the query ends in a specific record number, we're + // being asked for a specific record, so set the + // WHERE clause in our query. + if((URI_MATCHER.match(uri)) == SPECIFIC_MESSAGE){ + qBuilder.appendWhere("_id=" + uri.getPathLeafId()); + } + + // Make the query. + Cursor c = qBuilder.query(mDb, + projection, + selection, + selectionArgs, + groupBy, + having, + sortOrder); + c.setNotificationUri(getContext().getContentResolver(), uri); + return c;</pre> + * + * @param uri The URI to query. This will be the full URI sent by the client; + * if the client is requesting a specific record, the URI will end in a record number + * that the implementation should parse and add to a WHERE or HAVING clause, specifying + * that _id value. + * @param projection The list of columns to put into the cursor. If + * null all columns are included. + * @param selection A selection criteria to apply when filtering rows. + * If null then all rows are included. + * @param sortOrder How the rows in the cursor should be sorted. + * If null then the provider is free to define the sort order. + * @return a Cursor or null. + */ + public abstract Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + /** + * 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. + * 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>. + * + * @param uri the URI to query. + * @return a MIME type string, or null if there is no type. + */ + public abstract String getType(Uri uri); + + /** + * Implement this to insert a new row. + * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} + * after inserting. + * 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>. + * @param uri The content:// URI of the insertion request. + * @param values A set of column_name/value pairs to add to the database. + * @return The URI for the newly inserted item. + */ + public abstract Uri insert(Uri uri, ContentValues values); + + /** + * Implement this to insert a set of new rows, or the default implementation will + * iterate over the values and call {@link #insert} on each of them. + * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} + * after inserting. + * 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>. + * + * @param uri The content:// URI of the insertion request. + * @param values An array of sets of column_name/value pairs to add to the database. + * @return The number of values that were inserted. + */ + public int bulkInsert(Uri uri, ContentValues[] values) { + int numValues = values.length; + for (int i = 0; i < numValues; i++) { + insert(uri, values[i]); + } + return numValues; + } + + /** + * A request to delete one or more rows. The selection clause is applied when performing + * the deletion, allowing the operation to affect multiple rows in a + * directory. + * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyDelete()} + * after deleting. + * 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>. + * + * <p>The implementation is responsible for parsing out a row ID at the end + * of the URI, if a specific row is being deleted. That is, the client would + * pass in <code>content://contacts/people/22</code> and the implementation is + * responsible for parsing the record number (22) when creating a SQL statement. + * + * @param uri The full URI to query, including a row ID (if a specific record is requested). + * @param selection An optional restriction to apply to rows when deleting. + * @return The number of rows affected. + * @throws SQLException + */ + public abstract int delete(Uri uri, String selection, String[] selectionArgs); + + /** + * Update a content URI. All rows matching the optionally provided selection + * will have their columns listed as the keys in the values map with the + * values of those keys. + * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()} + * after updating. + * 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>. + * + * @param uri The URI to query. This can potentially have a record ID if this + * is an update request for a specific record. + * @param values A Bundle mapping from column names to new column values (NULL is a + * valid value). + * @param selection An optional filter to match rows to update. + * @return the number of rows affected. + */ + public abstract int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs); + + /** + * Open a file blob associated with a content URI. + * 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>. + * + * <p>Returns a + * ParcelFileDescriptor, from which you can obtain a + * {@link java.io.FileDescriptor} for use with + * {@link java.io.FileInputStream}, {@link java.io.FileOutputStream}, etc. + * This can be used to store large data (such as an image) associated with + * a particular piece of content. + * + * <p>The returned ParcelFileDescriptor is owned by the caller, so it is + * their responsibility to close it when done. That is, the implementation + * of this method should create a new ParcelFileDescriptor for each call. + * + * @param uri The URI whose file is to be opened. + * @param mode Access mode for the file. May be "r" for read-only access, + * "rw" for read and write access, or "rwt" for read and write access + * that truncates any existing file. + * + * @return Returns a new ParcelFileDescriptor which you can use to access + * the file. + * + * @throws FileNotFoundException Throws FileNotFoundException if there is + * no file associated with the given URI or the mode is invalid. + * @throws SecurityException Throws SecurityException if the caller does + * not have permission to access the file. + * + * @see #openAssetFile(Uri, String) + * @see #openFileHelper(Uri, String) + */ + public ParcelFileDescriptor openFile(Uri uri, String mode) + throws FileNotFoundException { + throw new FileNotFoundException("No files supported by provider at " + + uri); + } + + /** + * This is like {@link #openFile}, but can be implemented by providers + * that need to be able to return sub-sections of files, often assets + * inside of their .apk. Note that when implementing this your clients + * must be able to deal with such files, either directly with + * {@link ContentResolver#openAssetFileDescriptor + * ContentResolver.openAssetFileDescriptor}, or by using the higher-level + * {@link ContentResolver#openInputStream ContentResolver.openInputStream} + * or {@link ContentResolver#openOutputStream ContentResolver.openOutputStream} + * methods. + * + * <p><em>Note: if you are implementing this to return a full file, you + * should create the AssetFileDescriptor with + * {@link AssetFileDescriptor#UNKNOWN_LENGTH} to be compatible with + * applications that can not handle sub-sections of files.</em></p> + * + * @param uri The URI whose file is to be opened. + * @param mode Access mode for the file. May be "r" for read-only access, + * "w" for write-only access (erasing whatever data is currently in + * the file), "wa" for write-only access to append to any existing data, + * "rw" for read and write access on any existing data, and "rwt" for read + * and write access that truncates any existing file. + * + * @return Returns a new AssetFileDescriptor which you can use to access + * the file. + * + * @throws FileNotFoundException Throws FileNotFoundException if there is + * no file associated with the given URI or the mode is invalid. + * @throws SecurityException Throws SecurityException if the caller does + * not have permission to access the file. + * + * @see #openFile(Uri, String) + * @see #openFileHelper(Uri, String) + */ + public AssetFileDescriptor openAssetFile(Uri uri, String mode) + throws FileNotFoundException { + ParcelFileDescriptor fd = openFile(uri, mode); + return fd != null ? new AssetFileDescriptor(fd, 0, -1) : null; + } + + /** + * Convenience for subclasses that wish to implement {@link #openFile} + * by looking up a column named "_data" at the given URI. + * + * @param uri The URI to be opened. + * @param mode The file mode. May be "r" for read-only access, + * "w" for write-only access (erasing whatever data is currently in + * the file), "wa" for write-only access to append to any existing data, + * "rw" for read and write access on any existing data, and "rwt" for read + * and write access that truncates any existing file. + * + * @return Returns a new ParcelFileDescriptor that can be used by the + * client to access the file. + */ + protected final ParcelFileDescriptor openFileHelper(Uri uri, + String mode) throws FileNotFoundException { + Cursor c = query(uri, new String[]{"_data"}, null, null, null); + int count = (c != null) ? c.getCount() : 0; + if (count != 1) { + // If there is not exactly one result, throw an appropriate + // exception. + if (c != null) { + c.close(); + } + if (count == 0) { + throw new FileNotFoundException("No entry for " + uri); + } + throw new FileNotFoundException("Multiple items at " + uri); + } + + c.moveToFirst(); + int i = c.getColumnIndex("_data"); + String path = (i >= 0 ? c.getString(i) : null); + c.close(); + if (path == null) { + throw new FileNotFoundException("Column _data not found."); + } + + int modeBits = ContentResolver.modeToMode(uri, mode); + return ParcelFileDescriptor.open(new File(path), modeBits); + } + + /** + * 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 + */ + protected boolean isTemporary() { + return false; + } + + /** + * Returns the Binder object for this provider. + * + * @return the Binder object for this provider + * @hide + */ + public IContentProvider getIContentProvider() { + return mTransport; + } + + /** + * After being instantiated, this is called to tell the content provider + * about itself. + * + * @param context The context this provider is running in + * @param info Registered information about this content provider + */ + public void attachInfo(Context context, ProviderInfo info) { + + /* + * Only allow it to be set once, so after the content service gives + * this to us clients can't change it. + */ + if (mContext == null) { + mContext = context; + if (info != null) { + setReadPermission(info.readPermission); + setWritePermission(info.writePermission); + } + ContentProvider.this.onCreate(); + } + } +} diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java new file mode 100644 index 0000000..e5e3f74 --- /dev/null +++ b/core/java/android/content/ContentProviderNative.java @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2006 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.res.AssetFileDescriptor; +import android.database.BulkCursorNative; +import android.database.BulkCursorToCursorAdaptor; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.DatabaseUtils; +import android.database.IBulkCursor; +import android.database.IContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.FileNotFoundException; + +/** + * {@hide} + */ +abstract public class ContentProviderNative extends Binder implements IContentProvider { + private static final String TAG = "ContentProvider"; + + public ContentProviderNative() + { + attachInterface(this, descriptor); + } + + /** + * Cast a Binder object into a content resolver interface, generating + * a proxy if needed. + */ + static public IContentProvider asInterface(IBinder obj) + { + if (obj == null) { + return null; + } + IContentProvider in = + (IContentProvider)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + + return new ContentProviderProxy(obj); + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + switch (code) { + case QUERY_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + int num = data.readInt(); + String[] projection = null; + if (num > 0) { + projection = new String[num]; + for (int i = 0; i < num; i++) { + projection[i] = data.readString(); + } + } + String selection = data.readString(); + num = data.readInt(); + String[] selectionArgs = null; + if (num > 0) { + selectionArgs = new String[num]; + for (int i = 0; i < num; i++) { + selectionArgs[i] = data.readString(); + } + } + String sortOrder = data.readString(); + IContentObserver observer = IContentObserver.Stub. + asInterface(data.readStrongBinder()); + CursorWindow window = CursorWindow.CREATOR.createFromParcel(data); + + IBulkCursor bulkCursor = bulkQuery(url, projection, selection, + selectionArgs, sortOrder, observer, window); + reply.writeNoException(); + if (bulkCursor != null) { + reply.writeStrongBinder(bulkCursor.asBinder()); + } else { + reply.writeStrongBinder(null); + } + return true; + } + + case GET_TYPE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String type = getType(url); + reply.writeNoException(); + reply.writeString(type); + + return true; + } + + case INSERT_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + ContentValues values = ContentValues.CREATOR.createFromParcel(data); + + Uri out = insert(url, values); + reply.writeNoException(); + Uri.writeToParcel(reply, out); + return true; + } + + case BULK_INSERT_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + ContentValues[] values = data.createTypedArray(ContentValues.CREATOR); + + int count = bulkInsert(url, values); + reply.writeNoException(); + reply.writeInt(count); + return true; + } + + case DELETE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String selection = data.readString(); + String[] selectionArgs = data.readStringArray(); + + int count = delete(url, selection, selectionArgs); + + reply.writeNoException(); + reply.writeInt(count); + return true; + } + + case UPDATE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + ContentValues values = ContentValues.CREATOR.createFromParcel(data); + String selection = data.readString(); + String[] selectionArgs = data.readStringArray(); + + int count = update(url, values, selection, selectionArgs); + + reply.writeNoException(); + reply.writeInt(count); + return true; + } + + case OPEN_FILE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String mode = data.readString(); + + ParcelFileDescriptor fd; + fd = openFile(url, mode); + reply.writeNoException(); + if (fd != null) { + reply.writeInt(1); + fd.writeToParcel(reply, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + reply.writeInt(0); + } + return true; + } + + case OPEN_ASSET_FILE_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String mode = data.readString(); + + AssetFileDescriptor fd; + fd = openAssetFile(url, mode); + reply.writeNoException(); + if (fd != null) { + reply.writeInt(1); + fd.writeToParcel(reply, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + reply.writeInt(0); + } + 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); + return true; + } + + return super.onTransact(code, data, reply, flags); + } + + public IBinder asBinder() + { + return this; + } +} + + +final class ContentProviderProxy implements IContentProvider +{ + public ContentProviderProxy(IBinder remote) + { + mRemote = remote; + } + + public IBinder asBinder() + { + return mRemote; + } + + public IBulkCursor bulkQuery(Uri url, String[] projection, + String selection, String[] selectionArgs, String sortOrder, IContentObserver observer, + CursorWindow window) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + int length = 0; + if (projection != null) { + length = projection.length; + } + data.writeInt(length); + for (int i = 0; i < length; i++) { + data.writeString(projection[i]); + } + data.writeString(selection); + if (selectionArgs != null) { + length = selectionArgs.length; + } else { + length = 0; + } + data.writeInt(length); + for (int i = 0; i < length; i++) { + data.writeString(selectionArgs[i]); + } + data.writeString(sortOrder); + data.writeStrongBinder(observer.asBinder()); + window.writeToParcel(data, 0); + + mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + + IBulkCursor bulkCursor = null; + IBinder bulkCursorBinder = reply.readStrongBinder(); + if (bulkCursorBinder != null) { + bulkCursor = BulkCursorNative.asInterface(bulkCursorBinder); + } + + data.recycle(); + reply.recycle(); + + return bulkCursor; + } + + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder) throws RemoteException { + //TODO make a pool of windows so we can reuse memory dealers + CursorWindow window = new CursorWindow(false /* window will be used remotely */); + BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); + IBulkCursor bulkCursor = bulkQuery(url, projection, selection, selectionArgs, sortOrder, + adaptor.getObserver(), window); + + if (bulkCursor == null) { + return null; + } + adaptor.set(bulkCursor); + return adaptor; + } + + public String getType(Uri url) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + + mRemote.transact(IContentProvider.GET_TYPE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + String out = reply.readString(); + + data.recycle(); + reply.recycle(); + + return out; + } + + public Uri insert(Uri url, ContentValues values) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + values.writeToParcel(data, 0); + + mRemote.transact(IContentProvider.INSERT_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + Uri out = Uri.CREATOR.createFromParcel(reply); + + data.recycle(); + reply.recycle(); + + return out; + } + + public int bulkInsert(Uri url, ContentValues[] values) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeTypedArray(values, 0); + + mRemote.transact(IContentProvider.BULK_INSERT_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + int count = reply.readInt(); + + data.recycle(); + reply.recycle(); + + return count; + } + + public int delete(Uri url, String selection, String[] selectionArgs) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(selection); + data.writeStringArray(selectionArgs); + + mRemote.transact(IContentProvider.DELETE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + int count = reply.readInt(); + + data.recycle(); + reply.recycle(); + + return count; + } + + public int update(Uri url, ContentValues values, String selection, + String[] selectionArgs) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + values.writeToParcel(data, 0); + data.writeString(selection); + data.writeStringArray(selectionArgs); + + mRemote.transact(IContentProvider.UPDATE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + int count = reply.readInt(); + + data.recycle(); + reply.recycle(); + + return count; + } + + public ParcelFileDescriptor openFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(mode); + + mRemote.transact(IContentProvider.OPEN_FILE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(reply); + int has = reply.readInt(); + ParcelFileDescriptor fd = has != 0 ? reply.readFileDescriptor() : null; + + data.recycle(); + reply.recycle(); + + return fd; + } + + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(mode); + + mRemote.transact(IContentProvider.OPEN_ASSET_FILE_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(reply); + int has = reply.readInt(); + AssetFileDescriptor fd = has != 0 + ? AssetFileDescriptor.CREATOR.createFromParcel(reply) : null; + + data.recycle(); + reply.recycle(); + + 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/ContentQueryMap.java b/core/java/android/content/ContentQueryMap.java new file mode 100644 index 0000000..dbcb4a7 --- /dev/null +++ b/core/java/android/content/ContentQueryMap.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 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; + +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Handler; + +import java.util.HashMap; +import java.util.Map; +import java.util.Observable; + +/** + * Caches the contents of a cursor into a Map of String->ContentValues and optionally + * keeps the cache fresh by registering for updates on the content backing the cursor. The column of + * the database that is to be used as the key of the map is user-configurable, and the + * ContentValues contains all columns other than the one that is designated the key. + * <p> + * The cursor data is accessed by row key and column name via getValue(). + */ +public class ContentQueryMap extends Observable { + private Cursor mCursor; + private String[] mColumnNames; + private int mKeyColumn; + + private Handler mHandlerForUpdateNotifications = null; + private boolean mKeepUpdated = false; + + private Map<String, ContentValues> mValues = null; + + private ContentObserver mContentObserver; + + /** Set when a cursor change notification is received and is cleared on a call to requery(). */ + private boolean mDirty = false; + + /** + * Creates a ContentQueryMap that caches the content backing the cursor + * + * @param cursor the cursor whose contents should be cached + * @param columnNameOfKey the column that is to be used as the key of the values map + * @param keepUpdated true if the cursor's ContentProvider should be monitored for changes and + * the map updated when changes do occur + * @param handlerForUpdateNotifications the Handler that should be used to receive + * notifications of changes (if requested). Normally you pass null here, but if + * you know that the thread that is creating this isn't a thread that can receive + * messages then you can create your own handler and use that here. + */ + public ContentQueryMap(Cursor cursor, String columnNameOfKey, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + mCursor = cursor; + mColumnNames = mCursor.getColumnNames(); + mKeyColumn = mCursor.getColumnIndexOrThrow(columnNameOfKey); + mHandlerForUpdateNotifications = handlerForUpdateNotifications; + setKeepUpdated(keepUpdated); + + // If we aren't keeping the cache updated with the current state of the cursor's + // ContentProvider then read it once into the cache. Otherwise the cache will be filled + // automatically. + if (!keepUpdated) { + readCursorIntoCache(); + } + } + + /** + * Change whether or not the ContentQueryMap will register with the cursor's ContentProvider + * for change notifications. If you use a ContentQueryMap in an activity you should call this + * with false in onPause(), which means you need to call it with true in onResume() + * if want it to be kept updated. + * @param keepUpdated if true the ContentQueryMap should be registered with the cursor's + * ContentProvider, false otherwise + */ + public void setKeepUpdated(boolean keepUpdated) { + if (keepUpdated == mKeepUpdated) return; + mKeepUpdated = keepUpdated; + + if (!mKeepUpdated) { + mCursor.unregisterContentObserver(mContentObserver); + mContentObserver = null; + } else { + if (mHandlerForUpdateNotifications == null) { + mHandlerForUpdateNotifications = new Handler(); + } + if (mContentObserver == null) { + mContentObserver = new ContentObserver(mHandlerForUpdateNotifications) { + @Override + public void onChange(boolean selfChange) { + // If anyone is listening, we need to do this now to broadcast + // to the observers. Otherwise, we'll just set mDirty and + // let it query lazily when they ask for the values. + if (countObservers() != 0) { + requery(); + } else { + mDirty = true; + } + } + }; + } + mCursor.registerContentObserver(mContentObserver); + // mark dirty, since it is possible the cursor's backing data had changed before we + // registered for changes + mDirty = true; + } + } + + /** + * Access the ContentValues for the row specified by rowName + * @param rowName which row to read + * @return the ContentValues for the row, or null if the row wasn't present in the cursor + */ + public synchronized ContentValues getValues(String rowName) { + if (mDirty) requery(); + return mValues.get(rowName); + } + + /** Requeries the cursor and reads the contents into the cache */ + public void requery() { + mDirty = false; + mCursor.requery(); + readCursorIntoCache(); + setChanged(); + notifyObservers(); + } + + private synchronized void readCursorIntoCache() { + // Make a new map so old values returned by getRows() are undisturbed. + int capacity = mValues != null ? mValues.size() : 0; + mValues = new HashMap<String, ContentValues>(capacity); + while (mCursor.moveToNext()) { + ContentValues values = new ContentValues(); + for (int i = 0; i < mColumnNames.length; i++) { + if (i != mKeyColumn) { + values.put(mColumnNames[i], mCursor.getString(i)); + } + } + mValues.put(mCursor.getString(mKeyColumn), values); + } + } + + public synchronized Map<String, ContentValues> getRows() { + if (mDirty) requery(); + return mValues; + } + + public synchronized void close() { + if (mContentObserver != null) { + mCursor.unregisterContentObserver(mContentObserver); + mContentObserver = null; + } + mCursor.close(); + mCursor = null; + } + + @Override + protected void finalize() throws Throwable { + if (mCursor != null) close(); + super.finalize(); + } +} diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java new file mode 100644 index 0000000..0d886ee --- /dev/null +++ b/core/java/android/content/ContentResolver.java @@ -0,0 +1,784 @@ +/* + * Copyright (C) 2006 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.PackageManager.NameNotFoundException; +import android.content.res.AssetFileDescriptor; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.CursorWrapper; +import android.database.IContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.text.TextUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + + +/** + * This class provides applications access to the content model. + */ +public abstract class ContentResolver { + public final static String SYNC_EXTRAS_ACCOUNT = "account"; + public static final String SYNC_EXTRAS_EXPEDITED = "expedited"; + public static final String SYNC_EXTRAS_FORCE = "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"; + + public static final String SCHEME_CONTENT = "content"; + public static final String SCHEME_ANDROID_RESOURCE = "android.resource"; + public static final String SCHEME_FILE = "file"; + + /** + * This is the Android platform's base MIME type for a content: URI + * containing a Cursor of a single item. Applications should use this + * as the base type along with their own sub-type of their content: URIs + * that represent a particular item. For example, hypothetical IMAP email + * client may have a URI + * <code>content://com.company.provider.imap/inbox/1</code> for a particular + * message in the inbox, whose MIME type would be reported as + * <code>CURSOR_ITEM_BASE_TYPE + "/vnd.company.imap-msg"</code> + * + * <p>Compare with {@link #CURSOR_DIR_BASE_TYPE}. + */ + public static final String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item"; + + /** + * This is the Android platform's base MIME type for a content: URI + * containing a Cursor of zero or more items. Applications should use this + * as the base type along with their own sub-type of their content: URIs + * that represent a directory of items. For example, hypothetical IMAP email + * client may have a URI + * <code>content://com.company.provider.imap/inbox</code> for all of the + * messages in its inbox, whose MIME type would be reported as + * <code>CURSOR_DIR_BASE_TYPE + "/vnd.company.imap-msg"</code> + * + * <p>Note how the base MIME type varies between this and + * {@link #CURSOR_ITEM_BASE_TYPE} depending on whether there is + * one single item or multiple items in the data set, while the sub-type + * remains the same because in either case the data structure contained + * in the cursor is the same. + */ + public static final String CURSOR_DIR_BASE_TYPE = "vnd.android.cursor.dir"; + + public ContentResolver(Context context) + { + mContext = context; + } + + /** @hide */ + protected abstract IContentProvider acquireProvider(Context c, String name); + /** @hide */ + public abstract boolean releaseProvider(IContentProvider icp); + + /** + * Return the MIME type of the given content URL. + * + * @param url A Uri identifying content (either a list or specific type), + * using the content:// scheme. + * @return A MIME type for the content, or null if the URL is invalid or the type is unknown + */ + public final String getType(Uri url) + { + IContentProvider provider = acquireProvider(url); + if (provider == null) { + return null; + } + try { + return provider.getType(url); + } catch (RemoteException e) { + return null; + } catch (java.lang.Exception e) { + return null; + } finally { + releaseProvider(provider); + } + } + + /** + * Query the given URI, returning a {@link Cursor} over the result set. + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param projection A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading data + * from storage that isn't going to be used. + * @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 A Cursor object, which is positioned before the first entry, or null + * @see Cursor + */ + public final Cursor query(Uri uri, String[] projection, + String selection, String[] selectionArgs, String sortOrder) { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + return null; + } + try { + Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder); + if(qCursor == null) { + releaseProvider(provider); + return null; + } + //Wrap the cursor object into CursorWrapperInner object + return new CursorWrapperInner(qCursor, provider); + } catch (RemoteException e) { + releaseProvider(provider); + return null; + } catch(RuntimeException e) { + releaseProvider(provider); + 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. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li> + * <li>file ({@link #SCHEME_FILE})</li> + * </ul> + * + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * + * @param uri The desired URI. + * @return InputStream + * @throws FileNotFoundException if the provided URI could not be opened. + * @see #openAssetFileDescriptor(Uri, String) + */ + public final InputStream openInputStream(Uri uri) + throws FileNotFoundException { + String scheme = uri.getScheme(); + if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { + // Note: left here to avoid breaking compatibility. May be removed + // with sufficient testing. + OpenResourceIdResult r = getResourceId(uri); + try { + InputStream stream = r.r.openRawResource(r.id); + return stream; + } catch (Resources.NotFoundException ex) { + throw new FileNotFoundException("Resource does not exist: " + uri); + } + } else if (SCHEME_FILE.equals(scheme)) { + // Note: left here to avoid breaking compatibility. May be removed + // with sufficient testing. + return new FileInputStream(uri.getPath()); + } else { + AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r"); + try { + return fd != null ? fd.createInputStream() : null; + } catch (IOException e) { + throw new FileNotFoundException("Unable to create stream"); + } + } + } + + /** + * Synonym for {@link #openOutputStream(Uri, String) + * openOutputStream(uri, "w")}. + * @throws FileNotFoundException if the provided URI could not be opened. + */ + public final OutputStream openOutputStream(Uri uri) + throws FileNotFoundException { + return openOutputStream(uri, "w"); + } + + /** + * Open a stream on to the content associated with a content URI. If there + * is no data associated with the URI, FileNotFoundException is thrown. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>file ({@link #SCHEME_FILE})</li> + * </ul> + * + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * + * @param uri The desired URI. + * @param mode May be "w", "wa", "rw", or "rwt". + * @return OutputStream + * @throws FileNotFoundException if the provided URI could not be opened. + * @see #openAssetFileDescriptor(Uri, String) + */ + public final OutputStream openOutputStream(Uri uri, String mode) + throws FileNotFoundException { + AssetFileDescriptor fd = openAssetFileDescriptor(uri, mode); + try { + return fd != null ? fd.createOutputStream() : null; + } catch (IOException e) { + throw new FileNotFoundException("Unable to create stream"); + } + } + + /** + * Open a raw file descriptor to access data under a "content:" URI. This + * is like {@link #openAssetFileDescriptor(Uri, String)}, but uses the + * underlying {@link ContentProvider#openFile} + * ContentProvider.openFile()} method, so will <em>not</em> work with + * providers that return sub-sections of files. If at all possible, + * you should use {@link #openAssetFileDescriptor(Uri, String)}. You + * will receive a FileNotFoundException exception if the provider returns a + * sub-section of a file. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>file ({@link #SCHEME_FILE})</li> + * </ul> + * + * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information + * on these schemes. + * + * @param uri The desired URI to open. + * @param mode The file mode to use, as per {@link ContentProvider#openFile + * ContentProvider.openFile}. + * @return Returns a new ParcelFileDescriptor pointing to the file. You + * own this descriptor and are responsible for closing it when done. + * @throws FileNotFoundException Throws FileNotFoundException of no + * file exists under the URI or the mode is invalid. + * @see #openAssetFileDescriptor(Uri, String) + */ + public final ParcelFileDescriptor openFileDescriptor(Uri uri, + String mode) throws FileNotFoundException { + AssetFileDescriptor afd = openAssetFileDescriptor(uri, mode); + if (afd == null) { + return null; + } + + if (afd.getDeclaredLength() < 0) { + // This is a full file! + return afd.getParcelFileDescriptor(); + } + + // Client can't handle a sub-section of a file, so close what + // we got and bail with an exception. + try { + afd.close(); + } catch (IOException e) { + } + + throw new FileNotFoundException("Not a whole file"); + } + + /** + * Open a raw file descriptor to access data under a "content:" URI. This + * interacts with the underlying {@link ContentProvider#openAssetFile} + * ContentProvider.openAssetFile()} method of the provider associated with the + * given URI, to retrieve any file stored there. + * + * <h5>Accepts the following URI schemes:</h5> + * <ul> + * <li>content ({@link #SCHEME_CONTENT})</li> + * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li> + * <li>file ({@link #SCHEME_FILE})</li> + * </ul> + * <h5>The android.resource ({@link #SCHEME_ANDROID_RESOURCE}) Scheme</h5> + * <p> + * A Uri object can be used to reference a resource in an APK file. The + * Uri should be one of the following formats: + * <ul> + * <li><code>android.resource://package_name/id_number</code><br/> + * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. + * For example <code>com.example.myapp</code><br/> + * <code>id_number</code> is the int form of the ID.<br/> + * The easiest way to construct this form is + * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/" + R.raw.my_resource");</pre> + * </li> + * <li><code>android.resource://package_name/type/name</code><br/> + * <code>package_name</code> is your package name as listed in your AndroidManifest.xml. + * For example <code>com.example.myapp</code><br/> + * <code>type</code> is the string form of the resource type. For example, <code>raw</code> + * or <code>drawable</code>. + * <code>name</code> is the string form of the resource name. That is, whatever the file + * name was in your res directory, without the type extension. + * The easiest way to construct this form is + * <pre>Uri uri = Uri.parse("android.resource://com.example.myapp/raw/my_resource");</pre> + * </li> + * </ul> + * + * @param uri The desired URI to open. + * @param mode The file mode to use, as per {@link ContentProvider#openAssetFile + * ContentProvider.openAssetFile}. + * @return Returns a new ParcelFileDescriptor pointing to the file. You + * own this descriptor and are responsible for closing it when done. + * @throws FileNotFoundException Throws FileNotFoundException of no + * file exists under the URI or the mode is invalid. + */ + public final AssetFileDescriptor openAssetFileDescriptor(Uri uri, + String mode) throws FileNotFoundException { + String scheme = uri.getScheme(); + if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { + if (!"r".equals(mode)) { + throw new FileNotFoundException("Can't write resources: " + uri); + } + OpenResourceIdResult r = getResourceId(uri); + try { + return r.r.openRawResourceFd(r.id); + } catch (Resources.NotFoundException ex) { + throw new FileNotFoundException("Resource does not exist: " + uri); + } + } else if (SCHEME_FILE.equals(scheme)) { + ParcelFileDescriptor pfd = ParcelFileDescriptor.open( + new File(uri.getPath()), modeToMode(uri, mode)); + return new AssetFileDescriptor(pfd, 0, -1); + } else { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + throw new FileNotFoundException("No content provider: " + uri); + } + try { + AssetFileDescriptor fd = provider.openAssetFile(uri, mode); + if(fd == null) { + releaseProvider(provider); + return null; + } + ParcelFileDescriptor pfd = new ParcelFileDescriptorInner( + fd.getParcelFileDescriptor(), provider); + return new AssetFileDescriptor(pfd, fd.getStartOffset(), + fd.getDeclaredLength()); + } catch (RemoteException e) { + releaseProvider(provider); + throw new FileNotFoundException("Dead content provider: " + uri); + } catch (FileNotFoundException e) { + releaseProvider(provider); + throw e; + } catch (RuntimeException e) { + releaseProvider(provider); + throw e; + } + } + } + + class OpenResourceIdResult { + Resources r; + int id; + } + + OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException { + String authority = uri.getAuthority(); + Resources r; + if (TextUtils.isEmpty(authority)) { + throw new FileNotFoundException("No authority: " + uri); + } else { + try { + r = mContext.getPackageManager().getResourcesForApplication(authority); + } catch (NameNotFoundException ex) { + throw new FileNotFoundException("No package found for authority: " + uri); + } + } + List<String> path = uri.getPathSegments(); + if (path == null) { + throw new FileNotFoundException("No path: " + uri); + } + int len = path.size(); + int id; + if (len == 1) { + try { + id = Integer.parseInt(path.get(0)); + } catch (NumberFormatException e) { + throw new FileNotFoundException("Single path segment is not a resource ID: " + uri); + } + } else if (len == 2) { + id = r.getIdentifier(path.get(1), path.get(0), authority); + } else { + throw new FileNotFoundException("More than two path segments: " + uri); + } + if (id == 0) { + throw new FileNotFoundException("No resource found for: " + uri); + } + OpenResourceIdResult res = new OpenResourceIdResult(); + res.r = r; + res.id = id; + return res; + } + + /** @hide */ + static public int modeToMode(Uri uri, String mode) throws FileNotFoundException { + int modeBits; + if ("r".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_ONLY; + } else if ("w".equals(mode) || "wt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else if ("wa".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_APPEND; + } else if ("rw".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE; + } else if ("rwt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else { + throw new FileNotFoundException("Bad mode for " + uri + ": " + + mode); + } + return modeBits; + } + + /** + * Inserts a row into a table at the given URL. + * + * If the content provider supports transactions the insertion will be atomic. + * + * @param url The URL of the table to insert into. + * @param values The initial values for the newly inserted row. The key is the column name for + * the field. Passing an empty ContentValues will create an empty row. + * @return the URL of the newly created row. + */ + public final Uri insert(Uri url, ContentValues values) + { + IContentProvider provider = acquireProvider(url); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + url); + } + try { + return provider.insert(url, values); + } catch (RemoteException e) { + return null; + } finally { + releaseProvider(provider); + } + } + + /** + * Inserts multiple rows into a table at the given URL. + * + * This function make no guarantees about the atomicity of the insertions. + * + * @param url The URL of the table to insert into. + * @param values The initial values for the newly inserted rows. The key is the column name for + * the field. Passing null will create an empty row. + * @return the number of newly created rows. + */ + public final int bulkInsert(Uri url, ContentValues[] values) + { + IContentProvider provider = acquireProvider(url); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + url); + } + try { + return provider.bulkInsert(url, values); + } catch (RemoteException e) { + return 0; + } finally { + releaseProvider(provider); + } + } + + /** + * Deletes row(s) specified by a content URI. + * + * If the content provider supports transactions, the deletion will be atomic. + * + * @param url The URL of the row to delete. + * @param where A filter to apply to rows before deleting, formatted as an SQL WHERE clause + (excluding the WHERE itself). + * @return The number of rows deleted. + */ + public final int delete(Uri url, String where, String[] selectionArgs) + { + IContentProvider provider = acquireProvider(url); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + url); + } + try { + return provider.delete(url, where, selectionArgs); + } catch (RemoteException e) { + return -1; + } finally { + releaseProvider(provider); + } + } + + /** + * Update row(s) in a content URI. + * + * If the content provider supports transactions the update will be atomic. + * + * @param uri The URI to modify. + * @param values The new field values. The key is the column name for the field. + A null value will remove an existing field value. + * @param where A filter to apply to rows before deleting, formatted as an SQL WHERE clause + (excluding the WHERE itself). + * @return the URL of the newly created row + * @throws NullPointerException if uri or values are null + */ + public final int update(Uri uri, ContentValues values, String where, + String[] selectionArgs) { + IContentProvider provider = acquireProvider(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URI " + uri); + } + try { + return provider.update(uri, values, where, selectionArgs); + } catch (RemoteException e) { + return -1; + } finally { + releaseProvider(provider); + } + } + + /** + * Returns the content provider for the given content URI.. + * + * @param uri The URI to a content provider + * @return The ContentProvider for the given URI, or null if no content provider is found. + * @hide + */ + public final IContentProvider acquireProvider(Uri uri) + { + if (!SCHEME_CONTENT.equals(uri.getScheme())) { + return null; + } + String auth = uri.getAuthority(); + if (auth != null) { + return acquireProvider(mContext, uri.getAuthority()); + } + return null; + } + + /** + * @hide + */ + public final IContentProvider acquireProvider(String name) { + if(name == null) { + return null; + } + return acquireProvider(mContext, name); + } + + /** + * Register an observer class that gets callbacks when data identified by a + * given content URI changes. + * + * @param uri The URI to watch for changes. This can be a specific row URI, or a base URI + * for a whole class of content. + * @param notifyForDescendents If <code>true</code> changes to URIs beginning with <code>uri</code> + * will also cause notifications to be sent. If <code>false</code> only changes to the exact URI + * specified by <em>uri</em> will cause notifications to be sent. If true, than any URI values + * at or below the specified URI will also trigger a match. + * @param observer The object that receives callbacks when changes occur. + * @see #unregisterContentObserver + */ + public final void registerContentObserver(Uri uri, boolean notifyForDescendents, + ContentObserver observer) + { + try { + ContentServiceNative.getDefault().registerContentObserver(uri, notifyForDescendents, + observer.getContentObserver()); + } catch (RemoteException e) { + } + } + + /** + * Unregisters a change observer. + * + * @param observer The previously registered observer that is no longer needed. + * @see #registerContentObserver + */ + public final void unregisterContentObserver(ContentObserver observer) { + try { + IContentObserver contentObserver = observer.releaseContentObserver(); + if (contentObserver != null) { + ContentServiceNative.getDefault().unregisterContentObserver( + contentObserver); + } + } catch (RemoteException e) { + } + } + + /** + * Notify registered observers that a row was updated. + * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. + * By default, CursorAdapter objects will get this notification. + * + * @param uri + * @param observer The observer that originated the change, may be <code>null</null> + */ + public void notifyChange(Uri uri, ContentObserver observer) { + notifyChange(uri, observer, true /* sync to network */); + } + + /** + * Notify registered observers that a row was updated. + * To register, call {@link #registerContentObserver(android.net.Uri , boolean, android.database.ContentObserver) registerContentObserver()}. + * By default, CursorAdapter objects will get this notification. + * + * @param uri + * @param observer The observer that originated the change, may be <code>null</null> + * @param syncToNetwork If true, attempt to sync the change to the network. + */ + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + try { + ContentServiceNative.getDefault().notifyChange( + uri, observer == null ? null : observer.getContentObserver(), + observer != null && observer.deliverSelfNotifications(), syncToNetwork); + } catch (RemoteException e) { + } + } + + /** + * 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 uri the uri of the provider to sync or null to sync all providers. + * @param extras any extras to pass to the SyncAdapter. + */ + public void startSync(Uri uri, Bundle extras) { + validateSyncExtrasBundle(extras); + try { + ContentServiceNative.getDefault().startSync(uri, extras); + } catch (RemoteException e) { + } + } + + /** + * Check that only values of the following types are in the Bundle: + * <ul> + * <li>Integer</li> + * <li>Long</li> + * <li>Boolean</li> + * <li>Float</li> + * <li>Double</li> + * <li>String</li> + * <li>null</li> + * </ul> + * @param extras the Bundle to check + */ + public static void validateSyncExtrasBundle(Bundle extras) { + try { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value == null) continue; + if (value instanceof Long) continue; + if (value instanceof Integer) continue; + if (value instanceof Boolean) continue; + if (value instanceof Float) continue; + if (value instanceof Double) continue; + if (value instanceof String) continue; + throw new IllegalArgumentException("unexpected value type: " + + value.getClass().getName()); + } + } catch (IllegalArgumentException e) { + throw e; + } catch (RuntimeException exc) { + throw new IllegalArgumentException("error unparceling Bundle", exc); + } + } + + public void cancelSync(Uri uri) { + try { + ContentServiceNative.getDefault().cancelSync(uri); + } catch (RemoteException e) { + } + } + + private final class CursorWrapperInner extends CursorWrapper { + private IContentProvider mContentProvider; + public static final String TAG="CursorWrapperInner"; + private boolean mCloseFlag = false; + + CursorWrapperInner(Cursor cursor, IContentProvider icp) { + super(cursor); + mContentProvider = icp; + } + + @Override + public void close() { + super.close(); + ContentResolver.this.releaseProvider(mContentProvider); + mCloseFlag = true; + } + + @Override + protected void finalize() throws Throwable { + try { + if(!mCloseFlag) { + ContentResolver.this.releaseProvider(mContentProvider); + } + } finally { + super.finalize(); + } + } + } + + private final class ParcelFileDescriptorInner extends ParcelFileDescriptor { + private IContentProvider mContentProvider; + public static final String TAG="ParcelFileDescriptorInner"; + private boolean mReleaseProviderFlag = false; + + ParcelFileDescriptorInner(ParcelFileDescriptor pfd, IContentProvider icp) { + super(pfd); + mContentProvider = icp; + } + + @Override + public void close() throws IOException { + if(!mReleaseProviderFlag) { + super.close(); + ContentResolver.this.releaseProvider(mContentProvider); + mReleaseProviderFlag = true; + } + } + + @Override + protected void finalize() throws Throwable { + if (!mReleaseProviderFlag) { + close(); + } + } + } + + private final Context mContext; + private static final String TAG = "ContentResolver"; +} diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java new file mode 100644 index 0000000..b028868 --- /dev/null +++ b/core/java/android/content/ContentService.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2006 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.IContentObserver; +import android.database.sqlite.SQLiteException; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Config; +import android.util.Log; +import android.Manifest; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * {@hide} + */ +public final class ContentService extends ContentServiceNative { + private static final String TAG = "ContentService"; + private Context mContext; + private boolean mFactoryTest; + private final ObserverNode mRootNode = new ObserverNode(""); + private SyncManager mSyncManager = null; + private final Object mSyncManagerLock = new Object(); + + private SyncManager getSyncManager() { + synchronized(mSyncManagerLock) { + try { + // Try to create the SyncManager, return null if it fails (e.g. the disk is full). + if (mSyncManager == null) mSyncManager = new SyncManager(mContext, mFactoryTest); + } catch (SQLiteException e) { + Log.e(TAG, "Can't create SyncManager", e); + } + return mSyncManager; + } + } + + @Override + protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP, + "caller doesn't have the DUMP permission"); + + // 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 { + if (mSyncManager == null) { + pw.println("No SyncManager created! (Disk full?)"); + } else { + mSyncManager.dump(fd, pw); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + /*package*/ ContentService(Context context, boolean factoryTest) { + mContext = context; + mFactoryTest = factoryTest; + getSyncManager(); + } + + public void registerContentObserver(Uri uri, boolean notifyForDescendents, + IContentObserver observer) { + if (observer == null || uri == null) { + throw new IllegalArgumentException("You must pass a valid uri and observer"); + } + synchronized (mRootNode) { + mRootNode.addObserver(uri, observer, notifyForDescendents); + if (Config.LOGV) Log.v(TAG, "Registered observer " + observer + " at " + uri + + " with notifyForDescendents " + notifyForDescendents); + } + } + + public void unregisterContentObserver(IContentObserver observer) { + if (observer == null) { + throw new IllegalArgumentException("You must pass a valid observer"); + } + synchronized (mRootNode) { + mRootNode.removeObserver(observer); + if (Config.LOGV) Log.v(TAG, "Unregistered observer " + observer); + } + } + + public void notifyChange(Uri uri, IContentObserver observer, + boolean observerWantsSelfNotifications, boolean syncToNetwork) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Notifying update of " + uri + " from observer " + observer + + ", syncToNetwork " + syncToNetwork); + } + // 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 { + ArrayList<ObserverCall> calls = new ArrayList<ObserverCall>(); + synchronized (mRootNode) { + mRootNode.collectObservers(uri, 0, observer, observerWantsSelfNotifications, + calls); + } + final int numCalls = calls.size(); + for (int i=0; i<numCalls; i++) { + ObserverCall oc = calls.get(i); + try { + oc.mObserver.onChange(oc.mSelfNotify); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Notified " + oc.mObserver + " of " + "update at " + uri); + } + } catch (RemoteException ex) { + synchronized (mRootNode) { + Log.w(TAG, "Found dead observer, removing"); + IBinder binder = oc.mObserver.asBinder(); + final ArrayList<ObserverNode.ObserverEntry> list + = oc.mNode.mObservers; + int numList = list.size(); + for (int j=0; j<numList; j++) { + ObserverNode.ObserverEntry oe = list.get(j); + if (oe.observer.asBinder() == binder) { + list.remove(j); + j--; + numList--; + } + } + } + } + } + if (syncToNetwork) { + SyncManager syncManager = getSyncManager(); + if (syncManager != null) syncManager.scheduleLocalSync(uri); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + /** + * Hide this class since it is not part of api, + * but current unittest framework requires it to be public + * @hide + * + */ + public static final class ObserverCall { + final ObserverNode mNode; + final IContentObserver mObserver; + final boolean mSelfNotify; + + ObserverCall(ObserverNode node, IContentObserver observer, + boolean selfNotify) { + mNode = node; + mObserver = observer; + mSelfNotify = selfNotify; + } + } + + public void startSync(Uri url, 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); + } finally { + restoreCallingIdentity(identityToken); + } + } + + /** + * 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. + */ + public void cancelSync(Uri uri) { + // 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); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public static IContentService main(Context context, boolean factoryTest) { + ContentService service = new ContentService(context, factoryTest); + ServiceManager.addService("content", service); + return service; + } + + /** + * Hide this class since it is not part of api, + * but current unittest framework requires it to be public + * @hide + */ + public static final class ObserverNode { + private class ObserverEntry implements IBinder.DeathRecipient { + public IContentObserver observer; + public boolean notifyForDescendents; + + public ObserverEntry(IContentObserver o, boolean n) { + observer = o; + notifyForDescendents = n; + try { + observer.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + + public void binderDied() { + removeObserver(observer); + } + } + + public static final int INSERT_TYPE = 0; + public static final int UPDATE_TYPE = 1; + public static final int DELETE_TYPE = 2; + + private String mName; + private ArrayList<ObserverNode> mChildren = new ArrayList<ObserverNode>(); + private ArrayList<ObserverEntry> mObservers = new ArrayList<ObserverEntry>(); + + public ObserverNode(String name) { + mName = name; + } + + private String getUriSegment(Uri uri, int index) { + if (uri != null) { + if (index == 0) { + return uri.getAuthority(); + } else { + return uri.getPathSegments().get(index - 1); + } + } else { + return null; + } + } + + private int countUriSegments(Uri uri) { + if (uri == null) { + return 0; + } + return uri.getPathSegments().size() + 1; + } + + public void addObserver(Uri uri, IContentObserver observer, boolean notifyForDescendents) { + addObserver(uri, 0, observer, notifyForDescendents); + } + + private void addObserver(Uri uri, int index, IContentObserver observer, + boolean notifyForDescendents) { + + // If this is the leaf node add the observer + if (index == countUriSegments(uri)) { + mObservers.add(new ObserverEntry(observer, notifyForDescendents)); + return; + } + + // Look to see if the proper child already exists + String segment = getUriSegment(uri, index); + int N = mChildren.size(); + for (int i = 0; i < N; i++) { + ObserverNode node = mChildren.get(i); + if (node.mName.equals(segment)) { + node.addObserver(uri, index + 1, observer, notifyForDescendents); + return; + } + } + + // No child found, create one + ObserverNode node = new ObserverNode(segment); + mChildren.add(node); + node.addObserver(uri, index + 1, observer, notifyForDescendents); + } + + public boolean removeObserver(IContentObserver observer) { + int size = mChildren.size(); + for (int i = 0; i < size; i++) { + boolean empty = mChildren.get(i).removeObserver(observer); + if (empty) { + mChildren.remove(i); + i--; + size--; + } + } + + IBinder observerBinder = observer.asBinder(); + size = mObservers.size(); + for (int i = 0; i < size; i++) { + ObserverEntry entry = mObservers.get(i); + if (entry.observer.asBinder() == observerBinder) { + mObservers.remove(i); + // We no longer need to listen for death notifications. Remove it. + observerBinder.unlinkToDeath(entry, 0); + break; + } + } + + if (mChildren.size() == 0 && mObservers.size() == 0) { + return true; + } + return false; + } + + private void collectMyObservers(Uri uri, + boolean leaf, IContentObserver observer, boolean selfNotify, + ArrayList<ObserverCall> calls) + { + int N = mObservers.size(); + IBinder observerBinder = observer == null ? null : observer.asBinder(); + for (int i = 0; i < N; i++) { + ObserverEntry entry = mObservers.get(i); + + // Don't notify the observer if it sent the notification and isn't interesed + // in self notifications + if (entry.observer.asBinder() == observerBinder && !selfNotify) { + continue; + } + + // Make sure the observer is interested in the notification + if (leaf || (!leaf && entry.notifyForDescendents)) { + calls.add(new ObserverCall(this, entry.observer, selfNotify)); + } + } + } + + public void collectObservers(Uri uri, int index, IContentObserver observer, + boolean selfNotify, ArrayList<ObserverCall> calls) { + String segment = null; + int segmentCount = countUriSegments(uri); + if (index >= segmentCount) { + // This is the leaf node, notify all observers + collectMyObservers(uri, true, observer, selfNotify, calls); + } else if (index < segmentCount){ + segment = getUriSegment(uri, index); + // Notify any observers at this level who are interested in descendents + collectMyObservers(uri, false, observer, selfNotify, calls); + } + + int N = mChildren.size(); + for (int i = 0; i < N; i++) { + ObserverNode node = mChildren.get(i); + if (segment == null || node.mName.equals(segment)) { + // We found the child, + node.collectObservers(uri, index + 1, observer, selfNotify, calls); + if (segment != null) { + break; + } + } + } + } + } +} diff --git a/core/java/android/content/ContentServiceNative.java b/core/java/android/content/ContentServiceNative.java new file mode 100644 index 0000000..364f9ee --- /dev/null +++ b/core/java/android/content/ContentServiceNative.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2006 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.IContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.Parcel; +import android.os.ServiceManager; +import android.os.Bundle; +import android.util.Config; +import android.util.Log; + +/** + * {@hide} + */ +abstract class ContentServiceNative extends Binder implements IContentService +{ + public ContentServiceNative() + { + attachInterface(this, descriptor); + } + + /** + * Cast a Binder object into a content resolver interface, generating + * a proxy if needed. + */ + static public IContentService asInterface(IBinder obj) + { + if (obj == null) { + return null; + } + IContentService in = + (IContentService)obj.queryLocalInterface(descriptor); + if (in != null) { + return in; + } + + return new ContentServiceProxy(obj); + } + + /** + * Retrieve the system's default/global content service. + */ + static public IContentService getDefault() + { + if (gDefault != null) { + return gDefault; + } + IBinder b = ServiceManager.getService("content"); + if (Config.LOGV) Log.v("ContentService", "default service binder = " + b); + gDefault = asInterface(b); + if (Config.LOGV) Log.v("ContentService", "default service = " + gDefault); + return gDefault; + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + { + try { + switch (code) { + case 5038: { + data.readString(); // ignore the interface token that service generated + Uri uri = Uri.parse(data.readString()); + notifyChange(uri, null, false, false); + return true; + } + + case REGISTER_CONTENT_OBSERVER_TRANSACTION: { + Uri uri = Uri.CREATOR.createFromParcel(data); + boolean notifyForDescendents = data.readInt() != 0; + IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); + registerContentObserver(uri, notifyForDescendents, observer); + return true; + } + + case UNREGISTER_CHANGE_OBSERVER_TRANSACTION: { + IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); + unregisterContentObserver(observer); + return true; + } + + case NOTIFY_CHANGE_TRANSACTION: { + Uri uri = Uri.CREATOR.createFromParcel(data); + IContentObserver observer = IContentObserver.Stub.asInterface(data.readStrongBinder()); + boolean observerWantsSelfNotifications = data.readInt() != 0; + boolean syncToNetwork = data.readInt() != 0; + notifyChange(uri, observer, observerWantsSelfNotifications, syncToNetwork); + return true; + } + + case START_SYNC_TRANSACTION: { + Uri url = null; + int hasUrl = data.readInt(); + if (hasUrl != 0) { + url = Uri.CREATOR.createFromParcel(data); + } + startSync(url, data.readBundle()); + return true; + } + + case CANCEL_SYNC_TRANSACTION: { + Uri url = null; + int hasUrl = data.readInt(); + if (hasUrl != 0) { + url = Uri.CREATOR.createFromParcel(data); + } + cancelSync(url); + return true; + } + + default: + return super.onTransact(code, data, reply, flags); + } + } catch (Exception e) { + Log.e("ContentServiceNative", "Caught exception in transact", e); + } + + return false; + } + + public IBinder asBinder() + { + return this; + } + + private static IContentService gDefault; +} + + +final class ContentServiceProxy implements IContentService +{ + public ContentServiceProxy(IBinder remote) + { + mRemote = remote; + } + + public IBinder asBinder() + { + return mRemote; + } + + public void registerContentObserver(Uri uri, boolean notifyForDescendents, + IContentObserver observer) throws RemoteException + { + Parcel data = Parcel.obtain(); + uri.writeToParcel(data, 0); + data.writeInt(notifyForDescendents ? 1 : 0); + data.writeStrongInterface(observer); + mRemote.transact(REGISTER_CONTENT_OBSERVER_TRANSACTION, data, null, 0); + data.recycle(); + } + + public void unregisterContentObserver(IContentObserver observer) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeStrongInterface(observer); + mRemote.transact(UNREGISTER_CHANGE_OBSERVER_TRANSACTION, data, null, 0); + data.recycle(); + } + + public void notifyChange(Uri uri, IContentObserver observer, + boolean observerWantsSelfNotifications, boolean syncToNetwork) + throws RemoteException { + Parcel data = Parcel.obtain(); + uri.writeToParcel(data, 0); + data.writeStrongInterface(observer); + data.writeInt(observerWantsSelfNotifications ? 1 : 0); + data.writeInt(syncToNetwork ? 1 : 0); + mRemote.transact(NOTIFY_CHANGE_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } + + public void startSync(Uri url, Bundle extras) throws RemoteException { + Parcel data = Parcel.obtain(); + if (url == null) { + data.writeInt(0); + } else { + data.writeInt(1); + url.writeToParcel(data, 0); + } + extras.writeToParcel(data, 0); + mRemote.transact(START_SYNC_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); + data.recycle(); + } + + public void cancelSync(Uri url) throws RemoteException { + Parcel data = Parcel.obtain(); + if (url == null) { + data.writeInt(0); + } else { + data.writeInt(1); + url.writeToParcel(data, 0); + } + mRemote.transact(CANCEL_SYNC_TRANSACTION, data, null /* reply */, IBinder.FLAG_ONEWAY); + data.recycle(); + } + + private IBinder mRemote; +} + diff --git a/core/java/android/content/ContentUris.java b/core/java/android/content/ContentUris.java new file mode 100644 index 0000000..aa76034 --- /dev/null +++ b/core/java/android/content/ContentUris.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 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; + +import android.net.Uri; + +/** + * Utility methods useful for working with content {@link android.net.Uri}s, + * those with a "content" scheme. + */ +public class ContentUris { + + /** + * Converts the last path segment to a long. + * + * <p>This supports a common convention for content URIs where an ID is + * stored in the last segment. + * + * @throws UnsupportedOperationException if this isn't a hierarchical URI + * @throws NumberFormatException if the last segment isn't a number + * + * @return the long conversion of the last segment or -1 if the path is + * empty + */ + public static long parseId(Uri contentUri) { + String last = contentUri.getLastPathSegment(); + return last == null ? -1 : Long.parseLong(last); + } + + /** + * Appends the given ID to the end of the path. + * + * @param builder to append the ID to + * @param id to append + * + * @return the given builder + */ + public static Uri.Builder appendId(Uri.Builder builder, long id) { + return builder.appendEncodedPath(String.valueOf(id)); + } + + /** + * Appends the given ID to the end of the path. + * + * @param contentUri to start with + * @param id to append + * + * @return a new URI with the given ID appended to the end of the path + */ + public static Uri withAppendedId(Uri contentUri, long id) { + return appendId(contentUri.buildUpon(), id).build(); + } +} diff --git a/core/java/android/content/ContentValues.java b/core/java/android/content/ContentValues.java new file mode 100644 index 0000000..532cc03 --- /dev/null +++ b/core/java/android/content/ContentValues.java @@ -0,0 +1,501 @@ +/* + * Copyright (C) 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; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class is used to store a set of values that the {@link ContentResolver} + * can process. + */ +public final class ContentValues implements Parcelable { + public static final String TAG = "ContentValues"; + + /** Holds the actual values */ + private HashMap<String, Object> mValues; + + /** + * Creates an empty set of values using the default initial size + */ + public ContentValues() { + // Choosing a default size of 8 based on analysis of typical + // consumption by applications. + mValues = new HashMap<String, Object>(8); + } + + /** + * Creates an empty set of values using the given initial size + * + * @param size the initial size of the set of values + */ + public ContentValues(int size) { + mValues = new HashMap<String, Object>(size, 1.0f); + } + + /** + * Creates a set of values copied from the given set + * + * @param from the values to copy + */ + public ContentValues(ContentValues from) { + mValues = new HashMap<String, Object>(from.mValues); + } + + /** + * Creates a set of values copied from the given HashMap. This is used + * by the Parcel unmarshalling code. + * + * @param from the values to start with + * {@hide} + */ + private ContentValues(HashMap<String, Object> values) { + mValues = values; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof ContentValues)) { + return false; + } + return mValues.equals(((ContentValues) object).mValues); + } + + @Override + public int hashCode() { + return mValues.hashCode(); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, String value) { + mValues.put(key, value); + } + + /** + * Adds all values from the passed in ContentValues. + * + * @param other the ContentValues from which to copy + */ + public void putAll(ContentValues other) { + mValues.putAll(other.mValues); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Byte value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Short value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Integer value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Long value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Float value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Double value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, Boolean value) { + mValues.put(key, value); + } + + /** + * Adds a value to the set. + * + * @param key the name of the value to put + * @param value the data for the value to put + */ + public void put(String key, byte[] value) { + mValues.put(key, value); + } + + /** + * Adds a null value to the set. + * + * @param key the name of the value to make null + */ + public void putNull(String key) { + mValues.put(key, null); + } + + /** + * Returns the number of values. + * + * @return the number of values + */ + public int size() { + return mValues.size(); + } + + /** + * Remove a single value. + * + * @param key the name of the value to remove + */ + public void remove(String key) { + mValues.remove(key); + } + + /** + * Removes all values. + */ + public void clear() { + mValues.clear(); + } + + /** + * Returns true if this object has the named value. + * + * @param key the value to check for + * @return {@code true} if the value is present, {@code false} otherwise + */ + public boolean containsKey(String key) { + return mValues.containsKey(key); + } + + /** + * Gets a value. Valid value types are {@link String}, {@link Boolean}, and + * {@link Number} implementations. + * + * @param key the value to get + * @return the data for the value + */ + public Object get(String key) { + return mValues.get(key); + } + + /** + * Gets a value and converts it to a String. + * + * @param key the value to get + * @return the String for the value + */ + public String getAsString(String key) { + Object value = mValues.get(key); + return value != null ? mValues.get(key).toString() : null; + } + + /** + * Gets a value and converts it to a Long. + * + * @param key the value to get + * @return the Long value, or null if the value is missing or cannot be converted + */ + public Long getAsLong(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).longValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Long.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Long"); + return null; + } + } + } + + /** + * Gets a value and converts it to an Integer. + * + * @param key the value to get + * @return the Integer value, or null if the value is missing or cannot be converted + */ + public Integer getAsInteger(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).intValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Integer.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Integer"); + return null; + } + } + } + + /** + * Gets a value and converts it to a Short. + * + * @param key the value to get + * @return the Short value, or null if the value is missing or cannot be converted + */ + public Short getAsShort(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).shortValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Short.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Short value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Short"); + return null; + } + } + } + + /** + * Gets a value and converts it to a Byte. + * + * @param key the value to get + * @return the Byte value, or null if the value is missing or cannot be converted + */ + public Byte getAsByte(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).byteValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Byte.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Byte value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Byte"); + return null; + } + } + } + + /** + * Gets a value and converts it to a Double. + * + * @param key the value to get + * @return the Double value, or null if the value is missing or cannot be converted + */ + public Double getAsDouble(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).doubleValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Double.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Double value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Double"); + return null; + } + } + } + + /** + * Gets a value and converts it to a Float. + * + * @param key the value to get + * @return the Float value, or null if the value is missing or cannot be converted + */ + public Float getAsFloat(String key) { + Object value = mValues.get(key); + try { + return value != null ? ((Number) value).floatValue() : null; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + try { + return Float.valueOf(value.toString()); + } catch (NumberFormatException e2) { + Log.e(TAG, "Cannot parse Float value for " + value + " at key " + key); + return null; + } + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Float"); + return null; + } + } + } + + /** + * Gets a value and converts it to a Boolean. + * + * @param key the value to get + * @return the Boolean value, or null if the value is missing or cannot be converted + */ + public Boolean getAsBoolean(String key) { + Object value = mValues.get(key); + try { + return (Boolean) value; + } catch (ClassCastException e) { + if (value instanceof CharSequence) { + return Boolean.valueOf(value.toString()); + } else { + Log.e(TAG, "Cannot cast value for " + key + " to a Boolean"); + return null; + } + } + } + + /** + * Gets a value that is a byte array. Note that this method will not convert + * any other types to byte arrays. + * + * @param key the value to get + * @return the byte[] value, or null is the value is missing or not a byte[] + */ + public byte[] getAsByteArray(String key) { + Object value = mValues.get(key); + if (value instanceof byte[]) { + return (byte[]) value; + } else { + return null; + } + } + + /** + * Returns a set of all of the keys and values + * + * @return a set of all of the keys and values + */ + public Set<Map.Entry<String, Object>> valueSet() { + return mValues.entrySet(); + } + + public static final Parcelable.Creator<ContentValues> CREATOR = + new Parcelable.Creator<ContentValues>() { + @SuppressWarnings({"deprecation", "unchecked"}) + public ContentValues createFromParcel(Parcel in) { + // TODO - what ClassLoader should be passed to readHashMap? + HashMap<String, Object> values = in.readHashMap(null); + return new ContentValues(values); + } + + public ContentValues[] newArray(int size) { + return new ContentValues[size]; + } + }; + + public int describeContents() { + return 0; + } + + @SuppressWarnings("deprecation") + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeMap(mValues); + } + + /** + * Unsupported, here until we get proper bulk insert APIs. + * {@hide} + */ + @Deprecated + public void putStringArrayList(String key, ArrayList<String> value) { + mValues.put(key, value); + } + + /** + * Unsupported, here until we get proper bulk insert APIs. + * {@hide} + */ + @SuppressWarnings("unchecked") + @Deprecated + public ArrayList<String> getStringArrayList(String key) { + return (ArrayList<String>) mValues.get(key); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (String name : mValues.keySet()) { + String value = getAsString(name); + if (sb.length() > 0) sb.append(" "); + sb.append(name + "=" + value); + } + return sb.toString(); + } +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java new file mode 100644 index 0000000..e0fe533 --- /dev/null +++ b/core/java/android/content/Context.java @@ -0,0 +1,1654 @@ +/* + * Copyright (C) 2006 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.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface to global information about an application environment. This is + * an abstract class whose implementation is provided by + * the Android system. It + * allows access to application-specific resources and classes, as well as + * up-calls for application-level operations such as launching activities, + * broadcasting and receiving intents, etc. + */ +public abstract class Context { + /** + * File creation mode: the default mode, where the created file can only + * be accessed by the calling application (or all applications sharing the + * same user ID). + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + */ + public static final int MODE_PRIVATE = 0x0000; + /** + * File creation mode: allow all other applications to have read access + * to the created file. + * @see #MODE_PRIVATE + * @see #MODE_WORLD_WRITEABLE + */ + public static final int MODE_WORLD_READABLE = 0x0001; + /** + * File creation mode: allow all other applications to have write access + * to the created file. + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + */ + public static final int MODE_WORLD_WRITEABLE = 0x0002; + /** + * File creation mode: for use with {@link #openFileOutput}, if the file + * already exists then write data to the end of the existing file + * instead of erasing it. + * @see #openFileOutput + */ + public static final int MODE_APPEND = 0x8000; + + /** + * Flag for {@link #bindService}: automatically create the service as long + * as the binding exists. Note that while this will create the service, + * its {@link android.app.Service#onStart} method will still only be called due to an + * explicit call to {@link #startService}. Even without that, though, + * this still provides you with access to the service object while the + * service is created. + * + * <p>Specifying this flag also tells the system to treat the service + * as being as important as your own process -- that is, when deciding + * which process should be killed to free memory, the service will only + * be considered a candidate as long as the processes of any such bindings + * is also a candidate to be killed. This is to avoid situations where + * the service is being continually created and killed due to low memory. + */ + public static final int BIND_AUTO_CREATE = 0x0001; + + /** + * Flag for {@link #bindService}: include debugging help for mismatched + * calls to unbind. When this flag is set, the callstack of the following + * {@link #unbindService} call is retained, to be printed if a later + * incorrect unbind call is made. Note that doing this requires retaining + * information about the binding that was made for the lifetime of the app, + * resulting in a leak -- this should only be used for debugging. + */ + public static final int BIND_DEBUG_UNBIND = 0x0002; + + /** Return an AssetManager instance for your application's package. */ + public abstract AssetManager getAssets(); + + /** Return a Resources instance for your application's package. */ + public abstract Resources getResources(); + + /** Return PackageManager instance to find global package information. */ + public abstract PackageManager getPackageManager(); + + /** Return a ContentResolver instance for your application's package. */ + public abstract ContentResolver getContentResolver(); + + /** + * Return the Looper for the main thread of the current process. This is + * the thread used to dispatch calls to application components (activities, + * services, etc). + */ + public abstract Looper getMainLooper(); + + /** + * Return the context of the single, global Application object of the + * current process. + */ + public abstract Context getApplicationContext(); + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + public final CharSequence getText(int resId) { + return getResources().getText(resId); + } + + /** + * Return a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + */ + public final String getString(int resId) { + return getResources().getString(resId); + } + + /** + * Return a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + + public final String getString(int resId, Object... formatArgs) { + return getResources().getString(resId, formatArgs); + } + + /** + * Set the base theme for this context. Note that this should be called + * before any views are instantiated in the Context (for example before + * calling {@link android.app.Activity#setContentView} or + * {@link android.view.LayoutInflater#inflate}). + * + * @param resid The style resource describing the theme. + */ + public abstract void setTheme(int resid); + + /** + * Return the Theme object associated with this Context. + */ + public abstract Resources.Theme getTheme(); + + /** + * Retrieve styled attribute information in this Context's theme. See + * {@link Resources.Theme#obtainStyledAttributes(int[])} + * for more information. + * + * @see Resources.Theme#obtainStyledAttributes(int[]) + */ + public final TypedArray obtainStyledAttributes( + int[] attrs) { + return getTheme().obtainStyledAttributes(attrs); + } + + /** + * Retrieve styled attribute information in this Context's theme. See + * {@link Resources.Theme#obtainStyledAttributes(int, int[])} + * for more information. + * + * @see Resources.Theme#obtainStyledAttributes(int, int[]) + */ + public final TypedArray obtainStyledAttributes( + int resid, int[] attrs) throws Resources.NotFoundException { + return getTheme().obtainStyledAttributes(resid, attrs); + } + + /** + * Retrieve styled attribute information in this Context's theme. See + * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} + * for more information. + * + * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + */ + public final TypedArray obtainStyledAttributes( + AttributeSet set, int[] attrs) { + return getTheme().obtainStyledAttributes(set, attrs, 0, 0); + } + + /** + * Retrieve styled attribute information in this Context's theme. See + * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} + * for more information. + * + * @see Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + */ + public final TypedArray obtainStyledAttributes( + AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { + return getTheme().obtainStyledAttributes( + set, attrs, defStyleAttr, defStyleRes); + } + + /** + * Return a class loader you can use to retrieve classes in this package. + */ + public abstract ClassLoader getClassLoader(); + + /** Return the name of this application's package. */ + public abstract String getPackageName(); + + /** + * {@hide} + * Return the full path to this context's resource files. This is the ZIP files + * containing the application's resources. + * + * <p>Note: this is not generally useful for applications, since they should + * not be directly accessing the file system. + * + * + * @return String Path to the resources. + */ + public abstract String getPackageResourcePath(); + + /** + * {@hide} + * Return the full path to this context's code and asset files. This is the ZIP files + * containing the application's code and assets. + * + * <p>Note: this is not generally useful for applications, since they should + * not be directly accessing the file system. + * + * + * @return String Path to the code and assets. + */ + public abstract String getPackageCodePath(); + + /** + * Retrieve and hold the contents of the preferences file 'name', returning + * a SharedPreferences through which you can retrieve and modify its + * values. Only one instance of the SharedPreferences object is returned + * to any callers for the same name, meaning they will see each other's + * edits as soon as they are made. + * + * @param name Desired preferences file. If a preferences file by this name + * does not exist, it will be created when you retrieve an + * editor (SharedPreferences.edit()) and then commit changes (Editor.commit()). + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_WORLD_READABLE} + * and {@link #MODE_WORLD_WRITEABLE} to control permissions. + * + * @return Returns the single SharedPreferences instance that can be used + * to retrieve and modify the preference values. + * + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + */ + public abstract SharedPreferences getSharedPreferences(String name, + int mode); + + /** + * Open a private file associated with this Context's application package + * for reading. + * + * @param name The name of the file to open; can not contain path + * separators. + * + * @return FileInputStream Resulting input stream. + * + * @see #openFileOutput + * @see #fileList + * @see #deleteFile + * @see java.io.FileInputStream#FileInputStream(String) + */ + public abstract FileInputStream openFileInput(String name) + throws FileNotFoundException; + + /** + * Open a private file associated with this Context's application package + * for writing. Creates the file if it doesn't already exist. + * + * @param name The name of the file to open; can not contain path + * separators. + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_APPEND} to append to an existing file, + * {@link #MODE_WORLD_READABLE} and {@link #MODE_WORLD_WRITEABLE} to control + * permissions. + * + * @return FileOutputStream Resulting output stream. + * + * @see #MODE_APPEND + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + * @see #openFileInput + * @see #fileList + * @see #deleteFile + * @see java.io.FileOutputStream#FileOutputStream(String) + */ + public abstract FileOutputStream openFileOutput(String name, int mode) + throws FileNotFoundException; + + /** + * Delete the given private file associated with this Context's + * application package. + * + * @param name The name of the file to delete; can not contain path + * separators. + * + * @return True if the file was successfully deleted; else + * false. + * + * @see #openFileInput + * @see #openFileOutput + * @see #fileList + * @see java.io.File#delete() + */ + public abstract boolean deleteFile(String name); + + /** + * Returns the absolute path on the filesystem where a file created with + * {@link #openFileOutput} is stored. + * + * @param name The name of the file for which you would like to get + * its path. + * + * @return Returns an absolute path to the given file. + * + * @see #openFileOutput + * @see #getFilesDir + * @see #getDir + */ + public abstract File getFileStreamPath(String name); + + /** + * Returns the absolute path to the directory on the filesystem where + * files created with {@link #openFileOutput} are stored. + * + * @return Returns the path of the directory holding application files. + * + * @see #openFileOutput + * @see #getFileStreamPath + * @see #getDir + */ + public abstract File getFilesDir(); + + /** + * Returns the absolute path to the application specific cache directory + * on the filesystem. These files will be ones that get deleted first when the + * device runs low on storage + * There is no guarantee when these files will be deleted. + * + * @return Returns the path of the directory holding application cache files. + * + * @see #openFileOutput + * @see #getFileStreamPath + * @see #getDir + */ + public abstract File getCacheDir(); + + /** + * Returns an array of strings naming the private files associated with + * this Context's application package. + * + * @return Array of strings naming the private files. + * + * @see #openFileInput + * @see #openFileOutput + * @see #deleteFile + */ + public abstract String[] fileList(); + + /** + * Retrieve, creating if needed, a new directory in which the application + * can place its own custom data files. You can use the returned File + * object to create and access files in this directory. Note that files + * created through a File object will only be accessible by your own + * application; you can only set the mode of the entire directory, not + * of individual files. + * + * @param name Name of the directory to retrieve. This is a directory + * that is created as part of your application data. + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_WORLD_READABLE} and + * {@link #MODE_WORLD_WRITEABLE} to control permissions. + * + * @return Returns a File object for the requested directory. The directory + * will have been created if it does not already exist. + * + * @see #openFileOutput(String, int) + */ + public abstract File getDir(String name, int mode); + + /** + * Open a new private SQLiteDatabase associated with this Context's + * application package. Create the database file if it doesn't exist. + * + * @param name The name (unique in the application package) of the database. + * @param mode Operating mode. Use 0 or {@link #MODE_PRIVATE} for the + * default operation, {@link #MODE_WORLD_READABLE} + * and {@link #MODE_WORLD_WRITEABLE} to control permissions. + * @param factory An optional factory class that is called to instantiate a + * cursor when query is called. + * + * @return The contents of a newly created database with the given name. + * @throws android.database.sqlite.SQLiteException if the database file could not be opened. + * + * @see #MODE_PRIVATE + * @see #MODE_WORLD_READABLE + * @see #MODE_WORLD_WRITEABLE + * @see #deleteDatabase + */ + public abstract SQLiteDatabase openOrCreateDatabase(String name, + int mode, CursorFactory factory); + + /** + * Delete an existing private SQLiteDatabase associated with this Context's + * application package. + * + * @param name The name (unique in the application package) of the + * database. + * + * @return True if the database was successfully deleted; else false. + * + * @see #openOrCreateDatabase + */ + public abstract boolean deleteDatabase(String name); + + /** + * Returns the absolute path on the filesystem where a database created with + * {@link #openOrCreateDatabase} is stored. + * + * @param name The name of the database for which you would like to get + * its path. + * + * @return Returns an absolute path to the given database. + * + * @see #openOrCreateDatabase + */ + public abstract File getDatabasePath(String name); + + /** + * Returns an array of strings naming the private databases associated with + * this Context's application package. + * + * @return Array of strings naming the private databases. + * + * @see #openOrCreateDatabase + * @see #deleteDatabase + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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. + */ + public abstract void clearWallpaper() throws IOException; + + /** + * Launch a new activity. You will not receive any information about when + * the activity exits. + * + * <p>Note that if this method is being called from outside of an + * {@link android.app.Activity} Context, then the Intent must include + * the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag. This is because, + * without being started from an existing Activity, there is no existing + * task in which to place the new activity and thus it needs to be placed + * in its own separate task. + * + * <p>This method throws {@link ActivityNotFoundException} + * if there was no Activity found to run the given Intent. + * + * @param intent The description of the activity to start. + * + * @throws ActivityNotFoundException + * + * @see PackageManager#resolveActivity + */ + public abstract void startActivity(Intent intent); + + /** + * 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 + * receivers and receivers can not abort the broadcast. If you want + * to allow receivers to propagate results or abort the broadcast, you must + * send an ordered broadcast using + * {@link #sendOrderedBroadcast(Intent, String)}. + * + * <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. + * + * @see android.content.BroadcastReceiver + * @see #registerReceiver + * @see #sendBroadcast(Intent, String) + * @see #sendOrderedBroadcast(Intent, String) + * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) + */ + public abstract void sendBroadcast(Intent intent); + + /** + * Broadcast the given intent to all interested BroadcastReceivers, allowing + * an optional required permission to be enforced. This + * call is asynchronous; it returns immediately, and you will continue + * executing while the receivers are run. No results are propagated from + * receivers and receivers can not abort the broadcast. If you want + * to allow receivers to propagate results or abort the broadcast, you must + * send an ordered broadcast using + * {@link #sendOrderedBroadcast(Intent, String)}. + * + * <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 receiverPermission (optional) String naming a permissions that + * a receiver must hold in order to receive your broadcast. + * If null, no permission is required. + * + * @see android.content.BroadcastReceiver + * @see #registerReceiver + * @see #sendBroadcast(Intent) + * @see #sendOrderedBroadcast(Intent, String) + * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) + */ + public abstract void sendBroadcast(Intent intent, + String receiverPermission); + + /** + * Broadcast the given intent to all interested BroadcastReceivers, delivering + * them one at a time to allow more preferred receivers to consume the + * broadcast before it is delivered to less preferred receivers. This + * call is asynchronous; it returns immediately, and you will continue + * executing while the receivers are run. + * + * <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 receiverPermission (optional) String naming a permissions that + * a receiver must hold in order to receive your broadcast. + * If null, no permission is required. + * + * @see android.content.BroadcastReceiver + * @see #registerReceiver + * @see #sendBroadcast(Intent) + * @see #sendOrderedBroadcast(Intent, String, BroadcastReceiver, Handler, int, String, Bundle) + */ + public abstract void sendOrderedBroadcast(Intent intent, + String receiverPermission); + + /** + * Version of {@link #sendBroadcast(Intent)} 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. If you use + * an <var>resultReceiver</var> with this method, then 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. + * + * <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 receiverPermission String naming a permissions that + * a receiver must hold in order to receive your broadcast. + * If null, no permission is required. + * @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 sendOrderedBroadcast(Intent intent, + String receiverPermission, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras); + + /** + * Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the + * Intent you are sending stays around after the broadcast is complete, + * so that others can quickly retrieve that data through the return + * value of {@link #registerReceiver(BroadcastReceiver, IntentFilter)}. In + * all other ways, this behaves the same as + * {@link #sendBroadcast(Intent)}. + * + * <p>You must hold the {@link android.Manifest.permission#BROADCAST_STICKY} + * permission in order to use this API. If you do not hold that + * permission, {@link SecurityException} will be thrown. + * + * @param intent The Intent to broadcast; all receivers matching this + * Intent will receive the broadcast, and the Intent will be held to + * be re-broadcast to future receivers. + * + * @see #sendBroadcast(Intent) + */ + public abstract void sendStickyBroadcast(Intent intent); + + /** + * Remove the data previously sent with {@link #sendStickyBroadcast}, + * so that it is as if the sticky broadcast had never happened. + * + * <p>You must hold the {@link android.Manifest.permission#BROADCAST_STICKY} + * permission in order to use this API. If you do not hold that + * permission, {@link SecurityException} will be thrown. + * + * @param intent The Intent that was previously broadcast. + * + * @see #sendStickyBroadcast + */ + public abstract void removeStickyBroadcast(Intent intent); + + /** + * Register an BroadcastReceiver to be run in the main activity thread. The + * <var>receiver</var> will be called with any broadcast Intent that + * matches <var>filter</var>, in the main application thread. + * + * <p>The system may broadcast Intents that are "sticky" -- these stay + * around after the broadcast as finished, to be sent to any later + * registrations. If your IntentFilter matches one of these sticky + * Intents, that Intent will be returned by this function + * <strong>and</strong> sent to your <var>receiver</var> as if it had just + * been broadcast. + * + * <p>There may be multiple sticky Intents that match <var>filter</var>, + * in which case each of these will be sent to <var>receiver</var>. In + * this case, only one of these can be returned directly by the function; + * which of these that is returned is arbitrarily decided by the system. + * + * <p>If you know the Intent your are registering for is sticky, you can + * supply null for your <var>receiver</var>. In this case, no receiver is + * registered -- the function simply returns the sticky Intent that + * matches <var>filter</var>. In the case of multiple matches, the same + * rules as described above apply. + * + * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts. + * + * <p class="note">Note: this method <em>can not be called from an + * {@link BroadcastReceiver} component</em>. It is okay, however, to use + * this method from another BroadcastReceiver that has itself been registered with + * {@link #registerReceiver}, since the lifetime of such an BroadcastReceiver + * is tied to another object (the one that registered it).</p> + * + * @param receiver The BroadcastReceiver to handle the broadcast. + * @param filter Selects the Intent broadcasts to be received. + * + * @return The first sticky intent found that matches <var>filter</var>, + * or null if there are none. + * + * @see #registerReceiver(BroadcastReceiver, IntentFilter, String, Handler) + * @see #sendBroadcast + * @see #unregisterReceiver + */ + public abstract Intent registerReceiver(BroadcastReceiver receiver, + IntentFilter filter); + + /** + * Register to receive intent broadcasts, to run in the context of + * <var>scheduler</var>. See + * {@link #registerReceiver(BroadcastReceiver, IntentFilter)} for more + * information. This allows you to enforce permissions on who can + * broadcast intents to your receiver, or have the receiver run in + * a different thread than the main application thread. + * + * <p>See {@link BroadcastReceiver} for more information on Intent broadcasts. + * + * @param receiver The BroadcastReceiver to handle the broadcast. + * @param filter Selects the Intent broadcasts to be received. + * @param broadcastPermission String naming a permissions that a + * broadcaster must hold in order to send an Intent to you. If null, + * no permission is required. + * @param scheduler Handler identifying the thread that will receive + * the Intent. If null, the main thread of the process will be used. + * + * @return The first sticky intent found that matches <var>filter</var>, + * or null if there are none. + * + * @see #registerReceiver(BroadcastReceiver, IntentFilter) + * @see #sendBroadcast + * @see #unregisterReceiver + */ + public abstract Intent registerReceiver(BroadcastReceiver receiver, + IntentFilter filter, + String broadcastPermission, + Handler scheduler); + + /** + * Unregister a previously registered BroadcastReceiver. <em>All</em> + * filters that have been registered for this BroadcastReceiver will be + * removed. + * + * @param receiver The BroadcastReceiver to unregister. + * + * @see #registerReceiver + */ + public abstract void unregisterReceiver(BroadcastReceiver receiver); + + /** + * Request that a given application service be started. The Intent + * can either contain the complete class name of a specific service + * implementation to start, or an abstract definition through the + * action and other fields of the kind of service to start. If this service + * is not already running, it will be instantiated and started (creating a + * process for it if needed); if it is running then it remains running. + * + * <p>Every call to this method will result in a corresponding call to + * the target service's {@link android.app.Service#onStart} method, + * with the <var>intent</var> given here. This provides a convenient way + * to submit jobs to a service without having to bind and call on to its + * interface. + * + * <p>Using startService() overrides the default service lifetime that is + * managed by {@link #bindService}: it requires the service to remain + * running until {@link #stopService} is called, regardless of whether + * any clients are connected to it. Note that calls to startService() + * are not nesting: no matter how many times you call startService(), + * a single call to {@link #stopService} will stop it. + * + * <p>The system attempts to keep running services around as much as + * possible. The only time they should be stopped is if the current + * foreground application is using so many resources that the service needs + * to be killed. If any errors happen in the service's process, it will + * automatically be restarted. + * + * <p>This function will throw {@link SecurityException} if you do not + * have permission to start the given service. + * + * @param service Identifies the service to be started. The Intent may + * specify either an explicit component name to start, or a logical + * description (action, category, etc) to match an + * {@link IntentFilter} published by a service. Additional values + * may be included in the Intent extras to supply arguments along with + * this specific start call. + * + * @return If the service is being started or is already running, the + * {@link ComponentName} of the actual service that was started is + * returned; else if the service does not exist null is returned. + * + * @throws SecurityException + * + * @see #stopService + * @see #bindService + */ + public abstract ComponentName startService(Intent service); + + /** + * Request that a given application service be stopped. If the service is + * not running, nothing happens. Otherwise it is stopped. Note that calls + * to startService() are not counted -- this stops the service no matter + * how many times it was started. + * + * <p>Note that if a stopped service still has {@link ServiceConnection} + * objects bound to it with the {@link #BIND_AUTO_CREATE} set, it will + * not be destroyed until all of these bindings are removed. See + * the {@link android.app.Service} documentation for more details on a + * service's lifecycle. + * + * <p>This function will throw {@link SecurityException} if you do not + * have permission to stop the given service. + * + * @param service Description of the service to be stopped. The Intent may + * specify either an explicit component name to start, or a logical + * description (action, category, etc) to match an + * {@link IntentFilter} published by a service. + * + * @return If there is a service matching the given Intent that is already + * running, then it is stopped and true is returned; else false is returned. + * + * @throws SecurityException + * + * @see #startService + */ + public abstract boolean stopService(Intent service); + + /** + * Connect to an application service, creating it if needed. This defines + * a dependency between your application and the service. The given + * <var>conn</var> will receive the service object when its created and be + * told if it dies and restarts. The service will be considered required + * by the system only for as long as the calling context exists. For + * example, if this Context is an Activity that is stopped, the service will + * not be required to continue running until the Activity is resumed. + * + * <p>This function will throw {@link SecurityException} if you do not + * have permission to bind to the given service. + * + * <p class="note">Note: this method <em>can not be called from an + * {@link BroadcastReceiver} component</em>. A pattern you can use to + * communicate from an BroadcastReceiver to a Service is to call + * {@link #startService} with the arguments containing the command to be + * sent, with the service calling its + * {@link android.app.Service#stopSelf(int)} method when done executing + * that command. See the API demo App/Service/Service Start Arguments + * Controller for an illustration of this. It is okay, however, to use + * this method from an BroadcastReceiver that has been registered with + * {@link #registerReceiver}, since the lifetime of this BroadcastReceiver + * is tied to another object (the one that registered it).</p> + * + * @param service Identifies the service to connect to. The Intent may + * specify either an explicit component name, or a logical + * description (action, category, etc) to match an + * {@link IntentFilter} published by a service. + * @param conn Receives information as the service is started and stopped. + * @param flags Operation options for the binding. May be 0 or + * {@link #BIND_AUTO_CREATE}. + * @return If you have successfully bound to the service, true is returned; + * false is returned if the connection is not made so you will not + * receive the service object. + * + * @throws SecurityException + * + * @see #unbindService + * @see #startService + * @see #BIND_AUTO_CREATE + */ + public abstract boolean bindService(Intent service, ServiceConnection conn, + int flags); + + /** + * Disconnect from an application service. You will no longer receive + * calls as the service is restarted, and the service is now allowed to + * stop at any time. + * + * @param conn The connection interface previously supplied to + * bindService(). + * + * @see #bindService + */ + public abstract void unbindService(ServiceConnection conn); + + /** + * Start executing an {@link android.app.Instrumentation} class. The given + * Instrumentation component will be run by killing its target application + * (if currently running), starting the target process, instantiating the + * instrumentation component, and then letting it drive the application. + * + * <p>This function is not synchronous -- it returns as soon as the + * instrumentation has started and while it is running. + * + * <p>Instrumentation is normally only allowed to run against a package + * that is either unsigned or signed with a signature that the + * the instrumentation package is also signed with (ensuring the target + * trusts the instrumentation). + * + * @param className Name of the Instrumentation component to be run. + * @param profileFile Optional path to write profiling data as the + * instrumentation runs, or null for no profiling. + * @param arguments Additional optional arguments to pass to the + * instrumentation, or null. + * + * @return Returns true if the instrumentation was successfully started, + * else false if it could not be found. + */ + public abstract boolean startInstrumentation(ComponentName className, + String profileFile, Bundle arguments); + + /** + * Return the handle to a system-level service by name. The class of the + * returned object varies by the requested name. Currently available names + * are: + * + * <dl> + * <dt> {@link #WINDOW_SERVICE} ("window") + * <dd> The top-level window manager in which you can place custom + * windows. The returned object is a {@link android.view.WindowManager}. + * <dt> {@link #LAYOUT_INFLATER_SERVICE} ("layout_inflater") + * <dd> A {@link android.view.LayoutInflater} for inflating layout resources + * in this context. + * <dt> {@link #ACTIVITY_SERVICE} ("activity") + * <dd> A {@link android.app.ActivityManager} for interacting with the + * global activity state of the system. + * <dt> {@link #POWER_SERVICE} ("power") + * <dd> A {@link android.os.PowerManager} for controlling power + * management. + * <dt> {@link #ALARM_SERVICE} ("alarm") + * <dd> A {@link android.app.AlarmManager} for receiving intents at the + * time of your choosing. + * <dt> {@link #NOTIFICATION_SERVICE} ("notification") + * <dd> A {@link android.app.NotificationManager} for informing the user + * of background events. + * <dt> {@link #KEYGUARD_SERVICE} ("keyguard") + * <dd> A {@link android.app.KeyguardManager} for controlling keyguard. + * <dt> {@link #LOCATION_SERVICE} ("location") + * <dd> A {@link android.location.LocationManager} for controlling location + * (e.g., GPS) updates. + * <dt> {@link #SEARCH_SERVICE} ("search") + * <dd> A {@link android.app.SearchManager} for handling search. + * <dt> {@link #VIBRATOR_SERVICE} ("vibrator") + * <dd> A {@link android.os.Vibrator} for interacting with the vibrator + * hardware. + * <dt> {@link #CONNECTIVITY_SERVICE} ("connection") + * <dd> A {@link android.net.ConnectivityManager ConnectivityManager} for + * handling management of network connections. + * <dt> {@link #WIFI_SERVICE} ("wifi") + * <dd> A {@link android.net.wifi.WifiManager WifiManager} for management of + * Wi-Fi connectivity. + * <dt> {@link #INPUT_METHOD_SERVICE} ("input_method") + * <dd> An {@link android.view.inputmethod.InputMethodManager InputMethodManager} + * for management of input methods. + * </dl> + * + * <p>Note: System services obtained via this API may be closely associated with + * the Context in which they are obtained from. In general, do not share the + * service objects between various different contexts (Activities, Applications, + * Services, Providers, etc.) + * + * @param name The name of the desired service. + * + * @return The service or null if the name does not exist. + * + * @see #WINDOW_SERVICE + * @see android.view.WindowManager + * @see #LAYOUT_INFLATER_SERVICE + * @see android.view.LayoutInflater + * @see #ACTIVITY_SERVICE + * @see android.app.ActivityManager + * @see #POWER_SERVICE + * @see android.os.PowerManager + * @see #ALARM_SERVICE + * @see android.app.AlarmManager + * @see #NOTIFICATION_SERVICE + * @see android.app.NotificationManager + * @see #KEYGUARD_SERVICE + * @see android.app.KeyguardManager + * @see #LOCATION_SERVICE + * @see android.location.LocationManager + * @see #SEARCH_SERVICE + * @see android.app.SearchManager + * @see #SENSOR_SERVICE + * @see android.hardware.SensorManager + * @see #VIBRATOR_SERVICE + * @see android.os.Vibrator + * @see #CONNECTIVITY_SERVICE + * @see android.net.ConnectivityManager + * @see #WIFI_SERVICE + * @see android.net.wifi.WifiManager + * @see #AUDIO_SERVICE + * @see android.media.AudioManager + * @see #TELEPHONY_SERVICE + * @see android.telephony.TelephonyManager + * @see #INPUT_METHOD_SERVICE + * @see android.view.inputmethod.InputMethodManager + */ + public abstract Object getSystemService(String name); + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.os.PowerManager} for controlling power management, + * including "wake locks," which let you keep the device on while + * you're running long tasks. + */ + public static final String POWER_SERVICE = "power"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.view.WindowManager} for accessing the system's window + * manager. + * + * @see #getSystemService + * @see android.view.WindowManager + */ + public static final String WINDOW_SERVICE = "window"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.view.LayoutInflater} for inflating layout resources in this + * context. + * + * @see #getSystemService + * @see android.view.LayoutInflater + */ + public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.app.ActivityManager} for interacting with the global + * system state. + * + * @see #getSystemService + * @see android.app.ActivityManager + */ + public static final String ACTIVITY_SERVICE = "activity"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.app.AlarmManager} for receiving intents at a + * time of your choosing. + * + * @see #getSystemService + * @see android.app.AlarmManager + */ + public static final String ALARM_SERVICE = "alarm"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.app.NotificationManager} for informing the user of + * background events. + * + * @see #getSystemService + * @see android.app.NotificationManager + */ + public static final String NOTIFICATION_SERVICE = "notification"; + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.app.NotificationManager} for controlling keyguard. + * + * @see #getSystemService + * @see android.app.KeyguardManager + */ + public static final String KEYGUARD_SERVICE = "keyguard"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.location.LocationManager} for controlling location + * updates. + * + * @see #getSystemService + * @see android.location.LocationManager + */ + public static final String LOCATION_SERVICE = "location"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.app.SearchManager} for handling searches. + * + * @see #getSystemService + * @see android.app.SearchManager + */ + public static final String SEARCH_SERVICE = "search"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.hardware.SensorManager} for accessing sensors. + * + * @see #getSystemService + * @see android.hardware.SensorManager + */ + 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. + * + * @see #getSystemService + */ + public static final String WALLPAPER_SERVICE = "wallpaper"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.os.Vibrator} for interacting with the vibration hardware. + * + * @see #getSystemService + * @see android.os.Vibrator + */ + public static final String VIBRATOR_SERVICE = "vibrator"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.app.StatusBarManager} for interacting with the status bar. + * + * @see #getSystemService + * @see android.app.StatusBarManager + * @hide + */ + public static final String STATUS_BAR_SERVICE = "statusbar"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.net.ConnectivityManager} for handling management of + * network connections. + * + * @see #getSystemService + * @see android.net.ConnectivityManager + */ + public static final String CONNECTIVITY_SERVICE = "connectivity"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.net.wifi.WifiManager} for handling management of + * Wi-Fi access. + * + * @see #getSystemService + * @see android.net.wifi.WifiManager + */ + public static final String WIFI_SERVICE = "wifi"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.media.AudioManager} for handling management of volume, + * ringer modes and audio routing. + * + * @see #getSystemService + * @see android.media.AudioManager + */ + public static final String AUDIO_SERVICE = "audio"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.telephony.TelephonyManager} for handling management the + * telephony features of the device. + * + * @see #getSystemService + * @see android.telephony.TelephonyManager + */ + public static final String TELEPHONY_SERVICE = "phone"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.text.ClipboardManager} for accessing and modifying + * the contents of the global clipboard. + * + * @see #getSystemService + * @see android.text.ClipboardManager + */ + public static final String CLIPBOARD_SERVICE = "clipboard"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.view.inputmethod.InputMethodManager} for accessing input + * methods. + * + * @see #getSystemService + */ + public static final String INPUT_METHOD_SERVICE = "input_method"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@blink android.gadget.GadgetManager} for accessing wallpapers. + * + * @hide + * @see #getSystemService + */ + public static final String GADGET_SERVICE = "gadget"; + + /** + * Determine whether the given permission is allowed for a particular + * process and user ID running in the system. + * + * @param permission The name of the permission being checked. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the given + * pid/uid is allowed that permission, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see PackageManager#checkPermission(String, String) + * @see #checkCallingPermission + */ + public abstract int checkPermission(String permission, int pid, int uid); + + /** + * Determine whether the calling process of an IPC you are handling has been + * granted a particular permission. This is basically the same as calling + * {@link #checkPermission(String, int, int)} with the pid and uid returned + * by {@link android.os.Binder#getCallingPid} and + * {@link android.os.Binder#getCallingUid}. One important difference + * is that if you are not currently processing an IPC, this function + * will always fail. This is done to protect against accidentally + * leaking permissions; you can use {@link #checkCallingOrSelfPermission} + * to avoid this protection. + * + * @param permission The name of the permission being checked. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the calling + * pid/uid is allowed that permission, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see PackageManager#checkPermission(String, String) + * @see #checkPermission + * @see #checkCallingOrSelfPermission + */ + public abstract int checkCallingPermission(String permission); + + /** + * Determine whether the calling process of an IPC <em>or you</em> have been + * granted a particular permission. This is the same as + * {@link #checkCallingPermission}, except it grants your own permissions + * if you are not currently processing an IPC. Use with care! + * + * @param permission The name of the permission being checked. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the calling + * pid/uid is allowed that permission, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see PackageManager#checkPermission(String, String) + * @see #checkPermission + * @see #checkCallingPermission + */ + public abstract int checkCallingOrSelfPermission(String permission); + + /** + * If the given permission is not allowed for a particular process + * and user ID running in the system, throw a {@link SecurityException}. + * + * @param permission The name of the permission being checked. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkPermission(String, int, int) + */ + public abstract void enforcePermission( + String permission, int pid, int uid, String message); + + /** + * If the calling process of an IPC you are handling has not been + * granted a particular permission, throw a {@link + * SecurityException}. This is basically the same as calling + * {@link #enforcePermission(String, int, int, String)} with the + * pid and uid returned by {@link android.os.Binder#getCallingPid} + * and {@link android.os.Binder#getCallingUid}. One important + * difference is that if you are not currently processing an IPC, + * this function will always throw the SecurityException. This is + * done to protect against accidentally leaking permissions; you + * can use {@link #enforceCallingOrSelfPermission} to avoid this + * protection. + * + * @param permission The name of the permission being checked. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkCallingPermission(String) + */ + public abstract void enforceCallingPermission( + String permission, String message); + + /** + * If neither you nor the calling process of an IPC you are + * handling has been granted a particular permission, throw a + * {@link SecurityException}. This is the same as {@link + * #enforceCallingPermission}, except it grants your own + * permissions if you are not currently processing an IPC. Use + * with care! + * + * @param permission The name of the permission being checked. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkCallingOrSelfPermission(String) + */ + public abstract void enforceCallingOrSelfPermission( + String permission, String message); + + /** + * Grant permission to access a specific Uri to another package, regardless + * of whether that package has general permission to access the Uri's + * content provider. This can be used to grant specific, temporary + * permissions, typically in response to user interaction (such as the + * user opening an attachment that you would like someone else to + * display). + * + * <p>Normally you should use {@link Intent#FLAG_GRANT_READ_URI_PERMISSION + * Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION + * Intent.FLAG_GRANT_WRITE_URI_PERMISSION} with the Intent being used to + * start an activity instead of this function directly. If you use this + * function directly, you should be sure to call + * {@link #revokeUriPermission} when the target should no longer be allowed + * to access it. + * + * <p>To succeed, the content provider owning the Uri must have set the + * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions + * grantUriPermissions} attribute in its manifest or included the + * {@link android.R.styleable#AndroidManifestGrantUriPermission + * <grant-uri-permissions>} tag. + * + * @param toPackage The package you would like to allow to access the Uri. + * @param uri The Uri you would like to grant access to. + * @param modeFlags The desired access modes. Any combination of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION + * Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION + * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @see #revokeUriPermission + */ + public abstract void grantUriPermission(String toPackage, Uri uri, + int modeFlags); + + /** + * Remove all permissions to access a particular content provider Uri + * that were previously added with {@link #grantUriPermission}. The given + * Uri will match all previously granted Uris that are the same or a + * sub-path of the given Uri. That is, revoking "content://foo/one" will + * revoke both "content://foo/target" and "content://foo/target/sub", but not + * "content://foo". + * + * @param uri The Uri you would like to revoke access to. + * @param modeFlags The desired access modes. Any combination of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION + * Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION + * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @see #grantUriPermission + */ + public abstract void revokeUriPermission(Uri uri, int modeFlags); + + /** + * Determine whether a particular process and user ID has been granted + * permission to access a specific URI. This only checks for permissions + * that have been explicitly granted -- if the given process/uid has + * more general access to the URI's content provider then this check will + * always fail. + * + * @param uri The uri that is being checked. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the given + * pid/uid is allowed to access that uri, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see #checkCallingUriPermission + */ + public abstract int checkUriPermission(Uri uri, int pid, int uid, int modeFlags); + + /** + * Determine whether the calling process and user ID has been + * granted permission to access a specific URI. This is basically + * the same as calling {@link #checkUriPermission(Uri, int, int, + * int)} with the pid and uid returned by {@link + * android.os.Binder#getCallingPid} and {@link + * android.os.Binder#getCallingUid}. One important difference is + * that if you are not currently processing an IPC, this function + * will always fail. + * + * @param uri The uri that is being checked. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller + * is allowed to access that uri, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see #checkUriPermission(Uri, int, int, int) + */ + public abstract int checkCallingUriPermission(Uri uri, int modeFlags); + + /** + * Determine whether the calling process of an IPC <em>or you</em> has been granted + * permission to access a specific URI. This is the same as + * {@link #checkCallingUriPermission}, except it grants your own permissions + * if you are not currently processing an IPC. Use with care! + * + * @param uri The uri that is being checked. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller + * is allowed to access that uri, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see #checkCallingUriPermission + */ + public abstract int checkCallingOrSelfUriPermission(Uri uri, int modeFlags); + + /** + * Check both a Uri and normal permission. This allows you to perform + * both {@link #checkPermission} and {@link #checkUriPermission} in one + * call. + * + * @param uri The Uri whose permission is to be checked, or null to not + * do this check. + * @param readPermission The permission that provides overall read access, + * or null to not do this check. + * @param writePermission The permission that provides overall write + * acess, or null to not do this check. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * + * @return Returns {@link PackageManager#PERMISSION_GRANTED} if the caller + * is allowed to access that uri or holds one of the given permissions, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + */ + public abstract int checkUriPermission(Uri uri, String readPermission, + String writePermission, int pid, int uid, int modeFlags); + + /** + * If a particular process and user ID has not been granted + * permission to access a specific URI, throw {@link + * SecurityException}. This only checks for permissions that have + * been explicitly granted -- if the given process/uid has more + * general access to the URI's content provider then this check + * will always fail. + * + * @param uri The uri that is being checked. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkUriPermission(Uri, int, int, int) + */ + public abstract void enforceUriPermission( + Uri uri, int pid, int uid, int modeFlags, String message); + + /** + * If the calling process and user ID has not been granted + * permission to access a specific URI, throw {@link + * SecurityException}. This is basically the same as calling + * {@link #enforceUriPermission(Uri, int, int, int, String)} with + * the pid and uid returned by {@link + * android.os.Binder#getCallingPid} and {@link + * android.os.Binder#getCallingUid}. One important difference is + * that if you are not currently processing an IPC, this function + * will always throw a SecurityException. + * + * @param uri The uri that is being checked. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkCallingUriPermission(Uri, int) + */ + public abstract void enforceCallingUriPermission( + Uri uri, int modeFlags, String message); + + /** + * If the calling process of an IPC <em>or you</em> has not been + * granted permission to access a specific URI, throw {@link + * SecurityException}. This is the same as {@link + * #enforceCallingUriPermission}, except it grants your own + * permissions if you are not currently processing an IPC. Use + * with care! + * + * @param uri The uri that is being checked. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkCallingOrSelfUriPermission(Uri, int) + */ + public abstract void enforceCallingOrSelfUriPermission( + Uri uri, int modeFlags, String message); + + /** + * Enforce both a Uri and normal permission. This allows you to perform + * both {@link #enforcePermission} and {@link #enforceUriPermission} in one + * call. + * + * @param uri The Uri whose permission is to be checked, or null to not + * do this check. + * @param readPermission The permission that provides overall read access, + * or null to not do this check. + * @param writePermission The permission that provides overall write + * acess, or null to not do this check. + * @param pid The process ID being checked against. Must be > 0. + * @param uid The user ID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param modeFlags The type of access to grant. May be one or both of + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_WRITE_URI_PERMISSION}. + * @param message A message to include in the exception if it is thrown. + * + * @see #checkUriPermission(Uri, String, String, int, int, int) + */ + public abstract void enforceUriPermission( + Uri uri, String readPermission, String writePermission, + int pid, int uid, int modeFlags, String message); + + /** + * Flag for use with {@link #createPackageContext}: include the application + * code with the context. This means loading code into the caller's + * process, so that {@link #getClassLoader()} can be used to instantiate + * the application's classes. Setting this flags imposes security + * restrictions on what application context you can access; if the + * requested application can not be safely loaded into your process, + * java.lang.SecurityException will be thrown. If this flag is not set, + * there will be no restrictions on the packages that can be loaded, + * but {@link #getClassLoader} will always return the default system + * class loader. + */ + public static final int CONTEXT_INCLUDE_CODE = 0x00000001; + + /** + * Flag for use with {@link #createPackageContext}: ignore any security + * restrictions on the Context being requested, allowing it to always + * be loaded. For use with {@link #CONTEXT_INCLUDE_CODE} to allow code + * to be loaded into a process even when it isn't safe to do so. Use + * with extreme care! + */ + public static final int CONTEXT_IGNORE_SECURITY = 0x00000002; + + /** + * Return a new Context object for the given application name. This + * Context is the same as what the named application gets when it is + * launched, containing the same resources and class loader. Each call to + * this method returns a new instance of a Context object; Context objects + * are not shared, however they share common state (Resources, ClassLoader, + * etc) so the Context instance itself is fairly lightweight. + * + * <p>Throws {@link PackageManager.NameNotFoundException} if there is no + * application with the given package name. + * + * <p>Throws {@link java.lang.SecurityException} if the Context requested + * can not be loaded into the caller's process for security reasons (see + * {@link #CONTEXT_INCLUDE_CODE} for more information}. + * + * @param packageName Name of the application's package. + * @param flags Option flags, one of {@link #CONTEXT_INCLUDE_CODE} + * or {@link #CONTEXT_IGNORE_SECURITY}. + * + * @return A Context for the application. + * + * @throws java.lang.SecurityException + * @throws PackageManager.NameNotFoundException if there is no application with + * the given package name + */ + public abstract Context createPackageContext(String packageName, + int flags) throws PackageManager.NameNotFoundException; +} diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java new file mode 100644 index 0000000..36e1c34 --- /dev/null +++ b/core/java/android/content/ContextWrapper.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2006 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.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Proxying implementation of Context that simply delegates all of its calls to + * another Context. Can be subclassed to modify behavior without changing + * the original Context. + */ +public class ContextWrapper extends Context { + Context mBase; + + public ContextWrapper(Context base) { + mBase = base; + } + + /** + * Set the base context for this ContextWrapper. All calls will then be + * delegated to the base context. Throws + * IllegalStateException if a base context has already been set. + * + * @param base The new base context for this wrapper. + */ + protected void attachBaseContext(Context base) { + if (mBase != null) { + throw new IllegalStateException("Base context already set"); + } + mBase = base; + } + + /** + * @return the base context as set by the constructor or setBaseContext + */ + public Context getBaseContext() { + return mBase; + } + + @Override + public AssetManager getAssets() { + return mBase.getAssets(); + } + + @Override + public Resources getResources() + { + return mBase.getResources(); + } + + @Override + public PackageManager getPackageManager() { + return mBase.getPackageManager(); + } + + @Override + public ContentResolver getContentResolver() { + return mBase.getContentResolver(); + } + + @Override + public Looper getMainLooper() { + return mBase.getMainLooper(); + } + + @Override + public Context getApplicationContext() { + return mBase.getApplicationContext(); + } + + @Override + public void setTheme(int resid) { + mBase.setTheme(resid); + } + + @Override + public Resources.Theme getTheme() { + return mBase.getTheme(); + } + + @Override + public ClassLoader getClassLoader() { + return mBase.getClassLoader(); + } + + @Override + public String getPackageName() { + return mBase.getPackageName(); + } + + @Override + public String getPackageResourcePath() { + return mBase.getPackageResourcePath(); + } + + @Override + public String getPackageCodePath() { + return mBase.getPackageCodePath(); + } + + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + return mBase.getSharedPreferences(name, mode); + } + + @Override + public FileInputStream openFileInput(String name) + throws FileNotFoundException { + return mBase.openFileInput(name); + } + + @Override + public FileOutputStream openFileOutput(String name, int mode) + throws FileNotFoundException { + return mBase.openFileOutput(name, mode); + } + + @Override + public boolean deleteFile(String name) { + return mBase.deleteFile(name); + } + + @Override + public File getFileStreamPath(String name) { + return mBase.getFileStreamPath(name); + } + + @Override + public String[] fileList() { + return mBase.fileList(); + } + + @Override + public File getFilesDir() { + return mBase.getFilesDir(); + } + + @Override + public File getCacheDir() { + return mBase.getCacheDir(); + } + + @Override + public File getDir(String name, int mode) { + return mBase.getDir(name, mode); + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) { + return mBase.openOrCreateDatabase(name, mode, factory); + } + + @Override + public boolean deleteDatabase(String name) { + return mBase.deleteDatabase(name); + } + + @Override + public File getDatabasePath(String name) { + return mBase.getDatabasePath(name); + } + + @Override + public String[] databaseList() { + return mBase.databaseList(); + } + + @Override + public Drawable getWallpaper() { + return mBase.getWallpaper(); + } + + @Override + public Drawable peekWallpaper() { + return mBase.peekWallpaper(); + } + + @Override + public int getWallpaperDesiredMinimumWidth() { + return mBase.getWallpaperDesiredMinimumWidth(); + } + + @Override + public int getWallpaperDesiredMinimumHeight() { + return mBase.getWallpaperDesiredMinimumHeight(); + } + + @Override + public void setWallpaper(Bitmap bitmap) throws IOException { + mBase.setWallpaper(bitmap); + } + + @Override + public void setWallpaper(InputStream data) throws IOException { + mBase.setWallpaper(data); + } + + @Override + public void clearWallpaper() throws IOException { + mBase.clearWallpaper(); + } + + @Override + public void startActivity(Intent intent) { + mBase.startActivity(intent); + } + + @Override + public void sendBroadcast(Intent intent) { + mBase.sendBroadcast(intent); + } + + @Override + public void sendBroadcast(Intent intent, String receiverPermission) { + mBase.sendBroadcast(intent, receiverPermission); + } + + @Override + public void sendOrderedBroadcast(Intent intent, + String receiverPermission) { + mBase.sendOrderedBroadcast(intent, receiverPermission); + } + + @Override + public void sendOrderedBroadcast( + Intent intent, String receiverPermission, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + mBase.sendOrderedBroadcast(intent, receiverPermission, + resultReceiver, scheduler, initialCode, + initialData, initialExtras); + } + + @Override + public void sendStickyBroadcast(Intent intent) { + mBase.sendStickyBroadcast(intent); + } + + @Override + public void removeStickyBroadcast(Intent intent) { + mBase.removeStickyBroadcast(intent); + } + + @Override + public Intent registerReceiver( + BroadcastReceiver receiver, IntentFilter filter) { + return mBase.registerReceiver(receiver, filter); + } + + @Override + public Intent registerReceiver( + BroadcastReceiver receiver, IntentFilter filter, + String broadcastPermission, Handler scheduler) { + return mBase.registerReceiver(receiver, filter, broadcastPermission, + scheduler); + } + + @Override + public void unregisterReceiver(BroadcastReceiver receiver) { + mBase.unregisterReceiver(receiver); + } + + @Override + public ComponentName startService(Intent service) { + return mBase.startService(service); + } + + @Override + public boolean stopService(Intent name) { + return mBase.stopService(name); + } + + @Override + public boolean bindService(Intent service, ServiceConnection conn, + int flags) { + return mBase.bindService(service, conn, flags); + } + + @Override + public void unbindService(ServiceConnection conn) { + mBase.unbindService(conn); + } + + @Override + public boolean startInstrumentation(ComponentName className, + String profileFile, Bundle arguments) { + return mBase.startInstrumentation(className, profileFile, arguments); + } + + @Override + public Object getSystemService(String name) { + return mBase.getSystemService(name); + } + + @Override + public int checkPermission(String permission, int pid, int uid) { + return mBase.checkPermission(permission, pid, uid); + } + + @Override + public int checkCallingPermission(String permission) { + return mBase.checkCallingPermission(permission); + } + + @Override + public int checkCallingOrSelfPermission(String permission) { + return mBase.checkCallingOrSelfPermission(permission); + } + + @Override + public void enforcePermission( + String permission, int pid, int uid, String message) { + mBase.enforcePermission(permission, pid, uid, message); + } + + @Override + public void enforceCallingPermission(String permission, String message) { + mBase.enforceCallingPermission(permission, message); + } + + @Override + public void enforceCallingOrSelfPermission( + String permission, String message) { + mBase.enforceCallingOrSelfPermission(permission, message); + } + + @Override + public void grantUriPermission(String toPackage, Uri uri, int modeFlags) { + mBase.grantUriPermission(toPackage, uri, modeFlags); + } + + @Override + public void revokeUriPermission(Uri uri, int modeFlags) { + mBase.revokeUriPermission(uri, modeFlags); + } + + @Override + public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { + return mBase.checkUriPermission(uri, pid, uid, modeFlags); + } + + @Override + public int checkCallingUriPermission(Uri uri, int modeFlags) { + return mBase.checkCallingUriPermission(uri, modeFlags); + } + + @Override + public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) { + return mBase.checkCallingOrSelfUriPermission(uri, modeFlags); + } + + @Override + public int checkUriPermission(Uri uri, String readPermission, + String writePermission, int pid, int uid, int modeFlags) { + return mBase.checkUriPermission(uri, readPermission, writePermission, + pid, uid, modeFlags); + } + + @Override + public void enforceUriPermission( + Uri uri, int pid, int uid, int modeFlags, String message) { + mBase.enforceUriPermission(uri, pid, uid, modeFlags, message); + } + + @Override + public void enforceCallingUriPermission( + Uri uri, int modeFlags, String message) { + mBase.enforceCallingUriPermission(uri, modeFlags, message); + } + + @Override + public void enforceCallingOrSelfUriPermission( + Uri uri, int modeFlags, String message) { + mBase.enforceCallingOrSelfUriPermission(uri, modeFlags, message); + } + + @Override + public void enforceUriPermission( + Uri uri, String readPermission, String writePermission, + int pid, int uid, int modeFlags, String message) { + mBase.enforceUriPermission( + uri, readPermission, writePermission, pid, uid, modeFlags, + message); + } + + @Override + public Context createPackageContext(String packageName, int flags) + throws PackageManager.NameNotFoundException { + return mBase.createPackageContext(packageName, flags); + } +} diff --git a/core/java/android/content/DefaultDataHandler.java b/core/java/android/content/DefaultDataHandler.java new file mode 100644 index 0000000..863c9f6 --- /dev/null +++ b/core/java/android/content/DefaultDataHandler.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2008 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.util.Xml; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Stack; + +/** + * Inserts default data from InputStream, should be in XML format. + * If the provider syncs data to the server, the imported data will be synced to the server. + * <p>Samples:</p> + * <br/> + * Insert one row: + * <pre> + * <row uri="content://contacts/people"> + * <Col column = "name" value = "foo feebe "/> + * <Col column = "addr" value = "Tx"/> + * </row></pre> + * <br/> + * Delete, it must be in order of uri, select, and arg: + * <pre> + * <del uri="content://contacts/people" select="name=? and addr=?" + * arg1 = "foo feebe" arg2 ="Tx"/></pre> + * <br/> + * Use first row's uri to insert into another table, + * content://contacts/people/1/phones: + * <pre> + * <row uri="content://contacts/people"> + * <col column = "name" value = "foo feebe"/> + * <col column = "addr" value = "Tx"/> + * <row postfix="phones"> + * <col column="number" value="512-514-6535"/> + * </row> + * <row postfix="phones"> + * <col column="cell" value="512-514-6535"/> + * </row> + * </row></pre> + * <br/> + * Insert multiple rows in to same table and same attributes: + * <pre> + * <row uri="content://contacts/people" > + * <row> + * <col column= "name" value = "foo feebe"/> + * <col column= "addr" value = "Tx"/> + * </row> + * <row> + * </row> + * </row></pre> + * + * @hide + */ +public class DefaultDataHandler implements ContentInsertHandler { + private final static String ROW = "row"; + private final static String COL = "col"; + private final static String URI_STR = "uri"; + private final static String POSTFIX = "postfix"; + private final static String DEL = "del"; + private final static String SELECT = "select"; + private final static String ARG = "arg"; + + private Stack<Uri> mUris = new Stack<Uri>(); + private ContentValues mValues; + private ContentResolver mContentResolver; + + public void insert(ContentResolver contentResolver, InputStream in) + throws IOException, SAXException { + mContentResolver = contentResolver; + Xml.parse(in, Xml.Encoding.UTF_8, this); + } + + public void insert(ContentResolver contentResolver, String in) + throws SAXException { + mContentResolver = contentResolver; + Xml.parse(in, this); + } + + private void parseRow(Attributes atts) throws SAXException { + String uriStr = atts.getValue(URI_STR); + Uri uri; + if (uriStr != null) { + // case 1 + uri = Uri.parse(uriStr); + if (uri == null) { + throw new SAXException("attribute " + + atts.getValue(URI_STR) + " parsing failure"); + } + + } else if (mUris.size() > 0){ + // case 2 + String postfix = atts.getValue(POSTFIX); + if (postfix != null) { + uri = Uri.withAppendedPath(mUris.lastElement(), + postfix); + } else { + uri = mUris.lastElement(); + } + } else { + throw new SAXException("attribute parsing failure"); + } + + mUris.push(uri); + + } + + private Uri insertRow() { + Uri u = mContentResolver.insert(mUris.lastElement(), mValues); + mValues = null; + return u; + } + + public void startElement(String uri, String localName, String name, + Attributes atts) throws SAXException { + if (ROW.equals(localName)) { + if (mValues != null) { + // case 2, <Col> before <Row> insert last uri + if (mUris.empty()) { + throw new SAXException("uri is empty"); + } + Uri nextUri = insertRow(); + if (nextUri == null) { + throw new SAXException("insert to uri " + + mUris.lastElement().toString() + " failure"); + } else { + // make sure the stack lastElement save uri for more than one row + mUris.pop(); + mUris.push(nextUri); + parseRow(atts); + } + } else { + int attrLen = atts.getLength(); + if (attrLen == 0) { + // case 3, share same uri as last level + mUris.push(mUris.lastElement()); + } else { + parseRow(atts); + } + } + } else if (COL.equals(localName)) { + int attrLen = atts.getLength(); + if (attrLen != 2) { + throw new SAXException("illegal attributes number " + attrLen); + } + String key = atts.getValue(0); + String value = atts.getValue(1); + if (key != null && key.length() > 0 && value != null && value.length() > 0) { + if (mValues == null) { + mValues = new ContentValues(); + } + mValues.put(key, value); + } else { + throw new SAXException("illegal attributes value"); + } + } else if (DEL.equals(localName)){ + Uri u = Uri.parse(atts.getValue(URI_STR)); + if (u == null) { + throw new SAXException("attribute " + + atts.getValue(URI_STR) + " parsing failure"); + } + int attrLen = atts.getLength() - 2; + if (attrLen > 0) { + String[] selectionArgs = new String[attrLen]; + for (int i = 0; i < attrLen; i++) { + selectionArgs[i] = atts.getValue(i+2); + } + mContentResolver.delete(u, atts.getValue(1), selectionArgs); + } else if (attrLen == 0){ + mContentResolver.delete(u, atts.getValue(1), null); + } else { + mContentResolver.delete(u, null, null); + } + + } else { + throw new SAXException("unknown element: " + localName); + } + } + + public void endElement(String uri, String localName, String name) + throws SAXException { + if (ROW.equals(localName)) { + if (mUris.empty()) { + throw new SAXException("uri mismatch"); + } + if (mValues != null) { + insertRow(); + } + mUris.pop(); + } + } + + + public void characters(char[] ch, int start, int length) + throws SAXException { + // TODO Auto-generated method stub + + } + + public void endDocument() throws SAXException { + // TODO Auto-generated method stub + + } + + public void endPrefixMapping(String prefix) throws SAXException { + // TODO Auto-generated method stub + + } + + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + // TODO Auto-generated method stub + + } + + public void processingInstruction(String target, String data) + throws SAXException { + // TODO Auto-generated method stub + + } + + public void setDocumentLocator(Locator locator) { + // TODO Auto-generated method stub + + } + + public void skippedEntity(String name) throws SAXException { + // TODO Auto-generated method stub + + } + + public void startDocument() throws SAXException { + // TODO Auto-generated method stub + + } + + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // TODO Auto-generated method stub + + } + +} diff --git a/core/java/android/content/DialogInterface.java b/core/java/android/content/DialogInterface.java new file mode 100644 index 0000000..4afa294 --- /dev/null +++ b/core/java/android/content/DialogInterface.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2006 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.view.KeyEvent; + +/** + * + */ +public interface DialogInterface { + /** + * The identifier for the positive button. + */ + public static final int BUTTON_POSITIVE = -1; + + /** + * The identifier for the negative button. + */ + public static final int BUTTON_NEGATIVE = -2; + + /** + * The identifier for the neutral button. + */ + public static final int BUTTON_NEUTRAL = -3; + + /** + * @deprecated Use {@link #BUTTON_POSITIVE} + */ + @Deprecated + public static final int BUTTON1 = BUTTON_POSITIVE; + + /** + * @deprecated Use {@link #BUTTON_NEGATIVE} + */ + @Deprecated + public static final int BUTTON2 = BUTTON_NEGATIVE; + + /** + * @deprecated Use {@link #BUTTON_NEUTRAL} + */ + @Deprecated + public static final int BUTTON3 = BUTTON_NEUTRAL; + + public void cancel(); + + public void dismiss(); + + /** + * Interface used to allow the creator of a dialog to run some code when the + * dialog is canceled. + * <p> + * This will only be called when the dialog is canceled, if the creator + * needs to know when it is dismissed in general, use + * {@link DialogInterface.OnDismissListener}. + */ + interface OnCancelListener { + /** + * This method will be invoked when the dialog is canceled. + * + * @param dialog The dialog that was canceled will be passed into the + * method. + */ + public void onCancel(DialogInterface dialog); + } + + /** + * Interface used to allow the creator of a dialog to run some code when the + * dialog is dismissed. + */ + interface OnDismissListener { + /** + * This method will be invoked when the dialog is dismissed. + * + * @param dialog The dialog that was dismissed will be passed into the + * method. + */ + public void onDismiss(DialogInterface dialog); + } + + /** + * Interface used to allow the creator of a dialog to run some code when an + * item on the dialog is clicked.. + */ + interface OnClickListener { + /** + * This method will be invoked when a button in the dialog is clicked. + * + * @param dialog The dialog that received the click. + * @param which The button that was clicked (e.g. + * {@link DialogInterface#BUTTON1}) or the position + * of the item clicked. + */ + /* TODO: Change to use BUTTON_POSITIVE after API council */ + public void onClick(DialogInterface dialog, int which); + } + + /** + * Interface used to allow the creator of a dialog to run some code when an + * item in a multi-choice dialog is clicked. + */ + interface OnMultiChoiceClickListener { + /** + * This method will be invoked when an item in the dialog is clicked. + * + * @param dialog The dialog where the selection was made. + * @param which The position of the item in the list that was clicked. + * @param isChecked True if the click checked the item, else false. + */ + public void onClick(DialogInterface dialog, int which, boolean isChecked); + } + + /** + * Interface definition for a callback to be invoked when a key event is + * dispatched to this dialog. The callback will be invoked before the key + * event is given to the dialog. + */ + interface OnKeyListener { + /** + * Called when a key is dispatched to a dialog. This allows listeners to + * get a chance to respond before the dialog. + * + * @param dialog The dialog the key has been dispatched to. + * @param keyCode The code for the physical key that was pressed + * @param event The KeyEvent object containing full information about + * the event. + * @return True if the listener has consumed the event, false otherwise. + */ + public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event); + } +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java new file mode 100644 index 0000000..0606956 --- /dev/null +++ b/core/java/android/content/IContentProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2006 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.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.IBulkCursor; +import android.database.IContentObserver; +import android.net.Uri; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.IInterface; +import android.os.ParcelFileDescriptor; + +import java.io.FileNotFoundException; + +/** + * The ipc interface to talk to a content provider. + * @hide + */ +public interface IContentProvider extends IInterface { + /** + * @hide - hide this because return type IBulkCursor and parameter + * IContentObserver are system private classes. + */ + public IBulkCursor bulkQuery(Uri url, String[] projection, + String selection, String[] selectionArgs, String sortOrder, IContentObserver observer, + CursorWindow window) throws RemoteException; + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder) throws RemoteException; + public String getType(Uri url) throws RemoteException; + public Uri insert(Uri url, ContentValues initialValues) + throws RemoteException; + public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException; + public int delete(Uri url, String selection, String[] selectionArgs) + throws RemoteException; + public int update(Uri url, ContentValues values, String selection, + String[] selectionArgs) throws RemoteException; + public ParcelFileDescriptor openFile(Uri url, String mode) + throws RemoteException, FileNotFoundException; + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException; + public ISyncAdapter getSyncAdapter() throws RemoteException; + + /* IPC constants */ + static final String descriptor = "android.content.IContentProvider"; + + static final int QUERY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; + static final int GET_TYPE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; + 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; +} diff --git a/core/java/android/content/IContentService.java b/core/java/android/content/IContentService.java new file mode 100644 index 0000000..a3047da --- /dev/null +++ b/core/java/android/content/IContentService.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2006 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.IContentObserver; +import android.net.Uri; +import android.os.RemoteException; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Bundle; + +/** + * {@hide} + */ +public interface IContentService extends IInterface +{ + public void registerContentObserver(Uri uri, boolean notifyForDescendentsn, + IContentObserver observer) throws RemoteException; + public void unregisterContentObserver(IContentObserver observer) throws RemoteException; + + public void notifyChange(Uri uri, IContentObserver observer, + boolean observerWantsSelfNotifications, boolean syncToNetwork) + throws RemoteException; + + public void startSync(Uri url, Bundle extras) throws RemoteException; + public void cancelSync(Uri uri) throws RemoteException; + + static final String SERVICE_NAME = "content"; + + /* IPC constants */ + static final String descriptor = "android.content.IContentService"; + + static final int REGISTER_CONTENT_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 1; + static final int UNREGISTER_CHANGE_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; + static final int NOTIFY_CHANGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; + static final int START_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 4; + static final int CANCEL_SYNC_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 5; +} + diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl new file mode 100644 index 0000000..671188c --- /dev/null +++ b/core/java/android/content/ISyncAdapter.aidl @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.Bundle; +import android.content.ISyncContext; + +/** + * Interface used to control the sync activity on a SyncAdapter + * @hide + */ +oneway interface ISyncAdapter { + /** + * Initiate a sync for this account. SyncAdapter-specific parameters may + * be specified in extras, which is guaranteed to not be null. + * + * @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 extras SyncAdapter-specific parameters + */ + void startSync(ISyncContext syncContext, String 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. + */ + void cancelSync(); +} diff --git a/core/java/android/content/ISyncContext.aidl b/core/java/android/content/ISyncContext.aidl new file mode 100644 index 0000000..6d18a1c --- /dev/null +++ b/core/java/android/content/ISyncContext.aidl @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008 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.SyncResult; + +/** + * Interface used by the SyncAdapter to indicate its progress. + * @hide + */ +interface ISyncContext { + /** + * Call to indicate that the SyncAdapter is making progress. E.g., if this SyncAdapter + * downloads or sends records to/from the server, this may be called after each record + * is downloaded or uploaded. + */ + void sendHeartbeat(); + + /** + * Signal that the corresponding sync session is completed. + * @param result information about this sync session + */ + void onFinished(in SyncResult result); +} diff --git a/core/java/android/content/Intent.aidl b/core/java/android/content/Intent.aidl new file mode 100644 index 0000000..568986b --- /dev/null +++ b/core/java/android/content/Intent.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/content/Intent.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 Intent; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java new file mode 100644 index 0000000..e1c1f64 --- /dev/null +++ b/core/java/android/content/Intent.java @@ -0,0 +1,4530 @@ +/* + * Copyright (C) 2006 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 org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import com.android.internal.util.XmlUtils; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * An intent is an abstract description of an operation to be performed. It + * can be used with {@link Context#startActivity(Intent) startActivity} to + * launch an {@link android.app.Activity}, + * {@link android.content.Context#sendBroadcast(Intent) broadcastIntent} to + * send it to any interested {@link BroadcastReceiver BroadcastReceiver} components, + * and {@link android.content.Context#startService} or + * {@link android.content.Context#bindService} to communicate with a + * background {@link android.app.Service}. + * + * <p>An Intent provides a facility for performing late runtime binding between + * the code in different applications. Its most significant use is in the + * launching of activities, where it can be thought of as the glue between + * activities. It is + * basically a passive data structure holding an abstract description of an + * action to be performed. The primary pieces of information in an intent + * are:</p> + * + * <ul> + * <li> <p><b>action</b> -- The general action to be performed, such as + * {@link #ACTION_VIEW}, {@link #ACTION_EDIT}, {@link #ACTION_MAIN}, + * etc.</p> + * </li> + * <li> <p><b>data</b> -- The data to operate on, such as a person record + * in the contacts database, expressed as a {@link android.net.Uri}.</p> + * </li> + * </ul> + * + * + * <p>Some examples of action/data pairs are:</p> + * + * <ul> + * <li> <p><b>{@link #ACTION_VIEW} <i>content://contacts/1</i></b> -- Display + * information about the person whose identifier is "1".</p> + * </li> + * <li> <p><b>{@link #ACTION_DIAL} <i>content://contacts/1</i></b> -- Display + * the phone dialer with the person filled in.</p> + * </li> + * <li> <p><b>{@link #ACTION_VIEW} <i>tel:123</i></b> -- Display + * the phone dialer with the given number filled in. Note how the + * VIEW action does what what is considered the most reasonable thing for + * a particular URI.</p> + * </li> + * <li> <p><b>{@link #ACTION_DIAL} <i>tel:123</i></b> -- Display + * the phone dialer with the given number filled in.</p> + * </li> + * <li> <p><b>{@link #ACTION_EDIT} <i>content://contacts/1</i></b> -- Edit + * information about the person whose identifier is "1".</p> + * </li> + * <li> <p><b>{@link #ACTION_VIEW} <i>content://contacts/</i></b> -- Display + * a list of people, which the user can browse through. This example is a + * typical top-level entry into the Contacts application, showing you the + * list of people. Selecting a particular person to view would result in a + * new intent { <b>{@link #ACTION_VIEW} <i>content://contacts/N</i></b> } + * being used to start an activity to display that person.</p> + * </li> + * </ul> + * + * <p>In addition to these primary attributes, there are a number of secondary + * attributes that you can also include with an intent:</p> + * + * <ul> + * <li> <p><b>category</b> -- Gives additional information about the action + * to execute. For example, {@link #CATEGORY_LAUNCHER} means it should + * appear in the Launcher as a top-level application, while + * {@link #CATEGORY_ALTERNATIVE} means it should be included in a list + * of alternative actions the user can perform on a piece of data.</p> + * <li> <p><b>type</b> -- Specifies an explicit type (a MIME type) of the + * intent data. Normally the type is inferred from the data itself. + * By setting this attribute, you disable that evaluation and force + * an explicit type.</p> + * <li> <p><b>component</b> -- Specifies an explicit name of a component + * class to use for the intent. Normally this is determined by looking + * at the other information in the intent (the action, data/type, and + * categories) and matching that with a component that can handle it. + * If this attribute is set then none of the evaluation is performed, + * and this component is used exactly as is. By specifying this attribute, + * all of the other Intent attributes become optional.</p> + * <li> <p><b>extras</b> -- This is a {@link Bundle} of any additional information. + * This can be used to provide extended information to the component. + * For example, if we have a action to send an e-mail message, we could + * also include extra pieces of data here to supply a subject, body, + * etc.</p> + * </ul> + * + * <p>Here are some examples of other operations you can specify as intents + * using these additional parameters:</p> + * + * <ul> + * <li> <p><b>{@link #ACTION_MAIN} with category {@link #CATEGORY_HOME}</b> -- + * Launch the home screen.</p> + * </li> + * <li> <p><b>{@link #ACTION_GET_CONTENT} with MIME type + * <i>{@link android.provider.Contacts.Phones#CONTENT_URI + * vnd.android.cursor.item/phone}</i></b> + * -- Display the list of people's phone numbers, allowing the user to + * browse through them and pick one and return it to the parent activity.</p> + * </li> + * <li> <p><b>{@link #ACTION_GET_CONTENT} with MIME type + * <i>*{@literal /}*</i> and category {@link #CATEGORY_OPENABLE}</b> + * -- Display all pickers for data that can be opened with + * {@link ContentResolver#openInputStream(Uri) ContentResolver.openInputStream()}, + * allowing the user to pick one of them and then some data inside of it + * and returning the resulting URI to the caller. This can be used, + * for example, in an e-mail application to allow the user to pick some + * data to include as an attachment.</p> + * </li> + * </ul> + * + * <p>There are a variety of standard Intent action and category constants + * defined in the Intent class, but applications can also define their own. + * These strings use java style scoping, to ensure they are unique -- for + * example, the standard {@link #ACTION_VIEW} is called + * "android.app.action.VIEW".</p> + * + * <p>Put together, the set of actions, data types, categories, and extra data + * defines a language for the system allowing for the expression of phrases + * such as "call john smith's cell". As applications are added to the system, + * they can extend this language by adding new actions, types, and categories, or + * they can modify the behavior of existing phrases by supplying their own + * activities that handle them.</p> + * + * <a name="IntentResolution"></a> + * <h3>Intent Resolution</h3> + * + * <p>There are two primary forms of intents you will use. + * + * <ul> + * <li> <p><b>Explicit Intents</b> have specified a component (via + * {@link #setComponent} or {@link #setClass}), which provides the exact + * class to be run. Often these will not include any other information, + * simply being a way for an application to launch various internal + * activities it has as the user interacts with the application. + * + * <li> <p><b>Implicit Intents</b> have not specified a component; + * instead, they must include enough information for the system to + * determine which of the available components is best to run for that + * intent. + * </ul> + * + * <p>When using implicit intents, given such an arbitrary intent we need to + * know what to do with it. This is handled by the process of <em>Intent + * resolution</em>, which maps an Intent to an {@link android.app.Activity}, + * {@link BroadcastReceiver}, or {@link android.app.Service} (or sometimes two or + * more activities/receivers) that can handle it.</p> + * + * <p>The intent resolution mechanism basically revolves around matching an + * Intent against all of the <intent-filter> descriptions in the + * installed application packages. (Plus, in the case of broadcasts, any {@link BroadcastReceiver} + * objects explicitly registered with {@link Context#registerReceiver}.) More + * details on this can be found in the documentation on the {@link + * IntentFilter} class.</p> + * + * <p>There are three pieces of information in the Intent that are used for + * resolution: the action, type, and category. Using this information, a query + * is done on the {@link PackageManager} for a component that can handle the + * intent. The appropriate component is determined based on the intent + * information supplied in the <code>AndroidManifest.xml</code> file as + * follows:</p> + * + * <ul> + * <li> <p>The <b>action</b>, if given, must be listed by the component as + * one it handles.</p> + * <li> <p>The <b>type</b> is retrieved from the Intent's data, if not + * already supplied in the Intent. Like the action, if a type is + * included in the intent (either explicitly or implicitly in its + * data), then this must be listed by the component as one it handles.</p> + * <li> For data that is not a <code>content:</code> URI and where no explicit + * type is included in the Intent, instead the <b>scheme</b> of the + * intent data (such as <code>http:</code> or <code>mailto:</code>) is + * considered. Again like the action, if we are matching a scheme it + * must be listed by the component as one it can handle. + * <li> <p>The <b>categories</b>, if supplied, must <em>all</em> be listed + * by the activity as categories it handles. That is, if you include + * the categories {@link #CATEGORY_LAUNCHER} and + * {@link #CATEGORY_ALTERNATIVE}, then you will only resolve to components + * with an intent that lists <em>both</em> of those categories. + * Activities will very often need to support the + * {@link #CATEGORY_DEFAULT} so that they can be found by + * {@link Context#startActivity Context.startActivity()}.</p> + * </ul> + * + * <p>For example, consider the Note Pad sample application that + * allows user to browse through a list of notes data and view details about + * individual items. Text in italics indicate places were you would replace a + * name with one specific to your own package.</p> + * + * <pre> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + * package="<i>com.android.notepad</i>"> + * <application android:icon="@drawable/app_notes" + * android:label="@string/app_name"> + * + * <provider class=".NotePadProvider" + * android:authorities="<i>com.google.provider.NotePad</i>" /> + * + * <activity class=".NotesList" android:label="@string/title_notes_list"> + * <intent-filter> + * <action android:value="android.intent.action.MAIN" /> + * <category android:value="android.intent.category.LAUNCHER" /> + * </intent-filter> + * <intent-filter> + * <action android:value="android.intent.action.VIEW" /> + * <action android:value="android.intent.action.EDIT" /> + * <action android:value="android.intent.action.PICK" /> + * <category android:value="android.intent.category.DEFAULT" /> + * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * </intent-filter> + * <intent-filter> + * <action android:value="android.intent.action.GET_CONTENT" /> + * <category android:value="android.intent.category.DEFAULT" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter> + * </activity> + * + * <activity class=".NoteEditor" android:label="@string/title_note"> + * <intent-filter android:label="@string/resolve_edit"> + * <action android:value="android.intent.action.VIEW" /> + * <action android:value="android.intent.action.EDIT" /> + * <category android:value="android.intent.category.DEFAULT" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter> + * + * <intent-filter> + * <action android:value="android.intent.action.INSERT" /> + * <category android:value="android.intent.category.DEFAULT" /> + * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * </intent-filter> + * + * </activity> + * + * <activity class=".TitleEditor" android:label="@string/title_edit_title" + * android:theme="@android:style/Theme.Dialog"> + * <intent-filter android:label="@string/resolve_title"> + * <action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /> + * <category android:value="android.intent.category.DEFAULT" /> + * <category android:value="android.intent.category.ALTERNATIVE" /> + * <category android:value="android.intent.category.SELECTED_ALTERNATIVE" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter> + * </activity> + * + * </application> + * </manifest></pre> + * + * <p>The first activity, + * <code>com.android.notepad.NotesList</code>, serves as our main + * entry into the app. It can do three things as described by its three intent + * templates: + * <ol> + * <li><pre> + * <intent-filter> + * <action android:value="{@link #ACTION_MAIN android.intent.action.MAIN}" /> + * <category android:value="{@link #CATEGORY_LAUNCHER android.intent.category.LAUNCHER}" /> + * </intent-filter></pre> + * <p>This provides a top-level entry into the NotePad application: the standard + * MAIN action is a main entry point (not requiring any other information in + * the Intent), and the LAUNCHER category says that this entry point should be + * listed in the application launcher.</p> + * <li><pre> + * <intent-filter> + * <action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /> + * <action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /> + * <action android:value="{@link #ACTION_PICK android.intent.action.PICK}" /> + * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * </intent-filter></pre> + * <p>This declares the things that the activity can do on a directory of + * notes. The type being supported is given with the <type> tag, where + * <code>vnd.android.cursor.dir/vnd.google.note</code> is a URI from which + * a Cursor of zero or more items (<code>vnd.android.cursor.dir</code>) can + * be retrieved which holds our note pad data (<code>vnd.google.note</code>). + * The activity allows the user to view or edit the directory of data (via + * the VIEW and EDIT actions), or to pick a particular note and return it + * to the caller (via the PICK action). Note also the DEFAULT category + * supplied here: this is <em>required</em> for the + * {@link Context#startActivity Context.startActivity} method to resolve your + * activity when its component name is not explicitly specified.</p> + * <li><pre> + * <intent-filter> + * <action android:value="{@link #ACTION_GET_CONTENT android.intent.action.GET_CONTENT}" /> + * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter></pre> + * <p>This filter describes the ability return to the caller a note selected by + * the user without needing to know where it came from. The data type + * <code>vnd.android.cursor.item/vnd.google.note</code> is a URI from which + * a Cursor of exactly one (<code>vnd.android.cursor.item</code>) item can + * be retrieved which contains our note pad data (<code>vnd.google.note</code>). + * The GET_CONTENT action is similar to the PICK action, where the activity + * will return to its caller a piece of data selected by the user. Here, + * however, the caller specifies the type of data they desire instead of + * the type of data the user will be picking from.</p> + * </ol> + * + * <p>Given these capabilities, the following intents will resolve to the + * NotesList activity:</p> + * + * <ul> + * <li> <p><b>{ action=android.app.action.MAIN }</b> matches all of the + * activities that can be used as top-level entry points into an + * application.</p> + * <li> <p><b>{ action=android.app.action.MAIN, + * category=android.app.category.LAUNCHER }</b> is the actual intent + * used by the Launcher to populate its top-level list.</p> + * <li> <p><b>{ action=android.app.action.VIEW + * data=content://com.google.provider.NotePad/notes }</b> + * displays a list of all the notes under + * "content://com.google.provider.NotePad/notes", which + * the user can browse through and see the details on.</p> + * <li> <p><b>{ action=android.app.action.PICK + * data=content://com.google.provider.NotePad/notes }</b> + * provides a list of the notes under + * "content://com.google.provider.NotePad/notes", from which + * the user can pick a note whose data URL is returned back to the caller.</p> + * <li> <p><b>{ action=android.app.action.GET_CONTENT + * type=vnd.android.cursor.item/vnd.google.note }</b> + * is similar to the pick action, but allows the caller to specify the + * kind of data they want back so that the system can find the appropriate + * activity to pick something of that data type.</p> + * </ul> + * + * <p>The second activity, + * <code>com.android.notepad.NoteEditor</code>, shows the user a single + * note entry and allows them to edit it. It can do two things as described + * by its two intent templates: + * <ol> + * <li><pre> + * <intent-filter android:label="@string/resolve_edit"> + * <action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /> + * <action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /> + * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter></pre> + * <p>The first, primary, purpose of this activity is to let the user interact + * with a single note, as decribed by the MIME type + * <code>vnd.android.cursor.item/vnd.google.note</code>. The activity can + * either VIEW a note or allow the user to EDIT it. Again we support the + * DEFAULT category to allow the activity to be launched without explicitly + * specifying its component.</p> + * <li><pre> + * <intent-filter> + * <action android:value="{@link #ACTION_INSERT android.intent.action.INSERT}" /> + * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * </intent-filter></pre> + * <p>The secondary use of this activity is to insert a new note entry into + * an existing directory of notes. This is used when the user creates a new + * note: the INSERT action is executed on the directory of notes, causing + * this activity to run and have the user create the new note data which + * it then adds to the content provider.</p> + * </ol> + * + * <p>Given these capabilities, the following intents will resolve to the + * NoteEditor activity:</p> + * + * <ul> + * <li> <p><b>{ action=android.app.action.VIEW + * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b> + * shows the user the content of note <var>{ID}</var>.</p> + * <li> <p><b>{ action=android.app.action.EDIT + * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b> + * allows the user to edit the content of note <var>{ID}</var>.</p> + * <li> <p><b>{ action=android.app.action.INSERT + * data=content://com.google.provider.NotePad/notes }</b> + * creates a new, empty note in the notes list at + * "content://com.google.provider.NotePad/notes" + * and allows the user to edit it. If they keep their changes, the URI + * of the newly created note is returned to the caller.</p> + * </ul> + * + * <p>The last activity, + * <code>com.android.notepad.TitleEditor</code>, allows the user to + * edit the title of a note. This could be implemented as a class that the + * application directly invokes (by explicitly setting its component in + * the Intent), but here we show a way you can publish alternative + * operations on existing data:</p> + * + * <pre> + * <intent-filter android:label="@string/resolve_title"> + * <action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /> + * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <category android:value="{@link #CATEGORY_ALTERNATIVE android.intent.category.ALTERNATIVE}" /> + * <category android:value="{@link #CATEGORY_SELECTED_ALTERNATIVE android.intent.category.SELECTED_ALTERNATIVE}" /> + * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * </intent-filter></pre> + * + * <p>In the single intent template here, we + * have created our own private action called + * <code>com.android.notepad.action.EDIT_TITLE</code> which means to + * edit the title of a note. It must be invoked on a specific note + * (data type <code>vnd.android.cursor.item/vnd.google.note</code>) like the previous + * view and edit actions, but here displays and edits the title contained + * in the note data. + * + * <p>In addition to supporting the default category as usual, our title editor + * also supports two other standard categories: ALTERNATIVE and + * SELECTED_ALTERNATIVE. Implementing + * these categories allows others to find the special action it provides + * without directly knowing about it, through the + * {@link android.content.pm.PackageManager#queryIntentActivityOptions} method, or + * more often to build dynamic menu items with + * {@link android.view.Menu#addIntentOptions}. Note that in the intent + * template here was also supply an explicit name for the template + * (via <code>android:label="@string/resolve_title"</code>) to better control + * what the user sees when presented with this activity as an alternative + * action to the data they are viewing. + * + * <p>Given these capabilities, the following intent will resolve to the + * TitleEditor activity:</p> + * + * <ul> + * <li> <p><b>{ action=com.android.notepad.action.EDIT_TITLE + * data=content://com.google.provider.NotePad/notes/<var>{ID}</var> }</b> + * displays and allows the user to edit the title associated + * with note <var>{ID}</var>.</p> + * </ul> + * + * <h3>Standard Activity Actions</h3> + * + * <p>These are the current standard actions that Intent defines for launching + * activities (usually through {@link Context#startActivity}. The most + * important, and by far most frequently used, are {@link #ACTION_MAIN} and + * {@link #ACTION_EDIT}. + * + * <ul> + * <li> {@link #ACTION_MAIN} + * <li> {@link #ACTION_VIEW} + * <li> {@link #ACTION_ATTACH_DATA} + * <li> {@link #ACTION_EDIT} + * <li> {@link #ACTION_PICK} + * <li> {@link #ACTION_CHOOSER} + * <li> {@link #ACTION_GET_CONTENT} + * <li> {@link #ACTION_DIAL} + * <li> {@link #ACTION_CALL} + * <li> {@link #ACTION_SEND} + * <li> {@link #ACTION_SENDTO} + * <li> {@link #ACTION_ANSWER} + * <li> {@link #ACTION_INSERT} + * <li> {@link #ACTION_DELETE} + * <li> {@link #ACTION_RUN} + * <li> {@link #ACTION_SYNC} + * <li> {@link #ACTION_PICK_ACTIVITY} + * <li> {@link #ACTION_SEARCH} + * <li> {@link #ACTION_WEB_SEARCH} + * <li> {@link #ACTION_FACTORY_TEST} + * </ul> + * + * <h3>Standard Broadcast Actions</h3> + * + * <p>These are the current standard actions that Intent defines for receiving + * broadcasts (usually through {@link Context#registerReceiver} or a + * <receiver> tag in a manifest). + * + * <ul> + * <li> {@link #ACTION_TIME_TICK} + * <li> {@link #ACTION_TIME_CHANGED} + * <li> {@link #ACTION_TIMEZONE_CHANGED} + * <li> {@link #ACTION_BOOT_COMPLETED} + * <li> {@link #ACTION_PACKAGE_ADDED} + * <li> {@link #ACTION_PACKAGE_CHANGED} + * <li> {@link #ACTION_PACKAGE_REMOVED} + * <li> {@link #ACTION_PACKAGE_RESTARTED} + * <li> {@link #ACTION_PACKAGE_DATA_CLEARED} + * <li> {@link #ACTION_UID_REMOVED} + * <li> {@link #ACTION_BATTERY_CHANGED} + * </ul> + * + * <h3>Standard Categories</h3> + * + * <p>These are the current standard categories that can be used to further + * clarify an Intent via {@link #addCategory}. + * + * <ul> + * <li> {@link #CATEGORY_DEFAULT} + * <li> {@link #CATEGORY_BROWSABLE} + * <li> {@link #CATEGORY_TAB} + * <li> {@link #CATEGORY_ALTERNATIVE} + * <li> {@link #CATEGORY_SELECTED_ALTERNATIVE} + * <li> {@link #CATEGORY_LAUNCHER} + * <li> {@link #CATEGORY_INFO} + * <li> {@link #CATEGORY_HOME} + * <li> {@link #CATEGORY_PREFERENCE} + * <li> {@link #CATEGORY_GADGET} + * <li> {@link #CATEGORY_TEST} + * </ul> + * + * <h3>Standard Extra Data</h3> + * + * <p>These are the current standard fields that can be used as extra data via + * {@link #putExtra}. + * + * <ul> + * <li> {@link #EXTRA_TEMPLATE} + * <li> {@link #EXTRA_INTENT} + * <li> {@link #EXTRA_STREAM} + * <li> {@link #EXTRA_TEXT} + * </ul> + * + * <h3>Flags</h3> + * + * <p>These are the possible flags that can be used in the Intent via + * {@link #setFlags} and {@link #addFlags}. See {@link #setFlags} for a list + * of all possible flags. + */ +public class Intent implements Parcelable { + // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // Standard intent activity actions (see action variable). + + /** + * Activity Action: Start as a main entry point, does not expect to + * receive data. + * <p>Input: nothing + * <p>Output: nothing + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MAIN = "android.intent.action.MAIN"; + + /** + * Activity Action: Display the data to the user. This is the most common + * action performed on data -- it is the generic action you can use on + * a piece of data to get the most reasonable thing to occur. For example, + * when used on a contacts entry it will view the entry; when used on a + * mailto: URI it will bring up a compose window filled with the information + * supplied by the URI; when used with a tel: URI it will invoke the + * dialer. + * <p>Input: {@link #getData} is URI from which to retrieve data. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_VIEW = "android.intent.action.VIEW"; + + /** + * A synonym for {@link #ACTION_VIEW}, the "standard" action that is + * performed on a piece of data. + */ + public static final String ACTION_DEFAULT = ACTION_VIEW; + + /** + * Used to indicate that some piece of data should be attached to some other + * place. For example, image data could be attached to a contact. It is up + * to the recipient to decide where the data should be attached; the intent + * does not specify the ultimate destination. + * <p>Input: {@link #getData} is URI of data to be attached. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ATTACH_DATA = "android.intent.action.ATTACH_DATA"; + + /** + * Activity Action: Provide explicit editable access to the given data. + * <p>Input: {@link #getData} is URI of data to be edited. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_EDIT = "android.intent.action.EDIT"; + + /** + * Activity Action: Pick an existing item, or insert a new item, and then edit it. + * <p>Input: {@link #getType} is the desired MIME type of the item to create or edit. + * The extras can contain type specific data to pass through to the editing/creating + * activity. + * <p>Output: The URI of the item that was picked. This must be a content: + * URI so that any receiver can access it. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT"; + + /** + * Activity Action: Pick an item from the data, returning what was selected. + * <p>Input: {@link #getData} is URI containing a directory of data + * (vnd.android.cursor.dir/*) from which to pick an item. + * <p>Output: The URI of the item that was picked. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PICK = "android.intent.action.PICK"; + + /** + * Activity Action: Creates a shortcut. + * <p>Input: Nothing.</p> + * <p>Output: An Intent representing the shortcut. The intent must contain three + * extras: SHORTCUT_INTENT (value: Intent), SHORTCUT_NAME (value: String), + * and SHORTCUT_ICON (value: Bitmap) or SHORTCUT_ICON_RESOURCE + * (value: ShortcutIconResource).</p> + * + * @see #EXTRA_SHORTCUT_INTENT + * @see #EXTRA_SHORTCUT_NAME + * @see #EXTRA_SHORTCUT_ICON + * @see #EXTRA_SHORTCUT_ICON_RESOURCE + * @see android.content.Intent.ShortcutIconResource + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CREATE_SHORTCUT = "android.intent.action.CREATE_SHORTCUT"; + + /** + * The name of the extra used to define the Intent of a shortcut. + * + * @see #ACTION_CREATE_SHORTCUT + */ + public static final String EXTRA_SHORTCUT_INTENT = "android.intent.extra.shortcut.INTENT"; + /** + * The name of the extra used to define the name of a shortcut. + * + * @see #ACTION_CREATE_SHORTCUT + */ + public static final String EXTRA_SHORTCUT_NAME = "android.intent.extra.shortcut.NAME"; + /** + * The name of the extra used to define the icon, as a Bitmap, of a shortcut. + * + * @see #ACTION_CREATE_SHORTCUT + */ + public static final String EXTRA_SHORTCUT_ICON = "android.intent.extra.shortcut.ICON"; + /** + * The name of the extra used to define the icon, as a ShortcutIconResource, of a shortcut. + * + * @see #ACTION_CREATE_SHORTCUT + * @see android.content.Intent.ShortcutIconResource + */ + public static final String EXTRA_SHORTCUT_ICON_RESOURCE = + "android.intent.extra.shortcut.ICON_RESOURCE"; + + /** + * Represents a shortcut/live folder icon resource. + * + * @see Intent#ACTION_CREATE_SHORTCUT + * @see Intent#EXTRA_SHORTCUT_ICON_RESOURCE + * @see android.provider.LiveFolders#ACTION_CREATE_LIVE_FOLDER + * @see android.provider.LiveFolders#EXTRA_LIVE_FOLDER_ICON + */ + public static class ShortcutIconResource implements Parcelable { + /** + * The package name of the application containing the icon. + */ + public String packageName; + + /** + * The resource name of the icon, including package, name and type. + */ + public String resourceName; + + /** + * Creates a new ShortcutIconResource for the specified context and resource + * identifier. + * + * @param context The context of the application. + * @param resourceId The resource idenfitier for the icon. + * @return A new ShortcutIconResource with the specified's context package name + * and icon resource idenfitier. + */ + public static ShortcutIconResource fromContext(Context context, int resourceId) { + ShortcutIconResource icon = new ShortcutIconResource(); + icon.packageName = context.getPackageName(); + icon.resourceName = context.getResources().getResourceName(resourceId); + return icon; + } + + /** + * Used to read a ShortcutIconResource from a Parcel. + */ + public static final Parcelable.Creator<ShortcutIconResource> CREATOR = + new Parcelable.Creator<ShortcutIconResource>() { + + public ShortcutIconResource createFromParcel(Parcel source) { + ShortcutIconResource icon = new ShortcutIconResource(); + icon.packageName = source.readString(); + icon.resourceName = source.readString(); + return icon; + } + + public ShortcutIconResource[] newArray(int size) { + return new ShortcutIconResource[size]; + } + }; + + /** + * No special parcel contents. + */ + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(packageName); + dest.writeString(resourceName); + } + + @Override + public String toString() { + return resourceName; + } + } + + /** + * Activity Action: Display an activity chooser, allowing the user to pick + * what they want to before proceeding. This can be used as an alternative + * to the standard activity picker that is displayed by the system when + * you try to start an activity with multiple possible matches, with these + * differences in behavior: + * <ul> + * <li>You can specify the title that will appear in the activity chooser. + * <li>The user does not have the option to make one of the matching + * activities a preferred activity, and all possible activities will + * always be shown even if one of them is currently marked as the + * preferred activity. + * </ul> + * <p> + * This action should be used when the user will naturally expect to + * select an activity in order to proceed. An example if when not to use + * it is when the user clicks on a "mailto:" link. They would naturally + * expect to go directly to their mail app, so startActivity() should be + * called directly: it will + * either launch the current preferred app, or put up a dialog allowing the + * user to pick an app to use and optionally marking that as preferred. + * <p> + * In contrast, if the user is selecting a menu item to send a picture + * they are viewing to someone else, there are many different things they + * may want to do at this point: send it through e-mail, upload it to a + * web service, etc. In this case the CHOOSER action should be used, to + * always present to the user a list of the things they can do, with a + * nice title given by the caller such as "Send this photo with:". + * <p> + * As a convenience, an Intent of this form can be created with the + * {@link #createChooser} function. + * <p>Input: No data should be specified. get*Extra must have + * a {@link #EXTRA_INTENT} field containing the Intent being executed, + * and can optionally have a {@link #EXTRA_TITLE} field containing the + * title text to display in the chooser. + * <p>Output: Depends on the protocol of {@link #EXTRA_INTENT}. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHOOSER = "android.intent.action.CHOOSER"; + + /** + * Convenience function for creating a {@link #ACTION_CHOOSER} Intent. + * + * @param target The Intent that the user will be selecting an activity + * to perform. + * @param title Optional title that will be displayed in the chooser. + * @return Return a new Intent object that you can hand to + * {@link Context#startActivity(Intent) Context.startActivity()} and + * related methods. + */ + public static Intent createChooser(Intent target, CharSequence title) { + Intent intent = new Intent(ACTION_CHOOSER); + intent.putExtra(EXTRA_INTENT, target); + if (title != null) { + intent.putExtra(EXTRA_TITLE, title); + } + return intent; + } + /** + * Activity Action: Allow the user to select a particular kind of data and + * return it. This is different than {@link #ACTION_PICK} in that here we + * just say what kind of data is desired, not a URI of existing data from + * which the user can pick. A ACTION_GET_CONTENT could allow the user to + * create the data as it runs (for example taking a picture or recording a + * sound), let them browser over the web and download the desired data, + * etc. + * <p> + * There are two main ways to use this action: if you want an specific kind + * of data, such as a person contact, you set the MIME type to the kind of + * data you want and launch it with {@link Context#startActivity(Intent)}. + * The system will then launch the best application to select that kind + * of data for you. + * <p> + * You may also be interested in any of a set of types of content the user + * can pick. For example, an e-mail application that wants to allow the + * user to add an attachment to an e-mail message can use this action to + * bring up a list of all of the types of content the user can attach. + * <p> + * In this case, you should wrap the GET_CONTENT intent with a chooser + * (through {@link #createChooser}), which will give the proper interface + * for the user to pick how to send your data and allow you to specify + * a prompt indicating what they are doing. You will usually specify a + * broad MIME type (such as image/* or {@literal *}/*), resulting in a + * broad range of content types the user can select from. + * <p> + * When using such a broad GET_CONTENT action, it is often desireable to + * only pick from data that can be represented as a stream. This is + * accomplished by requiring the {@link #CATEGORY_OPENABLE} in the Intent. + * <p> + * Input: {@link #getType} is the desired MIME type to retrieve. Note + * that no URI is supplied in the intent, as there are no constraints on + * where the returned data originally comes from. You may also include the + * {@link #CATEGORY_OPENABLE} if you can only accept data that can be + * opened as a stream. + * <p> + * Output: The URI of the item that was picked. This must be a content: + * URI so that any receiver can access it. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_GET_CONTENT = "android.intent.action.GET_CONTENT"; + /** + * Activity Action: Dial a number as specified by the data. This shows a + * UI with the number being dialed, allowing the user to explicitly + * initiate the call. + * <p>Input: If nothing, an empty dialer is started; else {@link #getData} + * is URI of a phone number to be dialed or a tel: URI of an explicit phone + * number. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_DIAL = "android.intent.action.DIAL"; + /** + * Activity Action: Perform a call to someone specified by the data. + * <p>Input: If nothing, an empty dialer is started; else {@link #getData} + * is URI of a phone number to be dialed or a tel: URI of an explicit phone + * number. + * <p>Output: nothing. + * + * <p>Note: there will be restrictions on which applications can initiate a + * call; most applications should use the {@link #ACTION_DIAL}. + * <p>Note: this Intent <strong>cannot</strong> be used to call emergency + * numbers. Applications can <strong>dial</strong> emergency numbers using + * {@link #ACTION_DIAL}, however. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CALL = "android.intent.action.CALL"; + /** + * Activity Action: Perform a call to an emergency number specified by the + * data. + * <p>Input: {@link #getData} is URI of a phone number to be dialed or a + * tel: URI of an explicit phone number. + * <p>Output: nothing. + * @hide + */ + public static final String ACTION_CALL_EMERGENCY = "android.intent.action.CALL_EMERGENCY"; + /** + * Activity action: Perform a call to any number (emergency or not) + * specified by the data. + * <p>Input: {@link #getData} is URI of a phone number to be dialed or a + * tel: URI of an explicit phone number. + * <p>Output: nothing. + * @hide + */ + public static final String ACTION_CALL_PRIVILEGED = "android.intent.action.CALL_PRIVILEGED"; + /** + * Activity Action: Send a message to someone specified by the data. + * <p>Input: {@link #getData} is URI describing the target. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SENDTO = "android.intent.action.SENDTO"; + /** + * Activity Action: Deliver some data to someone else. Who the data is + * being delivered to is not specified; it is up to the receiver of this + * action to ask the user where the data should be sent. + * <p> + * When launching a SEND intent, you should usually wrap it in a chooser + * (through {@link #createChooser}), which will give the proper interface + * for the user to pick how to send your data and allow you to specify + * a prompt indicating what they are doing. + * <p> + * Input: {@link #getType} is the MIME type of the data being sent. + * get*Extra can have either a {@link #EXTRA_TEXT} + * or {@link #EXTRA_STREAM} field, containing the data to be sent. If + * using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it + * should be the MIME type of the data in EXTRA_STREAM. Use {@literal *}/* + * if the MIME type is unknown (this will only allow senders that can + * handle generic data streams). + * <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}. + * <p> + * Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SEND = "android.intent.action.SEND"; + /** + * Activity Action: Handle an incoming phone call. + * <p>Input: nothing. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ANSWER = "android.intent.action.ANSWER"; + /** + * Activity Action: Insert an empty item into the given container. + * <p>Input: {@link #getData} is URI of the directory (vnd.android.cursor.dir/*) + * in which to place the data. + * <p>Output: URI of the new data that was created. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_INSERT = "android.intent.action.INSERT"; + /** + * Activity Action: Delete the given data from its container. + * <p>Input: {@link #getData} is URI of data to be deleted. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_DELETE = "android.intent.action.DELETE"; + /** + * Activity Action: Run the data, whatever that means. + * <p>Input: ? (Note: this is currently specific to the test harness.) + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_RUN = "android.intent.action.RUN"; + /** + * Activity Action: Perform a data synchronization. + * <p>Input: ? + * <p>Output: ? + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SYNC = "android.intent.action.SYNC"; + /** + * Activity Action: Pick an activity given an intent, returning the class + * selected. + * <p>Input: get*Extra field {@link #EXTRA_INTENT} is an Intent + * used with {@link PackageManager#queryIntentActivities} to determine the + * set of activities from which to pick. + * <p>Output: Class name of the activity that was selected. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PICK_ACTIVITY = "android.intent.action.PICK_ACTIVITY"; + /** + * Activity Action: Perform a search. + * <p>Input: {@link android.app.SearchManager#QUERY getStringExtra(SearchManager.QUERY)} + * is the text to search for. If empty, simply + * enter your search results Activity with the search UI activated. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SEARCH = "android.intent.action.SEARCH"; + /** + * Activity Action: Perform a web search. + * <p> + * Input: {@link android.app.SearchManager#QUERY + * getStringExtra(SearchManager.QUERY)} is the text to search for. If it is + * a url starts with http or https, the site will be opened. If it is plain + * text, Google search will be applied. + * <p> + * Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH"; + /** + * Activity Action: List all available applications + * <p>Input: Nothing. + * <p>Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ALL_APPS = "android.intent.action.ALL_APPS"; + /** + * Activity Action: Show settings for choosing wallpaper + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SET_WALLPAPER = "android.intent.action.SET_WALLPAPER"; + + /** + * Activity Action: Show activity for reporting a bug. + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_BUG_REPORT = "android.intent.action.BUG_REPORT"; + + /** + * Activity Action: Main entry point for factory tests. Only used when + * the device is booting in factory test node. The implementing package + * must be installed in the system image. + * <p>Input: nothing + * <p>Output: nothing + */ + public static final String ACTION_FACTORY_TEST = "android.intent.action.FACTORY_TEST"; + + /** + * Activity Action: The user pressed the "call" button to go to the dialer + * or other appropriate UI for placing a call. + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CALL_BUTTON = "android.intent.action.CALL_BUTTON"; + + /** + * Activity Action: Start Voice Command. + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND"; + + // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // Standard intent broadcast actions (see action variable). + + /** + * Broadcast Action: Sent after the screen turns off. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF"; + /** + * Broadcast Action: Sent after the screen turns on. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SCREEN_ON = "android.intent.action.SCREEN_ON"; + /** + * Broadcast Action: The current time has changed. Sent every + * minute. You can <em>not</em> receive this through components declared + * in manifests, only by exlicitly registering for it with + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_TIME_TICK = "android.intent.action.TIME_TICK"; + /** + * Broadcast Action: The time was set. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_TIME_CHANGED = "android.intent.action.TIME_SET"; + /** + * Broadcast Action: The date has changed. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DATE_CHANGED = "android.intent.action.DATE_CHANGED"; + /** + * Broadcast Action: The timezone has changed. The intent will have the following extra values:</p> + * <ul> + * <li><em>time-zone</em> - The java.util.TimeZone.getID() value identifying the new time zone.</li> + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_TIMEZONE_CHANGED = "android.intent.action.TIMEZONE_CHANGED"; + /** + * Alarm Changed Action: This is broadcast when the AlarmClock + * application's alarm is set or unset. It is used by the + * AlarmClock application and the StatusBar service. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ALARM_CHANGED = "android.intent.action.ALARM_CHANGED"; + /** + * Sync State Changed Action: This is broadcast when the sync starts or stops or when one has + * been failing for a long time. It is used by the SyncManager and the StatusBar service. + * @hide + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_SYNC_STATE_CHANGED + = "android.intent.action.SYNC_STATE_CHANGED"; + /** + * Broadcast Action: This is broadcast once, after the system has finished + * booting. It can be used to perform application-specific initialization, + * such as installing alarms. You must hold the + * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED} permission + * in order to receive this broadcast. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_BOOT_COMPLETED = "android.intent.action.BOOT_COMPLETED"; + /** + * Broadcast Action: This is broadcast when a user action should request a + * temporary system dialog to dismiss. Some examples of temporary system + * dialogs are the notification window-shade and the recent tasks dialog. + */ + public static final String ACTION_CLOSE_SYSTEM_DIALOGS = "android.intent.action.CLOSE_SYSTEM_DIALOGS"; + /** + * Broadcast Action: Trigger the download and eventual installation + * of a package. + * <p>Input: {@link #getData} is the URI of the package file to download. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_INSTALL = "android.intent.action.PACKAGE_INSTALL"; + /** + * Broadcast Action: A new application package has been installed on the + * device. The data contains the name of the package. + * <p>My include the following extras: + * <ul> + * <li> {@link #EXTRA_UID} containing the integer uid assigned to the new package. + * <li> {@link #EXTRA_REPLACING} is set to true if this is following + * an {@link #ACTION_PACKAGE_REMOVED} broadcast for the same package. + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED"; + /** + * Broadcast Action: An existing application package has been removed from + * the device. The data contains the name of the package. The package + * that is being installed does <em>not</em> receive this Intent. + * <ul> + * <li> {@link #EXTRA_UID} containing the integer uid previously assigned + * to the package. + * <li> {@link #EXTRA_DATA_REMOVED} is set to true if the entire + * application -- data and code -- is being removed. + * <li> {@link #EXTRA_REPLACING} is set to true if this will be followed + * by an {@link #ACTION_PACKAGE_ADDED} broadcast for the same package. + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_REMOVED = "android.intent.action.PACKAGE_REMOVED"; + /** + * Broadcast Action: An existing application package has been changed (e.g. a component has been + * enabled or disabled. The data contains the name of the package. + * <ul> + * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_CHANGED = "android.intent.action.PACKAGE_CHANGED"; + /** + * Broadcast Action: The user has restarted a package, and all of its + * processes have been killed. All runtime state + * associated with it (processes, alarms, notifications, etc) should + * be removed. The data contains the name of the package. + * <ul> + * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_RESTARTED = "android.intent.action.PACKAGE_RESTARTED"; + /** + * Broadcast Action: The user has cleared the data of a package. This should + * be preceded by {@link #ACTION_PACKAGE_RESTARTED}, after which all of + * its persistent data is erased and this broadcast sent. The data contains + * the name of the package. + * <ul> + * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PACKAGE_DATA_CLEARED = "android.intent.action.PACKAGE_DATA_CLEARED"; + /** + * Broadcast Action: A user ID has been removed from the system. The user + * ID number is stored in the extra data under {@link #EXTRA_UID}. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + 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. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED"; + /** + * Broadcast Action: The current device {@link android.content.res.Configuration} + * (orientation, locale, etc) has changed. When such a change happens, the + * UIs (view hierarchy) will need to be rebuilt based on this new + * information; for the most part, applications don't need to worry about + * this, because the system will take care of stopping and restarting the + * application to make sure it sees the new changes. Some system code that + * can not be restarted will need to watch for this action and handle it + * appropriately. + * + * @see android.content.res.Configuration + */ + @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. + * + * <p class="note"> + * You can <em>not</em> receive this through components declared + * in manifests, only by exlicitly registering for it with + * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) + * Context.registerReceiver()}. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_BATTERY_CHANGED = "android.intent.action.BATTERY_CHANGED"; + /** + * Broadcast Action: Indicates low battery condition on the device. + * This broadcast corresponds to the "Low battery warning" system dialog. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW"; + /** + * Broadcast Action: Indicates low memory condition on the device + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEVICE_STORAGE_LOW = "android.intent.action.DEVICE_STORAGE_LOW"; + /** + * Broadcast Action: Indicates low memory condition on the device no longer exists + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_DEVICE_STORAGE_OK = "android.intent.action.DEVICE_STORAGE_OK"; + /** + * Broadcast Action: Indicates low memory condition notification acknowledged by user + * and package management should be started. + * This is triggered by the user from the ACTION_DEVICE_STORAGE_LOW + * notification. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MANAGE_PACKAGE_STORAGE = "android.intent.action.MANAGE_PACKAGE_STORAGE"; + /** + * Broadcast Action: The device has entered USB Mass Storage mode. + * This is used mainly for the USB Settings panel. + * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified + * when the SD card file system is mounted or unmounted + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_UMS_CONNECTED = "android.intent.action.UMS_CONNECTED"; + + /** + * Broadcast Action: The device has exited USB Mass Storage mode. + * This is used mainly for the USB Settings panel. + * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified + * when the SD card file system is mounted or unmounted + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_UMS_DISCONNECTED = "android.intent.action.UMS_DISCONNECTED"; + + /** + * Broadcast Action: External media has been removed. + * The path to the mount point for the removed media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_REMOVED = "android.intent.action.MEDIA_REMOVED"; + + /** + * Broadcast Action: External media is present, but not mounted at its mount point. + * The path to the mount point for the removed media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_UNMOUNTED = "android.intent.action.MEDIA_UNMOUNTED"; + + /** + * Broadcast Action: External media is present, and being disk-checked + * The path to the mount point for the checking media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_CHECKING = "android.intent.action.MEDIA_CHECKING"; + + /** + * Broadcast Action: External media is present, but is using an incompatible fs (or is blank) + * The path to the mount point for the checking media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_NOFS = "android.intent.action.MEDIA_NOFS"; + + /** + * Broadcast Action: External media is present and mounted at its mount point. + * The path to the mount point for the removed media is contained in the Intent.mData field. + * The Intent contains an extra with name "read-only" and Boolean value to indicate if the + * media was mounted read only. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_MOUNTED = "android.intent.action.MEDIA_MOUNTED"; + + /** + * Broadcast Action: External media is unmounted because it is being shared via USB mass storage. + * The path to the mount point for the removed media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_SHARED = "android.intent.action.MEDIA_SHARED"; + + /** + * Broadcast Action: External media was removed from SD card slot, but mount point was not unmounted. + * The path to the mount point for the removed media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_BAD_REMOVAL = "android.intent.action.MEDIA_BAD_REMOVAL"; + + /** + * Broadcast Action: External media is present but cannot be mounted. + * The path to the mount point for the removed media is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_UNMOUNTABLE = "android.intent.action.MEDIA_UNMOUNTABLE"; + + /** + * Broadcast Action: User has expressed the desire to remove the external storage media. + * Applications should close all files they have open within the mount point when they receive this intent. + * The path to the mount point for the media to be ejected is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_EJECT = "android.intent.action.MEDIA_EJECT"; + + /** + * Broadcast Action: The media scanner has started scanning a directory. + * The path to the directory being scanned is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_SCANNER_STARTED = "android.intent.action.MEDIA_SCANNER_STARTED"; + + /** + * Broadcast Action: The media scanner has finished scanning a directory. + * The path to the scanned directory is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_SCANNER_FINISHED = "android.intent.action.MEDIA_SCANNER_FINISHED"; + + /** + * Broadcast Action: Request the media scanner to scan a file and add it to the media database. + * The path to the file is contained in the Intent.mData field. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_SCANNER_SCAN_FILE = "android.intent.action.MEDIA_SCANNER_SCAN_FILE"; + + /** + * Broadcast Action: The "Media Button" was pressed. Includes a single + * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that + * caused the broadcast. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_MEDIA_BUTTON = "android.intent.action.MEDIA_BUTTON"; + + /** + * Broadcast Action: The "Camera Button" was pressed. Includes a single + * extra field, {@link #EXTRA_KEY_EVENT}, containing the key event that + * caused the broadcast. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CAMERA_BUTTON = "android.intent.action.CAMERA_BUTTON"; + + // *** NOTE: @todo(*) The following really should go into a more domain-specific + // location; they are not general-purpose actions. + + /** + * Broadcast Action: An GTalk connection has been established. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_GTALK_SERVICE_CONNECTED = + "android.intent.action.GTALK_CONNECTED"; + + /** + * Broadcast Action: An GTalk connection has been disconnected. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_GTALK_SERVICE_DISCONNECTED = + "android.intent.action.GTALK_DISCONNECTED"; + + /** + * Broadcast Action: An input method has been changed. + * {@hide pending API Council approval} + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_INPUT_METHOD_CHANGED = + "android.intent.action.INPUT_METHOD_CHANGED"; + + /** + * <p>Broadcast Action: The user has switched the phone into or out of Airplane Mode. One or + * more radios have been turned off or on. The intent will have the following extra value:</p> + * <ul> + * <li><em>state</em> - A boolean value indicating whether Airplane Mode is on. If true, + * then cell radio and possibly other radios such as bluetooth or WiFi may have also been + * turned off</li> + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_AIRPLANE_MODE_CHANGED = "android.intent.action.AIRPLANE_MODE"; + + /** + * Broadcast Action: Some content providers have parts of their namespace + * where they publish new events or items that the user may be especially + * interested in. For these things, they may broadcast this action when the + * set of interesting items change. + * + * For example, GmailProvider sends this notification when the set of unread + * mail in the inbox changes. + * + * <p>The data of the intent identifies which part of which provider + * changed. When queried through the content resolver, the data URI will + * return the data set in question. + * + * <p>The intent will have the following extra values: + * <ul> + * <li><em>count</em> - The number of items in the data set. This is the + * same as the number of items in the cursor returned by querying the + * data URI. </li> + * </ul> + * + * This intent will be sent at boot (if the count is non-zero) and when the + * data set changes. It is possible for the data set to change without the + * count changing (for example, if a new unread message arrives in the same + * sync operation in which a message is archived). The phone should still + * ring/vibrate/etc as normal in this case. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_PROVIDER_CHANGED = + "android.intent.action.PROVIDER_CHANGED"; + + /** + * Broadcast Action: Wired Headset plugged in or unplugged. + * + * <p>The intent will have the following extra values: + * <ul> + * <li><em>state</em> - 0 for unplugged, 1 for plugged. </li> + * <li><em>name</em> - Headset type, human readable string </li> + * </ul> + * </ul> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_HEADSET_PLUG = + "android.intent.action.HEADSET_PLUG"; + + /** + * Broadcast Action: An outgoing call is about to be placed. + * + * <p>The Intent will have the following extra value: + * <ul> + * <li><em>{@link android.content.Intent#EXTRA_PHONE_NUMBER}</em> - + * the phone number originally intended to be dialed.</li> + * </ul> + * <p>Once the broadcast is finished, the resultData is used as the actual + * number to call. If <code>null</code>, no call will be placed.</p> + * <p>It is perfectly acceptable for multiple receivers to process the + * outgoing call in turn: for example, a parental control application + * might verify that the user is authorized to place the call at that + * time, then a number-rewriting application might add an area code if + * one was not specified.</p> + * <p>For consistency, any receiver whose purpose is to prohibit phone + * calls should have a priority of 0, to ensure it will see the final + * phone number to be dialed. + * Any receiver whose purpose is to rewrite phone numbers to be called + * should have a positive priority. + * Negative priorities are reserved for the system for this broadcast; + * using them may cause problems.</p> + * <p>Any BroadcastReceiver receiving this Intent <em>must not</em> + * abort the broadcast.</p> + * <p>Emergency calls cannot be intercepted using this mechanism, and + * other calls cannot be modified to call emergency numbers using this + * mechanism. + * <p>You must hold the + * {@link android.Manifest.permission#PROCESS_OUTGOING_CALLS} + * permission to receive this Intent.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NEW_OUTGOING_CALL = + "android.intent.action.NEW_OUTGOING_CALL"; + + /** + * Broadcast Action: Have the device reboot. This is only for use by + * system code. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_REBOOT = + "android.intent.action.REBOOT"; + + // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // Standard intent categories (see addCategory()). + + /** + * Set if the activity should be an option for the default action + * (center press) to perform on a piece of data. Setting this will + * hide from the user any activities without it set when performing an + * action on some data. Note that this is normal -not- set in the + * Intent when initiating an action -- it is for use in intent filters + * specified in packages. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_DEFAULT = "android.intent.category.DEFAULT"; + /** + * Activities that can be safely invoked from a browser must support this + * category. For example, if the user is viewing a web page or an e-mail + * and clicks on a link in the text, the Intent generated execute that + * link will require the BROWSABLE category, so that only activities + * supporting this category will be considered as possible actions. By + * supporting this category, you are promising that there is nothing + * damaging (without user intervention) that can happen by invoking any + * matching Intent. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE"; + /** + * Set if the activity should be considered as an alternative action to + * the data the user is currently viewing. See also + * {@link #CATEGORY_SELECTED_ALTERNATIVE} for an alternative action that + * applies to the selection in a list of items. + * + * <p>Supporting this category means that you would like your activity to be + * displayed in the set of alternative things the user can do, usually as + * part of the current activity's options menu. You will usually want to + * include a specific label in the <intent-filter> of this action + * describing to the user what it does. + * + * <p>The action of IntentFilter with this category is important in that it + * describes the specific action the target will perform. This generally + * should not be a generic action (such as {@link #ACTION_VIEW}, but rather + * a specific name such as "com.android.camera.action.CROP. Only one + * alternative of any particular action will be shown to the user, so using + * a specific action like this makes sure that your alternative will be + * displayed while also allowing other applications to provide their own + * overrides of that particular action. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE"; + /** + * Set if the activity should be considered as an alternative selection + * action to the data the user has currently selected. This is like + * {@link #CATEGORY_ALTERNATIVE}, but is used in activities showing a list + * of items from which the user can select, giving them alternatives to the + * default action that will be performed on it. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE"; + /** + * Intended to be used as a tab inside of an containing TabActivity. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_TAB = "android.intent.category.TAB"; + /** + * This activity can be embedded inside of another activity that is hosting + * gadgets. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_GADGET = "android.intent.category.GADGET"; + /** + * Should be displayed in the top-level launcher. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; + /** + * Provides information about the package it is in; typically used if + * a package does not contain a {@link #CATEGORY_LAUNCHER} to provide + * a front-door to the user without having to be shown in the all apps list. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_INFO = "android.intent.category.INFO"; + /** + * This is the home activity, that is the first activity that is displayed + * when the device boots. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_HOME = "android.intent.category.HOME"; + /** + * This activity is a preference panel. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_PREFERENCE = "android.intent.category.PREFERENCE"; + /** + * This activity is a development preference panel. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_DEVELOPMENT_PREFERENCE = "android.intent.category.DEVELOPMENT_PREFERENCE"; + /** + * Capable of running inside a parent activity container. + * + * <p>Note: being removed in favor of more explicit categories such as + * CATEGORY_GADGET + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_EMBED = "android.intent.category.EMBED"; + /** + * This activity may be exercised by the monkey or other automated test tools. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_MONKEY = "android.intent.category.MONKEY"; + /** + * To be used as a test (not part of the normal user experience). + */ + public static final String CATEGORY_TEST = "android.intent.category.TEST"; + /** + * To be used as a unit test (run through the Test Harness). + */ + public static final String CATEGORY_UNIT_TEST = "android.intent.category.UNIT_TEST"; + /** + * To be used as an sample code example (not part of the normal user + * experience). + */ + public static final String CATEGORY_SAMPLE_CODE = "android.intent.category.SAMPLE_CODE"; + /** + * Used to indicate that a GET_CONTENT intent only wants URIs that can be opened with + * ContentResolver.openInputStream. Openable URIs must support the columns in OpenableColumns + * when queried, though it is allowable for those columns to be blank. + */ + @SdkConstant(SdkConstantType.INTENT_CATEGORY) + public static final String CATEGORY_OPENABLE = "android.intent.category.OPENABLE"; + + /** + * To be used as code under test for framework instrumentation tests. + */ + public static final String CATEGORY_FRAMEWORK_INSTRUMENTATION_TEST = + "android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST"; + // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // Standard extra data keys. + + /** + * The initial data to place in a newly created record. Use with + * {@link #ACTION_INSERT}. The data here is a Map containing the same + * fields as would be given to the underlying ContentProvider.insert() + * call. + */ + public static final String EXTRA_TEMPLATE = "android.intent.extra.TEMPLATE"; + + /** + * A constant CharSequence that is associated with the Intent, used with + * {@link #ACTION_SEND} to supply the literal data to be sent. Note that + * this may be a styled CharSequence, so you must use + * {@link Bundle#getCharSequence(String) Bundle.getCharSequence()} to + * retrieve it. + */ + public static final String EXTRA_TEXT = "android.intent.extra.TEXT"; + + /** + * A content: URI holding a stream of data associated with the Intent, + * used with {@link #ACTION_SEND} to supply the data being sent. + */ + public static final String EXTRA_STREAM = "android.intent.extra.STREAM"; + + /** + * A String[] holding e-mail addresses that should be delivered to. + */ + public static final String EXTRA_EMAIL = "android.intent.extra.EMAIL"; + + /** + * A String[] holding e-mail addresses that should be carbon copied. + */ + public static final String EXTRA_CC = "android.intent.extra.CC"; + + /** + * A String[] holding e-mail addresses that should be blind carbon copied. + */ + public static final String EXTRA_BCC = "android.intent.extra.BCC"; + + /** + * A constant string holding the desired subject line of a message. + */ + public static final String EXTRA_SUBJECT = "android.intent.extra.SUBJECT"; + + /** + * An Intent describing the choices you would like shown with + * {@link #ACTION_PICK_ACTIVITY}. + */ + public static final String EXTRA_INTENT = "android.intent.extra.INTENT"; + + /** + * A CharSequence dialog title to provide to the user when used with a + * {@link #ACTION_CHOOSER}. + */ + public static final String EXTRA_TITLE = "android.intent.extra.TITLE"; + + /** + * 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"; + + /** + * 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. + */ + public static final String EXTRA_DONT_KILL_APP = "android.intent.extra.DONT_KILL_APP"; + + /** + * A String holding the phone number originally entered in + * {@link android.content.Intent#ACTION_NEW_OUTGOING_CALL}, or the actual + * number to call in a {@link android.content.Intent#ACTION_CALL}. + */ + public static final String EXTRA_PHONE_NUMBER = "android.intent.extra.PHONE_NUMBER"; + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_UID_REMOVED} + * intents to supply the uid the package had been assigned. Also an optional + * extra in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} or + * {@link android.content.Intent#ACTION_PACKAGE_CHANGED} for the same + * purpose. + */ + public static final String EXTRA_UID = "android.intent.extra.UID"; + + /** + * Used as a boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} + * intents to indicate whether this represents a full uninstall (removing + * both the code and its data) or a partial uninstall (leaving its data, + * implying that this is an update). + */ + public static final String EXTRA_DATA_REMOVED = "android.intent.extra.DATA_REMOVED"; + + /** + * Used as a boolean extra field in {@link android.content.Intent#ACTION_PACKAGE_REMOVED} + * intents to indicate that this is a replacement of the package, so this + * broadcast will immediately be followed by an add broadcast for a + * different version of the same package. + */ + public static final String EXTRA_REPLACING = "android.intent.extra.REPLACING"; + + /** + * Used as an int extra field in {@link android.app.AlarmManager} intents + * to tell the application being invoked how many pending alarms are being + * delievered with the intent. For one-shot alarms this will always be 1. + * For recurring alarms, this might be greater than 1 if the device was + * asleep or powered off at the time an earlier alarm would have been + * delivered. + */ + public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT"; + + /** + * Used as an int extra field in {@link android.content.Intent#ACTION_VOICE_COMMAND} + * intents to request which audio route the voice command should prefer. + * The value should be a route from {@link android.media.AudioManager}, for + * example ROUTE_BLUETOOTH_SCO. Providing this value is optional. + * {@hide pending API Council approval} + */ + public static final String EXTRA_AUDIO_ROUTE = "android.intent.extra.AUDIO_ROUTE"; + + // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // Intent flags (see mFlags variable). + + /** + * If set, the recipient of this Intent will be granted permission to + * perform read operations on the Uri in the Intent's data. + */ + public static final int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001; + /** + * If set, the recipient of this Intent will be granted permission to + * perform write operations on the Uri in the Intent's data. + */ + public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002; + /** + * Can be set by the caller to indicate that this Intent is coming from + * a background operation, not from direct user interaction. + */ + public static final int FLAG_FROM_BACKGROUND = 0x00000004; + /** + * A flag you can enable for debugging: when set, log messages will be + * printed during the resolution of this intent to show you what has + * been found to create the final resolved list. + */ + public static final int FLAG_DEBUG_LOG_RESOLUTION = 0x00000008; + + /** + * If set, the new activity is not kept in the history stack. As soon as + * the user navigates away from it, the activity is finished. This may also + * be set with the {@link android.R.styleable#AndroidManifestActivity_noHistory + * noHistory} attribute. + */ + public static final int FLAG_ACTIVITY_NO_HISTORY = 0x40000000; + /** + * If set, the activity will not be launched if it is already running + * at the top of the history stack. + */ + public static final int FLAG_ACTIVITY_SINGLE_TOP = 0x20000000; + /** + * If set, this activity will become the start of a new task on this + * history stack. A task (from the activity that started it to the + * next task activity) defines an atomic group of activities that the + * user can move to. Tasks can be moved to the foreground and background; + * all of the activities inside of a particular task always remain in + * the same order. See + * <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. + * + * <p>This flag is generally used by activities that want + * to present a "launcher" style behavior: they give the user a list of + * separate things that can be done, which otherwise run completely + * independently of the activity launching them. + * + * <p>When using this flag, if a task is already running for the activity + * you are now starting, then a new activity will not be started; instead, + * the current task will simply be brought to the front of the screen with + * the state it was last in. See {@link #FLAG_ACTIVITY_MULTIPLE_TASK} for a flag + * to disable this behavior. + * + * <p>This flag can not be used when the caller is requesting a result from + * the activity being launched. + */ + public static final int FLAG_ACTIVITY_NEW_TASK = 0x10000000; + /** + * <strong>Do not use this flag unless you are implementing your own + * top-level application launcher.</strong> Used in conjunction with + * {@link #FLAG_ACTIVITY_NEW_TASK} to disable the + * behavior of bringing an existing task to the foreground. When set, + * a new task is <em>always</em> started to host the Activity for the + * Intent, regardless of whether there is already an existing task running + * the same thing. + * + * <p><strong>Because the default system does not include graphical task management, + * you should not use this flag unless you provide some way for a user to + * return back to the tasks you have launched.</strong> + * + * <p>This flag is ignored if + * {@link #FLAG_ACTIVITY_NEW_TASK} is not set. + * + * <p>See <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. + */ + public static final int FLAG_ACTIVITY_MULTIPLE_TASK = 0x08000000; + /** + * If set, and the activity being launched is already running in the + * current task, then instead of launching a new instance of that activity, + * all of the other activities on top of it will be closed and this Intent + * will be delivered to the (now on top) old activity as a new Intent. + * + * <p>For example, consider a task consisting of the activities: A, B, C, D. + * If D calls startActivity() with an Intent that resolves to the component + * 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 + * 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. + * + * <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 + * of a task, it will bring any currently running instance of that task + * to the foreground, and then clear it to its root state. This is + * especially useful, for example, when launching an activity from the + * notification manager. + * + * <p>See <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> for more details on tasks. + */ + public static final int FLAG_ACTIVITY_CLEAR_TOP = 0x04000000; + /** + * If set and this intent is being used to launch a new activity from an + * existing one, then the reply target of the existing activity will be + * transfered to the new activity. This way the new activity can call + * {@link android.app.Activity#setResult} and have that result sent back to + * the reply target of the original activity. + */ + public static final int FLAG_ACTIVITY_FORWARD_RESULT = 0x02000000; + /** + * If set and this intent is being used to launch a new activity from an + * existing one, the current activity will not be counted as the top + * activity for deciding whether the new intent should be delivered to + * the top instead of starting a new one. The previous activity will + * be used as the top, with the assumption being that the current activity + * will finish itself immediately. + */ + public static final int FLAG_ACTIVITY_PREVIOUS_IS_TOP = 0x01000000; + /** + * If set, the new activity is not kept in the list of recently launched + * activities. + */ + public static final int FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS = 0x00800000; + /** + * This flag is not normally set by application code, but set for you by + * the system as described in the + * {@link android.R.styleable#AndroidManifestActivity_launchMode + * launchMode} documentation for the singleTask mode. + */ + public static final int FLAG_ACTIVITY_BROUGHT_TO_FRONT = 0x00400000; + /** + * If set, and this activity is either being started in a new task or + * bringing to the top an existing task, then it will be launched as + * the front door of the task. This will result in the application of + * any affinities needed to have that task in the proper state (either + * moving activities to or from it), or simply resetting that task to + * its initial state if needed. + */ + public static final int FLAG_ACTIVITY_RESET_TASK_IF_NEEDED = 0x00200000; + /** + * This flag is not normally set by application code, but set for you by + * the system if this activity is being launched from history + * (longpress home key). + */ + public static final int FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = 0x00100000; + /** + * If set, this marks a point in the task's activity stack that should + * be cleared when the task is reset. That is, the next time the task + * is broad to the foreground with + * {@link #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED} (typically as a result of + * the user re-launching it from home), this activity and all on top of + * it will be finished so that the user does not return to them, but + * instead returns to whatever activity preceeded it. + * + * <p>This is useful for cases where you have a logical break in your + * application. For example, an e-mail application may have a command + * to view an attachment, which launches an image view activity to + * display it. This activity should be part of the e-mail application's + * task, since it is a part of the task the user is involved in. However, + * if the user leaves that task, and later selects the e-mail app from + * home, we may like them to return to the conversation they were + * viewing, not the picture attachment, since that is confusing. By + * setting this flag when launching the image viewer, that viewer and + * any activities it starts will be removed the next time the user returns + * to mail. + */ + public static final int FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET = 0x00080000; + /** + * If set, this flag will prevent the normal {@link android.app.Activity#onUserLeaveHint} + * callback from occurring on the current frontmost activity before it is + * paused as the newly-started activity is brought to the front. + * + * <p>Typically, an activity can rely on that callback to indicate that an + * explicit user action has caused their activity to be moved out of the + * foreground. The callback marks an appropriate point in the activity's + * lifecycle for it to dismiss any notifications that it intends to display + * "until the user has seen them," such as a blinking LED. + * + * <p>If an activity is ever started via any non-user-driven events such as + * phone-call receipt or an alarm handler, this flag should be passed to {@link + * Context#startActivity Context.startActivity}, ensuring that the pausing + * activity does not think the user has acknowledged its notification. + */ + public static final int FLAG_ACTIVITY_NO_USER_ACTION = 0x00040000; + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause the launched activity to be brought to the front of its + * task's history stack if it is already running. + * + * <p>For example, consider a task consisting of four activities: A, B, C, D. + * If D calls startActivity() with an Intent that resolves to the component + * of activity B, then B will be brought to the front of the history stack, + * with this resulting order: A, C, D, B. + * + * This flag will be ignored if {@link #FLAG_ACTIVITY_CLEAR_TOP} is also + * specified. + */ + public static final int FLAG_ACTIVITY_REORDER_TO_FRONT = 0X00020000; + /** + * If set, when sending a broadcast only registered receivers will be + * called -- no BroadcastReceiver components will be launched. + */ + public static final int FLAG_RECEIVER_REGISTERED_ONLY = 0x40000000; + /** + * If set, when sending a broadcast <i>before boot has completed</i> only + * registered receivers will be called -- no BroadcastReceiver components + * will be launched. Sticky intent state will be recorded properly even + * if no receivers wind up being called. If {@link #FLAG_RECEIVER_REGISTERED_ONLY} + * is specified in the broadcast intent, this flag is unnecessary. + * + * <p>This flag is only for use by system sevices as a convenience to + * avoid having to implement a more complex mechanism around detection + * of boot completion. + * + * @hide + */ + public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x20000000; + + // --------------------------------------------------------------------- + + private String mAction; + private Uri mData; + private String mType; + private ComponentName mComponent; + private int mFlags; + private HashSet<String> mCategories; + private Bundle mExtras; + + // --------------------------------------------------------------------- + + /** + * Create an empty intent. + */ + public Intent() { + } + + /** + * Copy constructor. + */ + public Intent(Intent o) { + this.mAction = o.mAction; + this.mData = o.mData; + this.mType = o.mType; + this.mComponent = o.mComponent; + this.mFlags = o.mFlags; + if (o.mCategories != null) { + this.mCategories = new HashSet<String>(o.mCategories); + } + if (o.mExtras != null) { + this.mExtras = new Bundle(o.mExtras); + } + } + + @Override + public Object clone() { + return new Intent(this); + } + + private Intent(Intent o, boolean all) { + this.mAction = o.mAction; + this.mData = o.mData; + this.mType = o.mType; + this.mComponent = o.mComponent; + if (o.mCategories != null) { + this.mCategories = new HashSet<String>(o.mCategories); + } + } + + /** + * Make a clone of only the parts of the Intent that are relevant for + * filter matching: the action, data, type, component, and categories. + */ + public Intent cloneFilter() { + return new Intent(this, false); + } + + /** + * Create an intent with a given action. All other fields (data, type, + * class) are null. Note that the action <em>must</em> be in a + * namespace because Intents are used globally in the system -- for + * example the system VIEW action is android.intent.action.VIEW; an + * application's custom action would be something like + * com.google.app.myapp.CUSTOM_ACTION. + * + * @param action The Intent action, such as ACTION_VIEW. + */ + public Intent(String action) { + mAction = action; + } + + /** + * Create an intent with a given action and for a given data url. Note + * that the action <em>must</em> be in a namespace because Intents are + * used globally in the system -- for example the system VIEW action is + * android.intent.action.VIEW; an application's custom action would be + * something like com.google.app.myapp.CUSTOM_ACTION. + * + * @param action The Intent action, such as ACTION_VIEW. + * @param uri The Intent data URI. + */ + public Intent(String action, Uri uri) { + mAction = action; + mData = uri; + } + + /** + * Create an intent for a specific component. All other fields (action, data, + * type, class) are null, though they can be modified later with explicit + * calls. This provides a convenient way to create an intent that is + * intended to execute a hard-coded class name, rather than relying on the + * system to find an appropriate class for you; see {@link #setComponent} + * for more information on the repercussions of this. + * + * @param packageContext A Context of the application package implementing + * this class. + * @param cls The component class that is to be used for the intent. + * + * @see #setClass + * @see #setComponent + * @see #Intent(String, android.net.Uri , Context, Class) + */ + public Intent(Context packageContext, Class<?> cls) { + mComponent = new ComponentName(packageContext, cls); + } + + /** + * Create an intent for a specific component with a specified action and data. + * This is equivalent using {@link #Intent(String, android.net.Uri)} to + * construct the Intent and then calling {@link #setClass} to set its + * class. + * + * @param action The Intent action, such as ACTION_VIEW. + * @param uri The Intent data URI. + * @param packageContext A Context of the application package implementing + * this class. + * @param cls The component class that is to be used for the intent. + * + * @see #Intent(String, android.net.Uri) + * @see #Intent(Context, Class) + * @see #setClass + * @see #setComponent + */ + public Intent(String action, Uri uri, + Context packageContext, Class<?> cls) { + mAction = action; + mData = uri; + mComponent = new ComponentName(packageContext, cls); + } + + /** + * Create an intent from a URI. This URI may encode the action, + * category, and other intent fields, if it was returned by toURI(). If + * the Intent was not generate by toURI(), its data will be the entire URI + * and its action will be ACTION_VIEW. + * + * <p>The URI given here must not be relative -- that is, it must include + * the scheme and full path. + * + * @param uri The URI to turn into an Intent. + * + * @return Intent The newly created Intent object. + * + * @see #toURI + */ + public static Intent getIntent(String uri) throws URISyntaxException { + int i = 0; + try { + // simple case + i = uri.lastIndexOf("#"); + if (i == -1) return new Intent(ACTION_VIEW, Uri.parse(uri)); + + // old format Intent URI + if (!uri.startsWith("#Intent;", i)) return getIntentOld(uri); + + // new format + Intent intent = new Intent(ACTION_VIEW); + + // fetch data part, if present + if (i > 0) { + intent.mData = Uri.parse(uri.substring(0, i)); + } + i += "#Intent;".length(); + + // loop over contents of Intent, all name=value; + while (!uri.startsWith("end", i)) { + int eq = uri.indexOf('=', i); + int semi = uri.indexOf(';', eq); + String value = uri.substring(eq + 1, semi); + + // action + if (uri.startsWith("action=", i)) { + intent.mAction = value; + } + + // categories + else if (uri.startsWith("category=", i)) { + intent.addCategory(value); + } + + // type + else if (uri.startsWith("type=", i)) { + intent.mType = value; + } + + // launch flags + else if (uri.startsWith("launchFlags=", i)) { + intent.mFlags = Integer.decode(value).intValue(); + } + + // component + else if (uri.startsWith("component=", i)) { + intent.mComponent = ComponentName.unflattenFromString(value); + } + + // extra + else { + String key = Uri.decode(uri.substring(i + 2, eq)); + value = Uri.decode(value); + // create Bundle if it doesn't already exist + if (intent.mExtras == null) intent.mExtras = new Bundle(); + Bundle b = intent.mExtras; + // add EXTRA + if (uri.startsWith("S.", i)) b.putString(key, value); + else if (uri.startsWith("B.", i)) b.putBoolean(key, Boolean.parseBoolean(value)); + else if (uri.startsWith("b.", i)) b.putByte(key, Byte.parseByte(value)); + else if (uri.startsWith("c.", i)) b.putChar(key, value.charAt(0)); + else if (uri.startsWith("d.", i)) b.putDouble(key, Double.parseDouble(value)); + else if (uri.startsWith("f.", i)) b.putFloat(key, Float.parseFloat(value)); + else if (uri.startsWith("i.", i)) b.putInt(key, Integer.parseInt(value)); + else if (uri.startsWith("l.", i)) b.putLong(key, Long.parseLong(value)); + else if (uri.startsWith("s.", i)) b.putShort(key, Short.parseShort(value)); + else throw new URISyntaxException(uri, "unknown EXTRA type", i); + } + + // move to the next item + i = semi + 1; + } + + return intent; + + } catch (IndexOutOfBoundsException e) { + throw new URISyntaxException(uri, "illegal Intent URI format", i); + } + } + + public static Intent getIntentOld(String uri) throws URISyntaxException { + Intent intent; + + int i = uri.lastIndexOf('#'); + if (i >= 0) { + Uri data = null; + String action = null; + if (i > 0) { + data = Uri.parse(uri.substring(0, i)); + } + + i++; + + if (uri.regionMatches(i, "action(", 0, 7)) { + i += 7; + int j = uri.indexOf(')', i); + action = uri.substring(i, j); + i = j + 1; + } + + intent = new Intent(action, data); + + if (uri.regionMatches(i, "categories(", 0, 11)) { + i += 11; + int j = uri.indexOf(')', i); + while (i < j) { + int sep = uri.indexOf('!', i); + if (sep < 0) sep = j; + if (i < sep) { + intent.addCategory(uri.substring(i, sep)); + } + i = sep + 1; + } + i = j + 1; + } + + if (uri.regionMatches(i, "type(", 0, 5)) { + i += 5; + int j = uri.indexOf(')', i); + intent.mType = uri.substring(i, j); + i = j + 1; + } + + if (uri.regionMatches(i, "launchFlags(", 0, 12)) { + i += 12; + int j = uri.indexOf(')', i); + intent.mFlags = Integer.decode(uri.substring(i, j)).intValue(); + i = j + 1; + } + + if (uri.regionMatches(i, "component(", 0, 10)) { + i += 10; + int j = uri.indexOf(')', i); + int sep = uri.indexOf('!', i); + if (sep >= 0 && sep < j) { + String pkg = uri.substring(i, sep); + String cls = uri.substring(sep + 1, j); + intent.mComponent = new ComponentName(pkg, cls); + } + i = j + 1; + } + + if (uri.regionMatches(i, "extras(", 0, 7)) { + i += 7; + + final int closeParen = uri.indexOf(')', i); + if (closeParen == -1) throw new URISyntaxException(uri, + "EXTRA missing trailing ')'", i); + + while (i < closeParen) { + // fetch the key value + int j = uri.indexOf('=', i); + if (j <= i + 1 || i >= closeParen) { + throw new URISyntaxException(uri, "EXTRA missing '='", i); + } + char type = uri.charAt(i); + i++; + String key = uri.substring(i, j); + i = j + 1; + + // get type-value + j = uri.indexOf('!', i); + if (j == -1 || j >= closeParen) j = closeParen; + if (i >= j) throw new URISyntaxException(uri, "EXTRA missing '!'", i); + String value = uri.substring(i, j); + i = j; + + // create Bundle if it doesn't already exist + if (intent.mExtras == null) intent.mExtras = new Bundle(); + + // add item to bundle + try { + switch (type) { + case 'S': + intent.mExtras.putString(key, Uri.decode(value)); + break; + case 'B': + intent.mExtras.putBoolean(key, Boolean.parseBoolean(value)); + break; + case 'b': + intent.mExtras.putByte(key, Byte.parseByte(value)); + break; + case 'c': + intent.mExtras.putChar(key, Uri.decode(value).charAt(0)); + break; + case 'd': + intent.mExtras.putDouble(key, Double.parseDouble(value)); + break; + case 'f': + intent.mExtras.putFloat(key, Float.parseFloat(value)); + break; + case 'i': + intent.mExtras.putInt(key, Integer.parseInt(value)); + break; + case 'l': + intent.mExtras.putLong(key, Long.parseLong(value)); + break; + case 's': + intent.mExtras.putShort(key, Short.parseShort(value)); + break; + default: + throw new URISyntaxException(uri, "EXTRA has unknown type", i); + } + } catch (NumberFormatException e) { + throw new URISyntaxException(uri, "EXTRA value can't be parsed", i); + } + + char ch = uri.charAt(i); + if (ch == ')') break; + if (ch != '!') throw new URISyntaxException(uri, "EXTRA missing '!'", i); + i++; + } + } + + if (intent.mAction == null) { + // By default, if no action is specified, then use VIEW. + intent.mAction = ACTION_VIEW; + } + + } else { + intent = new Intent(ACTION_VIEW, Uri.parse(uri)); + } + + return intent; + } + + /** + * Retrieve the general action to be performed, such as + * {@link #ACTION_VIEW}. The action describes the general way the rest of + * the information in the intent should be interpreted -- most importantly, + * what to do with the data returned by {@link #getData}. + * + * @return The action of this intent or null if none is specified. + * + * @see #setAction + */ + public String getAction() { + return mAction; + } + + /** + * Retrieve data this intent is operating on. This URI specifies the name + * of the data; often it uses the content: scheme, specifying data in a + * content provider. Other schemes may be handled by specific activities, + * such as http: by the web browser. + * + * @return The URI of the data this intent is targeting or null. + * + * @see #getScheme + * @see #setData + */ + public Uri getData() { + return mData; + } + + /** + * The same as {@link #getData()}, but returns the URI as an encoded + * String. + */ + public String getDataString() { + return mData != null ? mData.toString() : null; + } + + /** + * Return the scheme portion of the intent's data. If the data is null or + * does not include a scheme, null is returned. Otherwise, the scheme + * prefix without the final ':' is returned, i.e. "http". + * + * <p>This is the same as calling getData().getScheme() (and checking for + * null data). + * + * @return The scheme of this intent. + * + * @see #getData + */ + public String getScheme() { + return mData != null ? mData.getScheme() : null; + } + + /** + * Retrieve any explicit MIME type included in the intent. This is usually + * null, as the type is determined by the intent data. + * + * @return If a type was manually set, it is returned; else null is + * returned. + * + * @see #resolveType(ContentResolver) + * @see #setType + */ + public String getType() { + return mType; + } + + /** + * Return the MIME data type of this intent. If the type field is + * explicitly set, that is simply returned. Otherwise, if the data is set, + * the type of that data is returned. If neither fields are set, a null is + * returned. + * + * @return The MIME type of this intent. + * + * @see #getType + * @see #resolveType(ContentResolver) + */ + public String resolveType(Context context) { + return resolveType(context.getContentResolver()); + } + + /** + * Return the MIME data type of this intent. If the type field is + * explicitly set, that is simply returned. Otherwise, if the data is set, + * the type of that data is returned. If neither fields are set, a null is + * returned. + * + * @param resolver A ContentResolver that can be used to determine the MIME + * type of the intent's data. + * + * @return The MIME type of this intent. + * + * @see #getType + * @see #resolveType(Context) + */ + public String resolveType(ContentResolver resolver) { + if (mType != null) { + return mType; + } + if (mData != null) { + if ("content".equals(mData.getScheme())) { + return resolver.getType(mData); + } + } + return null; + } + + /** + * Return the MIME data type of this intent, only if it will be needed for + * intent resolution. This is not generally useful for application code; + * it is used by the frameworks for communicating with back-end system + * services. + * + * @param resolver A ContentResolver that can be used to determine the MIME + * type of the intent's data. + * + * @return The MIME type of this intent, or null if it is unknown or not + * needed. + */ + public String resolveTypeIfNeeded(ContentResolver resolver) { + if (mComponent != null) { + return mType; + } + return resolveType(resolver); + } + + /** + * Check if an category exists in the intent. + * + * @param category The category to check. + * + * @return boolean True if the intent contains the category, else false. + * + * @see #getCategories + * @see #addCategory + */ + public boolean hasCategory(String category) { + return mCategories != null && mCategories.contains(category); + } + + /** + * Return the set of all categories in the intent. If there are no categories, + * returns NULL. + * + * @return Set The set of categories you can examine. Do not modify! + * + * @see #hasCategory + * @see #addCategory + */ + public Set<String> getCategories() { + return mCategories; + } + + /** + * Sets the ClassLoader that will be used when unmarshalling + * any Parcelable values from the extras of this Intent. + * + * @param loader a ClassLoader, or null to use the default loader + * at the time of unmarshalling. + */ + public void setExtrasClassLoader(ClassLoader loader) { + if (mExtras != null) { + mExtras.setClassLoader(loader); + } + } + + /** + * Returns true if an extra value is associated with the given name. + * @param name the extra's name + * @return true if the given extra is present. + */ + public boolean hasExtra(String name) { + return mExtras != null && mExtras.containsKey(name); + } + + /** + * Returns true if the Intent's extras contain a parcelled file descriptor. + * @return true if the Intent contains a parcelled file descriptor. + */ + public boolean hasFileDescriptors() { + return mExtras != null && mExtras.hasFileDescriptors(); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if none was found. + * + * @deprecated + * @hide + */ + @Deprecated + public Object getExtra(String name) { + return getExtra(name, null); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, boolean) + */ + public boolean getBooleanExtra(String name, boolean defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getBoolean(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, byte) + */ + public byte getByteExtra(String name, byte defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getByte(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, short) + */ + public short getShortExtra(String name, short defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getShort(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, char) + */ + public char getCharExtra(String name, char defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getChar(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, int) + */ + public int getIntExtra(String name, int defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getInt(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, long) + */ + public long getLongExtra(String name, long defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getLong(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra(), + * or the default value if no such item is present + * + * @see #putExtra(String, float) + */ + public float getFloatExtra(String name, float defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getFloat(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue the value to be returned if no value of the desired + * type is stored with the given name. + * + * @return the value of an item that previously added with putExtra() + * or the default value if none was found. + * + * @see #putExtra(String, double) + */ + public double getDoubleExtra(String name, double defaultValue) { + return mExtras == null ? defaultValue : + mExtras.getDouble(name, defaultValue); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no String value was found. + * + * @see #putExtra(String, String) + */ + public String getStringExtra(String name) { + return mExtras == null ? null : mExtras.getString(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no CharSequence value was found. + * + * @see #putExtra(String, CharSequence) + */ + public CharSequence getCharSequenceExtra(String name) { + return mExtras == null ? null : mExtras.getCharSequence(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no Parcelable value was found. + * + * @see #putExtra(String, Parcelable) + */ + public <T extends Parcelable> T getParcelableExtra(String name) { + return mExtras == null ? null : mExtras.<T>getParcelable(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no Parcelable[] value was found. + * + * @see #putExtra(String, Parcelable[]) + */ + public Parcelable[] getParcelableArrayExtra(String name) { + return mExtras == null ? null : mExtras.getParcelableArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no ArrayList<Parcelable> value was found. + * + * @see #putParcelableArrayListExtra(String, ArrayList) + */ + public <T extends Parcelable> ArrayList<T> getParcelableArrayListExtra(String name) { + return mExtras == null ? null : mExtras.<T>getParcelableArrayList(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no Serializable value was found. + * + * @see #putExtra(String, Serializable) + */ + public Serializable getSerializableExtra(String name) { + return mExtras == null ? null : mExtras.getSerializable(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no ArrayList<Integer> value was found. + * + * @see #putIntegerArrayListExtra(String, ArrayList) + */ + public ArrayList<Integer> getIntegerArrayListExtra(String name) { + return mExtras == null ? null : mExtras.getIntegerArrayList(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no ArrayList<String> value was found. + * + * @see #putStringArrayListExtra(String, ArrayList) + */ + public ArrayList<String> getStringArrayListExtra(String name) { + return mExtras == null ? null : mExtras.getStringArrayList(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no boolean array value was found. + * + * @see #putExtra(String, boolean[]) + */ + public boolean[] getBooleanArrayExtra(String name) { + return mExtras == null ? null : mExtras.getBooleanArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no byte array value was found. + * + * @see #putExtra(String, byte[]) + */ + public byte[] getByteArrayExtra(String name) { + return mExtras == null ? null : mExtras.getByteArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no short array value was found. + * + * @see #putExtra(String, short[]) + */ + public short[] getShortArrayExtra(String name) { + return mExtras == null ? null : mExtras.getShortArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no char array value was found. + * + * @see #putExtra(String, char[]) + */ + public char[] getCharArrayExtra(String name) { + return mExtras == null ? null : mExtras.getCharArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no int array value was found. + * + * @see #putExtra(String, int[]) + */ + public int[] getIntArrayExtra(String name) { + return mExtras == null ? null : mExtras.getIntArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no long array value was found. + * + * @see #putExtra(String, long[]) + */ + public long[] getLongArrayExtra(String name) { + return mExtras == null ? null : mExtras.getLongArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no float array value was found. + * + * @see #putExtra(String, float[]) + */ + public float[] getFloatArrayExtra(String name) { + return mExtras == null ? null : mExtras.getFloatArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no double array value was found. + * + * @see #putExtra(String, double[]) + */ + public double[] getDoubleArrayExtra(String name) { + return mExtras == null ? null : mExtras.getDoubleArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no String array value was found. + * + * @see #putExtra(String, String[]) + */ + public String[] getStringArrayExtra(String name) { + return mExtras == null ? null : mExtras.getStringArray(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no Bundle value was found. + * + * @see #putExtra(String, Bundle) + */ + public Bundle getBundleExtra(String name) { + return mExtras == null ? null : mExtras.getBundle(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * + * @return the value of an item that previously added with putExtra() + * or null if no IBinder value was found. + * + * @see #putExtra(String, IBinder) + * + * @deprecated + * @hide + */ + @Deprecated + public IBinder getIBinderExtra(String name) { + return mExtras == null ? null : mExtras.getIBinder(name); + } + + /** + * Retrieve extended data from the intent. + * + * @param name The name of the desired item. + * @param defaultValue The default value to return in case no item is + * associated with the key 'name' + * + * @return the value of an item that previously added with putExtra() + * or defaultValue if none was found. + * + * @see #putExtra + * + * @deprecated + * @hide + */ + @Deprecated + public Object getExtra(String name, Object defaultValue) { + Object result = defaultValue; + if (mExtras != null) { + Object result2 = mExtras.get(name); + if (result2 != null) { + result = result2; + } + } + + return result; + } + + /** + * Retrieves a map of extended data from the intent. + * + * @return the map of all extras previously added with putExtra(), + * or null if none have been added. + */ + public Bundle getExtras() { + return (mExtras != null) + ? new Bundle(mExtras) + : null; + } + + /** + * Retrieve any special flags associated with this intent. You will + * normally just set them with {@link #setFlags} and let the system + * take the appropriate action with them. + * + * @return int The currently set flags. + * + * @see #setFlags + */ + public int getFlags() { + return mFlags; + } + + /** + * Retrieve the concrete component associated with the intent. When receiving + * an intent, this is the component that was found to best handle it (that is, + * yourself) and will always be non-null; in all other cases it will be + * null unless explicitly set. + * + * @return The name of the application component to handle the intent. + * + * @see #resolveActivity + * @see #setComponent + */ + public ComponentName getComponent() { + return mComponent; + } + + /** + * Return the Activity component that should be used to handle this intent. + * The appropriate component is determined based on the information in the + * intent, evaluated as follows: + * + * <p>If {@link #getComponent} returns an explicit class, that is returned + * without any further consideration. + * + * <p>The activity must handle the {@link Intent#CATEGORY_DEFAULT} Intent + * category to be considered. + * + * <p>If {@link #getAction} is non-NULL, the activity must handle this + * action. + * + * <p>If {@link #resolveType} returns non-NULL, the activity must handle + * this type. + * + * <p>If {@link #addCategory} has added any categories, the activity must + * handle ALL of the categories specified. + * + * <p>If there are no activities that satisfy all of these conditions, a + * null string is returned. + * + * <p>If multiple activities are found to satisfy the intent, the one with + * the highest priority will be used. If there are multiple activities + * with the same priority, the system will either pick the best activity + * based on user preference, or resolve to a system class that will allow + * the user to pick an activity and forward from there. + * + * <p>This method is implemented simply by calling + * {@link PackageManager#resolveActivity} with the "defaultOnly" parameter + * true.</p> + * <p> This API is called for you as part of starting an activity from an + * intent. You do not normally need to call it yourself.</p> + * + * @param pm The package manager with which to resolve the Intent. + * + * @return Name of the component implementing an activity that can + * display the intent. + * + * @see #setComponent + * @see #getComponent + * @see #resolveActivityInfo + */ + public ComponentName resolveActivity(PackageManager pm) { + if (mComponent != null) { + return mComponent; + } + + ResolveInfo info = pm.resolveActivity( + this, PackageManager.MATCH_DEFAULT_ONLY); + if (info != null) { + return new ComponentName( + info.activityInfo.applicationInfo.packageName, + info.activityInfo.name); + } + + return null; + } + + /** + * Resolve the Intent into an {@link ActivityInfo} + * describing the activity that should execute the intent. Resolution + * follows the same rules as described for {@link #resolveActivity}, but + * you get back the completely information about the resolved activity + * instead of just its class name. + * + * @param pm The package manager with which to resolve the Intent. + * @param flags Addition information to retrieve as per + * {@link PackageManager#getActivityInfo(ComponentName, int) + * PackageManager.getActivityInfo()}. + * + * @return PackageManager.ActivityInfo + * + * @see #resolveActivity + */ + public ActivityInfo resolveActivityInfo(PackageManager pm, int flags) { + ActivityInfo ai = null; + if (mComponent != null) { + try { + ai = pm.getActivityInfo(mComponent, flags); + } catch (PackageManager.NameNotFoundException e) { + // ignore + } + } else { + ResolveInfo info = pm.resolveActivity( + this, PackageManager.MATCH_DEFAULT_ONLY); + if (info != null) { + ai = info.activityInfo; + } + } + + return ai; + } + + /** + * Set the general action to be performed. + * + * @param action An action name, such as ACTION_VIEW. Application-specific + * actions should be prefixed with the vendor's package name. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getAction + */ + public Intent setAction(String action) { + mAction = action; + return this; + } + + /** + * Set the data this intent is operating on. This method automatically + * clears any type that was previously set by {@link #setType}. + * + * @param data The URI of the data this intent is now targeting. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getData + * @see #setType + * @see #setDataAndType + */ + public Intent setData(Uri data) { + mData = data; + mType = null; + return this; + } + + /** + * Set an explicit MIME data type. This is used to create intents that + * only specify a type and not data, for example to indicate the type of + * data to return. This method automatically clears any data that was + * previously set by {@link #setData}. + * + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getType + * @see #setData + * @see #setDataAndType + */ + public Intent setType(String type) { + mData = null; + mType = type; + return this; + } + + /** + * (Usually optional) Set the data for the intent along with an explicit + * MIME data type. This method should very rarely be used -- it allows you + * to override the MIME type that would ordinarily be inferred from the + * data with your own type given here. + * + * @param data The URI of the data this intent is now targeting. + * @param type The MIME type of the data being handled by this intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setData + * @see #setType + */ + public Intent setDataAndType(Uri data, String type) { + mData = data; + mType = type; + return this; + } + + /** + * Add a new category to the intent. Categories provide additional detail + * about the action the intent is perform. When resolving an intent, only + * activities that provide <em>all</em> of the requested categories will be + * used. + * + * @param category The desired category. This can be either one of the + * predefined Intent categories, or a custom category in your own + * namespace. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #hasCategory + * @see #removeCategory + */ + public Intent addCategory(String category) { + if (mCategories == null) { + mCategories = new HashSet<String>(); + } + mCategories.add(category); + return this; + } + + /** + * Remove an category from an intent. + * + * @param category The category to remove. + * + * @see #addCategory + */ + public void removeCategory(String category) { + if (mCategories != null) { + mCategories.remove(category); + if (mCategories.size() == 0) { + mCategories = null; + } + } + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The boolean data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getBooleanExtra(String, boolean) + */ + public Intent putExtra(String name, boolean value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putBoolean(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The byte data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getByteExtra(String, byte) + */ + public Intent putExtra(String name, byte value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putByte(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The char data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getCharExtra(String, char) + */ + public Intent putExtra(String name, char value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putChar(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The short data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getShortExtra(String, short) + */ + public Intent putExtra(String name, short value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putShort(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The integer data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getIntExtra(String, int) + */ + public Intent putExtra(String name, int value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putInt(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The long data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getLongExtra(String, long) + */ + public Intent putExtra(String name, long value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putLong(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The float data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getFloatExtra(String, float) + */ + public Intent putExtra(String name, float value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putFloat(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The double data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getDoubleExtra(String, double) + */ + public Intent putExtra(String name, double value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putDouble(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The String data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getStringExtra(String) + */ + public Intent putExtra(String name, String value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putString(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The CharSequence data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getCharSequenceExtra(String) + */ + public Intent putExtra(String name, CharSequence value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putCharSequence(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The Parcelable data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getParcelableExtra(String) + */ + public Intent putExtra(String name, Parcelable value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putParcelable(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The Parcelable[] data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getParcelableArrayExtra(String) + */ + public Intent putExtra(String name, Parcelable[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putParcelableArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The ArrayList<Parcelable> data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getParcelableArrayListExtra(String) + */ + public Intent putParcelableArrayListExtra(String name, ArrayList<? extends Parcelable> value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putParcelableArrayList(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The ArrayList<Integer> data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getIntegerArrayListExtra(String) + */ + public Intent putIntegerArrayListExtra(String name, ArrayList<Integer> value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putIntegerArrayList(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The ArrayList<String> data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getStringArrayListExtra(String) + */ + public Intent putStringArrayListExtra(String name, ArrayList<String> value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putStringArrayList(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The Serializable data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getSerializableExtra(String) + */ + public Intent putExtra(String name, Serializable value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putSerializable(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The boolean array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getBooleanArrayExtra(String) + */ + public Intent putExtra(String name, boolean[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putBooleanArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The byte array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getByteArrayExtra(String) + */ + public Intent putExtra(String name, byte[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putByteArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The short array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getShortArrayExtra(String) + */ + public Intent putExtra(String name, short[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putShortArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The char array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getCharArrayExtra(String) + */ + public Intent putExtra(String name, char[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putCharArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The int array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getIntArrayExtra(String) + */ + public Intent putExtra(String name, int[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putIntArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The byte array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getLongArrayExtra(String) + */ + public Intent putExtra(String name, long[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putLongArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The float array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getFloatArrayExtra(String) + */ + public Intent putExtra(String name, float[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putFloatArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The double array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getDoubleArrayExtra(String) + */ + public Intent putExtra(String name, double[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putDoubleArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The String array data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getStringArrayExtra(String) + */ + public Intent putExtra(String name, String[] value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putStringArray(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The Bundle data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getBundleExtra(String) + */ + public Intent putExtra(String name, Bundle value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putBundle(name, value); + return this; + } + + /** + * Add extended data to the intent. The name must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param name The name of the extra data, with package prefix. + * @param value The IBinder data value. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #putExtras + * @see #removeExtra + * @see #getIBinderExtra(String) + * + * @deprecated + * @hide + */ + @Deprecated + public Intent putExtra(String name, IBinder value) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putIBinder(name, value); + return this; + } + + /** + * Copy all extras in 'src' in to this intent. + * + * @param src Contains the extras to copy. + * + * @see #putExtra + */ + public Intent putExtras(Intent src) { + if (src.mExtras != null) { + if (mExtras == null) { + mExtras = new Bundle(src.mExtras); + } else { + mExtras.putAll(src.mExtras); + } + } + return this; + } + + /** + * Add a set of extended data to the intent. The keys must include a package + * prefix, for example the app com.android.contacts would use names + * like "com.android.contacts.ShowAll". + * + * @param extras The Bundle of extras to add to this intent. + * + * @see #putExtra + * @see #removeExtra + */ + public Intent putExtras(Bundle extras) { + if (mExtras == null) { + mExtras = new Bundle(); + } + mExtras.putAll(extras); + return this; + } + + /** + * Completely replace the extras in the Intent with the extras in the + * given Intent. + * + * @param src The exact extras contained in this Intent are copied + * into the target intent, replacing any that were previously there. + */ + public Intent replaceExtras(Intent src) { + mExtras = src.mExtras != null ? new Bundle(src.mExtras) : null; + return this; + } + + /** + * Completely replace the extras in the Intent with the given Bundle of + * extras. + * + * @param extras The new set of extras in the Intent, or null to erase + * all extras. + */ + public Intent replaceExtras(Bundle extras) { + mExtras = extras != null ? new Bundle(extras) : null; + return this; + } + + /** + * Remove extended data from the intent. + * + * @see #putExtra + */ + public void removeExtra(String name) { + if (mExtras != null) { + mExtras.remove(name); + if (mExtras.size() == 0) { + mExtras = null; + } + } + } + + /** + * Set special flags controlling how this intent is handled. Most values + * here depend on the type of component being executed by the Intent, + * specifically the FLAG_ACTIVITY_* flags are all for use with + * {@link Context#startActivity Context.startActivity()} and the + * FLAG_RECEIVER_* flags are all for use with + * {@link Context#sendBroadcast(Intent) Context.sendBroadcast()}. + * + * <p>See the <a href="{@docRoot}guide/topics/fundamentals.html#acttask">Application Fundamentals: + * Activities and Tasks</a> documentation for important information on how some of these options impact + * the behavior of your application. + * + * @param flags The desired flags. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getFlags + * @see #addFlags + * + * @see #FLAG_GRANT_READ_URI_PERMISSION + * @see #FLAG_GRANT_WRITE_URI_PERMISSION + * @see #FLAG_DEBUG_LOG_RESOLUTION + * @see #FLAG_FROM_BACKGROUND + * @see #FLAG_ACTIVITY_BROUGHT_TO_FRONT + * @see #FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET + * @see #FLAG_ACTIVITY_CLEAR_TOP + * @see #FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + * @see #FLAG_ACTIVITY_FORWARD_RESULT + * @see #FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY + * @see #FLAG_ACTIVITY_MULTIPLE_TASK + * @see #FLAG_ACTIVITY_NEW_TASK + * @see #FLAG_ACTIVITY_NO_HISTORY + * @see #FLAG_ACTIVITY_NO_USER_ACTION + * @see #FLAG_ACTIVITY_PREVIOUS_IS_TOP + * @see #FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + * @see #FLAG_ACTIVITY_SINGLE_TOP + * @see #FLAG_RECEIVER_REGISTERED_ONLY + */ + public Intent setFlags(int flags) { + mFlags = flags; + return this; + } + + /** + * Add additional flags to the intent (or with existing flags + * value). + * + * @param flags The new flags to set. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setFlags + */ + public Intent addFlags(int flags) { + mFlags |= flags; + return this; + } + + /** + * (Usually optional) Explicitly set the component to handle the intent. + * If left with the default value of null, the system will determine the + * appropriate class to use based on the other fields (action, data, + * type, categories) in the Intent. If this class is defined, the + * specified class will always be used regardless of the other fields. You + * should only set this value when you know you absolutely want a specific + * class to be used; otherwise it is better to let the system find the + * appropriate class so that you will respect the installed applications + * and user preferences. + * + * @param component The name of the application component to handle the + * intent, or null to let the system find one for you. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setClass + * @see #setClassName(Context, String) + * @see #setClassName(String, String) + * @see #getComponent + * @see #resolveActivity + */ + public Intent setComponent(ComponentName component) { + mComponent = component; + return this; + } + + /** + * Convenience for calling {@link #setComponent} with an + * explicit class name. + * + * @param packageContext A Context of the application package implementing + * this class. + * @param className The name of a class inside of the application package + * that will be used as the component for this Intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setComponent + * @see #setClass + */ + public Intent setClassName(Context packageContext, String className) { + mComponent = new ComponentName(packageContext, className); + return this; + } + + /** + * Convenience for calling {@link #setComponent} with an + * explicit application package name and class name. + * + * @param packageName The name of the package implementing the desired + * component. + * @param className The name of a class inside of the application package + * that will be used as the component for this Intent. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setComponent + * @see #setClass + */ + public Intent setClassName(String packageName, String className) { + mComponent = new ComponentName(packageName, className); + return this; + } + + /** + * Convenience for calling {@link #setComponent(ComponentName)} with the + * name returned by a {@link Class} object. + * + * @param packageContext A Context of the application package implementing + * this class. + * @param cls The class name to set, equivalent to + * <code>setClassName(context, cls.getName())</code>. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #setComponent + */ + public Intent setClass(Context packageContext, Class<?> cls) { + mComponent = new ComponentName(packageContext, cls); + return this; + } + + /** + * Use with {@link #fillIn} to allow the current action value to be + * overwritten, even if it is already set. + */ + public static final int FILL_IN_ACTION = 1<<0; + + /** + * Use with {@link #fillIn} to allow the current data or type value + * overwritten, even if it is already set. + */ + public static final int FILL_IN_DATA = 1<<1; + + /** + * Use with {@link #fillIn} to allow the current categories to be + * overwritten, even if they are already set. + */ + public static final int FILL_IN_CATEGORIES = 1<<2; + + /** + * Use with {@link #fillIn} to allow the current component value to be + * overwritten, even if it is already set. + */ + public static final int FILL_IN_COMPONENT = 1<<3; + + /** + * Copy the contents of <var>other</var> in to this object, but only + * where fields are not defined by this object. For purposes of a field + * being defined, the following pieces of data in the Intent are + * considered to be separate fields: + * + * <ul> + * <li> action, as set by {@link #setAction}. + * <li> data URI and MIME type, as set by {@link #setData(Uri)}, + * {@link #setType(String)}, or {@link #setDataAndType(Uri, String)}. + * <li> categories, as set by {@link #addCategory}. + * <li> component, as set by {@link #setComponent(ComponentName)} or + * related methods. + * <li> each top-level name in the associated extras. + * </ul> + * + * <p>In addition, you can use the {@link #FILL_IN_ACTION}, + * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and + * {@link #FILL_IN_COMPONENT} to override the restriction where the + * corresponding field will not be replaced if it is already set. + * + * <p>For example, consider Intent A with {data="foo", categories="bar"} + * and Intent B with {action="gotit", data-type="some/thing", + * categories="one","two"}. + * + * <p>Calling A.fillIn(B, Intent.FILL_IN_DATA) will result in A now + * containing: {action="gotit", data-type="some/thing", + * categories="bar"}. + * + * @param other Another Intent whose values are to be used to fill in + * the current one. + * @param flags Options to control which fields can be filled in. + * + * @return Returns a bit mask of {@link #FILL_IN_ACTION}, + * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and + * {@link #FILL_IN_COMPONENT} indicating which fields were changed. + */ + public int fillIn(Intent other, int flags) { + int changes = 0; + if ((mAction == null && other.mAction == null) + || (flags&FILL_IN_ACTION) != 0) { + mAction = other.mAction; + changes |= FILL_IN_ACTION; + } + if ((mData == null && mType == null && + (other.mData != null || other.mType != null)) + || (flags&FILL_IN_DATA) != 0) { + mData = other.mData; + mType = other.mType; + changes |= FILL_IN_DATA; + } + if ((mCategories == null && other.mCategories == null) + || (flags&FILL_IN_CATEGORIES) != 0) { + if (other.mCategories != null) { + mCategories = new HashSet<String>(other.mCategories); + } + changes |= FILL_IN_CATEGORIES; + } + if ((mComponent == null && other.mComponent == null) + || (flags&FILL_IN_COMPONENT) != 0) { + mComponent = other.mComponent; + changes |= FILL_IN_COMPONENT; + } + mFlags |= other.mFlags; + if (mExtras == null) { + if (other.mExtras != null) { + mExtras = new Bundle(other.mExtras); + } + } else if (other.mExtras != null) { + try { + Bundle newb = new Bundle(other.mExtras); + newb.putAll(mExtras); + mExtras = newb; + } catch (RuntimeException e) { + // Modifying the extras can cause us to unparcel the contents + // of the bundle, and if we do this in the system process that + // may fail. We really should handle this (i.e., the Bundle + // impl shouldn't be on top of a plain map), but for now just + // ignore it and keep the original contents. :( + Log.w("Intent", "Failure filling in extras", e); + } + } + return changes; + } + + /** + * Wrapper class holding an Intent and implementing comparisons on it for + * the purpose of filtering. The class implements its + * {@link #equals equals()} and {@link #hashCode hashCode()} methods as + * simple calls to {@link Intent#filterEquals(Intent)} filterEquals()} and + * {@link android.content.Intent#filterHashCode()} filterHashCode()} + * on the wrapped Intent. + */ + public static final class FilterComparison { + private final Intent mIntent; + private final int mHashCode; + + public FilterComparison(Intent intent) { + mIntent = intent; + mHashCode = intent.filterHashCode(); + } + + /** + * Return the Intent that this FilterComparison represents. + * @return Returns the Intent held by the FilterComparison. Do + * not modify! + */ + public Intent getIntent() { + return mIntent; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FilterComparison) { + Intent other = ((FilterComparison)obj).mIntent; + return mIntent.filterEquals(other); + } + return false; + } + + @Override + public int hashCode() { + return mHashCode; + } + } + + /** + * Determine if two intents are the same for the purposes of intent + * resolution (filtering). That is, if their action, data, type, + * class, and categories are the same. This does <em>not</em> compare + * any extra data included in the intents. + * + * @param other The other Intent to compare against. + * + * @return Returns true if action, data, type, class, and categories + * are the same. + */ + public boolean filterEquals(Intent other) { + if (other == null) { + return false; + } + if (mAction != other.mAction) { + if (mAction != null) { + if (!mAction.equals(other.mAction)) { + return false; + } + } else { + if (!other.mAction.equals(mAction)) { + return false; + } + } + } + if (mData != other.mData) { + if (mData != null) { + if (!mData.equals(other.mData)) { + return false; + } + } else { + if (!other.mData.equals(mData)) { + return false; + } + } + } + if (mType != other.mType) { + if (mType != null) { + if (!mType.equals(other.mType)) { + return false; + } + } else { + if (!other.mType.equals(mType)) { + return false; + } + } + } + if (mComponent != other.mComponent) { + if (mComponent != null) { + if (!mComponent.equals(other.mComponent)) { + return false; + } + } else { + if (!other.mComponent.equals(mComponent)) { + return false; + } + } + } + if (mCategories != other.mCategories) { + if (mCategories != null) { + if (!mCategories.equals(other.mCategories)) { + return false; + } + } else { + if (!other.mCategories.equals(mCategories)) { + return false; + } + } + } + + return true; + } + + /** + * Generate hash code that matches semantics of filterEquals(). + * + * @return Returns the hash value of the action, data, type, class, and + * categories. + * + * @see #filterEquals + */ + public int filterHashCode() { + int code = 0; + if (mAction != null) { + code += mAction.hashCode(); + } + if (mData != null) { + code += mData.hashCode(); + } + if (mType != null) { + code += mType.hashCode(); + } + if (mComponent != null) { + code += mComponent.hashCode(); + } + if (mCategories != null) { + code += mCategories.hashCode(); + } + return code; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + + b.append("Intent {"); + if (mAction != null) b.append(" action=").append(mAction); + if (mCategories != null) { + b.append(" categories={"); + Iterator<String> i = mCategories.iterator(); + boolean didone = false; + while (i.hasNext()) { + if (didone) b.append(","); + didone = true; + b.append(i.next()); + } + b.append("}"); + } + if (mData != null) b.append(" data=").append(mData); + if (mType != null) b.append(" type=").append(mType); + if (mFlags != 0) b.append(" flags=0x").append(Integer.toHexString(mFlags)); + if (mComponent != null) b.append(" comp=").append(mComponent.toShortString()); + if (mExtras != null) b.append(" (has extras)"); + b.append(" }"); + + return b.toString(); + } + + public String toURI() { + StringBuilder uri = new StringBuilder(mData != null ? mData.toString() : ""); + + uri.append("#Intent;"); + + if (mAction != null) { + uri.append("action=").append(mAction).append(';'); + } + if (mCategories != null) { + for (String category : mCategories) { + uri.append("category=").append(category).append(';'); + } + } + if (mType != null) { + uri.append("type=").append(mType).append(';'); + } + if (mFlags != 0) { + uri.append("launchFlags=0x").append(Integer.toHexString(mFlags)).append(';'); + } + if (mComponent != null) { + uri.append("component=").append(mComponent.flattenToShortString()).append(';'); + } + if (mExtras != null) { + for (String key : mExtras.keySet()) { + final Object value = mExtras.get(key); + char entryType = + value instanceof String ? 'S' : + value instanceof Boolean ? 'B' : + value instanceof Byte ? 'b' : + value instanceof Character ? 'c' : + value instanceof Double ? 'd' : + value instanceof Float ? 'f' : + value instanceof Integer ? 'i' : + value instanceof Long ? 'l' : + value instanceof Short ? 's' : + '\0'; + + if (entryType != '\0') { + uri.append(entryType); + uri.append('.'); + uri.append(Uri.encode(key)); + uri.append('='); + uri.append(Uri.encode(value.toString())); + uri.append(';'); + } + } + } + + uri.append("end"); + + return uri.toString(); + } + + public int describeContents() { + return (mExtras != null) ? mExtras.describeContents() : 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mAction); + Uri.writeToParcel(out, mData); + out.writeString(mType); + out.writeInt(mFlags); + ComponentName.writeToParcel(mComponent, out); + + if (mCategories != null) { + out.writeInt(mCategories.size()); + for (String category : mCategories) { + out.writeString(category); + } + } else { + out.writeInt(0); + } + + out.writeBundle(mExtras); + } + + public static final Parcelable.Creator<Intent> CREATOR + = new Parcelable.Creator<Intent>() { + public Intent createFromParcel(Parcel in) { + return new Intent(in); + } + public Intent[] newArray(int size) { + return new Intent[size]; + } + }; + + private Intent(Parcel in) { + readFromParcel(in); + } + + public void readFromParcel(Parcel in) { + mAction = in.readString(); + mData = Uri.CREATOR.createFromParcel(in); + mType = in.readString(); + mFlags = in.readInt(); + mComponent = ComponentName.readFromParcel(in); + + int N = in.readInt(); + if (N > 0) { + mCategories = new HashSet<String>(); + int i; + for (i=0; i<N; i++) { + mCategories.add(in.readString()); + } + } else { + mCategories = null; + } + + mExtras = in.readBundle(); + } + + /** + * Parses the "intent" element (and its children) from XML and instantiates + * an Intent object. The given XML parser should be located at the tag + * where parsing should start (often named "intent"), from which the + * basic action, data, type, and package and class name will be + * retrieved. The function will then parse in to any child elements, + * looking for <category android:name="xxx"> tags to add categories and + * <extra android:name="xxx" android:value="yyy"> to attach extra data + * to the intent. + * + * @param resources The Resources to use when inflating resources. + * @param parser The XML parser pointing at an "intent" tag. + * @param attrs The AttributeSet interface for retrieving extended + * attribute data at the current <var>parser</var> location. + * @return An Intent object matching the XML data. + * @throws XmlPullParserException If there was an XML parsing error. + * @throws IOException If there was an I/O error. + */ + public static Intent parseIntent(Resources resources, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + Intent intent = new Intent(); + + TypedArray sa = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.Intent); + + intent.setAction(sa.getString(com.android.internal.R.styleable.Intent_action)); + + String data = sa.getString(com.android.internal.R.styleable.Intent_data); + String mimeType = sa.getString(com.android.internal.R.styleable.Intent_mimeType); + intent.setDataAndType(data != null ? Uri.parse(data) : null, mimeType); + + String packageName = sa.getString(com.android.internal.R.styleable.Intent_targetPackage); + String className = sa.getString(com.android.internal.R.styleable.Intent_targetClass); + if (packageName != null && className != null) { + intent.setComponent(new ComponentName(packageName, className)); + } + + sa.recycle(); + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String nodeName = parser.getName(); + if (nodeName.equals("category")) { + sa = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.IntentCategory); + String cat = sa.getString(com.android.internal.R.styleable.IntentCategory_name); + sa.recycle(); + + if (cat != null) { + intent.addCategory(cat); + } + XmlUtils.skipCurrentTag(parser); + + } else if (nodeName.equals("extra")) { + if (intent.mExtras == null) { + intent.mExtras = new Bundle(); + } + resources.parseBundleExtra("extra", attrs, intent.mExtras); + XmlUtils.skipCurrentTag(parser); + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + return intent; + } +} diff --git a/core/java/android/content/IntentFilter.aidl b/core/java/android/content/IntentFilter.aidl new file mode 100644 index 0000000..a9bcd5e --- /dev/null +++ b/core/java/android/content/IntentFilter.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 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 IntentFilter; diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java new file mode 100644 index 0000000..e81bc86 --- /dev/null +++ b/core/java/android/content/IntentFilter.java @@ -0,0 +1,1408 @@ +/* + * Copyright (C) 2006 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 org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Set; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PatternMatcher; +import android.util.AndroidException; +import android.util.Config; +import android.util.Log; +import android.util.Printer; +import com.android.internal.util.XmlUtils; + +/** + * Structured description of Intent values to be matched. An IntentFilter can + * match against actions, categories, and data (either via its type, scheme, + * and/or path) in an Intent. It also includes a "priority" value which is + * used to order multiple matching filters. + * + * <p>IntentFilter objects are often created in XML as part of a package's + * {@link android.R.styleable#AndroidManifest AndroidManifest.xml} file, + * using {@link android.R.styleable#AndroidManifestIntentFilter intent-filter} + * tags. + * + * <p>There are three Intent characteristics you can filter on: the + * <em>action</em>, <em>data</em>, and <em>categories</em>. For each of these + * characteristics you can provide + * multiple possible matching values (via {@link #addAction}, + * {@link #addDataType}, {@link #addDataScheme} {@link #addDataAuthority}, + * {@link #addDataPath}, and {@link #addCategory}, respectively). + * For actions, the field + * will not be tested if no values have been given (treating it as a wildcard); + * if no data characteristics are specified, however, then the filter will + * only match intents that contain no data. + * + * <p>The data characteristic is + * itself divided into three attributes: type, scheme, authority, and path. + * Any that are + * specified must match the contents of the Intent. If you specify a scheme + * but no type, only Intent that does not have a type (such as mailto:) will + * match; a content: URI will never match because they always have a MIME type + * that is supplied by their content provider. Specifying a type with no scheme + * has somewhat special meaning: it will match either an Intent with no URI + * field, or an Intent with a content: or file: URI. If you specify neither, + * then only an Intent with no data or type will match. To specify an authority, + * you must also specify one or more schemes that it is associated with. + * To specify a path, you also must specify both one or more authorities and + * one or more schemes it is associated with. + * + * <p>A match is based on the following rules. Note that + * for an IntentFilter to match an Intent, three conditions must hold: + * the <strong>action</strong> and <strong>category</strong> must match, and + * the data (both the <strong>data type</strong> and + * <strong>data scheme+authority+path</strong> if specified) must match. + * + * <p><strong>Action</strong> matches if any of the given values match the + * Intent action, <em>or</em> if no actions were specified in the filter. + * + * <p><strong>Data Type</strong> matches if any of the given values match the + * Intent type. The Intent + * type is determined by calling {@link Intent#resolveType}. A wildcard can be + * used for the MIME sub-type, in both the Intent and IntentFilter, so that the + * type "audio/*" will match "audio/mpeg", "audio/aiff", "audio/*", etc. + * + * <p><strong>Data Scheme</strong> matches if any of the given values match the + * Intent data's scheme. + * The Intent scheme is determined by calling {@link Intent#getData} + * and {@link android.net.Uri#getScheme} on that URI. + * + * <p><strong>Data Authority</strong> matches if any of the given values match + * the Intent's data authority <em>and</em> one of the data scheme's in the filter + * has matched the Intent, <em>or</em> no authories were supplied in the filter. + * The Intent authority is determined by calling + * {@link Intent#getData} and {@link android.net.Uri#getAuthority} on that URI. + * + * <p><strong>Data Path</strong> matches if any of the given values match the + * Intent's data path <em>and</em> both a scheme and authority in the filter + * has matched against the Intent, <em>or</em> no paths were supplied in the + * filter. The Intent authority is determined by calling + * {@link Intent#getData} and {@link android.net.Uri#getPath} on that URI. + * + * <p><strong>Categories</strong> match if <em>all</em> of the categories in + * the Intent match categories given in the filter. Extra categories in the + * filter that are not in the Intent will not cause the match to fail. Note + * that unlike the action, an IntentFilter with no categories + * will only match an Intent that does not have any categories. + */ +public class IntentFilter implements Parcelable { + private static final String SGLOB_STR = "sglob"; + private static final String PREFIX_STR = "prefix"; + private static final String LITERAL_STR = "literal"; + private static final String PATH_STR = "path"; + private static final String PORT_STR = "port"; + private static final String HOST_STR = "host"; + private static final String AUTH_STR = "auth"; + private static final String SCHEME_STR = "scheme"; + private static final String TYPE_STR = "type"; + private static final String CAT_STR = "cat"; + private static final String NAME_STR = "name"; + private static final String ACTION_STR = "action"; + + /** + * The filter {@link #setPriority} value at which system high-priority + * receivers are placed; that is, receivers that should execute before + * application code. Applications should never use filters with this or + * higher priorities. + * + * @see #setPriority + */ + public static final int SYSTEM_HIGH_PRIORITY = 1000; + + /** + * The filter {@link #setPriority} value at which system low-priority + * receivers are placed; that is, receivers that should execute after + * application code. Applications should never use filters with this or + * lower priorities. + * + * @see #setPriority + */ + public static final int SYSTEM_LOW_PRIORITY = -1000; + + /** + * The part of a match constant that describes the category of match + * that occurred. May be either {@link #MATCH_CATEGORY_EMPTY}, + * {@link #MATCH_CATEGORY_SCHEME}, {@link #MATCH_CATEGORY_HOST}, + * {@link #MATCH_CATEGORY_PORT}, + * {@link #MATCH_CATEGORY_PATH}, or {@link #MATCH_CATEGORY_TYPE}. Higher + * values indicate a better match. + */ + public static final int MATCH_CATEGORY_MASK = 0xfff0000; + + /** + * The part of a match constant that applies a quality adjustment to the + * basic category of match. The value {@link #MATCH_ADJUSTMENT_NORMAL} + * is no adjustment; higher numbers than that improve the quality, while + * lower numbers reduce it. + */ + public static final int MATCH_ADJUSTMENT_MASK = 0x000ffff; + + /** + * Quality adjustment applied to the category of match that signifies + * the default, base value; higher numbers improve the quality while + * lower numbers reduce it. + */ + public static final int MATCH_ADJUSTMENT_NORMAL = 0x8000; + + /** + * The filter matched an intent that had no data specified. + */ + public static final int MATCH_CATEGORY_EMPTY = 0x0100000; + /** + * The filter matched an intent with the same data URI scheme. + */ + public static final int MATCH_CATEGORY_SCHEME = 0x0200000; + /** + * The filter matched an intent with the same data URI scheme and + * authority host. + */ + public static final int MATCH_CATEGORY_HOST = 0x0300000; + /** + * The filter matched an intent with the same data URI scheme and + * authority host and port. + */ + public static final int MATCH_CATEGORY_PORT = 0x0400000; + /** + * The filter matched an intent with the same data URI scheme, + * authority, and path. + */ + public static final int MATCH_CATEGORY_PATH = 0x0500000; + /** + * The filter matched an intent with the same data MIME type. + */ + public static final int MATCH_CATEGORY_TYPE = 0x0600000; + + /** + * The filter didn't match due to different MIME types. + */ + public static final int NO_MATCH_TYPE = -1; + /** + * The filter didn't match due to different data URIs. + */ + public static final int NO_MATCH_DATA = -2; + /** + * The filter didn't match due to different actions. + */ + public static final int NO_MATCH_ACTION = -3; + /** + * The filter didn't match because it required one or more categories + * that were not in the Intent. + */ + public static final int NO_MATCH_CATEGORY = -4; + + private int mPriority; + private final ArrayList<String> mActions; + private ArrayList<String> mCategories = null; + private ArrayList<String> mDataSchemes = null; + private ArrayList<AuthorityEntry> mDataAuthorities = null; + private ArrayList<PatternMatcher> mDataPaths = null; + private ArrayList<String> mDataTypes = null; + private boolean mHasPartialTypes = false; + + // These functions are the start of more optimized code for managing + // the string sets... not yet implemented. + + private static int findStringInSet(String[] set, String string, + int[] lengths, int lenPos) { + if (set == null) return -1; + final int N = lengths[lenPos]; + for (int i=0; i<N; i++) { + if (set[i].equals(string)) return i; + } + return -1; + } + + private static String[] addStringToSet(String[] set, String string, + int[] lengths, int lenPos) { + if (findStringInSet(set, string, lengths, lenPos) >= 0) return set; + if (set == null) { + set = new String[2]; + set[0] = string; + lengths[lenPos] = 1; + return set; + } + final int N = lengths[lenPos]; + if (N < set.length) { + set[N] = string; + lengths[lenPos] = N+1; + return set; + } + + String[] newSet = new String[(N*3)/2 + 2]; + System.arraycopy(set, 0, newSet, 0, N); + set = newSet; + set[N] = string; + lengths[lenPos] = N+1; + return set; + } + + private static String[] removeStringFromSet(String[] set, String string, + int[] lengths, int lenPos) { + int pos = findStringInSet(set, string, lengths, lenPos); + if (pos < 0) return set; + final int N = lengths[lenPos]; + if (N > (set.length/4)) { + int copyLen = N-(pos+1); + if (copyLen > 0) { + System.arraycopy(set, pos+1, set, pos, copyLen); + } + set[N-1] = null; + lengths[lenPos] = N-1; + return set; + } + + String[] newSet = new String[set.length/3]; + if (pos > 0) System.arraycopy(set, 0, newSet, 0, pos); + if ((pos+1) < N) System.arraycopy(set, pos+1, newSet, pos, N-(pos+1)); + return newSet; + } + + /** + * This exception is thrown when a given MIME type does not have a valid + * syntax. + */ + public static class MalformedMimeTypeException extends AndroidException { + public MalformedMimeTypeException() { + } + + public MalformedMimeTypeException(String name) { + super(name); + } + }; + + /** + * Create a new IntentFilter instance with a specified action and MIME + * type, where you know the MIME type is correctly formatted. This catches + * the {@link MalformedMimeTypeException} exception that the constructor + * can call and turns it into a runtime exception. + * + * @param action The action to match, i.e. Intent.ACTION_VIEW. + * @param dataType The type to match, i.e. "vnd.android.cursor.dir/person". + * + * @return A new IntentFilter for the given action and type. + * + * @see #IntentFilter(String, String) + */ + public static IntentFilter create(String action, String dataType) { + try { + return new IntentFilter(action, dataType); + } catch (MalformedMimeTypeException e) { + throw new RuntimeException("Bad MIME type", e); + } + } + + /** + * New empty IntentFilter. + */ + public IntentFilter() { + mPriority = 0; + mActions = new ArrayList<String>(); + } + + /** + * New IntentFilter that matches a single action with no data. If + * no data characteristics are subsequently specified, then the + * filter will only match intents that contain no data. + * + * @param action The action to match, i.e. Intent.ACTION_MAIN. + */ + public IntentFilter(String action) { + mPriority = 0; + mActions = new ArrayList<String>(); + addAction(action); + } + + /** + * New IntentFilter that matches a single action and data type. + * + * <p>Throws {@link MalformedMimeTypeException} if the given MIME type is + * not syntactically correct. + * + * @param action The action to match, i.e. Intent.ACTION_VIEW. + * @param dataType The type to match, i.e. "vnd.android.cursor.dir/person". + * + */ + public IntentFilter(String action, String dataType) + throws MalformedMimeTypeException { + mPriority = 0; + mActions = new ArrayList<String>(); + addDataType(dataType); + } + + /** + * New IntentFilter containing a copy of an existing filter. + * + * @param o The original filter to copy. + */ + public IntentFilter(IntentFilter o) { + mPriority = o.mPriority; + mActions = new ArrayList<String>(o.mActions); + if (o.mCategories != null) { + mCategories = new ArrayList<String>(o.mCategories); + } + if (o.mDataTypes != null) { + mDataTypes = new ArrayList<String>(o.mDataTypes); + } + if (o.mDataSchemes != null) { + mDataSchemes = new ArrayList<String>(o.mDataSchemes); + } + if (o.mDataAuthorities != null) { + mDataAuthorities = new ArrayList<AuthorityEntry>(o.mDataAuthorities); + } + if (o.mDataPaths != null) { + mDataPaths = new ArrayList<PatternMatcher>(o.mDataPaths); + } + mHasPartialTypes = o.mHasPartialTypes; + } + + /** + * Modify priority of this filter. The default priority is 0. Positive + * values will be before the default, lower values will be after it. + * Applications must use a value that is larger than + * {@link #SYSTEM_LOW_PRIORITY} and smaller than + * {@link #SYSTEM_HIGH_PRIORITY} . + * + * @param priority The new priority value. + * + * @see #getPriority + * @see #SYSTEM_LOW_PRIORITY + * @see #SYSTEM_HIGH_PRIORITY + */ + public final void setPriority(int priority) { + mPriority = priority; + } + + /** + * Return the priority of this filter. + * + * @return The priority of the filter. + * + * @see #setPriority + */ + public final int getPriority() { + return mPriority; + } + + /** + * Add a new Intent action to match against. If any actions are included + * in the filter, then an Intent's action must be one of those values for + * it to match. If no actions are included, the Intent action is ignored. + * + * @param action Name of the action to match, i.e. Intent.ACTION_VIEW. + */ + public final void addAction(String action) { + if (!mActions.contains(action)) { + mActions.add(action.intern()); + } + } + + /** + * Return the number of actions in the filter. + */ + public final int countActions() { + return mActions.size(); + } + + /** + * Return an action in the filter. + */ + public final String getAction(int index) { + return mActions.get(index); + } + + /** + * Is the given action included in the filter? Note that if the filter + * does not include any actions, false will <em>always</em> be returned. + * + * @param action The action to look for. + * + * @return True if the action is explicitly mentioned in the filter. + */ + public final boolean hasAction(String action) { + return mActions.contains(action); + } + + /** + * Match this filter against an Intent's action. If the filter does not + * specify any actions, the match will always fail. + * + * @param action The desired action to look for. + * + * @return True if the action is listed in the filter or the filter does + * not specify any actions. + */ + public final boolean matchAction(String action) { + if (action == null || mActions == null || mActions.size() == 0) { + return false; + } + return mActions.contains(action); + } + + /** + * Return an iterator over the filter's actions. If there are no actions, + * returns null. + */ + public final Iterator<String> actionsIterator() { + return mActions != null ? mActions.iterator() : null; + } + + /** + * Add a new Intent data type to match against. If any types are + * included in the filter, then an Intent's data must be <em>either</em> + * one of these types <em>or</em> a matching scheme. If no data types + * are included, then an Intent will only match if it specifies no data. + * + * <p>Throws {@link MalformedMimeTypeException} if the given MIME type is + * not syntactically correct. + * + * @param type Name of the data type to match, i.e. "vnd.android.cursor.dir/person". + * + * @see #matchData + */ + public final void addDataType(String type) + throws MalformedMimeTypeException { + final int slashpos = type.indexOf('/'); + final int typelen = type.length(); + if (slashpos > 0 && typelen >= slashpos+2) { + if (mDataTypes == null) mDataTypes = new ArrayList<String>(); + if (typelen == slashpos+2 && type.charAt(slashpos+1) == '*') { + String str = type.substring(0, slashpos); + if (!mDataTypes.contains(str)) { + mDataTypes.add(str.intern()); + } + mHasPartialTypes = true; + } else { + if (!mDataTypes.contains(type)) { + mDataTypes.add(type.intern()); + } + } + return; + } + + throw new MalformedMimeTypeException(type); + } + + /** + * Is the given data type included in the filter? Note that if the filter + * does not include any type, false will <em>always</em> be returned. + * + * @param type The data type to look for. + * + * @return True if the type is explicitly mentioned in the filter. + */ + public final boolean hasDataType(String type) { + return mDataTypes != null && findMimeType(type); + } + + /** + * Return the number of data types in the filter. + */ + public final int countDataTypes() { + return mDataTypes != null ? mDataTypes.size() : 0; + } + + /** + * Return a data type in the filter. + */ + public final String getDataType(int index) { + return mDataTypes.get(index); + } + + /** + * Return an iterator over the filter's data types. + */ + public final Iterator<String> typesIterator() { + return mDataTypes != null ? mDataTypes.iterator() : null; + } + + /** + * Add a new Intent data scheme to match against. If any schemes are + * included in the filter, then an Intent's data must be <em>either</em> + * one of these schemes <em>or</em> a matching data type. If no schemes + * are included, then an Intent will match only if it includes no data. + * + * @param scheme Name of the scheme to match, i.e. "http". + * + * @see #matchData + */ + public final void addDataScheme(String scheme) { + if (mDataSchemes == null) mDataSchemes = new ArrayList<String>(); + if (!mDataSchemes.contains(scheme)) { + mDataSchemes.add(scheme.intern()); + } + } + + /** + * Return the number of data schemes in the filter. + */ + public final int countDataSchemes() { + return mDataSchemes != null ? mDataSchemes.size() : 0; + } + + /** + * Return a data scheme in the filter. + */ + public final String getDataScheme(int index) { + return mDataSchemes.get(index); + } + + /** + * Is the given data scheme included in the filter? Note that if the + * filter does not include any scheme, false will <em>always</em> be + * returned. + * + * @param scheme The data scheme to look for. + * + * @return True if the scheme is explicitly mentioned in the filter. + */ + public final boolean hasDataScheme(String scheme) { + return mDataSchemes != null && mDataSchemes.contains(scheme); + } + + /** + * Return an iterator over the filter's data schemes. + */ + public final Iterator<String> schemesIterator() { + return mDataSchemes != null ? mDataSchemes.iterator() : null; + } + + /** + * This is an entry for a single authority in the Iterator returned by + * {@link #authoritiesIterator()}. + */ + public final static class AuthorityEntry { + private final String mOrigHost; + private final String mHost; + private final boolean mWild; + private final int mPort; + + public AuthorityEntry(String host, String port) { + mOrigHost = host; + mWild = host.length() > 0 && host.charAt(0) == '*'; + mHost = mWild ? host.substring(1).intern() : host; + mPort = port != null ? Integer.parseInt(port) : -1; + } + + AuthorityEntry(Parcel src) { + mOrigHost = src.readString(); + mHost = src.readString(); + mWild = src.readInt() != 0; + mPort = src.readInt(); + } + + void writeToParcel(Parcel dest) { + dest.writeString(mOrigHost); + dest.writeString(mHost); + dest.writeInt(mWild ? 1 : 0); + dest.writeInt(mPort); + } + + public String getHost() { + return mOrigHost; + } + + public int getPort() { + return mPort; + } + + public int match(Uri data) { + String host = data.getHost(); + if (host == null) { + return NO_MATCH_DATA; + } + if (Config.LOGV) Log.v("IntentFilter", + "Match host " + host + ": " + mHost); + if (mWild) { + if (host.length() < mHost.length()) { + return NO_MATCH_DATA; + } + host = host.substring(host.length()-mHost.length()); + } + if (host.compareToIgnoreCase(mHost) != 0) { + return NO_MATCH_DATA; + } + if (mPort >= 0) { + if (mPort != data.getPort()) { + return NO_MATCH_DATA; + } + return MATCH_CATEGORY_PORT; + } + return MATCH_CATEGORY_HOST; + } + }; + + /** + * Add a new Intent data authority to match against. The filter must + * include one or more schemes (via {@link #addDataScheme}) for the + * authority to be considered. If any authorities are + * included in the filter, then an Intent's data must match one of + * them. If no authorities are included, then only the scheme must match. + * + * @param host The host part of the authority to match. May start with a + * single '*' to wildcard the front of the host name. + * @param port Optional port part of the authority to match. If null, any + * port is allowed. + * + * @see #matchData + * @see #addDataScheme + */ + public final void addDataAuthority(String host, String port) { + if (mDataAuthorities == null) mDataAuthorities = + new ArrayList<AuthorityEntry>(); + if (port != null) port = port.intern(); + mDataAuthorities.add(new AuthorityEntry(host.intern(), port)); + } + + /** + * Return the number of data authorities in the filter. + */ + public final int countDataAuthorities() { + return mDataAuthorities != null ? mDataAuthorities.size() : 0; + } + + /** + * Return a data authority in the filter. + */ + public final AuthorityEntry getDataAuthority(int index) { + return mDataAuthorities.get(index); + } + + /** + * Is the given data authority included in the filter? Note that if the + * filter does not include any authorities, false will <em>always</em> be + * returned. + * + * @param data The data whose authority is being looked for. + * + * @return Returns true if the data string matches an authority listed in the + * filter. + */ + public final boolean hasDataAuthority(Uri data) { + return matchDataAuthority(data) >= 0; + } + + /** + * Return an iterator over the filter's data authorities. + */ + public final Iterator<AuthorityEntry> authoritiesIterator() { + return mDataAuthorities != null ? mDataAuthorities.iterator() : null; + } + + /** + * Add a new Intent data oath to match against. The filter must + * include one or more schemes (via {@link #addDataScheme}) <em>and</em> + * one or more authorities (via {@link #addDataAuthority}) for the + * path to be considered. If any paths are + * included in the filter, then an Intent's data must match one of + * them. If no paths are included, then only the scheme/authority must + * match. + * + * <p>The path given here can either be a literal that must directly + * match or match against a prefix, or it can be a simple globbing pattern. + * If the latter, you can use '*' anywhere in the pattern to match zero + * or more instances of the previous character, '.' as a wildcard to match + * any character, and '\' to escape the next character. + * + * @param path Either a raw string that must exactly match the file + * path, or a simple pattern, depending on <var>type</var>. + * @param type Determines how <var>path</var> will be compared to + * determine a match: either {@link PatternMatcher#PATTERN_LITERAL}, + * {@link PatternMatcher#PATTERN_PREFIX}, or + * {@link PatternMatcher#PATTERN_SIMPLE_GLOB}. + * + * @see #matchData + * @see #addDataScheme + * @see #addDataAuthority + */ + public final void addDataPath(String path, int type) { + if (mDataPaths == null) mDataPaths = new ArrayList<PatternMatcher>(); + mDataPaths.add(new PatternMatcher(path.intern(), type)); + } + + /** + * Return the number of data paths in the filter. + */ + public final int countDataPaths() { + return mDataPaths != null ? mDataPaths.size() : 0; + } + + /** + * Return a data path in the filter. + */ + public final PatternMatcher getDataPath(int index) { + return mDataPaths.get(index); + } + + /** + * Is the given data path included in the filter? Note that if the + * filter does not include any paths, false will <em>always</em> be + * returned. + * + * @param data The data path to look for. This is without the scheme + * prefix. + * + * @return True if the data string matches a path listed in the + * filter. + */ + public final boolean hasDataPath(String data) { + if (mDataPaths == null) { + return false; + } + Iterator<PatternMatcher> i = mDataPaths.iterator(); + while (i.hasNext()) { + final PatternMatcher pe = i.next(); + if (pe.match(data)) { + return true; + } + } + return false; + } + + /** + * Return an iterator over the filter's data paths. + */ + public final Iterator<PatternMatcher> pathsIterator() { + return mDataPaths != null ? mDataPaths.iterator() : null; + } + + /** + * Match this intent filter against the given Intent data. This ignores + * the data scheme -- unlike {@link #matchData}, the authority will match + * regardless of whether there is a matching scheme. + * + * @param data The data whose authority is being looked for. + * + * @return Returns either {@link #MATCH_CATEGORY_HOST}, + * {@link #MATCH_CATEGORY_PORT}, {@link #NO_MATCH_DATA}. + */ + public final int matchDataAuthority(Uri data) { + if (mDataAuthorities == null) { + return NO_MATCH_DATA; + } + Iterator<AuthorityEntry> i = mDataAuthorities.iterator(); + while (i.hasNext()) { + final AuthorityEntry ae = i.next(); + int match = ae.match(data); + if (match >= 0) { + return match; + } + } + return NO_MATCH_DATA; + } + + /** + * Match this filter against an Intent's data (type, scheme and path). If + * the filter does not specify any types and does not specify any + * schemes/paths, the match will only succeed if the intent does not + * also specify a type or data. + * + * <p>Note that to match against an authority, you must also specify a base + * scheme the authority is in. To match against a data path, both a scheme + * and authority must be specified. If the filter does not specify any + * types or schemes that it matches against, it is considered to be empty + * (any authority or data path given is ignored, as if it were empty as + * well). + * + * @param type The desired data type to look for, as returned by + * Intent.resolveType(). + * @param scheme The desired data scheme to look for, as returned by + * Intent.getScheme(). + * @param data The full data string to match against, as supplied in + * Intent.data. + * + * @return Returns either a valid match constant (a combination of + * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}), + * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match + * or {@link #NO_MATCH_DATA} if the scheme/path didn't match. + * + * @see #match + */ + public final int matchData(String type, String scheme, Uri data) { + final ArrayList<String> types = mDataTypes; + final ArrayList<String> schemes = mDataSchemes; + final ArrayList<AuthorityEntry> authorities = mDataAuthorities; + final ArrayList<PatternMatcher> paths = mDataPaths; + + int match = MATCH_CATEGORY_EMPTY; + + if (types == null && schemes == null) { + return ((type == null && data == null) + ? (MATCH_CATEGORY_EMPTY+MATCH_ADJUSTMENT_NORMAL) : NO_MATCH_DATA); + } + + if (schemes != null) { + if (schemes.contains(scheme != null ? scheme : "")) { + match = MATCH_CATEGORY_SCHEME; + } else { + return NO_MATCH_DATA; + } + + if (authorities != null) { + int authMatch = matchDataAuthority(data); + if (authMatch >= 0) { + if (paths == null) { + match = authMatch; + } else if (hasDataPath(data.getPath())) { + match = MATCH_CATEGORY_PATH; + } else { + return NO_MATCH_DATA; + } + } else { + return NO_MATCH_DATA; + } + } + } else { + // Special case: match either an Intent with no data URI, + // or with a scheme: URI. This is to give a convenience for + // the common case where you want to deal with data in a + // content provider, which is done by type, and we don't want + // to force everyone to say they handle content: or file: URIs. + if (scheme != null && !"".equals(scheme) + && !"content".equals(scheme) + && !"file".equals(scheme)) { + return NO_MATCH_DATA; + } + } + + if (types != null) { + if (findMimeType(type)) { + match = MATCH_CATEGORY_TYPE; + } else { + return NO_MATCH_TYPE; + } + } else { + // If no MIME types are specified, then we will only match against + // an Intent that does not have a MIME type. + if (type != null) { + return NO_MATCH_TYPE; + } + } + + return match + MATCH_ADJUSTMENT_NORMAL; + } + + /** + * Add a new Intent category to match against. The semantics of + * categories is the opposite of actions -- an Intent includes the + * categories that it requires, all of which must be included in the + * filter in order to match. In other words, adding a category to the + * filter has no impact on matching unless that category is specified in + * the intent. + * + * @param category Name of category to match, i.e. Intent.CATEGORY_EMBED. + */ + public final void addCategory(String category) { + if (mCategories == null) mCategories = new ArrayList<String>(); + if (!mCategories.contains(category)) { + mCategories.add(category.intern()); + } + } + + /** + * Return the number of categories in the filter. + */ + public final int countCategories() { + return mCategories != null ? mCategories.size() : 0; + } + + /** + * Return a category in the filter. + */ + public final String getCategory(int index) { + return mCategories.get(index); + } + + /** + * Is the given category included in the filter? + * + * @param category The category that the filter supports. + * + * @return True if the category is explicitly mentioned in the filter. + */ + public final boolean hasCategory(String category) { + return mCategories != null && mCategories.contains(category); + } + + /** + * Return an iterator over the filter's categories. + */ + public final Iterator<String> categoriesIterator() { + return mCategories != null ? mCategories.iterator() : null; + } + + /** + * Match this filter against an Intent's categories. Each category in + * the Intent must be specified by the filter; if any are not in the + * filter, the match fails. + * + * @param categories The categories included in the intent, as returned by + * Intent.getCategories(). + * + * @return If all categories match (success), null; else the name of the + * first category that didn't match. + */ + public final String matchCategories(Set<String> categories) { + if (categories == null) { + return null; + } + + Iterator<String> it = categories.iterator(); + + if (mCategories == null) { + return it.hasNext() ? it.next() : null; + } + + while (it.hasNext()) { + final String category = it.next(); + if (!mCategories.contains(category)) { + return category; + } + } + + return null; + } + + /** + * Test whether this filter matches the given <var>intent</var>. + * + * @param intent The Intent to compare against. + * @param resolve If true, the intent's type will be resolved by calling + * Intent.resolveType(); otherwise a simple match against + * Intent.type will be performed. + * @param logTag Tag to use in debugging messages. + * + * @return Returns either a valid match constant (a combination of + * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}), + * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match, + * {@link #NO_MATCH_DATA} if the scheme/path didn't match, + * {@link #NO_MATCH_ACTION if the action didn't match, or + * {@link #NO_MATCH_CATEGORY} if one or more categories didn't match. + * + * @return How well the filter matches. Negative if it doesn't match, + * zero or positive positive value if it does with a higher + * value representing a better match. + * + * @see #match(String, String, String, android.net.Uri , Set, String) + */ + public final int match(ContentResolver resolver, Intent intent, + boolean resolve, String logTag) { + String type = resolve ? intent.resolveType(resolver) : intent.getType(); + return match(intent.getAction(), type, intent.getScheme(), + intent.getData(), intent.getCategories(), logTag); + } + + /** + * Test whether this filter matches the given intent data. A match is + * only successful if the actions and categories in the Intent match + * against the filter, as described in {@link IntentFilter}; in that case, + * the match result returned will be as per {@link #matchData}. + * + * @param action The intent action to match against (Intent.getAction). + * @param type The intent type to match against (Intent.resolveType()). + * @param scheme The data scheme to match against (Intent.getScheme()). + * @param data The data URI to match against (Intent.getData()). + * @param categories The categories to match against + * (Intent.getCategories()). + * @param logTag Tag to use in debugging messages. + * + * @return Returns either a valid match constant (a combination of + * {@link #MATCH_CATEGORY_MASK} and {@link #MATCH_ADJUSTMENT_MASK}), + * or one of the error codes {@link #NO_MATCH_TYPE} if the type didn't match, + * {@link #NO_MATCH_DATA} if the scheme/path didn't match, + * {@link #NO_MATCH_ACTION if the action didn't match, or + * {@link #NO_MATCH_CATEGORY} if one or more categories didn't match. + * + * @see #matchData + * @see Intent#getAction + * @see Intent#resolveType + * @see Intent#getScheme + * @see Intent#getData + * @see Intent#getCategories + */ + public final int match(String action, String type, String scheme, + Uri data, Set<String> categories, String logTag) { + if (action != null && !matchAction(action)) { + if (Config.LOGV) Log.v( + logTag, "No matching action " + action + " for " + this); + return NO_MATCH_ACTION; + } + + int dataMatch = matchData(type, scheme, data); + if (dataMatch < 0) { + if (Config.LOGV) { + if (dataMatch == NO_MATCH_TYPE) { + Log.v(logTag, "No matching type " + type + + " for " + this); + } + if (dataMatch == NO_MATCH_DATA) { + Log.v(logTag, "No matching scheme/path " + data + + " for " + this); + } + } + return dataMatch; + } + + String categoryMatch = matchCategories(categories); + if (categoryMatch != null) { + if (Config.LOGV) Log.v( + logTag, "No matching category " + + categoryMatch + " for " + this); + return NO_MATCH_CATEGORY; + } + + // It would be nice to treat container activities as more + // important than ones that can be embedded, but this is not the way... + if (false) { + if (categories != null) { + dataMatch -= mCategories.size() - categories.size(); + } + } + + return dataMatch; + } + + /** + * Write the contents of the IntentFilter as an XML stream. + */ + public void writeToXml(XmlSerializer serializer) throws IOException { + int N = countActions(); + for (int i=0; i<N; i++) { + serializer.startTag(null, ACTION_STR); + serializer.attribute(null, NAME_STR, mActions.get(i)); + serializer.endTag(null, ACTION_STR); + } + N = countCategories(); + for (int i=0; i<N; i++) { + serializer.startTag(null, CAT_STR); + serializer.attribute(null, NAME_STR, mCategories.get(i)); + serializer.endTag(null, CAT_STR); + } + N = countDataTypes(); + for (int i=0; i<N; i++) { + serializer.startTag(null, TYPE_STR); + String type = mDataTypes.get(i); + if (type.indexOf('/') < 0) type = type + "/*"; + serializer.attribute(null, NAME_STR, type); + serializer.endTag(null, TYPE_STR); + } + N = countDataSchemes(); + for (int i=0; i<N; i++) { + serializer.startTag(null, SCHEME_STR); + serializer.attribute(null, NAME_STR, mDataSchemes.get(i)); + serializer.endTag(null, SCHEME_STR); + } + N = countDataAuthorities(); + for (int i=0; i<N; i++) { + serializer.startTag(null, AUTH_STR); + AuthorityEntry ae = mDataAuthorities.get(i); + serializer.attribute(null, HOST_STR, ae.getHost()); + if (ae.getPort() >= 0) { + serializer.attribute(null, PORT_STR, Integer.toString(ae.getPort())); + } + serializer.endTag(null, AUTH_STR); + } + N = countDataPaths(); + for (int i=0; i<N; i++) { + serializer.startTag(null, PATH_STR); + PatternMatcher pe = mDataPaths.get(i); + switch (pe.getType()) { + case PatternMatcher.PATTERN_LITERAL: + serializer.attribute(null, LITERAL_STR, pe.getPath()); + break; + case PatternMatcher.PATTERN_PREFIX: + serializer.attribute(null, PREFIX_STR, pe.getPath()); + break; + case PatternMatcher.PATTERN_SIMPLE_GLOB: + serializer.attribute(null, SGLOB_STR, pe.getPath()); + break; + } + serializer.endTag(null, PATH_STR); + } + } + + public void readFromXml(XmlPullParser parser) throws XmlPullParserException, + IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG + || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals(ACTION_STR)) { + String name = parser.getAttributeValue(null, NAME_STR); + if (name != null) { + addAction(name); + } + } else if (tagName.equals(CAT_STR)) { + String name = parser.getAttributeValue(null, NAME_STR); + if (name != null) { + addCategory(name); + } + } else if (tagName.equals(TYPE_STR)) { + String name = parser.getAttributeValue(null, NAME_STR); + if (name != null) { + try { + addDataType(name); + } catch (MalformedMimeTypeException e) { + } + } + } else if (tagName.equals(SCHEME_STR)) { + String name = parser.getAttributeValue(null, NAME_STR); + if (name != null) { + addDataScheme(name); + } + } else if (tagName.equals(AUTH_STR)) { + String host = parser.getAttributeValue(null, HOST_STR); + String port = parser.getAttributeValue(null, PORT_STR); + if (host != null) { + addDataAuthority(host, port); + } + } else if (tagName.equals(PATH_STR)) { + String path = parser.getAttributeValue(null, LITERAL_STR); + if (path != null) { + addDataPath(path, PatternMatcher.PATTERN_LITERAL); + } else if ((path=parser.getAttributeValue(null, PREFIX_STR)) != null) { + addDataPath(path, PatternMatcher.PATTERN_PREFIX); + } else if ((path=parser.getAttributeValue(null, SGLOB_STR)) != null) { + addDataPath(path, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + } else { + Log.w("IntentFilter", "Unknown tag parsing IntentFilter: " + tagName); + } + XmlUtils.skipCurrentTag(parser); + } + } + + public void dump(Printer du, String prefix) { + if (mActions.size() > 0) { + Iterator<String> it = mActions.iterator(); + while (it.hasNext()) { + du.println(prefix + "Action: \"" + it.next() + "\""); + } + } + if (mCategories != null) { + Iterator<String> it = mCategories.iterator(); + while (it.hasNext()) { + du.println(prefix + "Category: \"" + it.next() + "\""); + } + } + if (mDataSchemes != null) { + Iterator<String> it = mDataSchemes.iterator(); + while (it.hasNext()) { + du.println(prefix + "Data Scheme: \"" + it.next() + "\""); + } + } + if (mDataAuthorities != null) { + Iterator<AuthorityEntry> it = mDataAuthorities.iterator(); + while (it.hasNext()) { + AuthorityEntry ae = it.next(); + du.println(prefix + "Data Authority: \"" + ae.mHost + "\":" + + ae.mPort + (ae.mWild ? " WILD" : "")); + } + } + if (mDataPaths != null) { + Iterator<PatternMatcher> it = mDataPaths.iterator(); + while (it.hasNext()) { + PatternMatcher pe = it.next(); + du.println(prefix + "Data Path: \"" + pe + "\""); + } + } + if (mDataTypes != null) { + Iterator<String> it = mDataTypes.iterator(); + while (it.hasNext()) { + du.println(prefix + "Data Type: \"" + it.next() + "\""); + } + } + du.println(prefix + "mPriority=" + mPriority + + ", mHasPartialTypes=" + mHasPartialTypes); + } + + public static final Parcelable.Creator<IntentFilter> CREATOR + = new Parcelable.Creator<IntentFilter>() { + public IntentFilter createFromParcel(Parcel source) { + return new IntentFilter(source); + } + + public IntentFilter[] newArray(int size) { + return new IntentFilter[size]; + } + }; + + public final int describeContents() { + return 0; + } + + public final void writeToParcel(Parcel dest, int flags) { + dest.writeStringList(mActions); + if (mCategories != null) { + dest.writeInt(1); + dest.writeStringList(mCategories); + } else { + dest.writeInt(0); + } + if (mDataSchemes != null) { + dest.writeInt(1); + dest.writeStringList(mDataSchemes); + } else { + dest.writeInt(0); + } + if (mDataTypes != null) { + dest.writeInt(1); + dest.writeStringList(mDataTypes); + } else { + dest.writeInt(0); + } + if (mDataAuthorities != null) { + final int N = mDataAuthorities.size(); + dest.writeInt(N); + for (int i=0; i<N; i++) { + mDataAuthorities.get(i).writeToParcel(dest); + } + } else { + dest.writeInt(0); + } + if (mDataPaths != null) { + final int N = mDataPaths.size(); + dest.writeInt(N); + for (int i=0; i<N; i++) { + mDataPaths.get(i).writeToParcel(dest, 0); + } + } else { + dest.writeInt(0); + } + dest.writeInt(mPriority); + dest.writeInt(mHasPartialTypes ? 1 : 0); + } + + /** + * For debugging -- perform a check on the filter, return true if it passed + * or false if it failed. + * + * {@hide} + */ + public boolean debugCheck() { + return true; + + // This code looks for intent filters that do not specify data. + /* + if (mActions != null && mActions.size() == 1 + && mActions.contains(Intent.ACTION_MAIN)) { + return true; + } + + if (mDataTypes == null && mDataSchemes == null) { + Log.w("IntentFilter", "QUESTIONABLE INTENT FILTER:"); + dump(Log.WARN, "IntentFilter", " "); + return false; + } + + return true; + */ + } + + private IntentFilter(Parcel source) { + mActions = new ArrayList<String>(); + source.readStringList(mActions); + if (source.readInt() != 0) { + mCategories = new ArrayList<String>(); + source.readStringList(mCategories); + } + if (source.readInt() != 0) { + mDataSchemes = new ArrayList<String>(); + source.readStringList(mDataSchemes); + } + if (source.readInt() != 0) { + mDataTypes = new ArrayList<String>(); + source.readStringList(mDataTypes); + } + int N = source.readInt(); + if (N > 0) { + mDataAuthorities = new ArrayList<AuthorityEntry>(); + for (int i=0; i<N; i++) { + mDataAuthorities.add(new AuthorityEntry(source)); + } + } + N = source.readInt(); + if (N > 0) { + mDataPaths = new ArrayList<PatternMatcher>(); + for (int i=0; i<N; i++) { + mDataPaths.add(new PatternMatcher(source)); + } + } + mPriority = source.readInt(); + mHasPartialTypes = source.readInt() > 0; + } + + private final boolean findMimeType(String type) { + final ArrayList<String> t = mDataTypes; + + if (type == null) { + return false; + } + + if (t.contains(type)) { + return true; + } + + // Deal with an Intent wanting to match every type in the IntentFilter. + final int typeLength = type.length(); + if (typeLength == 3 && type.equals("*/*")) { + return !t.isEmpty(); + } + + // Deal with this IntentFilter wanting to match every Intent type. + if (mHasPartialTypes && t.contains("*")) { + return true; + } + + final int slashpos = type.indexOf('/'); + if (slashpos > 0) { + if (mHasPartialTypes && t.contains(type.substring(0, slashpos))) { + return true; + } + if (typeLength == slashpos+2 && type.charAt(slashpos+1) == '*') { + // Need to look through all types for one that matches + // our base... + final Iterator<String> it = t.iterator(); + while (it.hasNext()) { + String v = it.next(); + if (type.regionMatches(0, v, 0, slashpos+1)) { + return true; + } + } + } + } + + return false; + } +} diff --git a/core/java/android/content/MutableContextWrapper.java b/core/java/android/content/MutableContextWrapper.java new file mode 100644 index 0000000..820479c --- /dev/null +++ b/core/java/android/content/MutableContextWrapper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2006 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; + +/** + * Special version of {@link ContextWrapper} that allows the base context to + * be modified after it is initially set. + */ +public class MutableContextWrapper extends ContextWrapper { + public MutableContextWrapper(Context base) { + super(base); + } + + /** + * Change the base context for this ContextWrapper. All calls will then be + * delegated to the base context. Unlike ContextWrapper, the base context + * can be changed even after one is already set. + * + * @param base The new base context for this wrapper. + */ + public void setBaseContext(Context base) { + mBase = base; + } +} diff --git a/core/java/android/content/ReceiverCallNotAllowedException.java b/core/java/android/content/ReceiverCallNotAllowedException.java new file mode 100644 index 0000000..96b269c --- /dev/null +++ b/core/java/android/content/ReceiverCallNotAllowedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2006 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.util.AndroidRuntimeException; + +/** + * This exception is thrown from {@link Context#registerReceiver} and + * {@link Context#bindService} when these methods are being used from + * an {@link BroadcastReceiver} component. In this case, the component will no + * longer be active upon returning from receiving the Intent, so it is + * not valid to use asynchronous APIs. + */ +public class ReceiverCallNotAllowedException extends AndroidRuntimeException { + public ReceiverCallNotAllowedException(String msg) { + super(msg); + } +} diff --git a/core/java/android/content/SearchRecentSuggestionsProvider.java b/core/java/android/content/SearchRecentSuggestionsProvider.java new file mode 100644 index 0000000..3d89e92 --- /dev/null +++ b/core/java/android/content/SearchRecentSuggestionsProvider.java @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2008 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.app.SearchManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +/** + * This superclass can be used to create a simple search suggestions provider for your application. + * It creates suggestions (as the user types) based on recent queries and/or recent views. + * + * <p>In order to use this class, you must do the following. + * + * <ul> + * <li>Implement and test query search, as described in {@link android.app.SearchManager}. (This + * provider will send any suggested queries via the standard + * {@link android.content.Intent#ACTION_SEARCH ACTION_SEARCH} Intent, which you'll already + * support once you have implemented and tested basic searchability.)</li> + * <li>Create a Content Provider within your application by extending + * {@link android.content.SearchRecentSuggestionsProvider}. The class you create will be + * very simple - typically, it will have only a constructor. But the constructor has a very + * important responsibility: When it calls {@link #setupSuggestions(String, int)}, it + * <i>configures</i> the provider to match the requirements of your searchable activity.</li> + * <li>Create a manifest entry describing your provider. Typically this would be as simple + * as adding the following lines: + * <pre class="prettyprint"> + * <!-- Content provider for search suggestions --> + * <provider android:name="YourSuggestionProviderClass" + * android:authorities="your.suggestion.authority" /></pre> + * </li> + * <li>Please note that you <i>do not</i> instantiate this content provider directly from within + * your code. This is done automatically by the system Content Resolver, when the search dialog + * looks for suggestions.</li> + * <li>In order for the Content Resolver to do this, you must update your searchable activity's + * XML configuration file with information about your content provider. The following additions + * are usually sufficient: + * <pre class="prettyprint"> + * android:searchSuggestAuthority="your.suggestion.authority" + * android:searchSuggestSelection=" ? "</pre> + * </li> + * <li>In your searchable activities, capture any user-generated queries and record them + * for future searches by calling {@link android.provider.SearchRecentSuggestions#saveRecentQuery + * SearchRecentSuggestions.saveRecentQuery()}.</li> + * </ul> + * + * @see android.provider.SearchRecentSuggestions + */ +public class SearchRecentSuggestionsProvider extends ContentProvider { + // debugging support + private static final String TAG = "SuggestionsProvider"; + + // client-provided configuration values + private String mAuthority; + private int mMode; + private boolean mTwoLineDisplay; + + // general database configuration and tables + private SQLiteOpenHelper mOpenHelper; + private static final String sDatabaseName = "suggestions.db"; + private static final String sSuggestions = "suggestions"; + private static final String ORDER_BY = "date DESC"; + private static final String NULL_COLUMN = "query"; + + // Table of database versions. Don't forget to update! + // NOTE: These version values are shifted left 8 bits (x 256) in order to create space for + // a small set of mode bitflags in the version int. + // + // 1 original implementation with queries, and 1 or 2 display columns + // 1->2 added UNIQUE constraint to display1 column + private static final int DATABASE_VERSION = 2 * 256; + + /** + * This mode bit configures the database to record recent queries. <i>required</i> + * + * @see #setupSuggestions(String, int) + */ + public static final int DATABASE_MODE_QUERIES = 1; + /** + * This mode bit configures the database to include a 2nd annotation line with each entry. + * <i>optional</i> + * + * @see #setupSuggestions(String, int) + */ + public static final int DATABASE_MODE_2LINES = 2; + + // Uri and query support + private static final int URI_MATCH_SUGGEST = 1; + + private Uri mSuggestionsUri; + private UriMatcher mUriMatcher; + + private String mSuggestSuggestionClause; + private String[] mSuggestionProjection; + + /** + * Builds the database. This version has extra support for using the version field + * as a mode flags field, and configures the database columns depending on the mode bits + * (features) requested by the extending class. + * + * @hide + */ + private static class DatabaseHelper extends SQLiteOpenHelper { + + private int mNewVersion; + + public DatabaseHelper(Context context, int newVersion) { + super(context, sDatabaseName, null, newVersion); + mNewVersion = newVersion; + } + + @Override + public void onCreate(SQLiteDatabase db) { + StringBuilder builder = new StringBuilder(); + builder.append("CREATE TABLE suggestions (" + + "_id INTEGER PRIMARY KEY" + + ",display1 TEXT UNIQUE ON CONFLICT REPLACE"); + if (0 != (mNewVersion & DATABASE_MODE_2LINES)) { + builder.append(",display2 TEXT"); + } + builder.append(",query TEXT" + + ",date LONG" + + ");"); + db.execSQL(builder.toString()); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS suggestions"); + onCreate(db); + } + } + + /** + * In order to use this class, you must extend it, and call this setup function from your + * constructor. In your application or activities, you must provide the same values when + * you create the {@link android.provider.SearchRecentSuggestions} helper. + * + * @param authority This must match the authority that you've declared in your manifest. + * @param mode You can use mode flags here to determine certain functional aspects of your + * database. Note, this value should not change from run to run, because when it does change, + * your suggestions database may be wiped. + * + * @see #DATABASE_MODE_QUERIES + * @see #DATABASE_MODE_2LINES + */ + protected void setupSuggestions(String authority, int mode) { + if (TextUtils.isEmpty(authority) || + ((mode & DATABASE_MODE_QUERIES) == 0)) { + throw new IllegalArgumentException(); + } + // unpack mode flags + mTwoLineDisplay = (0 != (mode & DATABASE_MODE_2LINES)); + + // saved values + mAuthority = new String(authority); + mMode = mode; + + // derived values + mSuggestionsUri = Uri.parse("content://" + mAuthority + "/suggestions"); + mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + mUriMatcher.addURI(mAuthority, SearchManager.SUGGEST_URI_PATH_QUERY, URI_MATCH_SUGGEST); + + if (mTwoLineDisplay) { + mSuggestSuggestionClause = "display1 LIKE ? OR display2 LIKE ?"; + + mSuggestionProjection = new String [] { + "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, + "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, + "display2 AS " + SearchManager.SUGGEST_COLUMN_TEXT_2, + "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, + "_id" + }; + } else { + mSuggestSuggestionClause = "display1 LIKE ?"; + + mSuggestionProjection = new String [] { + "0 AS " + SearchManager.SUGGEST_COLUMN_FORMAT, + "display1 AS " + SearchManager.SUGGEST_COLUMN_TEXT_1, + "query AS " + SearchManager.SUGGEST_COLUMN_QUERY, + "_id" + }; + } + + + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + final int length = uri.getPathSegments().size(); + if (length != 1) { + throw new IllegalArgumentException("Unknown Uri"); + } + + final String base = uri.getPathSegments().get(0); + int count = 0; + if (base.equals(sSuggestions)) { + count = db.delete(sSuggestions, selection, selectionArgs); + } else { + throw new IllegalArgumentException("Unknown Uri"); + } + getContext().getContentResolver().notifyChange(uri, null); + return count; + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + @Override + public String getType(Uri uri) { + if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { + return SearchManager.SUGGEST_MIME_TYPE; + } + int length = uri.getPathSegments().size(); + if (length >= 1) { + String base = uri.getPathSegments().get(0); + if (base.equals(sSuggestions)) { + if (length == 1) { + return "vnd.android.cursor.dir/suggestion"; + } else if (length == 2) { + return "vnd.android.cursor.item/suggestion"; + } + } + } + throw new IllegalArgumentException("Unknown Uri"); + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + + int length = uri.getPathSegments().size(); + if (length < 1) { + throw new IllegalArgumentException("Unknown Uri"); + } + // Note: This table has on-conflict-replace semantics, so insert() may actually replace() + long rowID = -1; + String base = uri.getPathSegments().get(0); + Uri newUri = null; + if (base.equals(sSuggestions)) { + if (length == 1) { + rowID = db.insert(sSuggestions, NULL_COLUMN, values); + if (rowID > 0) { + newUri = Uri.withAppendedPath(mSuggestionsUri, String.valueOf(rowID)); + } + } + } + if (rowID < 0) { + throw new IllegalArgumentException("Unknown Uri"); + } + getContext().getContentResolver().notifyChange(newUri, null); + return newUri; + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + @Override + public boolean onCreate() { + if (mAuthority == null || mMode == 0) { + throw new IllegalArgumentException("Provider not configured"); + } + int mWorkingDbVersion = DATABASE_VERSION + mMode; + mOpenHelper = new DatabaseHelper(getContext(), mWorkingDbVersion); + + return true; + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + // TODO: Confirm no injection attacks here, or rewrite. + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + // special case for actual suggestions (from search manager) + if (mUriMatcher.match(uri) == URI_MATCH_SUGGEST) { + String suggestSelection; + String[] myArgs; + if (TextUtils.isEmpty(selectionArgs[0])) { + suggestSelection = null; + myArgs = null; + } else { + String like = "%" + selectionArgs[0] + "%"; + if (mTwoLineDisplay) { + myArgs = new String [] { like, like }; + } else { + myArgs = new String [] { like }; + } + suggestSelection = mSuggestSuggestionClause; + } + // Suggestions are always performed with the default sort order + Cursor c = db.query(sSuggestions, mSuggestionProjection, + suggestSelection, myArgs, null, null, ORDER_BY, null); + c.setNotificationUri(getContext().getContentResolver(), uri); + return c; + } + + // otherwise process arguments and perform a standard query + int length = uri.getPathSegments().size(); + if (length != 1 && length != 2) { + throw new IllegalArgumentException("Unknown Uri"); + } + + String base = uri.getPathSegments().get(0); + if (!base.equals(sSuggestions)) { + throw new IllegalArgumentException("Unknown Uri"); + } + + String[] useProjection = null; + if (projection != null && projection.length > 0) { + useProjection = new String[projection.length + 1]; + System.arraycopy(projection, 0, useProjection, 0, projection.length); + useProjection[projection.length] = "_id AS _id"; + } + + StringBuilder whereClause = new StringBuilder(256); + if (length == 2) { + whereClause.append("(_id = ").append(uri.getPathSegments().get(1)).append(")"); + } + + // Tack on the user's selection, if present + if (selection != null && selection.length() > 0) { + if (whereClause.length() > 0) { + whereClause.append(" AND "); + } + + whereClause.append('('); + whereClause.append(selection); + whereClause.append(')'); + } + + // And perform the generic query as requested + Cursor c = db.query(base, useProjection, whereClause.toString(), + selectionArgs, null, null, sortOrder, + null); + c.setNotificationUri(getContext().getContentResolver(), uri); + return c; + } + + /** + * This method is provided for use by the ContentResolver. Do not override, or directly + * call from your own code. + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("Not implemented"); + } + +} diff --git a/core/java/android/content/ServiceConnection.java b/core/java/android/content/ServiceConnection.java new file mode 100644 index 0000000..d115ce4 --- /dev/null +++ b/core/java/android/content/ServiceConnection.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2006 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.IBinder; + +/** + * Interface for monitoring the state of an application service. See + * {@link android.app.Service} and + * {@link Context#bindService Context.bindService()} for more information. + * <p>Like many callbacks from the system, the methods on this class are called + * from the main thread of your process. + */ +public interface ServiceConnection { + /** + * Called when a connection to the Service has been established, with + * the {@link android.os.IBinder} of the communication channel to the + * Service. + * + * @param name The concrete component name of the service that has + * been connected. + * + * @param service The IBinder of the Service's communication channel, + * which you can now make calls on. + */ + public void onServiceConnected(ComponentName name, IBinder service); + + /** + * Called when a connection to the Service has been lost. This typically + * happens when the process hosting the service has crashed or been killed. + * This does <em>not</em> remove the ServiceConnection itself -- this + * binding to the service will remain active, and you will receive a call + * to {@link #onServiceConnected} when the Service is next running. + * + * @param name The concrete component name of the service whose + * connection has been lost. + */ + public void onServiceDisconnected(ComponentName name); +} diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java new file mode 100644 index 0000000..a15e29e --- /dev/null +++ b/core/java/android/content/SharedPreferences.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2006 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 java.util.Map; + +/** + * Interface for accessing and modifying preference data returned by {@link + * Context#getSharedPreferences}. For any particular set of preferences, + * there is a single instance of this class that all clients share. + * Modifications to the preferences must go through an {@link Editor} object + * to ensure the preference values remain in a consistent state and control + * when they are committed to storage. + * + * <p><em>Note: currently this class does not support use across multiple + * processes. This will be added later.</em> + * + * @see Context#getSharedPreferences + */ +public interface SharedPreferences { + /** + * Interface definition for a callback to be invoked when a shared + * preference is changed. + */ + public interface OnSharedPreferenceChangeListener { + /** + * Called when a shared preference is changed, added, or removed. This + * may be called even if a preference is set to its existing value. + * + * @param sharedPreferences The {@link SharedPreferences} that received + * the change. + * @param key The key of the preference that was changed, added, or + * removed. + */ + void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key); + } + + /** + * Interface used for modifying values in a {@link SharedPreferences} + * object. All changes you make in an editor are batched, and not copied + * back to the original {@link SharedPreferences} or persistent storage + * until you call {@link #commit}. + */ + public interface Editor { + /** + * Set a String value in the preferences editor, to be written back once + * {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param value The new value for the preference. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putString(String key, String value); + + /** + * Set an int value in the preferences editor, to be written back once + * {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param value The new value for the preference. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putInt(String key, int value); + + /** + * Set a long value in the preferences editor, to be written back once + * {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param value The new value for the preference. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putLong(String key, long value); + + /** + * Set a float value in the preferences editor, to be written back once + * {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param value The new value for the preference. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putFloat(String key, float value); + + /** + * Set a boolean value in the preferences editor, to be written back + * once {@link #commit} is called. + * + * @param key The name of the preference to modify. + * @param value The new value for the preference. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor putBoolean(String key, boolean value); + + /** + * Mark in the editor that a preference value should be removed, which + * will be done in the actual preferences once {@link #commit} is + * called. + * + * <p>Note that when committing back to the preferences, all removals + * are done first, regardless of whether you called remove before + * or after put methods on this editor. + * + * @param key The name of the preference to remove. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor remove(String key); + + /** + * Mark in the editor to remove <em>all</em> values from the + * preferences. Once commit is called, the only remaining preferences + * will be any that you have defined in this editor. + * + * <p>Note that when committing back to the preferences, the clear + * is done first, regardless of whether you called clear before + * or after put methods on this editor. + * + * @return Returns a reference to the same Editor object, so you can + * chain put calls together. + */ + Editor clear(); + + /** + * Commit your preferences changes back from this Editor to the + * {@link SharedPreferences} object it is editing. This atomically + * performs the requested modifications, replacing whatever is currently + * in the SharedPreferences. + * + * <p>Note that when two editors are modifying preferences at the same + * time, the last one to call commit wins. + * + * @return Returns true if the new values were successfully written + * to persistent storage. + */ + boolean commit(); + } + + /** + * Retrieve all values from the preferences. + * + * @return Returns a map containing a list of pairs key/value representing + * the preferences. + * + * @throws NullPointerException + */ + Map<String, ?> getAll(); + + /** + * Retrieve a String value from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValue Value to return if this preference does not exist. + * + * @return Returns the preference value if it exists, or defValue. Throws + * ClassCastException if there is a preference with this name that is not + * a String. + * + * @throws ClassCastException + */ + String getString(String key, String defValue); + + /** + * Retrieve an int value from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValue Value to return if this preference does not exist. + * + * @return Returns the preference value if it exists, or defValue. Throws + * ClassCastException if there is a preference with this name that is not + * an int. + * + * @throws ClassCastException + */ + int getInt(String key, int defValue); + + /** + * Retrieve a long value from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValue Value to return if this preference does not exist. + * + * @return Returns the preference value if it exists, or defValue. Throws + * ClassCastException if there is a preference with this name that is not + * a long. + * + * @throws ClassCastException + */ + long getLong(String key, long defValue); + + /** + * Retrieve a float value from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValue Value to return if this preference does not exist. + * + * @return Returns the preference value if it exists, or defValue. Throws + * ClassCastException if there is a preference with this name that is not + * a float. + * + * @throws ClassCastException + */ + float getFloat(String key, float defValue); + + /** + * Retrieve a boolean value from the preferences. + * + * @param key The name of the preference to retrieve. + * @param defValue Value to return if this preference does not exist. + * + * @return Returns the preference value if it exists, or defValue. Throws + * ClassCastException if there is a preference with this name that is not + * a boolean. + * + * @throws ClassCastException + */ + boolean getBoolean(String key, boolean defValue); + + /** + * Checks whether the preferences contains a preference. + * + * @param key The name of the preference to check. + * @return Returns true if the preference exists in the preferences, + * otherwise false. + */ + boolean contains(String key); + + /** + * Create a new Editor for these preferences, through which you can make + * modifications to the data in the preferences and atomically commit those + * changes back to the SharedPreferences object. + * + * <p>Note that you <em>must</em> call {@link Editor#commit} to have any + * changes you perform in the Editor actually show up in the + * SharedPreferences. + * + * @return Returns a new instance of the {@link Editor} interface, allowing + * you to modify the values in this SharedPreferences object. + */ + Editor edit(); + + /** + * Registers a callback to be invoked when a change happens to a preference. + * + * @param listener The callback that will run. + * @see #unregisterOnSharedPreferenceChangeListener + */ + void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); + + /** + * Unregisters a previous callback. + * + * @param listener The callback that should be unregistered. + * @see #registerOnSharedPreferenceChangeListener + */ + void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener); +} diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java new file mode 100644 index 0000000..7826e50 --- /dev/null +++ b/core/java/android/content/SyncAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 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.Bundle; +import android.os.RemoteException; + +/** + * @hide + */ +public abstract class SyncAdapter { + private static final String TAG = "SyncAdapter"; + + /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ + public static final int LOG_SYNC_DETAILS = 2743; + + class Transport extends ISyncAdapter.Stub { + public void startSync(ISyncContext syncContext, String account, + Bundle extras) throws RemoteException { + SyncAdapter.this.startSync(new SyncContext(syncContext), account, extras); + } + + public void cancelSync() throws RemoteException { + SyncAdapter.this.cancelSync(); + } + } + + Transport mTransport = new Transport(); + + /** + * Get the Transport object. (note this is package private). + */ + final ISyncAdapter getISyncAdapter() + { + return mTransport; + } + + /** + * Initiate a sync for this account. SyncAdapter-specific parameters may + * be specified in extras, which is guaranteed to not be null. IPC invocations + * of this method and cancelSync() are guaranteed to be serialized. + * + * @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 extras SyncAdapter-specific parameters + */ + public abstract void startSync(SyncContext syncContext, String account, Bundle extras); + + /** + * Cancel the most recently initiated sync. Due to race conditions, this may arrive + * after the ISyncContext.onFinished() for that sync was called. IPC invocations + * of this method and startSync() are guaranteed to be serialized. + */ + public abstract void cancelSync(); +} diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java new file mode 100644 index 0000000..f4faa04 --- /dev/null +++ b/core/java/android/content/SyncContext.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 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; +import android.os.SystemClock; + +/** + * @hide + */ +public class SyncContext { + private ISyncContext mSyncContext; + private long mLastHeartbeatSendTime; + + private static final long HEARTBEAT_SEND_INTERVAL_IN_MS = 1000; + + public SyncContext(ISyncContext syncContextInterface) { + mSyncContext = syncContextInterface; + mLastHeartbeatSendTime = 0; + } + + /** + * Call to update the status text for this sync. This internally invokes + * {@link #updateHeartbeat}, so it also takes the place of a call to that. + * + * @param message the current status message for this sync + */ + public void setStatusText(String message) { + updateHeartbeat(); + } + + /** + * Call to indicate that the SyncAdapter is making progress. E.g., if this SyncAdapter + * downloads or sends records to/from the server, this may be called after each record + * is downloaded or uploaded. + */ + public void updateHeartbeat() { + final long now = SystemClock.elapsedRealtime(); + if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return; + try { + mLastHeartbeatSendTime = now; + mSyncContext.sendHeartbeat(); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onFinished(SyncResult result) { + try { + mSyncContext.onFinished(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public ISyncContext getISyncContext() { + return mSyncContext; + } +} diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java new file mode 100644 index 0000000..96470c3 --- /dev/null +++ b/core/java/android/content/SyncManager.java @@ -0,0 +1,2175 @@ +/* + * Copyright (C) 2008 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 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.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +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.database.Cursor; +import android.database.DatabaseUtils; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.PowerManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.provider.Sync; +import android.provider.Settings; +import android.provider.Sync.History; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.text.format.Time; +import android.util.Config; +import android.util.EventLog; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Random; +import java.util.Observer; +import java.util.Observable; + +/** + * @hide + */ +class SyncManager { + private static final String TAG = "SyncManager"; + + // used during dumping of the Sync history + private static final long MILLIS_IN_HOUR = 1000 * 60 * 60; + private static final long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24; + private static final long MILLIS_IN_WEEK = MILLIS_IN_DAY * 7; + 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 + + /** + * 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 SYNC_NOTIFICATION_DELAY = 30 * 1000; // 30 seconds + + /** + * When retrying a sync for the first time use this delay. After that + * the retry time will double until it reached MAX_SYNC_RETRY_TIME. + * In milliseconds. + */ + private static final long INITIAL_SYNC_RETRY_TIME_IN_MS = 30 * 1000; // 30 seconds + + /** + * Default the max sync retry time to this value. + */ + private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour + + /** + * An error notification is sent if sync of any of the providers has been failing for this long. + */ + private static final long ERROR_NOTIFICATION_DELAY_MS = 1000 * 60 * 10; // 10 minutes + + private static final String SYNC_WAKE_LOCK = "SyncManagerSyncWakeLock"; + 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; + + volatile private PowerManager.WakeLock mSyncWakeLock; + volatile private PowerManager.WakeLock mHandleAlarmWakeLock; + volatile private boolean mDataConnectionIsConnected = false; + volatile private boolean mStorageIsLow = false; + private Sync.Settings.QueryMap mSyncSettings; + + private final NotificationManager mNotificationMgr; + private AlarmManager mAlarmService = null; + private HandlerThread mSyncThread; + + private volatile IPackageManager mPackageManager; + + private final SyncStorageEngine mSyncStorageEngine; + private final SyncQueue mSyncQueue; + + private ActiveSyncContext mActiveSyncContext = null; + + // set if the sync error indicator should be reported. + private boolean mNeedSyncErrorNotification = false; + // set if the sync active indicator should be reported + private boolean mNeedSyncActiveNotification = false; + + private volatile boolean mSyncPollInitialized; + private final PendingIntent mSyncAlarmIntent; + private final PendingIntent mSyncPollAlarmIntent; + + 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 */); + } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Internal storage is ok."); + } + mStorageIsLow = false; + sendCheckAlarmsMessage(); + } + } + }; + + private BroadcastReceiver mConnectivityIntentReceiver = + new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + NetworkInfo networkInfo = + intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + NetworkInfo.State state = (networkInfo == null ? NetworkInfo.State.UNKNOWN : + networkInfo.getState()); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "received connectivity action. network info: " + networkInfo); + } + + // only pay attention to the CONNECTED and DISCONNECTED states. + // if connected, we are connected. + // if disconnected, we may not be connected. in some cases, we may be connected on + // a different network. + // e.g., if switching from GPRS to WiFi, we may receive the CONNECTED to WiFi and + // DISCONNECTED for GPRS in any order. if we receive the CONNECTED first, and then + // a DISCONNECTED, we want to make sure we set mDataConnectionIsConnected to true + // since we still have a WiFi connection. + switch (state) { + case CONNECTED: + mDataConnectionIsConnected = true; + break; + case DISCONNECTED: + if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { + mDataConnectionIsConnected = false; + } else { + mDataConnectionIsConnected = true; + } + break; + default: + // ignore the rest of the states -- leave our boolean alone. + } + if (mDataConnectionIsConnected) { + initializeSyncPoll(); + sendCheckAlarmsMessage(); + } + } + }; + + private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM"; + private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM"; + private final SyncHandler mSyncHandler; + + private static final String[] SYNC_ACTIVE_PROJECTION = new String[]{ + Sync.Active.ACCOUNT, + Sync.Active.AUTHORITY, + Sync.Active.START_TIME, + }; + + private static final String[] SYNC_PENDING_PROJECTION = new String[]{ + Sync.Pending.ACCOUNT, + Sync.Pending.AUTHORITY + }; + + private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours + private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours + + private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; + + public SyncManager(Context context, boolean 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); + mSyncStorageEngine = SyncStorageEngine.getSingleton(); + mSyncQueue = new SyncQueue(mSyncStorageEngine); + + mContext = context; + + mSyncThread = new HandlerThread("SyncHandlerThread", Process.THREAD_PRIORITY_BACKGROUND); + mSyncThread.start(); + mSyncHandler = new SyncHandler(mSyncThread.getLooper()); + + mPackageManager = null; + + mSyncAlarmIntent = PendingIntent.getBroadcast( + mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); + + mSyncPollAlarmIntent = PendingIntent.getBroadcast( + mContext, 0 /* ignored */, new Intent(SYNC_POLL_ALARM), 0); + + IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + context.registerReceiver(mConnectivityIntentReceiver, intentFilter); + + intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + context.registerReceiver(mStorageIntentReceiver, intentFilter); + + if (!factoryTest) { + mNotificationMgr = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + context.registerReceiver(new SyncAlarmIntentReceiver(), + new IntentFilter(ACTION_SYNC_ALARM)); + } else { + mNotificationMgr = null; + } + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mSyncWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, SYNC_WAKE_LOCK); + mSyncWakeLock.setReferenceCounted(false); + + // This WakeLock is used to ensure that we stay awake between the time that we receive + // a sync alarm notification and when we finish processing it. We need to do this + // because we don't do the work in the alarm handler, rather we do it in a message + // handler. + mHandleAlarmWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + HANDLE_SYNC_ALARM_WAKE_LOCK); + mHandleAlarmWakeLock.setReferenceCounted(false); + + if (!factoryTest) { + AccountMonitorListener listener = new AccountMonitorListener() { + public void onAccountsUpdated(String[] accounts) { + final boolean hadAccountsAlready = mAccounts != null; + // copy the accounts into a new array and change mAccounts to point to it + String[] newAccounts = new String[accounts.length]; + System.arraycopy(accounts, 0, newAccounts, 0, accounts.length); + mAccounts = newAccounts; + + // if a sync is in progress yet it is no longer in the accounts list, cancel it + ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (activeSyncContext != null) { + if (!ArrayUtils.contains(newAccounts, + activeSyncContext.mSyncOperation.account)) { + Log.d(TAG, "canceling sync since the account has been removed"); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + // we must do this since we don't bother scheduling alarms when + // the accounts are not set yet + sendCheckAlarmsMessage(); + + mSyncStorageEngine.doDatabaseCleanup(accounts); + + if (hadAccountsAlready && mAccounts.length > 0) { + // request a sync so that if the password was changed we will retry any sync + // that failed when it was wrong + startSync(null /* all providers */, null /* no extras */); + } + } + }; + mAccountMonitor = new AccountMonitor(context, listener); + } + } + + private synchronized void initializeSyncPoll() { + if (mSyncPollInitialized) return; + mSyncPollInitialized = true; + + mContext.registerReceiver(new SyncPollAlarmReceiver(), new IntentFilter(SYNC_POLL_ALARM)); + + // load the next poll time from shared preferences + long absoluteAlarmTime = readSyncPollTime(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "initializeSyncPoll: absoluteAlarmTime is " + absoluteAlarmTime); + } + + // Convert absoluteAlarmTime to elapsed realtime. If this time was in the past then + // schedule the poll immediately, if it is too far in the future then cap it at + // MAX_SYNC_POLL_DELAY_SECONDS. + long absoluteNow = System.currentTimeMillis(); + long relativeNow = SystemClock.elapsedRealtime(); + long relativeAlarmTime = relativeNow; + if (absoluteAlarmTime > absoluteNow) { + long delayInMs = absoluteAlarmTime - absoluteNow; + final int maxDelayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; + if (delayInMs > maxDelayInMs) { + delayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; + } + relativeAlarmTime += delayInMs; + } + + // schedule an alarm for the next poll time + scheduleSyncPollAlarm(relativeAlarmTime); + } + + private void scheduleSyncPollAlarm(long relativeAlarmTime) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "scheduleSyncPollAlarm: relativeAlarmTime is " + relativeAlarmTime + + ", now is " + SystemClock.elapsedRealtime() + + ", delay is " + (relativeAlarmTime - SystemClock.elapsedRealtime())); + } + ensureAlarmService(); + mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, relativeAlarmTime, + mSyncPollAlarmIntent); + } + + /** + * Return a random value v that satisfies minValue <= v < maxValue. The difference between + * maxValue and minValue must be less than Integer.MAX_VALUE. + */ + private long jitterize(long minValue, long maxValue) { + Random random = new Random(SystemClock.elapsedRealtime()); + long spread = maxValue - minValue; + if (spread > Integer.MAX_VALUE) { + throw new IllegalArgumentException("the difference between the maxValue and the " + + "minValue must be less than " + Integer.MAX_VALUE); + } + return minValue + random.nextInt((int)spread); + } + + private void handleSyncPollAlarm() { + // determine the next poll time + long delayMs = jitterize(MIN_SYNC_POLL_DELAY_SECONDS, MAX_SYNC_POLL_DELAY_SECONDS) * 1000; + long nextRelativePollTimeMs = SystemClock.elapsedRealtime() + delayMs; + + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "handleSyncPollAlarm: delay " + delayMs); + + // write the absolute time to shared preferences + writeSyncPollTime(System.currentTimeMillis() + delayMs); + + // schedule an alarm for the next poll time + scheduleSyncPollAlarm(nextRelativePollTimeMs); + + // perform a poll + scheduleSync(null /* sync all syncable providers */, new Bundle(), 0 /* no delay */); + } + + private void writeSyncPollTime(long when) { + File f = new File(SYNCMANAGER_PREFS_FILENAME); + DataOutputStream str = null; + try { + str = new DataOutputStream(new FileOutputStream(f)); + str.writeLong(when); + } catch (FileNotFoundException e) { + Log.w(TAG, "error writing to file " + f, e); + } catch (IOException e) { + Log.w(TAG, "error writing to file " + f, e); + } finally { + if (str != null) { + try { + str.close(); + } catch (IOException e) { + Log.w(TAG, "error closing file " + f, e); + } + } + } + } + + private long readSyncPollTime() { + File f = new File(SYNCMANAGER_PREFS_FILENAME); + + DataInputStream str = null; + try { + str = new DataInputStream(new FileInputStream(f)); + return str.readLong(); + } catch (FileNotFoundException e) { + writeSyncPollTime(0); + } catch (IOException e) { + Log.w(TAG, "error reading file " + f, e); + } finally { + if (str != null) { + try { + str.close(); + } catch (IOException e) { + Log.w(TAG, "error closing file " + f, e); + } + } + } + return 0; + } + + public ActiveSyncContext getActiveSyncContext() { + return mActiveSyncContext; + } + + private Sync.Settings.QueryMap getSyncSettings() { + if (mSyncSettings == null) { + mSyncSettings = new Sync.Settings.QueryMap(mContext.getContentResolver(), true, + new Handler()); + mSyncSettings.addObserver(new Observer(){ + public void update(Observable o, Object arg) { + // force the sync loop to run if the settings change + sendCheckAlarmsMessage(); + } + }); + } + return mSyncSettings; + } + + private void ensureContentResolver() { + if (mContentResolver == null) { + mContentResolver = mContext.getContentResolver(); + } + } + + private void ensureAlarmService() { + if (mAlarmService == null) { + mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); + } + } + + public String getSyncingAccount() { + ActiveSyncContext activeSyncContext = mActiveSyncContext; + return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; + } + + /** + * Returns whether or not sync is enabled. Sync can be enabled by + * setting the system property "ro.config.sync" to the value "yes". + * This is normally done at boot time on builds that support sync. + * @return true if sync is enabled + */ + private boolean isSyncEnabled() { + // Require the precise value "yes" to discourage accidental activation. + return "yes".equals(SystemProperties.get("ro.config.sync")); + } + + /** + * Initiate a sync. This can start a sync for all providers + * (pass null to url, set onlyTicklable to false), only those + * providers that are marked as ticklable (pass null to url, + * set onlyTicklable to true), or a specific provider (set url + * to the content url of the provider). + * + * <p>If the ContentResolver.SYNC_EXTRAS_UPLOAD boolean in extras is + * true then initiate a sync that just checks for local changes to send + * to the server, otherwise initiate a sync that first gets any + * changes from the server before sending local changes back to + * the server. + * + * <p>If a specific provider is being synced (the url is non-null) + * then the extras can contain SyncAdapter-specific information + * to control what gets synced (e.g. which specific feed to 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 +* if the url is null. + * @param delay how many milliseconds in the future to wait before performing this + * sync. -1 means to make this the next sync to perform. + */ + public void scheduleSync(Uri url, Bundle extras, long delay) { + 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 (!isSyncEnabled()) { + if (isLoggable) { + Log.v(TAG, "not syncing because sync is disabled"); + } + setStatusText("Sync is disabled."); + return; + } + + if (mAccounts == null) setStatusText("The accounts aren't known yet."); + if (!mDataConnectionIsConnected) setStatusText("No data connection"); + if (mStorageIsLow) setStatusText("Memory low"); + + if (extras == null) extras = new Bundle(); + + Boolean expedited = extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false); + if (expedited) { + 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}; + } else { + // if the accounts aren't configured yet then we can't support an account-less + // sync request + accounts = mAccounts; + if (accounts == null) { + // not ready yet + if (isLoggable) { + Log.v(TAG, "scheduleSync: no accounts yet, dropping"); + } + return; + } + if (accounts.length == 0) { + if (isLoggable) { + Log.v(TAG, "scheduleSync: no accounts configured, dropping"); + } + setStatusText("No accounts are configured."); + return; + } + } + + final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); + final boolean force = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + + int source; + if (uploadOnly) { + source = Sync.History.SOURCE_LOCAL; + } else if (force) { + source = Sync.History.SOURCE_USER; + } else if (url == null) { + source = Sync.History.SOURCE_POLL; + } else { + // this isn't strictly server, since arbitrary callers can (and do) request + // a non-forced two-way sync on a specific url + source = Sync.History.SOURCE_SERVER; + } + + List<String> names = new ArrayList<String>(); + List<ProviderInfo> providers = new ArrayList<ProviderInfo>(); + populateProvidersList(url, names, providers); + + 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; + } + } + } + } + + private void setStatusText(String message) { + mStatusText = message; + } + + private void populateProvidersList(Uri url, List<String> names, List<ProviderInfo> providers) { + try { + final IPackageManager packageManager = getPackageManager(); + if (url == null) { + packageManager.querySyncProviders(names, providers); + } else { + final String authority = url.getAuthority(); + ProviderInfo info = packageManager.resolveContentProvider(url.getAuthority(), 0); + if (info != null) { + // only set this provider if the requested authority is the primary authority + String[] providerNames = info.authority.split(";"); + if (url.getAuthority().equals(providerNames[0])) { + names.add(authority); + providers.add(info); + } + } + } + } catch (RemoteException ex) { + // we should really never get this, but if we do then clear the lists, which + // will result in the dropping of the sync request + Log.e(TAG, "error trying to get the ProviderInfo for " + url, ex); + names.clear(); + providers.clear(); + } + } + + public void scheduleLocalSync(Uri url) { + final Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); + scheduleSync(url, extras, LOCAL_SYNC_DELAY); + } + + private IPackageManager getPackageManager() { + // Don't bother synchronizing on this. The worst that can happen is that two threads + // can try to get the package manager at the same time but only one result gets + // used. Since there is only one package manager in the system this doesn't matter. + if (mPackageManager == null) { + IBinder b = ServiceManager.getService("package"); + mPackageManager = IPackageManager.Stub.asInterface(b); + } + 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 void updateHeartbeatTime() { + mHeartbeatTime = SystemClock.elapsedRealtime(); + ensureContentResolver(); + mContentResolver.notifyChange(Sync.Active.CONTENT_URI, + null /* this change wasn't made through an observer */); + } + + private void sendSyncAlarmMessage() { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_ALARM"); + mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_SYNC_ALARM); + } + + private void sendCheckAlarmsMessage() { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_CHECK_ALARMS"); + mSyncHandler.sendEmptyMessage(SyncHandler.MESSAGE_CHECK_ALARMS); + } + + private void sendSyncFinishedOrCanceledMessage(ActiveSyncContext syncContext, + SyncResult syncResult) { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "sending MESSAGE_SYNC_FINISHED"); + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SYNC_FINISHED; + msg.obj = new SyncHandlerMessagePayload(syncContext, syncResult); + mSyncHandler.sendMessage(msg); + } + + class SyncHandlerMessagePayload { + public final ActiveSyncContext activeSyncContext; + public final SyncResult syncResult; + + SyncHandlerMessagePayload(ActiveSyncContext syncContext, SyncResult syncResult) { + this.activeSyncContext = syncContext; + this.syncResult = syncResult; + } + } + + class SyncAlarmIntentReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + mHandleAlarmWakeLock.acquire(); + sendSyncAlarmMessage(); + } + } + + class SyncPollAlarmReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + handleSyncPollAlarm(); + } + } + + private void rescheduleImmediately(SyncOperation syncOperation) { + SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation); + rescheduledSyncOperation.setDelay(0); + scheduleSyncOperation(rescheduledSyncOperation); + } + + private long rescheduleWithDelay(SyncOperation syncOperation) { + long newDelayInMs; + + 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)); + } else { + // Subsequent delays are the double of the previous delay + newDelayInMs = syncOperation.delay * 2; + } + + // Cap the delay + ensureContentResolver(); + long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver, + Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS, + DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); + if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { + newDelayInMs = maxSyncRetryTimeInSeconds * 1000; + } + + SyncOperation rescheduledSyncOperation = new SyncOperation(syncOperation); + rescheduledSyncOperation.setDelay(newDelayInMs); + scheduleSyncOperation(rescheduledSyncOperation); + return newDelayInMs; + } + + /** + * 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. + */ + public void cancelActiveSync(Uri uri) { + 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)) { + return; + } + } + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + /** + * Create and schedule a SyncOperation. + * + * @param syncOperation the SyncOperation to schedule + */ + public void scheduleSyncOperation(SyncOperation syncOperation) { + // If this operation is expedited and there is a sync in progress then + // reschedule the current operation and send a cancel for it. + final boolean expedited = syncOperation.delay < 0; + final ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (expedited && activeSyncContext != null) { + final boolean activeIsExpedited = activeSyncContext.mSyncOperation.delay < 0; + final boolean hasSameKey = + activeSyncContext.mSyncOperation.key.equals(syncOperation.key); + // This request is expedited and there is a sync in progress. + // Interrupt the current sync only if it is not expedited and if it has a different + // key than the one we are scheduling. + if (!activeIsExpedited && !hasSameKey) { + rescheduleImmediately(activeSyncContext.mSyncOperation); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + boolean operationEnqueued; + synchronized (mSyncQueue) { + operationEnqueued = mSyncQueue.add(syncOperation); + } + + if (operationEnqueued) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "scheduleSyncOperation: enqueued " + syncOperation); + } + sendCheckAlarmsMessage(); + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "scheduleSyncOperation: dropping duplicate sync operation " + + syncOperation); + } + } + } + + /** + * 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. + */ + public void clearScheduledSyncOperations(Uri uri) { + synchronized (mSyncQueue) { + mSyncQueue.clear(null, uri != null ? uri.getAuthority() : null); + } + } + + void maybeRescheduleSync(SyncResult syncResult, SyncOperation previousSyncOperation) { + boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG); + if (isLoggable) { + Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", " + + previousSyncOperation); + } + + // If the operation succeeded to some extent then retry immediately. + // If this was a two-way sync then retry soft errors with an exponential backoff. + // If this was an upward sync then schedule a two-way sync immediately. + // Otherwise do not reschedule. + + if (syncResult.madeSomeProgress()) { + if (isLoggable) { + Log.d(TAG, "retrying sync operation immediately because " + + "even though it had an error it achieved some success"); + } + rescheduleImmediately(previousSyncOperation); + } else if (previousSyncOperation.extras.getBoolean( + ContentResolver.SYNC_EXTRAS_UPLOAD, false)) { + final SyncOperation newSyncOperation = new SyncOperation(previousSyncOperation); + newSyncOperation.extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); + newSyncOperation.setDelay(0); + if (Config.LOGD) { + Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync " + + "encountered an error: " + previousSyncOperation); + } + scheduleSyncOperation(newSyncOperation); + } else if (syncResult.hasSoftError()) { + long delay = rescheduleWithDelay(previousSyncOperation); + if (delay >= 0) { + if (isLoggable) { + Log.d(TAG, "retrying sync operation in " + delay + " ms because " + + "it encountered a soft error: " + previousSyncOperation); + } + } + } else { + if (Config.LOGD) { + Log.d(TAG, "not retrying sync operation because the error is a hard error: " + + previousSyncOperation); + } + } + } + + /** + * Value type that represents a sync operation. + */ + static class SyncOperation implements Comparable { + final String account; + int syncSource; + String authority; + Bundle extras; + final String key; + long earliestRunTime; + long delay; + Long rowId = null; + + SyncOperation(String account, int source, String authority, Bundle extras, long delay) { + this.account = account; + this.syncSource = source; + this.authority = authority; + this.extras = new Bundle(extras); + this.setDelay(delay); + this.key = toKey(); + } + + SyncOperation(SyncOperation other) { + this.account = other.account; + this.syncSource = other.syncSource; + this.authority = other.authority; + this.extras = new Bundle(other.extras); + this.delay = other.delay; + this.earliestRunTime = other.earliestRunTime; + this.key = toKey(); + } + + public void setDelay(long delay) { + this.delay = delay; + if (delay >= 0) { + this.earliestRunTime = SystemClock.elapsedRealtime() + delay; + } else { + this.earliestRunTime = 0; + } + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("authority: ").append(authority); + sb.append(" account: ").append(account); + sb.append(" extras: "); + extrasToStringBuilder(extras, sb); + sb.append(" syncSource: ").append(syncSource); + sb.append(" when: ").append(earliestRunTime); + sb.append(" delay: ").append(delay); + sb.append(" key: {").append(key).append("}"); + if (rowId != null) sb.append(" rowId: ").append(rowId); + return sb.toString(); + } + + private String toKey() { + StringBuilder sb = new StringBuilder(); + sb.append("authority: ").append(authority); + sb.append(" account: ").append(account); + sb.append(" extras: "); + extrasToStringBuilder(extras, sb); + return sb.toString(); + } + + private static void extrasToStringBuilder(Bundle bundle, StringBuilder sb) { + sb.append("["); + for (String key : bundle.keySet()) { + sb.append(key).append("=").append(bundle.get(key)).append(" "); + } + sb.append("]"); + } + + public int compareTo(Object o) { + SyncOperation other = (SyncOperation)o; + if (earliestRunTime == other.earliestRunTime) { + return 0; + } + return (earliestRunTime < other.earliestRunTime) ? -1 : 1; + } + } + + /** + * @hide + */ + class ActiveSyncContext extends ISyncContext.Stub { + final SyncOperation mSyncOperation; + final long mHistoryRowId; + final IContentProvider mContentProvider; + final ISyncAdapter mSyncAdapter; + final long mStartTime; + long mTimeoutStartTime; + + public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider, + ISyncAdapter syncAdapter, long historyRowId) { + super(); + mSyncOperation = syncOperation; + mHistoryRowId = historyRowId; + mContentProvider = contentProvider; + mSyncAdapter = syncAdapter; + mStartTime = SystemClock.elapsedRealtime(); + mTimeoutStartTime = mStartTime; + } + + public void sendHeartbeat() { + // ignore this call if it corresponds to an old sync session + if (mActiveSyncContext == this) { + SyncManager.this.updateHeartbeatTime(); + } + } + + public void onFinished(SyncResult result) { + // include "this" in the message so that the handler can ignore it if this + // ActiveSyncContext is no longer the mActiveSyncContext at message handling + // time + sendSyncFinishedOrCanceledMessage(this, result); + } + + public void toString(StringBuilder sb) { + sb.append("startTime ").append(mStartTime) + .append(", mTimeoutStartTime ").append(mTimeoutStartTime) + .append(", mHistoryRowId ").append(mHistoryRowId) + .append(", syncOperation ").append(mSyncOperation); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } + } + + protected void dump(FileDescriptor fd, PrintWriter pw) { + StringBuilder sb = new StringBuilder(); + dumpSyncState(sb); + sb.append("\n"); + if (isSyncEnabled()) { + dumpSyncHistory(sb); + } + pw.println(sb.toString()); + } + + protected void dumpSyncState(StringBuilder sb) { + sb.append("sync enabled: ").append(isSyncEnabled()).append("\n"); + sb.append("data connected: ").append(mDataConnectionIsConnected).append("\n"); + sb.append("memory low: ").append(mStorageIsLow).append("\n"); + + final String[] accounts = mAccounts; + sb.append("accounts: "); + if (accounts != null) { + sb.append(accounts.length); + } else { + sb.append("none"); + } + sb.append("\n"); + final long now = SystemClock.elapsedRealtime(); + sb.append("now: ").append(now).append("\n"); + sb.append("uptime: ").append(DateUtils.formatElapsedTime(now/1000)).append(" (HH:MM:SS)\n"); + sb.append("time spent syncing : ") + .append(DateUtils.formatElapsedTime( + mSyncHandler.mSyncTimeTracker.timeSpentSyncing() / 1000)) + .append(" (HH:MM:SS), sync ") + .append(mSyncHandler.mSyncTimeTracker.mLastWasSyncing ? "" : "not ") + .append("in progress").append("\n"); + if (mSyncHandler.mAlarmScheduleTime != null) { + sb.append("next alarm time: ").append(mSyncHandler.mAlarmScheduleTime) + .append(" (") + .append(DateUtils.formatElapsedTime((mSyncHandler.mAlarmScheduleTime-now)/1000)) + .append(" (HH:MM:SS) from now)\n"); + } else { + sb.append("no alarm is scheduled (there had better not be any pending syncs)\n"); + } + + sb.append("active sync: ").append(mActiveSyncContext).append("\n"); + + sb.append("notification info: "); + mSyncHandler.mSyncNotificationInfo.toString(sb); + sb.append("\n"); + + synchronized (mSyncQueue) { + sb.append("sync queue: "); + mSyncQueue.dump(sb); + } + + Cursor c = mSyncStorageEngine.query(Sync.Active.CONTENT_URI, + SYNC_ACTIVE_PROJECTION, null, null, null); + sb.append("\n"); + try { + if (c.moveToNext()) { + final long durationInSeconds = (now - c.getLong(2)) / 1000; + sb.append("Active sync: ").append(c.getString(0)) + .append(" ").append(c.getString(1)) + .append(", duration is ") + .append(DateUtils.formatElapsedTime(durationInSeconds)).append(".\n"); + } else { + sb.append("No sync is in progress.\n"); + } + } finally { + c.close(); + } + + c = mSyncStorageEngine.query(Sync.Pending.CONTENT_URI, + SYNC_PENDING_PROJECTION, null, null, "account, authority"); + sb.append("\nPending Syncs\n"); + try { + if (c.getCount() != 0) { + dumpSyncPendingHeader(sb); + while (c.moveToNext()) { + dumpSyncPendingRow(sb, c); + } + dumpSyncPendingFooter(sb); + } else { + sb.append("none\n"); + } + } finally { + c.close(); + } + + String currentAccount = null; + c = mSyncStorageEngine.query(Sync.Status.CONTENT_URI, + STATUS_PROJECTION, null, null, "account, authority"); + sb.append("\nSync history by account and authority\n"); + try { + while (c.moveToNext()) { + if (!TextUtils.equals(currentAccount, c.getString(0))) { + if (currentAccount != null) { + dumpSyncHistoryFooter(sb); + } + currentAccount = c.getString(0); + dumpSyncHistoryHeader(sb, currentAccount); + } + + dumpSyncHistoryRow(sb, c); + } + if (c.getCount() > 0) dumpSyncHistoryFooter(sb); + } finally { + c.close(); + } + } + + private void dumpSyncHistoryHeader(StringBuilder sb, String account) { + sb.append(" Account: ").append(account).append("\n"); + sb.append(" ___________________________________________________________________________________________________________________________\n"); + sb.append(" | | num times synced | total | last success | |\n"); + sb.append(" | authority | local | poll | server | user | total | duration | source | time | result if failing |\n"); + } + + private static String[] STATUS_PROJECTION = new String[]{ + Sync.Status.ACCOUNT, // 0 + Sync.Status.AUTHORITY, // 1 + Sync.Status.NUM_SYNCS, // 2 + Sync.Status.TOTAL_ELAPSED_TIME, // 3 + Sync.Status.NUM_SOURCE_LOCAL, // 4 + Sync.Status.NUM_SOURCE_POLL, // 5 + Sync.Status.NUM_SOURCE_SERVER, // 6 + Sync.Status.NUM_SOURCE_USER, // 7 + Sync.Status.LAST_SUCCESS_SOURCE, // 8 + Sync.Status.LAST_SUCCESS_TIME, // 9 + Sync.Status.LAST_FAILURE_SOURCE, // 10 + Sync.Status.LAST_FAILURE_TIME, // 11 + Sync.Status.LAST_FAILURE_MESG // 12 + }; + + private void dumpSyncHistoryRow(StringBuilder sb, Cursor c) { + boolean hasSuccess = !c.isNull(9); + boolean hasFailure = !c.isNull(11); + Time timeSuccess = new Time(); + if (hasSuccess) timeSuccess.set(c.getLong(9)); + Time timeFailure = new Time(); + if (hasFailure) timeFailure.set(c.getLong(11)); + sb.append(String.format(" | %-15s | %5d | %5d | %6d | %5d | %5d | %8s | %7s | %19s | %19s |\n", + c.getString(1), + c.getLong(4), + c.getLong(5), + c.getLong(6), + c.getLong(7), + c.getLong(2), + DateUtils.formatElapsedTime(c.getLong(3)/1000), + hasSuccess ? Sync.History.SOURCES[c.getInt(8)] : "", + hasSuccess ? timeSuccess.format("%Y-%m-%d %H:%M:%S") : "", + hasFailure ? History.mesgToString(c.getString(12)) : "")); + } + + private void dumpSyncHistoryFooter(StringBuilder sb) { + sb.append(" |___________________________________________________________________________________________________________________________|\n"); + } + + private void dumpSyncPendingHeader(StringBuilder sb) { + sb.append(" ____________________________________________________\n"); + sb.append(" | account | authority |\n"); + } + + private void dumpSyncPendingRow(StringBuilder sb, Cursor c) { + sb.append(String.format(" | %-30s | %-15s |\n", c.getString(0), c.getString(1))); + } + + private void dumpSyncPendingFooter(StringBuilder sb) { + sb.append(" |__________________________________________________|\n"); + } + + protected void dumpSyncHistory(StringBuilder sb) { + Cursor c = mSyncStorageEngine.query(Sync.History.CONTENT_URI, null, "event=?", + new String[]{String.valueOf(Sync.History.EVENT_STOP)}, + Sync.HistoryColumns.EVENT_TIME + " desc"); + try { + long numSyncsLastHour = 0, durationLastHour = 0; + long numSyncsLastDay = 0, durationLastDay = 0; + long numSyncsLastWeek = 0, durationLastWeek = 0; + long numSyncsLast4Weeks = 0, durationLast4Weeks = 0; + long numSyncsTotal = 0, durationTotal = 0; + + long now = System.currentTimeMillis(); + int indexEventTime = c.getColumnIndexOrThrow(Sync.History.EVENT_TIME); + int indexElapsedTime = c.getColumnIndexOrThrow(Sync.History.ELAPSED_TIME); + while (c.moveToNext()) { + long duration = c.getLong(indexElapsedTime); + long endTime = c.getLong(indexEventTime) + duration; + long millisSinceStart = now - endTime; + numSyncsTotal++; + durationTotal += duration; + if (millisSinceStart < MILLIS_IN_HOUR) { + numSyncsLastHour++; + durationLastHour += duration; + } + if (millisSinceStart < MILLIS_IN_DAY) { + numSyncsLastDay++; + durationLastDay += duration; + } + if (millisSinceStart < MILLIS_IN_WEEK) { + numSyncsLastWeek++; + durationLastWeek += duration; + } + if (millisSinceStart < MILLIS_IN_4WEEKS) { + numSyncsLast4Weeks++; + durationLast4Weeks += duration; + } + } + dumpSyncIntervalHeader(sb); + dumpSyncInterval(sb, "hour", MILLIS_IN_HOUR, numSyncsLastHour, durationLastHour); + dumpSyncInterval(sb, "day", MILLIS_IN_DAY, numSyncsLastDay, durationLastDay); + dumpSyncInterval(sb, "week", MILLIS_IN_WEEK, numSyncsLastWeek, durationLastWeek); + dumpSyncInterval(sb, "4 weeks", + MILLIS_IN_4WEEKS, numSyncsLast4Weeks, durationLast4Weeks); + dumpSyncInterval(sb, "total", 0, numSyncsTotal, durationTotal); + dumpSyncIntervalFooter(sb); + } finally { + c.close(); + } + } + + private void dumpSyncIntervalHeader(StringBuilder sb) { + sb.append("Sync Stats\n"); + sb.append(" ___________________________________________________________\n"); + sb.append(" | | | duration in sec | |\n"); + sb.append(" | interval | count | average | total | % of interval |\n"); + } + + private void dumpSyncInterval(StringBuilder sb, String label, + long interval, long numSyncs, long duration) { + sb.append(String.format(" | %-8s | %6d | %8.1f | %8.1f", + label, numSyncs, ((float)duration/numSyncs)/1000, (float)duration/1000)); + if (interval > 0) { + sb.append(String.format(" | %13.2f |\n", ((float)duration/interval)*100.0)); + } else { + sb.append(String.format(" | %13s |\n", "na")); + } + } + + private void dumpSyncIntervalFooter(StringBuilder sb) { + sb.append(" |_________________________________________________________|\n"); + } + + /** + * A helper object to keep track of the time we have spent syncing since the last boot + */ + private class SyncTimeTracker { + /** True if a sync was in progress on the most recent call to update() */ + boolean mLastWasSyncing = false; + /** Used to track when lastWasSyncing was last set */ + long mWhenSyncStarted = 0; + /** The cumulative time we have spent syncing */ + private long mTimeSpentSyncing; + + /** Call to let the tracker know that the sync state may have changed */ + public synchronized void update() { + final boolean isSyncInProgress = mActiveSyncContext != null; + if (isSyncInProgress == mLastWasSyncing) return; + final long now = SystemClock.elapsedRealtime(); + if (isSyncInProgress) { + mWhenSyncStarted = now; + } else { + mTimeSpentSyncing += now - mWhenSyncStarted; + } + mLastWasSyncing = isSyncInProgress; + } + + /** Get how long we have been syncing, in ms */ + public synchronized long timeSpentSyncing() { + if (!mLastWasSyncing) return mTimeSpentSyncing; + + final long now = SystemClock.elapsedRealtime(); + return mTimeSpentSyncing + (now - mWhenSyncStarted); + } + } + + /** + * Handles SyncOperation Messages that are posted to the associated + * HandlerThread. + */ + class SyncHandler extends Handler { + // Messages that can be sent on mHandler + private static final int MESSAGE_SYNC_FINISHED = 1; + private static final int MESSAGE_SYNC_ALARM = 2; + private static final int MESSAGE_CHECK_ALARMS = 3; + + public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); + private Long mAlarmScheduleTime = null; + public final SyncTimeTracker mSyncTimeTracker = new SyncTimeTracker(); + + // 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; + + /** + * 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; + + // only valid if isActive is true + public String authority; + + // true iff the notification manager has been asked to send the notification + public boolean isActive = false; + + // Set when we transition from not running a sync to running a sync, and cleared on + // the opposite transition. + public Long startTime = null; + + public void toString(StringBuilder sb) { + sb.append("account ").append(account) + .append(", authority ").append(authority) + .append(", isActive ").append(isActive) + .append(", startTime ").append(startTime); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } + } + + public SyncHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + handleSyncHandlerMessage(msg); + } + + private void handleSyncHandlerMessage(Message msg) { + try { + switch (msg.what) { + case SyncHandler.MESSAGE_SYNC_FINISHED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_FINISHED"); + } + SyncHandlerMessagePayload payload = (SyncHandlerMessagePayload)msg.obj; + if (mActiveSyncContext != payload.activeSyncContext) { + if (Config.LOGD) { + Log.d(TAG, "handleSyncHandlerMessage: sync context doesn't match, " + + "dropping: mActiveSyncContext " + mActiveSyncContext + + " != " + payload.activeSyncContext); + } + return; + } + runSyncFinishedOrCanceled(payload.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) { + Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_SYNC_ALARM"); + } + mAlarmScheduleTime = null; + try { + if (mActiveSyncContext != null) { + if (isLoggable) { + Log.v(TAG, "handleSyncHandlerMessage: sync context is active"); + } + runStateSyncing(); + } + + // if the above call to runStateSyncing() resulted in the end of a sync, + // check if it is time to start a new sync + if (mActiveSyncContext == null) { + if (isLoggable) { + Log.v(TAG, "handleSyncHandlerMessage: " + + "sync context is not active"); + } + runStateIdle(); + } + } finally { + mHandleAlarmWakeLock.release(); + } + break; + } + + case SyncHandler.MESSAGE_CHECK_ALARMS: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "handleSyncHandlerMessage: MESSAGE_CHECK_ALARMS"); + } + // we do all the work for this case in the finally block + break; + } + } finally { + final boolean isSyncInProgress = mActiveSyncContext != null; + if (!isSyncInProgress) { + mSyncWakeLock.release(); + } + manageSyncNotification(); + manageErrorNotification(); + manageSyncAlarm(); + mSyncTimeTracker.update(); + } + } + + private void runStateSyncing() { + // if the sync timeout has been reached then cancel it + + ActiveSyncContext activeSyncContext = mActiveSyncContext; + + final long now = SystemClock.elapsedRealtime(); + if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) { + SyncOperation nextSyncOperation; + synchronized (mSyncQueue) { + nextSyncOperation = mSyncQueue.head(); + } + if (nextSyncOperation != null && nextSyncOperation.earliestRunTime <= now) { + if (Config.LOGD) { + Log.d(TAG, "canceling and rescheduling sync because it ran too long: " + + activeSyncContext.mSyncOperation); + } + rescheduleImmediately(activeSyncContext.mSyncOperation); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } else { + activeSyncContext.mTimeoutStartTime = now + MAX_TIME_PER_SYNC; + } + } + + // no need to schedule an alarm, as that will be done by our caller. + } + + private void runStateIdle() { + boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); + if (isLoggable) Log.v(TAG, "runStateIdle"); + + // If we aren't ready to run (e.g. the data connection is down), get out. + if (!mDataConnectionIsConnected) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: no data connection, skipping"); + } + setStatusText("No data connection"); + return; + } + + if (mStorageIsLow) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: memory low, skipping"); + } + setStatusText("Memory low"); + return; + } + + // 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; + if (accounts == null) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: accounts not known, skipping"); + } + setStatusText("Accounts not known yet"); + return; + } + + // 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 Sync.Settings.QueryMap syncSettings = getSyncSettings(); + final ConnectivityManager connManager = (ConnectivityManager) + mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + final boolean backgroundDataSetting = connManager.getBackgroundDataSetting(); + synchronized (mSyncQueue) { + while (true) { + syncOperation = mSyncQueue.head(); + if (syncOperation == null) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: no more sync operations, returning"); + } + return; + } + + // Sync is disabled, drop this operation. + if (!isSyncEnabled()) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: sync disabled, dropping " + syncOperation); + } + 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 + || !syncSettings.getListenForNetworkTickles() + || !syncSettings.getSyncProviderAutomatically( + syncOperation.authority))) { + if (isLoggable) { + Log.v(TAG, "runStateIdle: sync off, dropping " + syncOperation); + } + mSyncQueue.popHead(); + continue; + } + + // skip the sync if the account of this operation no longer exists + if (!ArrayUtils.contains(accounts, syncOperation.account)) { + mSyncQueue.popHead(); + if (isLoggable) { + Log.v(TAG, "runStateIdle: account not present, dropping " + + syncOperation); + } + continue; + } + + // go ahead and try to sync this syncOperation + if (isLoggable) { + Log.v(TAG, "runStateIdle: found sync candidate: " + syncOperation); + } + break; + } + + // 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 (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "runStateIdle: the time is " + now + " yet the next " + + "sync operation is for " + syncOperation.earliestRunTime + + ": " + syncOperation); + } + return; + } + + // 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); + } + 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; + } + } catch (RemoteException remoteExc) { + Log.e(TAG, "Caught a RemoteException while preparing for sync, rescheduling " + + syncOperation, remoteExc); + rescheduleWithDelay(syncOperation); + return; + } catch (RuntimeException exc) { + Log.e(TAG, "Caught a RuntimeException while validating sync of " + providerName, + exc); + return; + } + + final long historyRowId = insertStartSyncEvent(syncOperation); + + 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); + } catch (RemoteException remoteExc) { + if (Config.LOGD) { + Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc); + } + mActiveSyncContext = null; + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + rescheduleWithDelay(syncOperation); + } catch (RuntimeException exc) { + 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; + mActiveSyncContext = null; + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + + final SyncOperation syncOperation = activeSyncContext.mSyncOperation; + + final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime; + + String historyMessage; + int downstreamActivity; + int upstreamActivity; + if (syncResult != null) { + if (isLoggable) { + Log.v(TAG, "runSyncFinishedOrCanceled: is a finished: operation " + + syncOperation + ", result " + syncResult); + } + + if (!syncResult.hasError()) { + if (isLoggable) { + Log.v(TAG, "finished sync operation " + syncOperation); + } + historyMessage = History.MESG_SUCCESS; + // TODO: set these correctly when the SyncResult is extended to include it + downstreamActivity = 0; + upstreamActivity = 0; + } else { + maybeRescheduleSync(syncResult, syncOperation); + if (Config.LOGD) { + Log.d(TAG, "failed sync operation " + syncOperation); + } + historyMessage = Integer.toString(syncResultToErrorNumber(syncResult)); + // TODO: set these correctly when the SyncResult is extended to include it + downstreamActivity = 0; + upstreamActivity = 0; + } + } else { + if (isLoggable) { + 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 + } + historyMessage = History.MESG_CANCELED; + downstreamActivity = 0; + upstreamActivity = 0; + } + + stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage, + upstreamActivity, downstreamActivity, elapsedTime); + + mContentResolver.releaseProvider(activeSyncContext.mContentProvider); + + if (syncResult != null && syncResult.tooManyDeletions) { + installHandleTooManyDeletesNotification(syncOperation.account, + syncOperation.authority, syncResult.stats.numDeletes); + } else { + mNotificationMgr.cancel( + syncOperation.account.hashCode() ^ syncOperation.authority.hashCode()); + } + + if (syncResult != null && syncResult.fullSyncRequested) { + scheduleSyncOperation(new SyncOperation(syncOperation.account, + syncOperation.syncSource, syncOperation.authority, new Bundle(), 0)); + } + // no need to schedule an alarm, as that will be done by our caller. + } + + /** + * Convert the error-containing SyncResult into the Sync.History error number. Since + * the SyncResult may indicate multiple errors at once, this method just returns the + * most "serious" error. + * @param syncResult the SyncResult from which to read + * @return the most "serious" error set in the SyncResult + * @throws IllegalStateException if the SyncResult does not indicate any errors. + * If SyncResult.error() is true then it is safe to call this. + */ + private int syncResultToErrorNumber(SyncResult syncResult) { + if (syncResult.syncAlreadyInProgress) return History.ERROR_SYNC_ALREADY_IN_PROGRESS; + if (syncResult.stats.numAuthExceptions > 0) return History.ERROR_AUTHENTICATION; + if (syncResult.stats.numIoExceptions > 0) return History.ERROR_IO; + if (syncResult.stats.numParseExceptions > 0) return History.ERROR_PARSE; + if (syncResult.stats.numConflictDetectedExceptions > 0) return History.ERROR_CONFLICT; + if (syncResult.tooManyDeletions) return History.ERROR_TOO_MANY_DELETIONS; + if (syncResult.tooManyRetries) return History.ERROR_TOO_MANY_RETRIES; + if (syncResult.databaseError) return History.ERROR_INTERNAL; + throw new IllegalStateException("we are not in an error state, " + syncResult); + } + + private void manageSyncNotification() { + boolean shouldCancel; + boolean shouldInstall; + + if (mActiveSyncContext == null) { + mSyncNotificationInfo.startTime = null; + + // we aren't syncing. if the notification is active then remember that we need + // to cancel it and then clear out the info + shouldCancel = mSyncNotificationInfo.isActive; + shouldInstall = false; + } else { + // we are syncing + final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; + + final long now = SystemClock.elapsedRealtime(); + if (mSyncNotificationInfo.startTime == null) { + mSyncNotificationInfo.startTime = now; + } + + // cancel the notification if it is up and the authority or account is wrong + shouldCancel = mSyncNotificationInfo.isActive && + (!syncOperation.authority.equals(mSyncNotificationInfo.authority) + || !syncOperation.account.equals(mSyncNotificationInfo.account)); + + // there are four cases: + // - the notification is up and there is no change: do nothing + // - the notification is up but we should cancel since it is stale: + // need to install + // - the notification is not up but it isn't time yet: don't install + // - the notification is not up and it is time: need to install + + if (mSyncNotificationInfo.isActive) { + shouldInstall = shouldCancel; + } else { + final boolean timeToShowNotification = + now > mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY; + final boolean syncIsForced = syncOperation.extras + .getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + shouldInstall = timeToShowNotification || syncIsForced; + } + } + + if (shouldCancel && !shouldInstall) { + mNeedSyncActiveNotification = false; + sendSyncStateIntent(); + mSyncNotificationInfo.isActive = false; + } + + if (shouldInstall) { + SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; + mNeedSyncActiveNotification = true; + sendSyncStateIntent(); + mSyncNotificationInfo.isActive = true; + mSyncNotificationInfo.account = syncOperation.account; + mSyncNotificationInfo.authority = syncOperation.authority; + } + } + + /** + * Check if there were any long-lasting errors, if so install the error notification, + * otherwise cancel the error notification. + */ + private void manageErrorNotification() { + // + long when = mSyncStorageEngine.getInitialSyncFailureTime(); + if ((when > 0) && (when + ERROR_NOTIFICATION_DELAY_MS < System.currentTimeMillis())) { + if (!mErrorNotificationInstalled) { + mNeedSyncErrorNotification = true; + sendSyncStateIntent(); + } + mErrorNotificationInstalled = true; + } else { + if (mErrorNotificationInstalled) { + mNeedSyncErrorNotification = false; + sendSyncStateIntent(); + } + mErrorNotificationInstalled = false; + } + } + + private void manageSyncAlarm() { + // in each of these cases the sync loop will be kicked, which will cause this + // method to be called again + if (!mDataConnectionIsConnected) return; + if (mAccounts == null) return; + if (mStorageIsLow) return; + + // Compute the alarm fire time: + // - not syncing: time of the next sync operation + // - syncing, no notification: time from sync start to notification create time + // - syncing, with notification: time till timeout of the active sync operation + Long alarmTime = null; + ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (activeSyncContext == null) { + SyncOperation syncOperation; + synchronized (mSyncQueue) { + syncOperation = mSyncQueue.head(); + } + if (syncOperation != null) { + alarmTime = syncOperation.earliestRunTime; + } + } else { + final long notificationTime = + mSyncHandler.mSyncNotificationInfo.startTime + SYNC_NOTIFICATION_DELAY; + final long timeoutTime = + mActiveSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC; + if (mSyncHandler.mSyncNotificationInfo.isActive) { + alarmTime = timeoutTime; + } else { + alarmTime = Math.min(notificationTime, timeoutTime); + } + } + + // adjust the alarmTime so that we will wake up when it is time to + // install the error notification + if (!mErrorNotificationInstalled) { + long when = mSyncStorageEngine.getInitialSyncFailureTime(); + if (when > 0) { + when += ERROR_NOTIFICATION_DELAY_MS; + // convert when fron absolute time to elapsed run time + long delay = when - System.currentTimeMillis(); + when = SystemClock.elapsedRealtime() + delay; + alarmTime = alarmTime != null ? Math.min(alarmTime, when) : when; + } + } + + // determine if we need to set or cancel the alarm + boolean shouldSet = false; + boolean shouldCancel = false; + final boolean alarmIsActive = mAlarmScheduleTime != null; + final boolean needAlarm = alarmTime != null; + if (needAlarm) { + if (!alarmIsActive || alarmTime < mAlarmScheduleTime) { + shouldSet = true; + } + } else { + shouldCancel = alarmIsActive; + } + + // set or cancel the alarm as directed + ensureAlarmService(); + if (shouldSet) { + mAlarmScheduleTime = alarmTime; + mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime, + mSyncAlarmIntent); + } else if (shouldCancel) { + mAlarmScheduleTime = null; + mAlarmService.cancel(mSyncAlarmIntent); + } + } + + private void sendSyncStateIntent() { + Intent syncStateIntent = new Intent(Intent.ACTION_SYNC_STATE_CHANGED); + syncStateIntent.putExtra("active", mNeedSyncActiveNotification); + syncStateIntent.putExtra("failing", mNeedSyncErrorNotification); + mContext.sendBroadcast(syncStateIntent); + } + + private void installHandleTooManyDeletesNotification(String account, String authority, + long numDeletes) { + if (mNotificationMgr == null) return; + Intent clickIntent = new Intent(); + clickIntent.setClassName("com.android.providers.subscribedfeeds", + "com.android.settings.SyncActivityTooManyDeletes"); + clickIntent.putExtra("account", account); + clickIntent.putExtra("provider", authority); + clickIntent.putExtra("numDeletes", numDeletes); + + if (!isActivityAvailable(clickIntent)) { + Log.w(TAG, "No activity found to handle too many deletes."); + return; + } + + final PendingIntent pendingIntent = PendingIntent + .getActivity(mContext, 0, clickIntent, PendingIntent.FLAG_CANCEL_CURRENT); + + 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]), + pendingIntent); + notification.flags |= Notification.FLAG_ONGOING_EVENT; + mNotificationMgr.notify(account.hashCode() ^ authority.hashCode(), notification); + } + + /** + * Checks whether an activity exists on the system image for the given intent. + * + * @param intent The intent for an activity. + * @return Whether or not an activity exists. + */ + private boolean isActivityAvailable(Intent intent) { + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); + int listSize = list.size(); + for (int i = 0; i < listSize; i++) { + ResolveInfo resolveInfo = list.get(i); + if ((resolveInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) + != 0) { + return true; + } + } + + return false; + } + + public long insertStartSyncEvent(SyncOperation syncOperation) { + final int source = syncOperation.syncSource; + final long now = System.currentTimeMillis(); + + EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_START, source); + + return mSyncStorageEngine.insertStartSyncEvent( + syncOperation.account, syncOperation.authority, now, source); + } + + public void stopSyncEvent(long rowId, SyncOperation syncOperation, String resultMessage, + int upstreamActivity, int downstreamActivity, long elapsedTime) { + EventLog.writeEvent(2720, syncOperation.authority, Sync.History.EVENT_STOP, syncOperation.syncSource); + + mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage, + downstreamActivity, upstreamActivity); + } + } + + static class SyncQueue { + private SyncStorageEngine mSyncStorageEngine; + private final String[] COLUMNS = new String[]{ + "_id", + "authority", + "account", + "extras", + "source" + }; + private static final int COLUMN_ID = 0; + private static final int COLUMN_AUTHORITY = 1; + private static final int COLUMN_ACCOUNT = 2; + private static final int COLUMN_EXTRAS = 3; + private static final int COLUMN_SOURCE = 4; + + private static final boolean DEBUG_CHECK_DATA_CONSISTENCY = false; + + // A priority queue of scheduled SyncOperations that is designed to make it quick + // to find the next SyncOperation that should be considered for running. + private final PriorityQueue<SyncOperation> mOpsByWhen = new PriorityQueue<SyncOperation>(); + + // A Map of SyncOperations operationKey -> SyncOperation that is designed for + // quick lookup of an enqueued SyncOperation. + private final HashMap<String, SyncOperation> mOpsByKey = Maps.newHashMap(); + + public SyncQueue(SyncStorageEngine syncStorageEngine) { + mSyncStorageEngine = syncStorageEngine; + Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS); + try { + while (cursor.moveToNext()) { + add(cursorToOperation(cursor), + true /* this is being added from the database */); + } + } finally { + cursor.close(); + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + } + } + + public boolean add(SyncOperation operation) { + return add(new SyncOperation(operation), + false /* this is not coming from the database */); + } + + private boolean add(SyncOperation operation, boolean fromDatabase) { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + + // If this operation is expedited then set its earliestRunTime to be immediately + // before the head of the list, or not if none are in the list. + if (operation.delay < 0) { + SyncOperation headOperation = head(); + if (headOperation != null) { + operation.earliestRunTime = Math.min(SystemClock.elapsedRealtime(), + headOperation.earliestRunTime - 1); + } else { + operation.earliestRunTime = SystemClock.elapsedRealtime(); + } + } + + // - if an operation with the same key exists and this one should run earlier, + // delete the old one and add the new one + // - if an operation with the same key exists and if this one should run + // later, ignore it + // - if no operation exists then add the new one + final String operationKey = operation.key; + 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 + if (existingOperation != null && existingOperation.delay > 0) { + if (!operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false)) { + return false; + } + } + + if (existingOperation != null + && operation.earliestRunTime >= existingOperation.earliestRunTime) { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + return false; + } + + if (existingOperation != null) { + removeByKey(operationKey); + } + + if (operation.rowId == null) { + byte[] extrasData = null; + Parcel parcel = Parcel.obtain(); + try { + operation.extras.writeToParcel(parcel, 0); + extrasData = parcel.marshall(); + } finally { + parcel.recycle(); + } + ContentValues values = new ContentValues(); + values.put("account", operation.account); + values.put("authority", operation.authority); + values.put("source", operation.syncSource); + values.put("extras", extrasData); + Uri pendingUri = mSyncStorageEngine.insertIntoPending(values); + operation.rowId = pendingUri == null ? null : ContentUris.parseId(pendingUri); + if (operation.rowId == null) { + throw new IllegalStateException("error adding pending sync operation " + + operation); + } + } + + if (DEBUG_CHECK_DATA_CONSISTENCY) { + debugCheckDataStructures( + false /* don't compare with the DB, since we know + it is inconsistent right now */ ); + } + mOpsByKey.put(operationKey, operation); + mOpsByWhen.add(operation); + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(!fromDatabase); + return true; + } + + public void removeByKey(String operationKey) { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + SyncOperation operationToRemove = mOpsByKey.remove(operationKey); + if (!mOpsByWhen.remove(operationToRemove)) { + throw new IllegalStateException( + "unable to find " + operationToRemove + " in mOpsByWhen"); + } + + if (mSyncStorageEngine.deleteFromPending(operationToRemove.rowId) != 1) { + throw new IllegalStateException("unable to find pending row for " + + operationToRemove); + } + + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + } + + public SyncOperation head() { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + return mOpsByWhen.peek(); + } + + public void popHead() { + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + SyncOperation operation = mOpsByWhen.remove(); + if (mOpsByKey.remove(operation.key) == null) { + throw new IllegalStateException("unable to find " + operation + " in mOpsByKey"); + } + + if (mSyncStorageEngine.deleteFromPending(operation.rowId) != 1) { + throw new IllegalStateException("unable to find pending row for " + operation); + } + + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + } + + public void clear(String account, String authority) { + Iterator<Map.Entry<String, SyncOperation>> entries = mOpsByKey.entrySet().iterator(); + while (entries.hasNext()) { + Map.Entry<String, SyncOperation> entry = entries.next(); + SyncOperation syncOperation = entry.getValue(); + if (account != null && !syncOperation.account.equals(account)) continue; + if (authority != null && !syncOperation.authority.equals(authority)) continue; + + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + entries.remove(); + if (!mOpsByWhen.remove(syncOperation)) { + throw new IllegalStateException( + "unable to find " + syncOperation + " in mOpsByWhen"); + } + + if (mSyncStorageEngine.deleteFromPending(syncOperation.rowId) != 1) { + throw new IllegalStateException("unable to find pending row for " + + syncOperation); + } + + if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); + } + } + + public void dump(StringBuilder sb) { + sb.append("SyncQueue: ").append(mOpsByWhen.size()).append(" operation(s)\n"); + for (SyncOperation operation : mOpsByWhen) { + sb.append(operation).append("\n"); + } + } + + private void debugCheckDataStructures(boolean checkDatabase) { + if (mOpsByKey.size() != mOpsByWhen.size()) { + throw new IllegalStateException("size mismatch: " + + mOpsByKey .size() + " != " + mOpsByWhen.size()); + } + for (SyncOperation operation : mOpsByWhen) { + if (!mOpsByKey.containsKey(operation.key)) { + throw new IllegalStateException( + "operation " + operation + " is in mOpsByWhen but not mOpsByKey"); + } + } + for (Map.Entry<String, SyncOperation> entry : mOpsByKey.entrySet()) { + final SyncOperation operation = entry.getValue(); + final String key = entry.getKey(); + if (!key.equals(operation.key)) { + throw new IllegalStateException( + "operation " + operation + " in mOpsByKey doesn't match key " + key); + } + if (!mOpsByWhen.contains(operation)) { + throw new IllegalStateException( + "operation " + operation + " is in mOpsByKey but not mOpsByWhen"); + } + } + + if (checkDatabase) { + // check that the DB contains the same rows as the in-memory data structures + Cursor cursor = mSyncStorageEngine.getPendingSyncsCursor(COLUMNS); + try { + if (mOpsByKey.size() != cursor.getCount()) { + StringBuilder sb = new StringBuilder(); + DatabaseUtils.dumpCursor(cursor, sb); + sb.append("\n"); + dump(sb); + throw new IllegalStateException("DB size mismatch: " + + mOpsByKey .size() + " != " + cursor.getCount() + "\n" + + sb.toString()); + } + } finally { + cursor.close(); + } + } + } + + private SyncOperation cursorToOperation(Cursor cursor) { + byte[] extrasData = cursor.getBlob(COLUMN_EXTRAS); + Bundle extras; + Parcel parcel = Parcel.obtain(); + try { + parcel.unmarshall(extrasData, 0, extrasData.length); + parcel.setDataPosition(0); + extras = parcel.readBundle(); + } catch (RuntimeException e) { + // A RuntimeException is thrown if we were unable to parse the parcel. + // Create an empty parcel in this case. + extras = new Bundle(); + } finally { + parcel.recycle(); + } + + SyncOperation syncOperation = new SyncOperation( + cursor.getString(COLUMN_ACCOUNT), + cursor.getInt(COLUMN_SOURCE), + cursor.getString(COLUMN_AUTHORITY), + extras, + 0 /* delay */); + syncOperation.rowId = cursor.getLong(COLUMN_ID); + return syncOperation; + } + } +} diff --git a/core/java/android/content/SyncProvider.java b/core/java/android/content/SyncProvider.java new file mode 100644 index 0000000..6ddd046 --- /dev/null +++ b/core/java/android/content/SyncProvider.java @@ -0,0 +1,53 @@ +// Copyright 2007 The Android Open Source Project +package android.content; + +import android.database.Cursor; +import android.net.Uri; + +/** + * ContentProvider that tracks the sync data and overall sync + * history on the device. + * + * @hide + */ +public class SyncProvider extends ContentProvider { + public SyncProvider() { + } + + private SyncStorageEngine mSyncStorageEngine; + + @Override + public boolean onCreate() { + mSyncStorageEngine = SyncStorageEngine.getSingleton(); + return true; + } + + @Override + public Cursor query(Uri url, String[] projectionIn, + String selection, String[] selectionArgs, String sort) { + return mSyncStorageEngine.query(url, projectionIn, selection, selectionArgs, sort); + } + + @Override + public Uri insert(Uri url, ContentValues initialValues) { + return mSyncStorageEngine.insert(true /* the caller is the provider */, + url, initialValues); + } + + @Override + public int delete(Uri url, String where, String[] whereArgs) { + return mSyncStorageEngine.delete(true /* the caller is the provider */, + url, where, whereArgs); + } + + @Override + public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) { + return mSyncStorageEngine.update(true /* the caller is the provider */, + url, initialValues, where, whereArgs); + } + + @Override + public String getType(Uri url) { + return mSyncStorageEngine.getType(url); + } +} diff --git a/core/java/android/content/SyncResult.aidl b/core/java/android/content/SyncResult.aidl new file mode 100644 index 0000000..061b81c --- /dev/null +++ b/core/java/android/content/SyncResult.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 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 SyncResult; diff --git a/core/java/android/content/SyncResult.java b/core/java/android/content/SyncResult.java new file mode 100644 index 0000000..f3260f3 --- /dev/null +++ b/core/java/android/content/SyncResult.java @@ -0,0 +1,178 @@ +package android.content; + +import android.os.Parcel; +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; + public boolean tooManyDeletions; + public boolean tooManyRetries; + public boolean databaseError; + public boolean fullSyncRequested; + public boolean partialSyncUnavailable; + public boolean moreRecordsToGet; + public final SyncStats stats; + public static final SyncResult ALREADY_IN_PROGRESS; + + static { + ALREADY_IN_PROGRESS = new SyncResult(true); + } + + public SyncResult() { + this(false); + } + + private SyncResult(boolean syncAlreadyInProgress) { + this.syncAlreadyInProgress = syncAlreadyInProgress; + this.tooManyDeletions = false; + this.tooManyRetries = false; + this.fullSyncRequested = false; + this.partialSyncUnavailable = false; + this.moreRecordsToGet = false; + this.stats = new SyncStats(); + } + + private SyncResult(Parcel parcel) { + syncAlreadyInProgress = parcel.readInt() != 0; + tooManyDeletions = parcel.readInt() != 0; + tooManyRetries = parcel.readInt() != 0; + databaseError = parcel.readInt() != 0; + fullSyncRequested = parcel.readInt() != 0; + partialSyncUnavailable = parcel.readInt() != 0; + moreRecordsToGet = parcel.readInt() != 0; + stats = new SyncStats(parcel); + } + + public boolean hasHardError() { + return stats.numParseExceptions > 0 + || stats.numConflictDetectedExceptions > 0 + || stats.numAuthExceptions > 0 + || tooManyDeletions + || tooManyRetries + || databaseError; + } + + public boolean hasSoftError() { + return syncAlreadyInProgress || stats.numIoExceptions > 0; + } + + public boolean hasError() { + return hasSoftError() || hasHardError(); + } + + public boolean madeSomeProgress() { + return ((stats.numDeletes > 0) && !tooManyDeletions) + || stats.numInserts > 0 + || stats.numUpdates > 0; + } + + public void clear() { + if (syncAlreadyInProgress) { + throw new UnsupportedOperationException( + "you are not allowed to clear the ALREADY_IN_PROGRESS SyncStats"); + } + tooManyDeletions = false; + tooManyRetries = false; + databaseError = false; + fullSyncRequested = false; + partialSyncUnavailable = false; + moreRecordsToGet = false; + stats.clear(); + } + + public static final Creator<SyncResult> CREATOR = new Creator<SyncResult>() { + public SyncResult createFromParcel(Parcel in) { + return new SyncResult(in); + } + + public SyncResult[] newArray(int size) { + return new SyncResult[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(syncAlreadyInProgress ? 1 : 0); + parcel.writeInt(tooManyDeletions ? 1 : 0); + parcel.writeInt(tooManyRetries ? 1 : 0); + parcel.writeInt(databaseError ? 1 : 0); + parcel.writeInt(fullSyncRequested ? 1 : 0); + parcel.writeInt(partialSyncUnavailable ? 1 : 0); + parcel.writeInt(moreRecordsToGet ? 1 : 0); + stats.writeToParcel(parcel, flags); + } + + @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); + return sb.toString(); + } + + /** + * Generates a debugging string indicating the status. + * The string consist of a sequence of code letter followed by the count. + * Code letters are f - fullSyncRequested, r - partialSyncUnavailable, + * X - hardError, e - numParseExceptions, c - numConflictDetectedExceptions, + * a - numAuthExceptions, D - tooManyDeletions, R - tooManyRetries, + * b - databaseError, x - softError, l - syncAlreadyInProgress, + * I - numIoExceptions + * @return debugging string. + */ + public String toDebugString() { + StringBuffer sb = new StringBuffer(); + + if (fullSyncRequested) { + sb.append("f1"); + } + if (partialSyncUnavailable) { + sb.append("r1"); + } + if (hasHardError()) { + sb.append("X1"); + } + if (stats.numParseExceptions > 0) { + sb.append("e").append(stats.numParseExceptions); + } + if (stats.numConflictDetectedExceptions > 0) { + sb.append("c").append(stats.numConflictDetectedExceptions); + } + if (stats.numAuthExceptions > 0) { + sb.append("a").append(stats.numAuthExceptions); + } + if (tooManyDeletions) { + sb.append("D1"); + } + if (tooManyRetries) { + sb.append("R1"); + } + if (databaseError) { + sb.append("b1"); + } + if (hasSoftError()) { + sb.append("x1"); + } + if (syncAlreadyInProgress) { + sb.append("l1"); + } + if (stats.numIoExceptions > 0) { + sb.append("I").append(stats.numIoExceptions); + } + return sb.toString(); + } +} diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java new file mode 100644 index 0000000..f503e6f --- /dev/null +++ b/core/java/android/content/SyncStateContentProviderHelper.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 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; + +import com.android.internal.util.ArrayUtils; + +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +/** + * Extends the schema of a ContentProvider to include the _sync_state table + * and implements query/insert/update/delete to access that table using the + * authority "syncstate". This can be used to store the sync state for a + * set of accounts. + * + * @hide + */ +public class SyncStateContentProviderHelper { + final SQLiteOpenHelper mOpenHelper; + + private static final String SYNC_STATE_AUTHORITY = "syncstate"; + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private static final int STATE = 0; + + private static final Uri CONTENT_URI = + Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); + + private static final String ACCOUNT_WHERE = "_sync_account = ?"; + + private final Provider mInternalProviderInterface; + + private static final String SYNC_STATE_TABLE = "_sync_state"; + private static long DB_VERSION = 2; + + private static final String[] ACCOUNT_PROJECTION = new String[]{"_sync_account"}; + + static { + sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); + } + + public SyncStateContentProviderHelper(SQLiteOpenHelper openHelper) { + mOpenHelper = openHelper; + mInternalProviderInterface = new Provider(); + } + + public ContentProvider asContentProvider() { + return mInternalProviderInterface; + } + + public void createDatabase(SQLiteDatabase db) { + db.execSQL("DROP TABLE IF EXISTS _sync_state"); + db.execSQL("CREATE TABLE _sync_state (" + + "_id INTEGER PRIMARY KEY," + + "_sync_account TEXT," + + "data TEXT," + + "UNIQUE(_sync_account)" + + ");"); + + db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); + db.execSQL("CREATE TABLE _sync_state_metadata (" + + "version INTEGER" + + ");"); + ContentValues values = new ContentValues(); + values.put("version", DB_VERSION); + db.insert("_sync_state_metadata", "version", values); + } + + protected void onDatabaseOpened(SQLiteDatabase db) { + long version = DatabaseUtils.longForQuery(db, + "select version from _sync_state_metadata", null); + if (version != DB_VERSION) { + createDatabase(db); + } + } + + class Provider extends ContentProvider { + public boolean onCreate() { + throw new UnsupportedOperationException("not implemented"); + } + + public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + int match = sURIMatcher.match(url); + switch (match) { + case STATE: + return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs, + null, null, sortOrder); + default: + throw new UnsupportedOperationException("Cannot query URL: " + url); + } + } + + public String getType(Uri uri) { + throw new UnsupportedOperationException("not implemented"); + } + + public Uri insert(Uri url, ContentValues values) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int match = sURIMatcher.match(url); + switch (match) { + case STATE: { + long id = db.insert(SYNC_STATE_TABLE, "feed", values); + return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); + } + default: + throw new UnsupportedOperationException("Cannot insert into URL: " + url); + } + } + + public int delete(Uri url, String userWhere, String[] whereArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (sURIMatcher.match(url)) { + case STATE: + return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs); + default: + throw new IllegalArgumentException("Unknown URL " + url); + } + + } + + public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + switch (sURIMatcher.match(url)) { + case STATE: + return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs); + default: + throw new UnsupportedOperationException("Cannot update URL: " + url); + } + + } + } + + /** + * Check if the url matches content that this ContentProvider manages. + * @param url the Uri to check + * @return true if this ContentProvider can handle that Uri. + */ + public boolean matches(Uri url) { + return (SYNC_STATE_AUTHORITY.equals(url.getAuthority())); + } + + /** + * Replaces the contents of the _sync_state table in the destination ContentProvider + * with the row that matches account, if any, in the source ContentProvider. + * <p> + * The ContentProviders must expose the _sync_state table as URI content://syncstate/state. + * @param dbSrc the database to read from + * @param dbDest the database to write to + * @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_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)); + dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); + } + } finally { + c.close(); + } + } + + public void onAccountsChanged(String[] 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); + if (!ArrayUtils.contains(accounts, account)) { + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + } + } + } finally { + c.close(); + } + } + + public void discardSyncData(SQLiteDatabase db, String account) { + if (account != null) { + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + } else { + db.delete(SYNC_STATE_TABLE, null, null); + } + } + + /** + * Retrieves the SyncData bytes for the given account. The byte array returned may be null. + */ + public byte[] readSyncDataBytes(SQLiteDatabase db, String account) { + Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, + new String[]{account}, null, null, null); + try { + if (c.moveToFirst()) { + return c.getBlob(c.getColumnIndexOrThrow("data")); + } + } finally { + c.close(); + } + return null; + } + + /** + * Sets the SyncData bytes for the given account. The bytes array may be null. + */ + public void writeSyncDataBytes(SQLiteDatabase db, String account, byte[] data) { + ContentValues values = new ContentValues(); + values.put("data", data); + db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, new String[]{account}); + } +} diff --git a/core/java/android/content/SyncStats.aidl b/core/java/android/content/SyncStats.aidl new file mode 100644 index 0000000..dff0ebf --- /dev/null +++ b/core/java/android/content/SyncStats.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 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 SyncStats; diff --git a/core/java/android/content/SyncStats.java b/core/java/android/content/SyncStats.java new file mode 100644 index 0000000..b561b05 --- /dev/null +++ b/core/java/android/content/SyncStats.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 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; + +import android.os.Parcelable; +import android.os.Parcel; + +/** + * @hide + */ +public class SyncStats implements Parcelable { + public long numAuthExceptions; + public long numIoExceptions; + public long numParseExceptions; + public long numConflictDetectedExceptions; + public long numInserts; + public long numUpdates; + public long numDeletes; + public long numEntries; + public long numSkippedEntries; + + public SyncStats() { + numAuthExceptions = 0; + numIoExceptions = 0; + numParseExceptions = 0; + numConflictDetectedExceptions = 0; + numInserts = 0; + numUpdates = 0; + numDeletes = 0; + numEntries = 0; + numSkippedEntries = 0; + } + + public SyncStats(Parcel in) { + numAuthExceptions = in.readLong(); + numIoExceptions = in.readLong(); + numParseExceptions = in.readLong(); + numConflictDetectedExceptions = in.readLong(); + numInserts = in.readLong(); + numUpdates = in.readLong(); + numDeletes = in.readLong(); + numEntries = in.readLong(); + numSkippedEntries = in.readLong(); + } + + @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); + return sb.toString(); + } + + public void clear() { + numAuthExceptions = 0; + numIoExceptions = 0; + numParseExceptions = 0; + numConflictDetectedExceptions = 0; + numInserts = 0; + numUpdates = 0; + numDeletes = 0; + numEntries = 0; + numSkippedEntries = 0; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(numAuthExceptions); + dest.writeLong(numIoExceptions); + dest.writeLong(numParseExceptions); + dest.writeLong(numConflictDetectedExceptions); + dest.writeLong(numInserts); + dest.writeLong(numUpdates); + dest.writeLong(numDeletes); + dest.writeLong(numEntries); + dest.writeLong(numSkippedEntries); + } + + public static final Creator<SyncStats> CREATOR = new Creator<SyncStats>() { + public SyncStats createFromParcel(Parcel in) { + return new SyncStats(in); + } + + public SyncStats[] newArray(int size) { + return new SyncStats[size]; + } + }; +} diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java new file mode 100644 index 0000000..282f6e7 --- /dev/null +++ b/core/java/android/content/SyncStorageEngine.java @@ -0,0 +1,758 @@ +package android.content; + +import android.Manifest; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.provider.Sync; +import android.text.TextUtils; +import android.util.Config; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +/** + * ContentProvider that tracks the sync data and overall sync + * history on the device. + * + * @hide + */ +public class SyncStorageEngine { + private static final String TAG = "SyncManager"; + + private static final String DATABASE_NAME = "syncmanager.db"; + private static final int DATABASE_VERSION = 10; + + private static final int STATS = 1; + private static final int STATS_ID = 2; + private static final int HISTORY = 3; + private static final int HISTORY_ID = 4; + private static final int SETTINGS = 5; + private static final int PENDING = 7; + private static final int ACTIVE = 8; + private static final int STATUS = 9; + + private static final UriMatcher sURLMatcher = + new UriMatcher(UriMatcher.NO_MATCH); + + private static final HashMap<String,String> HISTORY_PROJECTION_MAP; + private static final HashMap<String,String> PENDING_PROJECTION_MAP; + private static final HashMap<String,String> ACTIVE_PROJECTION_MAP; + private static final HashMap<String,String> STATUS_PROJECTION_MAP; + + private final Context mContext; + private final SQLiteOpenHelper mOpenHelper; + private static SyncStorageEngine sSyncStorageEngine = null; + + static { + sURLMatcher.addURI("sync", "stats", STATS); + sURLMatcher.addURI("sync", "stats/#", STATS_ID); + sURLMatcher.addURI("sync", "history", HISTORY); + sURLMatcher.addURI("sync", "history/#", HISTORY_ID); + sURLMatcher.addURI("sync", "settings", SETTINGS); + sURLMatcher.addURI("sync", "status", STATUS); + sURLMatcher.addURI("sync", "active", ACTIVE); + sURLMatcher.addURI("sync", "pending", PENDING); + + HashMap<String,String> map; + PENDING_PROJECTION_MAP = map = new HashMap<String,String>(); + map.put(Sync.History._ID, Sync.History._ID); + map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT); + map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY); + + ACTIVE_PROJECTION_MAP = map = new HashMap<String,String>(); + map.put(Sync.History._ID, Sync.History._ID); + map.put(Sync.History.ACCOUNT, Sync.History.ACCOUNT); + map.put(Sync.History.AUTHORITY, Sync.History.AUTHORITY); + map.put("startTime", "startTime"); + + HISTORY_PROJECTION_MAP = map = new HashMap<String,String>(); + map.put(Sync.History._ID, "history._id as _id"); + map.put(Sync.History.ACCOUNT, "stats.account as account"); + map.put(Sync.History.AUTHORITY, "stats.authority as authority"); + map.put(Sync.History.EVENT, Sync.History.EVENT); + map.put(Sync.History.EVENT_TIME, Sync.History.EVENT_TIME); + map.put(Sync.History.ELAPSED_TIME, Sync.History.ELAPSED_TIME); + map.put(Sync.History.SOURCE, Sync.History.SOURCE); + map.put(Sync.History.UPSTREAM_ACTIVITY, Sync.History.UPSTREAM_ACTIVITY); + map.put(Sync.History.DOWNSTREAM_ACTIVITY, Sync.History.DOWNSTREAM_ACTIVITY); + map.put(Sync.History.MESG, Sync.History.MESG); + + STATUS_PROJECTION_MAP = map = new HashMap<String,String>(); + map.put(Sync.Status._ID, "status._id as _id"); + map.put(Sync.Status.ACCOUNT, "stats.account as account"); + map.put(Sync.Status.AUTHORITY, "stats.authority as authority"); + map.put(Sync.Status.TOTAL_ELAPSED_TIME, Sync.Status.TOTAL_ELAPSED_TIME); + map.put(Sync.Status.NUM_SYNCS, Sync.Status.NUM_SYNCS); + map.put(Sync.Status.NUM_SOURCE_LOCAL, Sync.Status.NUM_SOURCE_LOCAL); + map.put(Sync.Status.NUM_SOURCE_POLL, Sync.Status.NUM_SOURCE_POLL); + map.put(Sync.Status.NUM_SOURCE_SERVER, Sync.Status.NUM_SOURCE_SERVER); + map.put(Sync.Status.NUM_SOURCE_USER, Sync.Status.NUM_SOURCE_USER); + map.put(Sync.Status.LAST_SUCCESS_SOURCE, Sync.Status.LAST_SUCCESS_SOURCE); + map.put(Sync.Status.LAST_SUCCESS_TIME, Sync.Status.LAST_SUCCESS_TIME); + map.put(Sync.Status.LAST_FAILURE_SOURCE, Sync.Status.LAST_FAILURE_SOURCE); + map.put(Sync.Status.LAST_FAILURE_TIME, Sync.Status.LAST_FAILURE_TIME); + map.put(Sync.Status.LAST_FAILURE_MESG, Sync.Status.LAST_FAILURE_MESG); + map.put(Sync.Status.PENDING, Sync.Status.PENDING); + } + + private static final String[] STATS_ACCOUNT_PROJECTION = + new String[] { Sync.Stats.ACCOUNT }; + + private static final int MAX_HISTORY_EVENTS_TO_KEEP = 5000; + + private static final String SELECT_INITIAL_FAILURE_TIME_QUERY_STRING = "" + + "SELECT min(a) " + + "FROM (" + + " SELECT initialFailureTime AS a " + + " FROM status " + + " WHERE stats_id=? AND a IS NOT NULL " + + " UNION " + + " SELECT ? AS a" + + " )"; + + private SyncStorageEngine(Context context) { + mContext = context; + mOpenHelper = new SyncStorageEngine.DatabaseHelper(context); + sSyncStorageEngine = this; + } + + public static SyncStorageEngine newTestInstance(Context context) { + return new SyncStorageEngine(context); + } + + public static void init(Context context) { + if (sSyncStorageEngine != null) { + throw new IllegalStateException("already initialized"); + } + sSyncStorageEngine = new SyncStorageEngine(context); + } + + public static SyncStorageEngine getSingleton() { + if (sSyncStorageEngine == null) { + throw new IllegalStateException("not initialized"); + } + return sSyncStorageEngine; + } + + private class DatabaseHelper extends SQLiteOpenHelper { + DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE pending (" + + "_id INTEGER PRIMARY KEY," + + "authority TEXT NOT NULL," + + "account TEXT NOT NULL," + + "extras BLOB NOT NULL," + + "source INTEGER NOT NULL" + + ");"); + + db.execSQL("CREATE TABLE stats (" + + "_id INTEGER PRIMARY KEY," + + "account TEXT, " + + "authority TEXT, " + + "syncdata TEXT, " + + "UNIQUE (account, authority)" + + ");"); + + db.execSQL("CREATE TABLE history (" + + "_id INTEGER PRIMARY KEY," + + "stats_id INTEGER," + + "eventTime INTEGER," + + "elapsedTime INTEGER," + + "source INTEGER," + + "event INTEGER," + + "upstreamActivity INTEGER," + + "downstreamActivity INTEGER," + + "mesg TEXT);"); + + db.execSQL("CREATE TABLE status (" + + "_id INTEGER PRIMARY KEY," + + "stats_id INTEGER NOT NULL," + + "totalElapsedTime INTEGER NOT NULL DEFAULT 0," + + "numSyncs INTEGER NOT NULL DEFAULT 0," + + "numSourcePoll INTEGER NOT NULL DEFAULT 0," + + "numSourceServer INTEGER NOT NULL DEFAULT 0," + + "numSourceLocal INTEGER NOT NULL DEFAULT 0," + + "numSourceUser INTEGER NOT NULL DEFAULT 0," + + "lastSuccessTime INTEGER," + + "lastSuccessSource INTEGER," + + "lastFailureTime INTEGER," + + "lastFailureSource INTEGER," + + "lastFailureMesg STRING," + + "initialFailureTime INTEGER," + + "pending INTEGER NOT NULL DEFAULT 0);"); + + db.execSQL("CREATE TABLE active (" + + "_id INTEGER PRIMARY KEY," + + "authority TEXT," + + "account TEXT," + + "startTime INTEGER);"); + + db.execSQL("CREATE INDEX historyEventTime ON history (eventTime)"); + + db.execSQL("CREATE TABLE settings (" + + "name TEXT PRIMARY KEY," + + "value TEXT);"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion == 9 && newVersion == 10) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will preserve old data"); + db.execSQL("ALTER TABLE status ADD COLUMN initialFailureTime INTEGER"); + return; + } + + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS pending"); + db.execSQL("DROP TABLE IF EXISTS stats"); + db.execSQL("DROP TABLE IF EXISTS history"); + db.execSQL("DROP TABLE IF EXISTS settings"); + db.execSQL("DROP TABLE IF EXISTS active"); + db.execSQL("DROP TABLE IF EXISTS status"); + onCreate(db); + } + + @Override + public void onOpen(SQLiteDatabase db) { + if (!db.isReadOnly()) { + db.delete("active", null, null); + db.insert("active", "account", null); + } + } + } + + protected void doDatabaseCleanup(String[] accounts) { + HashSet<String> currentAccounts = new HashSet<String>(); + for (String account : accounts) currentAccounts.add(account); + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = db.query("stats", STATS_ACCOUNT_PROJECTION, + null /* where */, null /* where args */, Sync.Stats.ACCOUNT, + null /* having */, null /* order by */); + try { + while (cursor.moveToNext()) { + String account = cursor.getString(0); + if (TextUtils.isEmpty(account)) { + continue; + } + if (!currentAccounts.contains(account)) { + String where = Sync.Stats.ACCOUNT + "=?"; + int numDeleted; + numDeleted = db.delete("stats", where, new String[]{account}); + if (Config.LOGD) { + Log.d(TAG, "deleted " + numDeleted + + " records from stats table" + + " for account " + account); + } + } + } + } finally { + cursor.close(); + } + } + + protected void setActiveSync(SyncManager.ActiveSyncContext activeSyncContext) { + if (activeSyncContext != null) { + updateActiveSync(activeSyncContext.mSyncOperation.account, + activeSyncContext.mSyncOperation.authority, activeSyncContext.mStartTime); + } else { + // we indicate that the sync is not active by passing null for all the parameters + updateActiveSync(null, null, null); + } + } + + private int updateActiveSync(String account, String authority, Long startTime) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("account", account); + values.put("authority", authority); + values.put("startTime", startTime); + int numChanges = db.update("active", values, null, null); + if (numChanges > 0) { + mContext.getContentResolver().notifyChange(Sync.Active.CONTENT_URI, + null /* this change wasn't made through an observer */); + } + return numChanges; + } + + /** + * Implements the {@link ContentProvider#query} method + */ + public Cursor query(Uri url, String[] projectionIn, + String selection, String[] selectionArgs, String sort) { + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); + + // Generate the body of the query + int match = sURLMatcher.match(url); + String groupBy = null; + switch (match) { + case STATS: + qb.setTables("stats"); + break; + case STATS_ID: + qb.setTables("stats"); + qb.appendWhere("_id="); + qb.appendWhere(url.getPathSegments().get(1)); + break; + case HISTORY: + // join the stats and history tables, so the caller can get + // the account and authority information as part of this query. + qb.setTables("stats, history"); + qb.setProjectionMap(HISTORY_PROJECTION_MAP); + qb.appendWhere("stats._id = history.stats_id"); + break; + case ACTIVE: + qb.setTables("active"); + qb.setProjectionMap(ACTIVE_PROJECTION_MAP); + qb.appendWhere("account is not null"); + break; + case PENDING: + qb.setTables("pending"); + qb.setProjectionMap(PENDING_PROJECTION_MAP); + groupBy = "account, authority"; + break; + case STATUS: + // join the stats and status tables, so the caller can get + // the account and authority information as part of this query. + qb.setTables("stats, status"); + qb.setProjectionMap(STATUS_PROJECTION_MAP); + qb.appendWhere("stats._id = status.stats_id"); + break; + case HISTORY_ID: + // join the stats and history tables, so the caller can get + // the account and authority information as part of this query. + qb.setTables("stats, history"); + qb.setProjectionMap(HISTORY_PROJECTION_MAP); + qb.appendWhere("stats._id = history.stats_id"); + qb.appendWhere("AND history._id="); + qb.appendWhere(url.getPathSegments().get(1)); + break; + case SETTINGS: + qb.setTables("settings"); + break; + default: + throw new IllegalArgumentException("Unknown URL " + url); + } + + if (match == SETTINGS) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, + "no permission to read the sync settings"); + } else { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, + "no permission to read the sync stats"); + } + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor c = qb.query(db, projectionIn, selection, selectionArgs, groupBy, null, sort); + c.setNotificationUri(mContext.getContentResolver(), url); + return c; + } + + /** + * Implements the {@link ContentProvider#insert} method + * @param callerIsTheProvider true if this is being called via the + * {@link ContentProvider#insert} in method rather than directly. + * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't + * for the Settings table. + */ + public Uri insert(boolean callerIsTheProvider, Uri url, ContentValues values) { + String table; + long rowID; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sURLMatcher.match(url); + checkCaller(callerIsTheProvider, match); + switch (match) { + case SETTINGS: + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + table = "settings"; + rowID = db.replace(table, null, values); + break; + default: + throw new IllegalArgumentException("Unknown URL " + url); + } + + + if (rowID > 0) { + mContext.getContentResolver().notifyChange(url, null /* observer */); + return Uri.parse("content://sync/" + table + "/" + rowID); + } + + return null; + } + + private static void checkCaller(boolean callerIsTheProvider, int match) { + if (callerIsTheProvider && match != SETTINGS) { + throw new UnsupportedOperationException( + "only the settings are modifiable via the ContentProvider interface"); + } + } + + /** + * Implements the {@link ContentProvider#delete} method + * @param callerIsTheProvider true if this is being called via the + * {@link ContentProvider#delete} in method rather than directly. + * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't + * for the Settings table. + */ + public int delete(boolean callerIsTheProvider, Uri url, String where, String[] whereArgs) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int match = sURLMatcher.match(url); + + int numRows; + switch (match) { + case SETTINGS: + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + numRows = db.delete("settings", where, whereArgs); + break; + default: + throw new UnsupportedOperationException("Cannot delete URL: " + url); + } + + if (numRows > 0) { + mContext.getContentResolver().notifyChange(url, null /* observer */); + } + return numRows; + } + + /** + * Implements the {@link ContentProvider#update} method + * @param callerIsTheProvider true if this is being called via the + * {@link ContentProvider#update} in method rather than directly. + * @throws UnsupportedOperationException if callerIsTheProvider is true and the url isn't + * for the Settings table. + */ + public int update(boolean callerIsTheProvider, Uri url, ContentValues initialValues, + String where, String[] whereArgs) { + switch (sURLMatcher.match(url)) { + case SETTINGS: + throw new UnsupportedOperationException("updating url " + url + + " is not allowed, use insert instead"); + default: + throw new UnsupportedOperationException("Cannot update URL: " + url); + } + } + + /** + * Implements the {@link ContentProvider#getType} method + */ + public String getType(Uri url) { + int match = sURLMatcher.match(url); + switch (match) { + case SETTINGS: + return "vnd.android.cursor.dir/sync-settings"; + default: + throw new IllegalArgumentException("Unknown URL"); + } + } + + protected Uri insertIntoPending(ContentValues values) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + try { + db.beginTransaction(); + long rowId = db.insert("pending", Sync.Pending.ACCOUNT, values); + if (rowId < 0) return null; + String account = values.getAsString(Sync.Pending.ACCOUNT); + String authority = values.getAsString(Sync.Pending.AUTHORITY); + + long statsId = createStatsRowIfNecessary(account, authority); + createStatusRowIfNecessary(statsId); + + values.clear(); + values.put(Sync.Status.PENDING, 1); + int numUpdatesStatus = db.update("status", values, "stats_id=" + statsId, null); + + db.setTransactionSuccessful(); + + mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, + null /* no observer initiated this change */); + if (numUpdatesStatus > 0) { + mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, + null /* no observer initiated this change */); + } + return ContentUris.withAppendedId(Sync.Pending.CONTENT_URI, rowId); + } finally { + db.endTransaction(); + } + } + + int deleteFromPending(long rowId) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + String account; + String authority; + Cursor c = db.query("pending", + new String[]{Sync.Pending.ACCOUNT, Sync.Pending.AUTHORITY}, + "_id=" + rowId, null, null, null, null); + try { + if (c.getCount() != 1) { + return 0; + } + c.moveToNext(); + account = c.getString(0); + authority = c.getString(1); + } finally { + c.close(); + } + db.delete("pending", "_id=" + rowId, null /* no where args */); + final String[] accountAuthorityWhereArgs = new String[]{account, authority}; + boolean isPending = 0 < DatabaseUtils.longForQuery(db, + "SELECT COUNT(*) FROM PENDING WHERE account=? AND authority=?", + accountAuthorityWhereArgs); + if (!isPending) { + long statsId = createStatsRowIfNecessary(account, authority); + db.execSQL("UPDATE status SET pending=0 WHERE stats_id=" + statsId); + } + db.setTransactionSuccessful(); + + mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, + null /* no observer initiated this change */); + if (!isPending) { + mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, + null /* no observer initiated this change */); + } + return 1; + } finally { + db.endTransaction(); + } + } + + int clearPending() { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + int numChanges = db.delete("pending", null, null /* no where args */); + if (numChanges > 0) { + db.execSQL("UPDATE status SET pending=0"); + mContext.getContentResolver().notifyChange(Sync.Pending.CONTENT_URI, + null /* no observer initiated this change */); + mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, + null /* no observer initiated this change */); + } + db.setTransactionSuccessful(); + return numChanges; + } finally { + db.endTransaction(); + } + } + + /** + * Returns a cursor over all the pending syncs in no particular order. This cursor is not + * "live", in that if changes are made to the pending table any observers on this cursor + * will not be notified. + * @param projection Return only these columns. If null then all columns are returned. + * @return the cursor of pending syncs + */ + public Cursor getPendingSyncsCursor(String[] projection) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + return db.query("pending", projection, null, null, null, null, null); + } + + // @VisibleForTesting + static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; + + private boolean purgeOldHistoryEvents(long now) { + // remove events that are older than MILLIS_IN_4WEEKS + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + int numDeletes = db.delete("history", "eventTime<" + (now - MILLIS_IN_4WEEKS), null); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + if (numDeletes > 0) { + Log.v(TAG, "deleted " + numDeletes + " old event(s) from the sync history"); + } + } + + // keep only the last MAX_HISTORY_EVENTS_TO_KEEP history events + numDeletes += db.delete("history", "eventTime < (select min(eventTime) from " + + "(select eventTime from history order by eventTime desc limit ?))", + new String[]{String.valueOf(MAX_HISTORY_EVENTS_TO_KEEP)}); + + return numDeletes > 0; + } + + public long insertStartSyncEvent(String account, String authority, long now, int source) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + long statsId = createStatsRowIfNecessary(account, authority); + + purgeOldHistoryEvents(now); + ContentValues values = new ContentValues(); + values.put(Sync.History.STATS_ID, statsId); + values.put(Sync.History.EVENT_TIME, now); + values.put(Sync.History.SOURCE, source); + values.put(Sync.History.EVENT, Sync.History.EVENT_START); + long rowId = db.insert("history", null, values); + mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, null /* observer */); + mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, null /* observer */); + return rowId; + } + + public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage, + long downstreamActivity, long upstreamActivity) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put(Sync.History.ELAPSED_TIME, elapsedTime); + values.put(Sync.History.EVENT, Sync.History.EVENT_STOP); + values.put(Sync.History.MESG, resultMessage); + values.put(Sync.History.DOWNSTREAM_ACTIVITY, downstreamActivity); + values.put(Sync.History.UPSTREAM_ACTIVITY, upstreamActivity); + + int count = db.update("history", values, "_id=?", + new String[]{Long.toString(historyId)}); + // We think that count should always be 1 but don't want to change this until after + // launch. + if (count > 0) { + int source = (int) DatabaseUtils.longForQuery(db, + "SELECT source FROM history WHERE _id=" + historyId, null); + long eventTime = DatabaseUtils.longForQuery(db, + "SELECT eventTime FROM history WHERE _id=" + historyId, null); + long statsId = DatabaseUtils.longForQuery(db, + "SELECT stats_id FROM history WHERE _id=" + historyId, null); + + createStatusRowIfNecessary(statsId); + + // update the status table to reflect this sync + StringBuilder sb = new StringBuilder(); + ArrayList<String> bindArgs = new ArrayList<String>(); + sb.append("UPDATE status SET"); + sb.append(" numSyncs=numSyncs+1"); + sb.append(", totalElapsedTime=totalElapsedTime+" + elapsedTime); + switch (source) { + case Sync.History.SOURCE_LOCAL: + sb.append(", numSourceLocal=numSourceLocal+1"); + break; + case Sync.History.SOURCE_POLL: + sb.append(", numSourcePoll=numSourcePoll+1"); + break; + case Sync.History.SOURCE_USER: + sb.append(", numSourceUser=numSourceUser+1"); + break; + case Sync.History.SOURCE_SERVER: + sb.append(", numSourceServer=numSourceServer+1"); + break; + } + + final String statsIdString = String.valueOf(statsId); + final long lastSyncTime = (eventTime + elapsedTime); + if (Sync.History.MESG_SUCCESS.equals(resultMessage)) { + // - if successful, update the successful columns + sb.append(", lastSuccessTime=" + lastSyncTime); + sb.append(", lastSuccessSource=" + source); + sb.append(", lastFailureTime=null"); + sb.append(", lastFailureSource=null"); + sb.append(", lastFailureMesg=null"); + sb.append(", initialFailureTime=null"); + } else if (!Sync.History.MESG_CANCELED.equals(resultMessage)) { + sb.append(", lastFailureTime=" + lastSyncTime); + sb.append(", lastFailureSource=" + source); + sb.append(", lastFailureMesg=?"); + bindArgs.add(resultMessage); + long initialFailureTime = DatabaseUtils.longForQuery(db, + SELECT_INITIAL_FAILURE_TIME_QUERY_STRING, + new String[]{statsIdString, String.valueOf(lastSyncTime)}); + sb.append(", initialFailureTime=" + initialFailureTime); + } + sb.append(" WHERE stats_id=?"); + bindArgs.add(statsIdString); + db.execSQL(sb.toString(), bindArgs.toArray()); + db.setTransactionSuccessful(); + mContext.getContentResolver().notifyChange(Sync.History.CONTENT_URI, + null /* observer */); + mContext.getContentResolver().notifyChange(Sync.Status.CONTENT_URI, + null /* observer */); + } + } finally { + db.endTransaction(); + } + } + + /** + * 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 + * failing then return 0. + */ + public long getInitialSyncFailureTime() { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + // Join the settings for a provider with the status so that we can easily + // check if each provider is enabled for syncing. We also join in the overall + // enabled flag ("listen_for_tickles") to each row so that we don't need to + // make a separate DB lookup to access it. + Cursor c = db.rawQuery("" + + "SELECT initialFailureTime, s1.value, s2.value " + + "FROM status " + + "LEFT JOIN stats ON status.stats_id=stats._id " + + "LEFT JOIN settings as s1 ON 'sync_provider_' || authority=s1.name " + + "LEFT JOIN settings as s2 ON s2.name='listen_for_tickles' " + + "where initialFailureTime is not null " + + " AND lastFailureMesg!=" + Sync.History.ERROR_TOO_MANY_DELETIONS + + " AND lastFailureMesg!=" + Sync.History.ERROR_AUTHENTICATION + + " AND lastFailureMesg!=" + Sync.History.ERROR_SYNC_ALREADY_IN_PROGRESS + + " AND authority!='subscribedfeeds' " + + " ORDER BY initialFailureTime", null); + try { + while (c.moveToNext()) { + // these settings default to true, so if they are null treat them as enabled + final String providerEnabledString = c.getString(1); + if (providerEnabledString != null && !Boolean.parseBoolean(providerEnabledString)) { + continue; + } + final String allEnabledString = c.getString(2); + if (allEnabledString != null && !Boolean.parseBoolean(allEnabledString)) { + continue; + } + return c.getLong(0); + } + } finally { + c.close(); + } + return 0; + } + + private void createStatusRowIfNecessary(long statsId) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + boolean statusExists = 0 != DatabaseUtils.longForQuery(db, + "SELECT count(*) FROM status WHERE stats_id=" + statsId, null); + if (!statusExists) { + ContentValues values = new ContentValues(); + values.put("stats_id", statsId); + db.insert("status", null, values); + } + } + + private long createStatsRowIfNecessary(String account, String authority) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + StringBuilder where = new StringBuilder(); + where.append(Sync.Stats.ACCOUNT + "= ?"); + where.append(" and " + Sync.Stats.AUTHORITY + "= ?"); + Cursor cursor = query(Sync.Stats.CONTENT_URI, + Sync.Stats.SYNC_STATS_PROJECTION, + where.toString(), new String[] { account, authority }, + null /* order */); + try { + long id; + if (cursor.moveToFirst()) { + id = cursor.getLong(cursor.getColumnIndexOrThrow(Sync.Stats._ID)); + } else { + ContentValues values = new ContentValues(); + values.put(Sync.Stats.ACCOUNT, account); + values.put(Sync.Stats.AUTHORITY, authority); + id = db.insert("stats", null, values); + } + return id; + } finally { + cursor.close(); + } + } +} diff --git a/core/java/android/content/SyncUIContext.java b/core/java/android/content/SyncUIContext.java new file mode 100644 index 0000000..6dde004 --- /dev/null +++ b/core/java/android/content/SyncUIContext.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 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; + +/** + * 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(); +} diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java new file mode 100644 index 0000000..e0cd786 --- /dev/null +++ b/core/java/android/content/SyncableContentProvider.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 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; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import java.util.Map; + +/** + * A specialization of the ContentProvider that centralizes functionality + * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider + * inside of database transactions. + * + * @hide + */ +public abstract class SyncableContentProvider extends ContentProvider { + protected abstract boolean isTemporary(); + + /** + * Close resources that must be closed. You must call this to properly release + * the resources used by the SyncableContentProvider. + */ + public abstract void close(); + + /** + * Override to create your schema and do anything else you need to do with a new database. + * This is run inside a transaction (so you don't need to use one). + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + */ + protected abstract void bootstrapDatabase(SQLiteDatabase db); + + /** + * Override to upgrade your database from an old version to the version you specified. + * Don't set the DB version, this will automatically be done after the method returns. + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + * + * @param oldVersion version of the existing database + * @param newVersion current version to upgrade to + * @return true if the upgrade was lossless, false if it was lossy + */ + protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion); + + /** + * Override to do anything (like cleanups or checks) you need to do after opening a database. + * Does nothing by default. This is run inside a transaction (so you don't need to use one). + * This method may not use getDatabase(), or call content provider methods, it must only + * use the database handle passed to it. + */ + protected abstract void onDatabaseOpened(SQLiteDatabase db); + + /** + * Get a non-persistent instance of this content provider. + * You must call {@link #close} on the returned + * SyncableContentProvider when you are done with it. + * + * @return a non-persistent content provider with the same layout as this + * provider. + */ + public abstract SyncableContentProvider getTemporaryInstance(); + + public abstract SQLiteDatabase getDatabase(); + + public abstract boolean getContainsDiffs(); + + public abstract void setContainsDiffs(boolean containsDiffs); + + /** + * Each subclass of this class should define a subclass of {@link + * AbstractTableMerger} for each table they wish to merge. It + * should then override this method and return one instance of + * each merger, in sequence. Their {@link + * AbstractTableMerger#merge merge} methods will be called, one at a + * time, in the order supplied. + * + * <p>The default implementation returns an empty list, so that no + * merging will occur. + * @return A sequence of subclasses of {@link + * AbstractTableMerger}, one for each table that should be merged. + */ + protected abstract Iterable<? extends AbstractTableMerger> getMergers(); + + /** + * 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 + */ + public abstract boolean changeRequiresLocalSync(Uri uri); + + /** + * Called right before a sync is started. + * + * @param context the sync context for the operation + * @param account + */ + public abstract void onSyncStart(SyncContext context, String account); + + /** + * Called right after a sync is completed + * + * @param context the sync context for the operation + * @param success true if the sync succeeded, false if an error occurred + */ + public abstract void onSyncStop(SyncContext context, boolean success); + + /** + * The account of the most recent call to onSyncStart() + * @return the account + */ + public abstract String getSyncingAccount(); + + /** + * Merge diffs from a sync source with this content provider. + * + * @param context the SyncContext within which this merge is taking place + * @param diffs A temporary content provider containing diffs from a sync + * source. + * @param result a MergeResult that contains information about the merge, including + * a temporary content provider with the same layout as this provider containing + * @param syncResult + */ + public abstract void merge(SyncContext context, SyncableContentProvider diffs, + TempProviderSyncResult result, SyncResult syncResult); + + + /** + * Invoked when the active sync has been canceled. The default + * implementation doesn't do anything (except ensure that this + * provider is syncable). Subclasses of ContentProvider + * that support canceling of sync should override this. + */ + public abstract void onSyncCanceled(); + + + public abstract boolean isMergeCancelled(); + + /** + * Subclasses should override this instead of update(). See update() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract int updateInternal(Uri url, ContentValues values, + String selection, String[] selectionArgs); + + /** + * Subclasses should override this instead of delete(). See delete() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs); + + /** + * Subclasses should override this instead of insert(). See insert() + * for details. + * + * <p> This method is called within a acquireDbLock()/releaseDbLock() block, + * which means a database transaction will be active during the call; + */ + protected abstract Uri insertInternal(Uri url, ContentValues values); + + /** + * Subclasses should override this instead of query(). See query() + * for details. + * + * <p> This method is *not* called within a acquireDbLock()/releaseDbLock() + * block for performance reasons. If an implementation needs atomic access + * to the database the lock can be acquired then. + */ + protected abstract Cursor queryInternal(Uri url, String[] projection, + String selection, String[] selectionArgs, String sortOrder); + + /** + * 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); + + /** + * A helper method to delete all rows whose account is not in the accounts + * map. The accountColumnName is the name of the column that is expected + * to hold the account. If a row has an empty account it is never deleted. + * + * @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); + + /** + * Called when the sync system determines that this provider should no longer + * contain records for the specified account. + */ + public abstract void wipeAccount(String account); + + /** + * Retrieves the SyncData bytes for the given account. The byte array returned may be null. + */ + public abstract byte[] readSyncDataBytes(String account); + + /** + * Sets the SyncData bytes for the given account. The bytes array may be null. + */ + public abstract void writeSyncDataBytes(String account, byte[] data); +} + diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java new file mode 100644 index 0000000..eb3a5da --- /dev/null +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -0,0 +1,550 @@ +package android.content; + +import android.database.SQLException; +import android.os.Bundle; +import android.os.Debug; +import android.os.NetStat; +import android.os.Parcelable; +import android.os.Process; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Config; +import android.util.EventLog; +import android.util.Log; +import android.util.TimingLogger; + +/** + * @hide + */ +public abstract class TempProviderSyncAdapter extends SyncAdapter { + private static final String TAG = "Sync"; + + private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20; + private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10; + private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5; + private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20; + + private volatile SyncableContentProvider mProvider; + private volatile SyncThread mSyncThread = null; + private volatile boolean mProviderSyncStarted; + private volatile boolean mAdapterSyncStarted; + + public TempProviderSyncAdapter(SyncableContentProvider provider) { + super(); + mProvider = provider; + } + + /** + * Used by getServerDiffs() to track the sync progress for a given + * sync adapter. Implementations of SyncAdapter generally specialize + * this class in order to track specific data about that SyncAdapter's + * sync. If an implementation of SyncAdapter doesn't need to store + * any data for a sync it may use TrivialSyncData. + */ + public static abstract class SyncData implements Parcelable { + + } + + public final void setContext(Context context) { + mContext = context; + } + + /** + * Retrieve the Context this adapter is running in. Only available + * once onSyncStarting() is called (not available from constructor). + */ + final public Context getContext() { + return mContext; + } + + /** + * Called right before a sync is started. + * + * @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 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, + SyncResult result); + + /** + * Called right after a sync is completed + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param success true if the sync suceeded, false if an error occured + */ + public abstract void onSyncEnding(SyncContext context, boolean success); + + /** + * Implement this to return true if the data in your content provider + * is read only. + */ + public abstract boolean isReadOnly(); + + /** + * Get diffs from the server since the last completed sync and put them + * into a temporary provider. + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param syncData used to track the progress this client has made in syncing data + * from the server + * @param tempProvider this is where the diffs should be stored + * @param extras any extra data describing the sync that is desired + * @param syncInfo sync adapter-specific data that is used during a single sync operation + * @param syncResult information to track what happened during this sync attempt + */ + public abstract void getServerDiffs(SyncContext context, + SyncData syncData, SyncableContentProvider tempProvider, + Bundle extras, Object syncInfo, SyncResult syncResult); + + /** + * Send client diffs to the server, optionally receiving more diffs from the server + * + * @param context allows you to publish status and interact with the + * user during interactive syncs. + * @param clientDiffs the diffs from the client + * @param serverDiffs the SyncableContentProvider that should be populated with +* the entries that were returned in response to an insert/update/delete request +* to the server + * @param syncResult information to track what happened during this sync attempt + * @param dontActuallySendDeletes + */ + public abstract void sendClientDiffs(SyncContext context, + SyncableContentProvider clientDiffs, + SyncableContentProvider serverDiffs, SyncResult syncResult, + boolean dontActuallySendDeletes); + + /** + * Reads the sync data from the ContentProvider + * @param contentProvider the ContentProvider to read from + * @return the SyncData for the provider. This may be null. + */ + public SyncData readSyncData(SyncableContentProvider contentProvider) { + return null; + } + + /** + * Create and return a new, empty SyncData object + */ + public SyncData newSyncData() { + return null; + } + + /** + * Stores the sync data in the Sync Stats database, keying it by + * the account that was set in the last call to onSyncStarting() + */ + public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {} + + /** + * Indicate to the SyncAdapter that the last sync that was started has + * been cancelled. + */ + public abstract void onSyncCanceled(); + + /** + * Initializes the temporary content providers used during + * {@link TempProviderSyncAdapter#sendClientDiffs}. + * May copy relevant data from the underlying db into this provider so + * joins, etc., can work. + * + * @param cp The ContentProvider to initialize. + */ + protected void initTempProvider(SyncableContentProvider cp) {} + + protected Object createSyncInfo() { + return null; + } + + /** + * Called when the accounts list possibly changed, to give the + * SyncAdapter a chance to do any necessary bookkeeping, e.g. + * to make sure that any required SubscribedFeeds subscriptions + * exist. + * @param accounts the list of accounts + */ + public abstract void onAccountsChanged(String[] accounts); + + private Context mContext; + + private class SyncThread extends Thread { + private final String mAccount; + private final Bundle mExtras; + private final SyncContext mSyncContext; + private volatile boolean mIsCanceled = false; + private long mInitialTxBytes; + private long mInitialRxBytes; + private final SyncResult mResult; + + SyncThread(SyncContext syncContext, String account, Bundle extras) { + super("SyncThread"); + mAccount = account; + mExtras = extras; + mSyncContext = syncContext; + mResult = new SyncResult(); + } + + void cancelSync() { + mIsCanceled = true; + if (mAdapterSyncStarted) onSyncCanceled(); + if (mProviderSyncStarted) mProvider.onSyncCanceled(); + // We may lose the last few sync events when canceling. Oh well. + int uid = Process.myUid(); + logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); + } + + @Override + public void run() { + Process.setThreadPriority(Process.myTid(), + Process.THREAD_PRIORITY_BACKGROUND); + int uid = Process.myUid(); + mInitialTxBytes = NetStat.getUidTxBytes(uid); + mInitialRxBytes = NetStat.getUidRxBytes(uid); + try { + sync(mSyncContext, mAccount, mExtras); + } catch (SQLException e) { + Log.e(TAG, "Sync failed", e); + mResult.databaseError = true; + } finally { + mSyncThread = null; + if (!mIsCanceled) { + logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, + NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); + mSyncContext.onFinished(mResult); + } + } + } + + private void sync(SyncContext syncContext, String account, Bundle extras) { + mIsCanceled = false; + + mProviderSyncStarted = false; + mAdapterSyncStarted = false; + String message = null; + + boolean syncForced = extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false); + + try { + mProvider.onSyncStart(syncContext, account); + mProviderSyncStarted = true; + onSyncStarting(syncContext, account, syncForced, mResult); + if (mResult.hasError()) { + message = "SyncAdapter failed while trying to start sync"; + return; + } + mAdapterSyncStarted = true; + if (mIsCanceled) { + return; + } + final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing"); + final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue); + try { + if (syncTracingEnabled) { + System.gc(); + System.gc(); + Debug.startMethodTracing("synctrace." + System.currentTimeMillis()); + } + runSyncLoop(syncContext, account, extras); + } finally { + if (syncTracingEnabled) Debug.stopMethodTracing(); + } + onSyncEnding(syncContext, !mResult.hasError()); + mAdapterSyncStarted = false; + mProvider.onSyncStop(syncContext, true); + mProviderSyncStarted = false; + } finally { + if (mAdapterSyncStarted) { + mAdapterSyncStarted = false; + onSyncEnding(syncContext, false); + } + if (mProviderSyncStarted) { + mProviderSyncStarted = false; + mProvider.onSyncStop(syncContext, false); + } + if (!mIsCanceled) { + if (message != null) syncContext.setStatusText(message); + } + } + } + + private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) { + TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); + syncTimer.addSplit("start"); + int loopCount = 0; + boolean tooManyGetServerDiffsAttempts = false; + + final boolean overrideTooManyDeletions = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS, + false); + final boolean discardLocalDeletions = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false); + boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, + false /* default this flag to false */); + SyncableContentProvider serverDiffs = null; + TempProviderSyncResult result = new TempProviderSyncResult(); + try { + if (!uploadOnly) { + /** + * This loop repeatedly calls SyncAdapter.getServerDiffs() + * (to get changes from the feed) followed by + * ContentProvider.merge() (to incorporate these changes + * into the provider), stopping when the SyncData returned + * from getServerDiffs() indicates that all the data was + * fetched. + */ + while (!mIsCanceled) { + // Don't let a bad sync go forever + if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) { + Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs " + + getClass().getName()); + // TODO: change the structure here to schedule a new sync + // with a backoff time, keeping track to be sure + // we don't keep doing this forever (due to some bug or + // mismatch between the client and the server) + tooManyGetServerDiffsAttempts = true; + break; + } + + // Get an empty content provider to put the diffs into + if (serverDiffs != null) serverDiffs.close(); + serverDiffs = mProvider.getTemporaryInstance(); + + // Get records from the server which will be put into the serverDiffs + initTempProvider(serverDiffs); + Object syncInfo = createSyncInfo(); + SyncData syncData = readSyncData(serverDiffs); + // syncData will only be null if there was a demarshalling error + // while reading the sync data. + if (syncData == null) { + mProvider.wipeAccount(account); + syncData = newSyncData(); + } + mResult.clear(); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData " + + syncData.toString()); + } + getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo, + mResult); + + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + if (mResult.hasError()) return; + if (mResult.partialSyncUnavailable) { + if (Config.LOGD) { + Log.d(TAG, "partialSyncUnavailable is set, setting " + + "ignoreSyncData and retrying"); + } + mProvider.wipeAccount(account); + continue; + } + + // write the updated syncData back into the temp provider + writeSyncData(syncData, serverDiffs); + + // apply the downloaded changes to the provider + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: running merge"); + } + mProvider.merge(syncContext, serverDiffs, + null /* don't return client diffs */, mResult); + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + // if the server has no more changes then break out of the loop + if (!mResult.moreRecordsToGet) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: fetched all data, moving on"); + } + break; + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: more data to fetch, looping"); + } + } + } + + /** + * This loop repeatedly calls ContentProvider.merge() followed + * by SyncAdapter.merge() until either indicate that there is + * no more work to do by returning null. + * <p> + * The initial ContentProvider.merge() returns a temporary + * ContentProvider that contains any local changes that need + * to be committed to the server. + * <p> + * The SyncAdapter.merge() calls upload the changes to the server + * and populates temporary provider (the serverDiffs) with the + * result. + * <p> + * Subsequent calls to ContentProvider.merge() incoporate the + * result of previous SyncAdapter.merge() calls into the + * real ContentProvider and again return a temporary + * ContentProvider that contains any local changes that need + * to be committed to the server. + */ + loopCount = 0; + boolean readOnly = isReadOnly(); + long previousNumModifications = 0; + if (serverDiffs != null) { + serverDiffs.close(); + serverDiffs = null; + } + + // If we are discarding local deletions then we need to redownload all the items + // again (since some of them might have been deleted). We do this by deleting the + // sync data for the current account by writing in a null one. + if (discardLocalDeletions) { + serverDiffs = mProvider.getTemporaryInstance(); + initTempProvider(serverDiffs); + writeSyncData(null, serverDiffs); + } + + while (!mIsCanceled) { + if (Config.LOGV) { + Log.v(TAG, "runSyncLoop: Merging diffs from server to client"); + } + if (result.tempContentProvider != null) { + result.tempContentProvider.close(); + result.tempContentProvider = null; + } + mResult.clear(); + mProvider.merge(syncContext, serverDiffs, readOnly ? null : result, + mResult); + if (mIsCanceled) return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + SyncableContentProvider clientDiffs = + readOnly ? null : result.tempContentProvider; + if (clientDiffs == null) { + // Nothing to commit back to the server + if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs"); + break; + } + + long numModifications = mResult.stats.numUpdates + + mResult.stats.numDeletes + + mResult.stats.numInserts; + + // as long as we are making progress keep resetting the loop count + if (numModifications < previousNumModifications) { + loopCount = 0; + } + previousNumModifications = numModifications; + + // Don't let a bad sync go forever + if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) { + Log.e(TAG, "runSyncLoop: Hit max loop count while syncing " + + getClass().getName()); + mResult.tooManyRetries = true; + break; + } + + if (!overrideTooManyDeletions && !discardLocalDeletions + && hasTooManyDeletions(mResult.stats)) { + if (Config.LOGD) { + Log.d(TAG, "runSyncLoop: Too many deletions were found in provider " + + getClass().getName() + ", not doing any more updates"); + } + long numDeletes = mResult.stats.numDeletes; + mResult.stats.clear(); + mResult.tooManyDeletions = true; + mResult.stats.numDeletes = numDeletes; + break; + } + + if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server"); + if (serverDiffs != null) serverDiffs.close(); + serverDiffs = clientDiffs.getTemporaryInstance(); + initTempProvider(serverDiffs); + mResult.clear(); + sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult, + discardLocalDeletions); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: result: " + mResult); + } + + if (!mResult.madeSomeProgress()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: No data from client diffs merge"); + } + break; + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: made some progress, looping"); + } + } + + // add in any status codes that we saved from earlier + mResult.tooManyRetries |= tooManyGetServerDiffsAttempts; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runSyncLoop: final result: " + mResult); + } + } finally { + // do this in the finally block to guarantee that is is set and not overwritten + if (discardLocalDeletions) { + mResult.fullSyncRequested = true; + } + if (serverDiffs != null) serverDiffs.close(); + if (result.tempContentProvider != null) result.tempContentProvider.close(); + syncTimer.addSplit("stop"); + syncTimer.dumpToLog(); + } + } + } + + /** + * 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 + */ + protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { + EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); + } + + public void startSync(SyncContext syncContext, String account, Bundle extras) { + if (mSyncThread != null) { + syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); + return; + } + + mSyncThread = new SyncThread(syncContext, account, extras); + mSyncThread.start(); + } + + public void cancelSync() { + if (mSyncThread != null) { + mSyncThread.cancelSync(); + } + } + + protected boolean hasTooManyDeletions(SyncStats stats) { + long numEntries = stats.numEntries; + long numDeletedEntries = stats.numDeletes; + + long percentDeleted = (numDeletedEntries == 0) + ? 0 + : (100 * numDeletedEntries / + (numEntries + numDeletedEntries)); + boolean tooManyDeletions = + (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS) + && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS); + return tooManyDeletions; + } +} diff --git a/core/java/android/content/TempProviderSyncResult.java b/core/java/android/content/TempProviderSyncResult.java new file mode 100644 index 0000000..81f6f79 --- /dev/null +++ b/core/java/android/content/TempProviderSyncResult.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 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; + +/** + * Used to hold data returned from a given phase of a TempProviderSync. + * @hide + */ +public class TempProviderSyncResult { + /** + * An interface to a temporary content provider that contains + * the result of updates that were sent to the server. This + * provider must be merged into the permanent content provider. + * This may be null, which indicates that there is nothing to + * merge back into the content provider. + */ + public SyncableContentProvider tempContentProvider; + + public TempProviderSyncResult() { + tempContentProvider = null; + } +} diff --git a/core/java/android/content/UriMatcher.java b/core/java/android/content/UriMatcher.java new file mode 100644 index 0000000..a98e6d5 --- /dev/null +++ b/core/java/android/content/UriMatcher.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2006 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 java.util.ArrayList; +import java.util.regex.Pattern; + +/** +Utility class to aid in matching URIs in content providers. + +<p>To use this class, build up a tree of UriMatcher objects. +Typically, it looks something like this: +<pre> + private static final int PEOPLE = 1; + private static final int PEOPLE_ID = 2; + private static final int PEOPLE_PHONES = 3; + private static final int PEOPLE_PHONES_ID = 4; + private static final int PEOPLE_CONTACTMETHODS = 7; + private static final int PEOPLE_CONTACTMETHODS_ID = 8; + + private static final int DELETED_PEOPLE = 20; + + private static final int PHONES = 9; + private static final int PHONES_ID = 10; + private static final int PHONES_FILTER = 14; + + private static final int CONTACTMETHODS = 18; + private static final int CONTACTMETHODS_ID = 19; + + private static final int CALLS = 11; + private static final int CALLS_ID = 12; + private static final int CALLS_FILTER = 15; + + private static final UriMatcher sURIMatcher = new UriMatcher(); + + static + { + sURIMatcher.addURI("contacts", "/people", PEOPLE); + sURIMatcher.addURI("contacts", "/people/#", PEOPLE_ID); + sURIMatcher.addURI("contacts", "/people/#/phones", PEOPLE_PHONES); + sURIMatcher.addURI("contacts", "/people/#/phones/#", PEOPLE_PHONES_ID); + sURIMatcher.addURI("contacts", "/people/#/contact_methods", PEOPLE_CONTACTMETHODS); + sURIMatcher.addURI("contacts", "/people/#/contact_methods/#", PEOPLE_CONTACTMETHODS_ID); + sURIMatcher.addURI("contacts", "/deleted_people", DELETED_PEOPLE); + sURIMatcher.addURI("contacts", "/phones", PHONES); + sURIMatcher.addURI("contacts", "/phones/filter/*", PHONES_FILTER); + sURIMatcher.addURI("contacts", "/phones/#", PHONES_ID); + sURIMatcher.addURI("contacts", "/contact_methods", CONTACTMETHODS); + sURIMatcher.addURI("contacts", "/contact_methods/#", CONTACTMETHODS_ID); + sURIMatcher.addURI("call_log", "/calls", CALLS); + sURIMatcher.addURI("call_log", "/calls/filter/*", CALLS_FILTER); + sURIMatcher.addURI("call_log", "/calls/#", CALLS_ID); + } +</pre> +<p>Then when you need to match match against a URI, call {@link #match}, providing +the tokenized url you've been given, and the value you want if there isn't +a match. You can use the result to build a query, return a type, insert or +delete a row, or whatever you need, without duplicating all of the if-else +logic you'd otherwise need. Like this: +<pre> + public String getType(String[] url) + { + int match = sURIMatcher.match(url, NO_MATCH); + switch (match) + { + case PEOPLE: + return "vnd.android.cursor.dir/person"; + case PEOPLE_ID: + return "vnd.android.cursor.item/person"; +... snip ... + return "vnd.android.cursor.dir/snail-mail"; + case PEOPLE_ADDRESS_ID: + return "vnd.android.cursor.item/snail-mail"; + default: + return null; + } + } +</pre> +instead of +<pre> + public String getType(String[] url) + { + if (url.length >= 2) { + if (url[1].equals("people")) { + if (url.length == 2) { + return "vnd.android.cursor.dir/person"; + } else if (url.length == 3) { + return "vnd.android.cursor.item/person"; +... snip ... + return "vnd.android.cursor.dir/snail-mail"; + } else if (url.length == 3) { + return "vnd.android.cursor.item/snail-mail"; + } + } + } + return null; + } +</pre> +*/ +public class UriMatcher +{ + public static final int NO_MATCH = -1; + /** + * Creates the root node of the URI tree. + * + * @param code the code to match for the root URI + */ + public UriMatcher(int code) + { + mCode = code; + mWhich = -1; + mChildren = new ArrayList<UriMatcher>(); + mText = null; + } + + private UriMatcher() + { + mCode = NO_MATCH; + mWhich = -1; + mChildren = new ArrayList<UriMatcher>(); + mText = null; + } + + /** + * Add a URI to match, and the code to return when this URI is + * matched. URI nodes may be exact match string, the token "*" + * that matches any text, or the token "#" that matches only + * numbers. + * + * @param authority the authority to match + * @param path the path to match. * may be used as a wild card for + * any text, and # may be used as a wild card for numbers. + * @param code the code that is returned when a URI is matched + * against the given components. Must be positive. + */ + public void addURI(String authority, String path, int code) + { + if (code < 0) { + throw new IllegalArgumentException("code " + code + " is invalid: it must be positive"); + } + String[] tokens = path != null ? PATH_SPLIT_PATTERN.split(path) : null; + int numTokens = tokens != null ? tokens.length : 0; + UriMatcher node = this; + for (int i = -1; i < numTokens; i++) { + String token = i < 0 ? authority : tokens[i]; + ArrayList<UriMatcher> children = node.mChildren; + int numChildren = children.size(); + UriMatcher child; + int j; + for (j = 0; j < numChildren; j++) { + child = children.get(j); + if (token.equals(child.mText)) { + node = child; + break; + } + } + if (j == numChildren) { + // Child not found, create it + child = new UriMatcher(); + if (token.equals("#")) { + child.mWhich = NUMBER; + } else if (token.equals("*")) { + child.mWhich = TEXT; + } else { + child.mWhich = EXACT; + } + child.mText = token; + node.mChildren.add(child); + node = child; + } + } + node.mCode = code; + } + + static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/"); + + /** + * Try to match against the path in a url. + * + * @param uri The url whose path we will match against. + * + * @return The code for the matched node (added using addURI), + * or -1 if there is no matched node. + */ + public int match(Uri uri) + { + final int li = uri.getPathSegments().size(); + + UriMatcher node = this; + + if (li == 0 && uri.getAuthority() == null) { + return this.mCode; + } + + for (int i=-1; i<li; i++) { + String u = i < 0 ? uri.getAuthority() : uri.getPathSegments().get(i); + ArrayList<UriMatcher> list = node.mChildren; + if (list == null) { + break; + } + node = null; + int lj = list.size(); + for (int j=0; j<lj; j++) { + UriMatcher n = list.get(j); + which_switch: + switch (n.mWhich) { + case EXACT: + if (n.mText.equals(u)) { + node = n; + } + break; + case NUMBER: + int lk = u.length(); + for (int k=0; k<lk; k++) { + char c = u.charAt(k); + if (c < '0' || c > '9') { + break which_switch; + } + } + node = n; + break; + case TEXT: + node = n; + break; + } + if (node != null) { + break; + } + } + if (node == null) { + return NO_MATCH; + } + } + + return node.mCode; + } + + private static final int EXACT = 0; + private static final int NUMBER = 1; + private static final int TEXT = 2; + + private int mCode; + private int mWhich; + private String mText; + private ArrayList<UriMatcher> mChildren; +} diff --git a/core/java/android/content/package.html b/core/java/android/content/package.html new file mode 100644 index 0000000..dd5360f --- /dev/null +++ b/core/java/android/content/package.html @@ -0,0 +1,650 @@ +<html> +<head> +<script type="text/javascript" src="http://www.corp.google.com/style/prettify.js"></script> +<script src="http://www.corp.google.com/eng/techpubs/include/navbar.js" type="text/javascript"></script> +</head> + +<body> + +<p>Contains classes for accessing and publishing data +on the device. It includes three main categories of APIs: +the {@link android.content.res.Resources Resources} for +retrieving resource data associated with an application; +{@link android.content.ContentProvider Content Providers} and +{@link android.content.ContentResolver ContentResolver} for managing and +publishing persistent data associated with an application; and +the {@link android.content.pm.PackageManager Package Manager} +for finding out information about the application packages installed +on the device.</p> + +<p>In addition, the {@link android.content.Context Context} abstract class +is a base API for pulling these pieces together, allowing you to access +an application's resources and transfer data between applications.</p> + +<p>This package builds on top of the lower-level Android packages +{@link android.database}, {@link android.text}, +{@link android.graphics.drawable}, {@link android.graphics}, +{@link android.os}, and {@link android.util}.</p> + +<ol> + <li> <a href="#Resources">Resources</a> + <ol> + <li> <a href="#ResourcesTerminology">Terminology</a> + <li> <a href="#ResourcesQuickStart">Examples</a> + <ol> + <li> <a href="#UsingSystemResources">Using System Resources</a> + <li> <a href="#StringResources">String Resources</a> + <li> <a href="#ColorResources">Color Resources</a> + <li> <a href="#DrawableResources">Drawable Resources</a> + <li> <a href="#LayoutResources">Layout Resources</a> + <li> <a href="#ReferencesToResources">References to Resources</a> + <li> <a href="#ReferencesToThemeAttributes">References to Theme Attributes</a> + <li> <a href="#StyleResources">Style Resources</a> + <li> <a href="#StylesInLayoutResources">Styles in Layout Resources</a> + </ol> + </ol> +</ol> + +<a name="Resources"></a> +<h2>Resources</h2> + +<p>This topic includes a terminology list associated with resources, and a series + of examples of using resources in code. For a complete guide on creating and + using resources, see the document on <a href="{@docRoot}guide/topics/resources/resources-i18n.html">Resources + and Internationalization</a>. For a reference on the supported Android resource types, + see <a href="{@docRoot}guide/topics/resources/available-resources.html">Available Resource Types</a>.</p> +<p>The Android resource system keeps track of all non-code + assets associated with an application. You use the + {@link android.content.res.Resources Resources} class to access your + application's resources; the Resources instance associated with your + application can generally be found through + {@link android.content.Context#getResources Context.getResources()}.</p> +<p>An application's resources are compiled into the application +binary at build time for you by the build system. To use a resource, +you must install it correctly in the source tree and build your +application. As part of the build process, Java symbols for each +of the resources are generated that you can use in your source +code -- this allows the compiler to verify that your application code matches +up with the resources you defined.</p> + +<p>The rest of this section is organized as a tutorial on how to +use resources in an application.</p> + +<a name="ResourcesTerminology"></a> +<h3>Terminology</h3> + +<p>The resource system brings a number of different pieces together to +form the final complete resource functionality. To help understand the +overall system, here are some brief definitions of the core concepts and +components you will encounter in using it:</p> + +<p><b>Asset</b>: A single blob of data associated with an application. This +includes Java object files, graphics (such as PNG images), XML files, etc. +These files are organized in a directory hierarchy that, during final packaging +of the application, is bundled together into a single ZIP file.</p> + +<p><b>aapt</b>: The tool that generates the final ZIP file of application +assets. In addition to collecting raw assets together, it also parses +resource definitions into binary asset data.</p> + +<p><b>Resource Table</b>: A special asset that aapt generates for you, +describing all of the resources contained in an application/package. +This file is accessed for you by the Resources class; it is not touched +directly by applications.</p> + +<p><b>Resource</b>: An entry in the Resource Table describing a single +named value. Broadly, there are two types of resources: primitives and +bags.</p> + +<p><b>Resource Identifier</b>: In the Resource Table all resources are +identified by a unique integer number. In source code (resource descriptions, +XML files, Java code) you can use symbolic names that stand as constants for +the actual resource identifier integer.</p> + +<p><b>Primitive Resource</b>: All primitive resources can be written as a +simple string, using formatting to describe a variety of primitive types +included in the resource system: integers, colors, strings, references to +other resources, etc. Complex resources, such as bitmaps and XML +describes, are stored as a primitive string resource whose value is the path +of the underlying Asset holding its actual data.</p> + +<p><b>Bag Resource</b>: A special kind of resource entry that, instead of a +simple string, holds an arbitrary list of name/value pairs. Each name is +itself a resource identifier, and each value can hold +the same kinds of string formatted data as a normal resource. Bags also +support inheritance: a bag can inherit the values from another bag, selectively +replacing or extending them to generate its own contents.</p> + +<p><b>Kind</b>: The resource kind is a way to organize resource identifiers +for various purposes. For example, drawable resources are used to +instantiate Drawable objects, so their data is a primitive resource containing +either a color constant or string path to a bitmap or XML asset. Other +common resource kinds are string (localized string primitives), color +(color primitives), layout (a string path to an XML asset describing a view +layout), and style (a bag resource describing user interface attributes). +There is also a standard "attr" resource kind, which defines the resource +identifiers to be used for naming bag items and XML attributes</p> + +<p><b>Style</b>: The name of the resource kind containing bags that are used +to supply a set of user interface attributes. For example, a TextView class may +be given a style resource that defines its text size, color, and alignment. +In a layout XML file, you associate a style with a bag using the "style" +attribute, whose value is the name of the style resource.</p> + +<p><b>Style Class</b>: Specifies a related set of attribute resources. +This data is not placed in the resource table itself, but used to generate +Java constants that make it easier for you to retrieve values out of +a style resource and/or XML tag's attributes. For example, the +Android platform defines a "View" style class that +contains all of the standard view attributes: padding, visibility, +background, etc.; when View is inflated it uses this style class to +retrieve those values from the XML file (at which point style and theme +information is applied as approriate) and load them into its instance.</p> + +<p><b>Configuration</b>: For any particular resource identifier, there may be +multiple different available values depending on the current configuration. +The configuration includes the locale (language and country), screen +orientation, screen density, etc. The current configuration is used to +select which resource values are in effect when the resource table is +loaded.</p> + +<p><b>Theme</b>: A standard style resource that supplies global +attribute values for a particular context. For example, when writing a +Activity the application developer can select a standard theme to use, such +as the Theme.White or Theme.Black styles; this style supplies information +such as the screen background image/color, default text color, button style, +text editor style, text size, etc. When inflating a layout resource, most +values for widgets (the text color, selector, background) if not explicitly +set will come from the current theme; style and attribute +values supplied in the layout can also assign their value from explicitly +named values in the theme attributes if desired.</p> + +<p><b>Overlay</b>: A resource table that does not define a new set of resources, +but instead replaces the values of resources that are in another resource table. +Like a configuration, this is applied at load time +to the resource data; it can add new configuration values (for example +strings in a new locale), replace existing values (for example change +the standard white background image to a "Hello Kitty" background image), +and modify resource bags (for example change the font size of the Theme.White +style to have an 18 pt font size). This is the facility that allows the +user to select between different global appearances of their device, or +download files with new appearances.</p> + +<a name="ResourcesQuickStart"></a> +<h3>Examples</h3> + +<p>This section gives a few quick examples you can use to make your own resources. + For more details on how to define and use resources, see <a + href="{@docRoot}guide/topics/resources/resources-i18n.html">Resources and + Internationalization</a>. </p> + +<a name="UsingSystemResources"></a> +<h4>Using System Resources</h4> + +<p>Many resources included with the system are available to applications. +All such resources are defined under the class "android.R". For example, +you can display the standard application icon in a screen with the following +code:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + requestScreenFeatures(FEATURE_BADGE_IMAGE); + + super.onStart(); + + setBadgeResource(android.R.drawable.sym_def_app_icon); + } +} +</pre> + +<p>In a similar way, this code will apply to your screen the standard +"green background" visual treatment defined by the system:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + super.onStart(); + + setTheme(android.R.style.Theme_Black); + } +} +</pre> + +<a name="StringResources"></a> +<h4>String Resources</h4> + +<p>String resources are defined using an XML resource description syntax. +The file or multiple files containing these resources can be given any name +(as long as it has a .xml suffix) and placed at an appropriate location in +the source tree for the desired configuration (locale/orientation/density). + +<p>Here is a simple resource file describing a few strings:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string id="mainLabel">Hello <u>th<ignore>e</ignore>re</u>, <i>you</i> <b>Activity</b>!</string> + <string id="back">Back</string> + <string id="clear">Clear</string> +</resources> +</pre> + +<p>Typically this file will be called "strings.xml", and must be placed +in the <code>values</code> directory:</p> + +<pre> +MyApp/res/values/strings.xml +</pre> + +<p>The strings can now be retrieved by your application through the +symbol specified in the "id" attribute:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + super.onStart(); + + String back = getResources().getString(R.string.back).toString(); + back = getString(R.string.back).toString(); // synonym + } +} +</pre> + +<p>Unlike system resources, the resource symbol (the R class) we are using +here comes from our own application's package, not android.R.</p> + +<p>Note that the "mainLabel" string is complex, including style information. +To support this, the <code>getString()</code> method returns a +<code>CharSequence</code> object that you can pass to a +<code>TextView</code> to retain those style. This is why code +must call <code>toString()</code> on the returned resource if it wants +a raw string.</p> + +<a name="ColorResources"></a> +<h4>Color Resources</h4> + +<p>Color resources are created in a way very similar to string resources, +but with the <color> resource tag. The data for these resources +must be a hex color constant of the form "#rgb", "#argb", "#rrggbb", or +"#aarrggbb". The alpha channel is 0xff (or 0xf) for opaque and 0 +for transparent.</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color id="opaque_red">#ffff0000</color> + <color id="transparent_red">#80ff0000</color> + <color id="opaque_blue">#0000ff</color> + <color id="opaque_green">#0f0</color> +</resources> +</pre> + +<p>While color definitions could be placed in the same resource file +as the previously shown string data, usually you will place the colors in +their own file:</p> + +<pre> +MyApp/res/values/colors.xml +</pre> + +<p>The colors can now be retrieved by your application through the +symbol specified in the "id" attribute:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + super.onStart(); + + int red = getResources().getColor(R.color.opaque_red); + } +} +</pre> + +<a name="DrawableResources"></a> +<h4>Drawable Resources</h4> + +<p>For simple drawable resources, all you need to do is place your +image in a special resource sub-directory called "drawable". Files here +are things that can be handled by an implementation of the +{@link android.graphics.drawable.Drawable Drawable} class, often bitmaps +(such as PNG images) but also various kinds of XML descriptions +for selectors, gradients, etc.</p> + +<p>The drawable files will be scanned by the +resource tool, automatically generating a resource entry for each found. +For example the file <code>res/drawable/<myimage>.<ext></code> +will result in a resource symbol named "myimage" (without the extension). Note +that these file names <em>must</em> be valid Java identifiers, and should +have only lower-case letters.</p> + +<p>For example, to use your own custom image as a badge in a screen, +you can place the image here:</p> + +<pre> +MyApp/res/drawable/my_badge.png +</pre> + +<p>The image can then be used in your code like this:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + requestScreenFeatures(FEATURE_BADGE_IMAGE); + + super.onStart(); + + setBadgeResource(R.drawable.my_badge); + } +} +</pre> + +<p>For drawables that are a single solid color, you can also define them +in a resource file very much like colors shown previously. The only +difference is that here we use the <drawable> tag to create a +drawable resource.</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <drawable id="opaque_red">#ffff0000</drawable> + <drawable id="transparent_red">#80ff0000</drawable> + <drawable id="opaque_blue">#0000ff</drawable> + <drawable id="opaque_green">#0f0</drawable> +</resources> +</pre> + +<p>These resource entries are often placed in the same resource file +as color definitions:</p> + +<pre> +MyApp/res/values/colors.xml +</pre> + +<a name="LayoutResources"></a> +<h4>Layout Resources</h4> + +<p>Layout resources describe a view hierarchy configuration that is +generated at runtime. These resources are XML files placed in the +resource directory "layout", and are how you should create the content +views inside of your screen (instead of creating them by hand) so that +they can be themed, styled, configured, and overlayed.</p> + +<p>Here is a simple layout resource consisting of a single view, a text +editor:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill-parent" android:layout_height="fill-parent" + android:text="Hello, World!" /> +</root> +</pre> + +<p>To use this layout, it can be placed in a file like this:</p> + +<pre> +MyApp/res/layout/my_layout.xml +</pre> + +<p>The layout can then be instantiated in your screen like this:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + super.onStart(); + setContentView(R.layout.my_layout); + } +} +</pre> + +<p>Note that there are a number of visual attributes that can be supplied +to TextView (including textSize, textColor, and textStyle) that we did +not define in the previous example; in such a sitation, the default values for +those attributes come from the theme. If we want to customize them, we +can supply them explicitly in the XML file:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill_parent" android:layout_height="fill_parent" + <b>android:textSize="18" android:textColor="#008"</b> + android:text="Hello, World!" /> +</root> +</pre> + +<p>However, usually these kinds of attributes (those being attributes that +usually make sense to vary with theme or overlay) should be defined through +the theme or separate style resource. Later we will see how this is done.</p> + +<a name="ReferencesToResources"></a> +<h4>References to Resources</h4> + +<p>A value supplied in an attribute (or resource) can also be a reference to +a resource. This is often used in layout files to supply strings (so they +can be localized) and images (which exist in another file), though a reference +can be do any resource type including colors and integers.</p> + +<p>For example, if we have the previously defined color resources, we can +write a layout file that sets the text color size to be the value contained in +one of those resources:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill_parent" android:layout_height="fill_parent" + <b>android:textColor="@color/opaque_red"</b> + android:text="Hello, World!" /> +</root> +</pre> + +<p>Note here the use of the '@' prefix to introduce a resource reference -- the +text following that is the name of a resource in the form +of <code>@[package:]type/name</code>. In this case we didn't need to specify +the package because we are referencing a resource in our own package. To +reference a system resource, you would need to write:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill_parent" android:layout_height="fill_parent" + android:textColor="@<b>android:</b>color/opaque_red" + android:text="Hello, World!" /> +</root> +</pre> + +<p>As another example, you should always use resource references when supplying +strings in a layout file so that they can be localized:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill_parent" android:layout_height="fill_parent" + android:textColor="@android:color/opaque_red" + android:text="@string/hello_world" /> +</root> +</pre> + +<p>This facility can also be used to create references between resources. +For example, we can create new drawable resources that are aliases for +existing images:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <drawable id="my_background">@android:drawable/theme2_background</drawable> +</resources> +</pre> + +<a name="ReferencesToThemeAttributes"></a> +<h4>References to Theme Attributes</h4> + +<p>Another kind of resource value allows you to reference the value of an +attribute in the current theme. This attribute reference can <em>only</em> +be used in style resources and XML attributes; it allows you to customize the +look of UI elements by changing them to standard variations supplied by the +current theme, instead of supplying more concrete values.</p> + +<p>As an example, we can use this in our layout to set the text color to +one of the standard colors defined in the base system theme:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text" + android:layout_width="fill_parent" android:layout_height="fill_parent" + <b>android:textColor="?android:textDisabledColor"</b> + android:text="@string/hello_world" /> +</root> +</pre> + +<p>Note that this is very similar to a resource reference, except we are using +an '?' prefix instead of '@'. When you use this markup, you are supplying +the name of an attribute resource that will be looked up in the theme -- +because the resource tool knows that an attribute resource is expected, +you do not need to explicitly state the type (which would be +<code>?android:attr/android:textDisabledColor</code>).</p> + +<p>Other than using this resource identifier to find the value in the +theme instead of raw resources, the name syntax is identical to the '@' format: +<code>?[package:]type/name</code> with the type here being optional.</p> + +<a name="StyleResources"></a> +<h4>Style Resources</h4> + +<p>A style resource is a set of name/value pairs describing a group +of related attributes. There are two main uses for these resources: +defining overall visual themes, and describing a set of visual attributes +to apply to a class in a layout resource. In this section we will look +at their use to describe themes; later we will look at using them in +conjunction with layouts.</p> + +<p>Like strings, styles are defined through a resource XML file. In the +situation where we want to define a new theme, we can create a custom theme +style that inherits from one of the standard system themes:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style id="Theme" parent="android:Theme.White"> + <item id="android:foregroundColor">#FFF8D96F</item> + <item id="android:textColor">@color/opaque_blue</item> + <item id="android:textSelectedColor">?android:textColor</item> + </style> +</resources> +</pre> + +<p>Typically these resource definitions will be placed in a file +called "styles.xml" , and must be placed in the <code>values</code> +directory:</p> + +<pre> +MyApp/res/values/styles.xml +</pre> + +<p>Similar to how we previously used a system style for an Activity theme, +you can apply this style to your Activity:</p> + +<pre class="prettyprint"> +public class MyActivity extends Activity +{ + public void onStart() + { + super.onStart(); + + setTheme(R.style.Theme); + } +} +</pre> + +<p>In the style resource shown here, we used the <code>parent</code> +attribute to specify another style resource from which it inherits +its values -- in this case the <code>Theme.White</code> system resource:</p> + +<pre> + <style id="Home" parent="android:Theme.White"> + ... + </style> +</pre> + +<p>Note, when doing this, that you must use the "android" prefix in front +to tell the compiler the namespace to look in for the resource -- +the resources you are specifying here are in your application's namespace, +not the system. This explicit namespace specification ensures that names +the application uses will not accidentally conflict with those defined by +the system.</p> + +<p>If you don't specify an explicit parent style, it will be inferred +from the style name -- everything before the final '.' in the name of the +style being defined is taken as the parent style name. Thus, to make +another style in your application that inherits from this base Theme style, +you can write:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style id="Theme.WhiteText"> + <item id="android:foregroundColor">#FFFFFFFF</item> + <item id="android:textColor">?android:foregroundColor</item> + </style> +</resources> +</pre> + +<p>This results in the symbol <code>R.style.Theme_WhiteText</code> that +can be used in Java just like we did with <code>R.style.Theme</code> +above.</p> + +<a name="StylesInLayoutResources"></a> +<h4>Styles in Layout Resources</h4> + +<p>Often you will have a number fo views in a layout that all use the same +set of attributes, or want to allow resource overlays to modify the values of +attributes. Style resources can be used for both of these purposes, to put +attribute definitions in a single place that can be references by multiple +XML tags and modified by overlays. To do this, you simply define a +new style resource with the desired values:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style id="SpecialText"> + <item id="android:textSize">18</item> + <item id="android:textColor">#008</item> + </style> +</resources> +</pre> + +<p>You can now apply this style to your TextView in the XML file:</p> + +<pre> +<?xml version="1.0" encoding="utf-8"?> +<root> + <EditText id="text1" <b>style="@style/SpecialText"</b> + android:layout_width="fill_parent" android:layout_height="wrap_content" + android:text="Hello, World!" /> + <EditText id="text2" <b>style="@style/SpecialText"</b> + android:layout_width="fill_parent" android:layout_height="wrap_content" + android:text="I love you all." /> +</root></pre> +<h4> </h4> + +</body> +</html> + diff --git a/core/java/android/content/pm/ActivityInfo.aidl b/core/java/android/content/pm/ActivityInfo.aidl new file mode 100755 index 0000000..dd90302 --- /dev/null +++ b/core/java/android/content/pm/ActivityInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable ActivityInfo; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java new file mode 100644 index 0000000..85d877a --- /dev/null +++ b/core/java/android/content/pm/ActivityInfo.java @@ -0,0 +1,353 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Printer; + +/** + * Information you can retrieve about a particular application + * activity or receiver. This corresponds to information collected + * from the AndroidManifest.xml's <activity> and + * <receiver> tags. + */ +public class ActivityInfo extends ComponentInfo + implements Parcelable { + /** + * A style resource identifier (in the package's resources) of this + * activity's theme. From the "theme" attribute or, if not set, 0. + */ + public int theme; + + /** + * Constant corresponding to <code>standard</code> in + * the {@link android.R.attr#launchMode} attribute. + */ + public static final int LAUNCH_MULTIPLE = 0; + /** + * Constant corresponding to <code>singleTop</code> in + * the {@link android.R.attr#launchMode} attribute. + */ + public static final int LAUNCH_SINGLE_TOP = 1; + /** + * Constant corresponding to <code>singleTask</code> in + * the {@link android.R.attr#launchMode} attribute. + */ + public static final int LAUNCH_SINGLE_TASK = 2; + /** + * Constant corresponding to <code>singleInstance</code> in + * the {@link android.R.attr#launchMode} attribute. + */ + public static final int LAUNCH_SINGLE_INSTANCE = 3; + /** + * The launch mode style requested by the activity. From the + * {@link android.R.attr#launchMode} attribute, one of + * {@link #LAUNCH_MULTIPLE}, + * {@link #LAUNCH_SINGLE_TOP}, {@link #LAUNCH_SINGLE_TASK}, or + * {@link #LAUNCH_SINGLE_INSTANCE}. + */ + public int launchMode; + + /** + * Optional name of a permission required to be able to access this + * Activity. From the "permission" attribute. + */ + public String permission; + + /** + * The affinity this activity has for another task in the system. The + * string here is the name of the task, often the package name of the + * overall package. If null, the activity has no affinity. Set from the + * {@link android.R.attr#taskAffinity} attribute. + */ + public String taskAffinity; + + /** + * If this is an activity alias, this is the real activity class to run + * for it. Otherwise, this is null. + */ + public String targetActivity; + + /** + * Bit in {@link #flags} indicating whether this activity is able to + * run in multiple processes. If + * true, the system may instantiate it in the some process as the + * process starting it in order to conserve resources. If false, the + * default, it always runs in {@link #processName}. Set from the + * {@link android.R.attr#multiprocess} attribute. + */ + public static final int FLAG_MULTIPROCESS = 0x0001; + /** + * Bit in {@link #flags} indicating that, when the activity's task is + * relaunched from home, this activity should be finished. + * Set from the + * {@link android.R.attr#finishOnTaskLaunch} attribute. + */ + public static final int FLAG_FINISH_ON_TASK_LAUNCH = 0x0002; + /** + * Bit in {@link #flags} indicating that, when the activity is the root + * of a task, that task's stack should be cleared each time the user + * re-launches it from home. As a result, the user will always + * return to the original activity at the top of the task. + * This flag only applies to activities that + * are used to start the root of a new task. Set from the + * {@link android.R.attr#clearTaskOnLaunch} attribute. + */ + public static final int FLAG_CLEAR_TASK_ON_LAUNCH = 0x0004; + /** + * Bit in {@link #flags} indicating that, when the activity is the root + * of a task, that task's stack should never be cleared when it is + * relaunched from home. Set from the + * {@link android.R.attr#alwaysRetainTaskState} attribute. + */ + public static final int FLAG_ALWAYS_RETAIN_TASK_STATE = 0x0008; + /** + * Bit in {@link #flags} indicating that the activity's state + * is not required to be saved, so that if there is a failure the + * activity will not be removed from the activity stack. Set from the + * {@link android.R.attr#stateNotNeeded} attribute. + */ + public static final int FLAG_STATE_NOT_NEEDED = 0x0010; + /** + * Bit in {@link #flags} that indicates that the activity should not + * appear in the list of recently launched activities. Set from the + * {@link android.R.attr#excludeFromRecents} attribute. + */ + public static final int FLAG_EXCLUDE_FROM_RECENTS = 0x0020; + /** + * Bit in {@link #flags} that indicates that the activity can be moved + * between tasks based on its task affinity. Set from the + * {@link android.R.attr#allowTaskReparenting} attribute. + */ + public static final int FLAG_ALLOW_TASK_REPARENTING = 0x0040; + /** + * Bit in {@link #flags} indicating that, when the user navigates away + * from an activity, it should be finished. + * Set from the + * {@link android.R.attr#noHistory} attribute. + */ + public static final int FLAG_NO_HISTORY = 0x0080; + /** + * 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}. + */ + public int flags; + + /** + * Constant corresponding to <code>unspecified</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_UNSPECIFIED = -1; + /** + * Constant corresponding to <code>landscape</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_LANDSCAPE = 0; + /** + * Constant corresponding to <code>portrait</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_PORTRAIT = 1; + /** + * Constant corresponding to <code>user</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_USER = 2; + /** + * Constant corresponding to <code>behind</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_BEHIND = 3; + /** + * Constant corresponding to <code>sensor</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_SENSOR = 4; + + /** + * Constant corresponding to <code>sensor</code> in + * the {@link android.R.attr#screenOrientation} attribute. + */ + public static final int SCREEN_ORIENTATION_NOSENSOR = 5; + /** + * The preferred screen orientation this activity would like to run in. + * From the {@link android.R.attr#screenOrientation} attribute, one of + * {@link #SCREEN_ORIENTATION_UNSPECIFIED}, + * {@link #SCREEN_ORIENTATION_LANDSCAPE}, + * {@link #SCREEN_ORIENTATION_PORTRAIT}, + * {@link #SCREEN_ORIENTATION_USER}, + * {@link #SCREEN_ORIENTATION_BEHIND}, + * {@link #SCREEN_ORIENTATION_SENSOR}, + * {@link #SCREEN_ORIENTATION_NOSENSOR}. + */ + public int screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED; + + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the IMSI MCC. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_MCC = 0x0001; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the IMSI MNC. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_MNC = 0x0002; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the locale. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_LOCALE = 0x0004; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the touchscreen type. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_TOUCHSCREEN = 0x0008; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the keyboard type. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + 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. + * Set from the {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_KEYBOARD_HIDDEN = 0x0020; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the navigation type. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_NAVIGATION = 0x0040; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the screen orientation. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_ORIENTATION = 0x0080; + /** + * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the font scaling factor. Set from the + * {@link android.R.attr#configChanges} attribute. This is + * not a core resource configutation, but a higher-level value, so its + * constant starts at the high bits. + */ + public static final int CONFIG_FONT_SCALE = 0x40000000; + + /** + * Bit mask of kinds of configuration changes that this activity + * can handle itself (without being restarted by the system). + * Contains any combination of {@link #CONFIG_FONT_SCALE}, + * {@link #CONFIG_MCC}, {@link #CONFIG_MNC}, + * {@link #CONFIG_LOCALE}, {@link #CONFIG_TOUCHSCREEN}, + * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION}, and + * {@link #CONFIG_ORIENTATION}. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public int configChanges; + + /** + * The desired soft input mode for this activity's main window. + * Set from the {@link android.R.attr#windowSoftInputMode} attribute + * in the activity's manifest. May be any of the same values allowed + * for {@link android.view.WindowManager.LayoutParams#softInputMode + * WindowManager.LayoutParams.softInputMode}. If 0 (unspecified), + * the mode from the theme will be used. + */ + public int softInputMode; + + public ActivityInfo() { + } + + public ActivityInfo(ActivityInfo orig) { + super(orig); + theme = orig.theme; + launchMode = orig.launchMode; + permission = orig.permission; + taskAffinity = orig.taskAffinity; + targetActivity = orig.targetActivity; + flags = orig.flags; + screenOrientation = orig.screenOrientation; + configChanges = orig.configChanges; + softInputMode = orig.softInputMode; + } + + /** + * Return the theme resource identifier to use for this activity. If + * the activity defines a theme, that is used; else, the application + * theme is used. + * + * @return The theme associated with this activity. + */ + public final int getThemeResource() { + return theme != 0 ? theme : applicationInfo.theme; + } + + public void dump(Printer pw, String prefix) { + super.dumpFront(pw, prefix); + pw.println(prefix + "permission=" + permission); + pw.println(prefix + "taskAffinity=" + taskAffinity + + " targetActivity=" + targetActivity); + pw.println(prefix + "launchMode=" + launchMode + + " flags=0x" + Integer.toHexString(flags) + + " theme=0x" + Integer.toHexString(theme)); + pw.println(prefix + "screenOrientation=" + screenOrientation + + " configChanges=0x" + Integer.toHexString(configChanges) + + " softInputMode=0x" + Integer.toHexString(softInputMode)); + super.dumpBack(pw, prefix); + } + + public String toString() { + return "ActivityInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeInt(theme); + dest.writeInt(launchMode); + dest.writeString(permission); + dest.writeString(taskAffinity); + dest.writeString(targetActivity); + dest.writeInt(flags); + dest.writeInt(screenOrientation); + dest.writeInt(configChanges); + dest.writeInt(softInputMode); + } + + public static final Parcelable.Creator<ActivityInfo> CREATOR + = new Parcelable.Creator<ActivityInfo>() { + public ActivityInfo createFromParcel(Parcel source) { + return new ActivityInfo(source); + } + public ActivityInfo[] newArray(int size) { + return new ActivityInfo[size]; + } + }; + + private ActivityInfo(Parcel source) { + super(source); + theme = source.readInt(); + launchMode = source.readInt(); + permission = source.readString(); + taskAffinity = source.readString(); + targetActivity = source.readString(); + flags = source.readInt(); + screenOrientation = source.readInt(); + configChanges = source.readInt(); + softInputMode = source.readInt(); + } +} diff --git a/core/java/android/content/pm/ApplicationInfo.aidl b/core/java/android/content/pm/ApplicationInfo.aidl new file mode 100755 index 0000000..006d1bd --- /dev/null +++ b/core/java/android/content/pm/ApplicationInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable ApplicationInfo; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java new file mode 100644 index 0000000..8d727ed --- /dev/null +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -0,0 +1,310 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Printer; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Information you can retrieve about a particular application. This + * corresponds to information collected from the AndroidManifest.xml's + * <application> tag. + */ +public class ApplicationInfo extends PackageItemInfo implements Parcelable { + + /** + * Default task affinity of all activities in this application. See + * {@link ActivityInfo#taskAffinity} for more information. This comes + * from the "taskAffinity" attribute. + */ + public String taskAffinity; + + /** + * Optional name of a permission required to be able to access this + * application's components. From the "permission" attribute. + */ + public String permission; + + /** + * The name of the process this application should run in. From the + * "process" attribute or, if not set, the same as + * <var>packageName</var>. + */ + public String processName; + + /** + * Class implementing the Application object. From the "class" + * attribute. + */ + public String className; + + /** + * A style resource identifier (in the package's resources) of the + * description of an application. From the "description" attribute + * or, if not set, 0. + */ + public int descriptionRes; + + /** + * A style resource identifier (in the package's resources) of the + * default visual theme of the application. From the "theme" attribute + * or, if not set, 0. + */ + public int theme; + + /** + * Class implementing the Application's manage space + * functionality. From the "manageSpaceActivity" + * attribute. This is an optional attribute and will be null if + * application's dont specify it in their manifest + */ + public String manageSpaceActivityName; + + /** + * Value for {@link #flags}: if set, this application is installed in the + * device's system image. + */ + public static final int FLAG_SYSTEM = 1<<0; + + /** + * Value for {@link #flags}: set to true if this application would like to + * allow debugging of its + * code, even when installed on a non-development system. Comes + * from {@link android.R.styleable#AndroidManifestApplication_debuggable + * android:debuggable} of the <application> tag. + */ + public static final int FLAG_DEBUGGABLE = 1<<1; + + /** + * Value for {@link #flags}: set to true if this application has code + * associated with it. Comes + * from {@link android.R.styleable#AndroidManifestApplication_hasCode + * android:hasCode} of the <application> tag. + */ + public static final int FLAG_HAS_CODE = 1<<2; + + /** + * Value for {@link #flags}: set to true if this application is persistent. + * Comes from {@link android.R.styleable#AndroidManifestApplication_persistent + * android:persistent} of the <application> tag. + */ + public static final int FLAG_PERSISTENT = 1<<3; + + /** + * Value for {@link #flags}: set to true iif this application holds the + * {@link android.Manifest.permission#FACTORY_TEST} permission and the + * device is running in factory test mode. + */ + public static final int FLAG_FACTORY_TEST = 1<<4; + + /** + * Value for {@link #flags}: default value for the corresponding ActivityInfo flag. + * Comes from {@link android.R.styleable#AndroidManifestApplication_allowTaskReparenting + * android:allowTaskReparenting} of the <application> tag. + */ + public static final int FLAG_ALLOW_TASK_REPARENTING = 1<<5; + + /** + * Value for {@link #flags}: default value for the corresponding ActivityInfo flag. + * Comes from {@link android.R.styleable#AndroidManifestApplication_allowClearUserData + * android:allowClearUserData} of the <application> tag. + */ + public static final int FLAG_ALLOW_CLEAR_USER_DATA = 1<<6; + + + /** + * Value for {@link #flags}: default value for the corresponding ActivityInfo flag. + * {@hide} + */ + public static final int FLAG_UPDATED_SYSTEM_APP = 1<<7; + + /** + * Flags associated with the application. Any combination of + * {@link #FLAG_SYSTEM}, {@link #FLAG_DEBUGGABLE}, {@link #FLAG_HAS_CODE}, + * {@link #FLAG_PERSISTENT}, {@link #FLAG_FACTORY_TEST}, and + * {@link #FLAG_ALLOW_TASK_REPARENTING} + * {@link #FLAG_ALLOW_CLEAR_USER_DATA}. + */ + public int flags = 0; + + /** + * Full path to the location of this package. + */ + public String sourceDir; + + /** + * Full path to the location of the publicly available parts of this package (i.e. the resources + * and manifest). For non-forward-locked apps this will be the same as {@link #sourceDir). + */ + public String publicSourceDir; + + /** + * Paths to all shared libraries this application is linked against. This + * field is only set if the {@link PackageManager#GET_SHARED_LIBRARY_FILES + * PackageManager.GET_SHARED_LIBRARY_FILES} flag was used when retrieving + * the structure. + */ + public String[] sharedLibraryFiles; + + /** + * Full path to a directory assigned to the package for its persistent + * data. + */ + public String dataDir; + + /** + * The kernel user-ID that has been assigned to this application; + * currently this is not a unique ID (multiple applications can have + * the same uid). + */ + public int uid; + + /** + * When false, indicates that all components within this application are + * considered disabled, regardless of their individually set enabled status. + */ + public boolean enabled = true; + + public void dump(Printer pw, String prefix) { + super.dumpFront(pw, prefix); + pw.println(prefix + "className=" + className); + pw.println(prefix + "permission=" + permission + + " uid=" + uid); + pw.println(prefix + "taskAffinity=" + taskAffinity); + pw.println(prefix + "theme=0x" + Integer.toHexString(theme)); + pw.println(prefix + "flags=0x" + Integer.toHexString(flags) + + " processName=" + processName); + pw.println(prefix + "sourceDir=" + sourceDir); + pw.println(prefix + "publicSourceDir=" + publicSourceDir); + pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles); + pw.println(prefix + "dataDir=" + dataDir); + pw.println(prefix + "enabled=" + enabled); + pw.println(prefix+"manageSpaceActivityName="+manageSpaceActivityName); + pw.println(prefix+"description=0x"+Integer.toHexString(descriptionRes)); + super.dumpBack(pw, prefix); + } + + public static class DisplayNameComparator + implements Comparator<ApplicationInfo> { + public DisplayNameComparator(PackageManager pm) { + mPM = pm; + } + + public final int compare(ApplicationInfo aa, ApplicationInfo ab) { + CharSequence sa = mPM.getApplicationLabel(aa); + if (sa == null) { + sa = aa.packageName; + } + CharSequence sb = mPM.getApplicationLabel(ab); + if (sb == null) { + sb = ab.packageName; + } + + return sCollator.compare(sa.toString(), sb.toString()); + } + + private final Collator sCollator = Collator.getInstance(); + private PackageManager mPM; + } + + public ApplicationInfo() { + } + + public ApplicationInfo(ApplicationInfo orig) { + super(orig); + taskAffinity = orig.taskAffinity; + permission = orig.permission; + processName = orig.processName; + className = orig.className; + theme = orig.theme; + flags = orig.flags; + sourceDir = orig.sourceDir; + publicSourceDir = orig.publicSourceDir; + sharedLibraryFiles = orig.sharedLibraryFiles; + dataDir = orig.dataDir; + uid = orig.uid; + enabled = orig.enabled; + manageSpaceActivityName = orig.manageSpaceActivityName; + descriptionRes = orig.descriptionRes; + } + + + public String toString() { + return "ApplicationInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(taskAffinity); + dest.writeString(permission); + dest.writeString(processName); + dest.writeString(className); + dest.writeInt(theme); + dest.writeInt(flags); + dest.writeString(sourceDir); + dest.writeString(publicSourceDir); + dest.writeStringArray(sharedLibraryFiles); + dest.writeString(dataDir); + dest.writeInt(uid); + dest.writeInt(enabled ? 1 : 0); + dest.writeString(manageSpaceActivityName); + dest.writeInt(descriptionRes); + } + + public static final Parcelable.Creator<ApplicationInfo> CREATOR + = new Parcelable.Creator<ApplicationInfo>() { + public ApplicationInfo createFromParcel(Parcel source) { + return new ApplicationInfo(source); + } + public ApplicationInfo[] newArray(int size) { + return new ApplicationInfo[size]; + } + }; + + private ApplicationInfo(Parcel source) { + super(source); + taskAffinity = source.readString(); + permission = source.readString(); + processName = source.readString(); + className = source.readString(); + theme = source.readInt(); + flags = source.readInt(); + sourceDir = source.readString(); + publicSourceDir = source.readString(); + sharedLibraryFiles = source.readStringArray(); + dataDir = source.readString(); + uid = source.readInt(); + enabled = source.readInt() != 0; + manageSpaceActivityName = source.readString(); + descriptionRes = source.readInt(); + } + + /** + * Retrieve the textual description of the application. This + * will call back on the given PackageManager to load the description from + * the application. + * + * @param pm A PackageManager from which the label can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a CharSequence containing the application's description. + * If there is no description, null is returned. + */ + public CharSequence loadDescription(PackageManager pm) { + if (descriptionRes != 0) { + CharSequence label = pm.getText(packageName, descriptionRes, null); + if (label != null) { + return label; + } + } + return null; + } +} diff --git a/core/java/android/content/pm/ComponentInfo.java b/core/java/android/content/pm/ComponentInfo.java new file mode 100644 index 0000000..73c9244 --- /dev/null +++ b/core/java/android/content/pm/ComponentInfo.java @@ -0,0 +1,138 @@ +package android.content.pm; + +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.util.Printer; + +/** + * Base class containing information common to all application components + * ({@link ActivityInfo}, {@link ServiceInfo}). This class is not intended + * to be used by itself; it is simply here to share common definitions + * between all application components. As such, it does not itself + * implement Parcelable, but does provide convenience methods to assist + * in the implementation of Parcelable in subclasses. + */ +public class ComponentInfo extends PackageItemInfo { + /** + * Global information about the application/package this component is a + * part of. + */ + public ApplicationInfo applicationInfo; + + /** + * The name of the process this component should run in. + * From the "android:process" attribute or, if not set, the same + * as <var>applicationInfo.processName</var>. + */ + public String processName; + + /** + * Indicates whether or not this component may be instantiated. Note that this value can be + * overriden by the one in its parent {@link ApplicationInfo}. + */ + public boolean enabled = true; + + /** + * Set to true if this component is available for use by other applications. + * Comes from {@link android.R.attr#exported android:exported} of the + * <activity>, <receiver>, <service>, or + * <provider> tag. + */ + public boolean exported = false; + + public ComponentInfo() { + } + + public ComponentInfo(ComponentInfo orig) { + super(orig); + applicationInfo = orig.applicationInfo; + processName = orig.processName; + enabled = orig.enabled; + exported = orig.exported; + } + + @Override public CharSequence loadLabel(PackageManager pm) { + if (nonLocalizedLabel != null) { + return nonLocalizedLabel; + } + ApplicationInfo ai = applicationInfo; + CharSequence label; + if (labelRes != 0) { + label = pm.getText(packageName, labelRes, ai); + if (label != null) { + return label; + } + } + if (ai.nonLocalizedLabel != null) { + return ai.nonLocalizedLabel; + } + if (ai.labelRes != 0) { + label = pm.getText(packageName, ai.labelRes, ai); + if (label != null) { + return label; + } + } + return name; + } + + @Override public Drawable loadIcon(PackageManager pm) { + ApplicationInfo ai = applicationInfo; + Drawable dr; + if (icon != 0) { + dr = pm.getDrawable(packageName, icon, ai); + if (dr != null) { + return dr; + } + } + if (ai.icon != 0) { + dr = pm.getDrawable(packageName, ai.icon, ai); + if (dr != null) { + return dr; + } + } + return pm.getDefaultActivityIcon(); + } + + /** + * Return the icon resource identifier to use for this component. If + * the component defines an icon, that is used; else, the application + * icon is used. + * + * @return The icon associated with this component. + */ + public final int getIconResource() { + return icon != 0 ? icon : applicationInfo.icon; + } + + protected void dumpFront(Printer pw, String prefix) { + super.dumpFront(pw, prefix); + pw.println(prefix + "enabled=" + enabled + " exported=" + exported + + " processName=" + processName); + } + + protected void dumpBack(Printer pw, String prefix) { + if (applicationInfo != null) { + pw.println(prefix + "ApplicationInfo:"); + applicationInfo.dump(pw, prefix + " "); + } else { + pw.println(prefix + "ApplicationInfo: null"); + } + super.dumpBack(pw, prefix); + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + applicationInfo.writeToParcel(dest, parcelableFlags); + dest.writeString(processName); + dest.writeInt(enabled ? 1 : 0); + dest.writeInt(exported ? 1 : 0); + } + + protected ComponentInfo(Parcel source) { + super(source); + applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source); + processName = source.readString(); + enabled = (source.readInt() != 0); + exported = (source.readInt() != 0); + } +} diff --git a/core/java/android/content/pm/ConfigurationInfo.java b/core/java/android/content/pm/ConfigurationInfo.java new file mode 100755 index 0000000..dcc7463 --- /dev/null +++ b/core/java/android/content/pm/ConfigurationInfo.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2008 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.os.Parcel; +import android.os.Parcelable; + +/** + * Information you can retrieve about hardware configuration preferences + * declared by an application. This corresponds to information collected from the + * AndroidManifest.xml's <uses-configuration> tags. + */ +public class ConfigurationInfo implements Parcelable { + /** + * The kind of touch screen attached to the device. + * One of: {@link android.content.res.Configuration#TOUCHSCREEN_NOTOUCH}, + * {@link android.content.res.Configuration#TOUCHSCREEN_STYLUS}, + * {@link android.content.res.Configuration#TOUCHSCREEN_FINGER}. + */ + public int reqTouchScreen; + + /** + * Application's input method preference. + * One of: {@link android.content.res.Configuration#KEYBOARD_UNDEFINED}, + * {@link android.content.res.Configuration#KEYBOARD_NOKEYS}, + * {@link android.content.res.Configuration#KEYBOARD_QWERTY}, + * {@link android.content.res.Configuration#KEYBOARD_12KEY} + */ + public int reqKeyboardType; + + /** + * A flag indicating whether any keyboard is available. + * one of: {@link android.content.res.Configuration#NAVIGATION_UNDEFINED}, + * {@link android.content.res.Configuration#NAVIGATION_DPAD}, + * {@link android.content.res.Configuration#NAVIGATION_TRACKBALL}, + * {@link android.content.res.Configuration#NAVIGATION_WHEEL} + */ + public int reqNavigation; + + /** + * Value for {@link #reqInputFeatures}: if set, indicates that the application + * requires a hard keyboard + */ + public static final int INPUT_FEATURE_HARD_KEYBOARD = 0x00000001; + + /** + * Value for {@link #reqInputFeatures}: if set, indicates that the application + * requires a five way navigation device + */ + public static final int INPUT_FEATURE_FIVE_WAY_NAV = 0x00000002; + + /** + * Flags associated with the input features. Any combination of + * {@link #INPUT_FEATURE_HARD_KEYBOARD}, + * {@link #INPUT_FEATURE_FIVE_WAY_NAV} + */ + public int reqInputFeatures = 0; + + public ConfigurationInfo() { + } + + public ConfigurationInfo(ConfigurationInfo orig) { + reqTouchScreen = orig.reqTouchScreen; + reqKeyboardType = orig.reqKeyboardType; + reqNavigation = orig.reqNavigation; + reqInputFeatures = orig.reqInputFeatures; + } + + public String toString() { + return "ApplicationHardwarePreferences{" + + Integer.toHexString(System.identityHashCode(this)) + + ", touchscreen = " + reqTouchScreen + "}" + + ", inputMethod = " + reqKeyboardType + "}" + + ", navigation = " + reqNavigation + "}" + + ", reqInputFeatures = " + reqInputFeatures + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeInt(reqTouchScreen); + dest.writeInt(reqKeyboardType); + dest.writeInt(reqNavigation); + dest.writeInt(reqInputFeatures); + } + + public static final Creator<ConfigurationInfo> CREATOR = + new Creator<ConfigurationInfo>() { + public ConfigurationInfo createFromParcel(Parcel source) { + return new ConfigurationInfo(source); + } + public ConfigurationInfo[] newArray(int size) { + return new ConfigurationInfo[size]; + } + }; + + private ConfigurationInfo(Parcel source) { + reqTouchScreen = source.readInt(); + reqKeyboardType = source.readInt(); + reqNavigation = source.readInt(); + reqInputFeatures = source.readInt(); + } +} diff --git a/core/java/android/content/pm/IPackageDataObserver.aidl b/core/java/android/content/pm/IPackageDataObserver.aidl new file mode 100755 index 0000000..d010ee4 --- /dev/null +++ b/core/java/android/content/pm/IPackageDataObserver.aidl @@ -0,0 +1,28 @@ +/* +** +** 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.pm; + +/** + * API for package data change related callbacks from the Package Manager. + * Some usage scenarios include deletion of cache directory, generate + * statistics related to code, data, cache usage(TODO) + * {@hide} + */ +oneway interface IPackageDataObserver { + void onRemoveCompleted(in String packageName, boolean succeeded); +} diff --git a/core/java/android/content/pm/IPackageDeleteObserver.aidl b/core/java/android/content/pm/IPackageDeleteObserver.aidl new file mode 100644 index 0000000..bc16b3e --- /dev/null +++ b/core/java/android/content/pm/IPackageDeleteObserver.aidl @@ -0,0 +1,28 @@ +/* +** +** 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.pm; + +/** + * API for deletion callbacks from the Package Manager. + * + * {@hide} + */ +oneway interface IPackageDeleteObserver { + void packageDeleted(in boolean succeeded); +} + diff --git a/core/java/android/content/pm/IPackageInstallObserver.aidl b/core/java/android/content/pm/IPackageInstallObserver.aidl new file mode 100644 index 0000000..e83bbc6 --- /dev/null +++ b/core/java/android/content/pm/IPackageInstallObserver.aidl @@ -0,0 +1,27 @@ +/* +** +** 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.pm; + +/** + * API for installation callbacks from the Package Manager. + * + */ +oneway interface IPackageInstallObserver { + void packageInstalled(in String packageName, int returnCode); +} + diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl new file mode 100644 index 0000000..d3f6f3c --- /dev/null +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -0,0 +1,269 @@ +/* +** +** 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.pm; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageInstallObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageStatsObserver; +import android.content.pm.InstrumentationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.net.Uri; +import android.app.PendingIntent; + +/** + * See {@link PackageManager} for documentation on most of the APIs + * here. + * + * {@hide} + */ +interface IPackageManager { + PackageInfo getPackageInfo(String packageName, int flags); + int getPackageUid(String packageName); + int[] getPackageGids(String packageName); + + PermissionInfo getPermissionInfo(String name, int flags); + + List<PermissionInfo> queryPermissionsByGroup(String group, int flags); + + PermissionGroupInfo getPermissionGroupInfo(String name, int flags); + + List<PermissionGroupInfo> getAllPermissionGroups(int flags); + + ApplicationInfo getApplicationInfo(String packageName, int flags); + + ActivityInfo getActivityInfo(in ComponentName className, int flags); + + ActivityInfo getReceiverInfo(in ComponentName className, int flags); + + ServiceInfo getServiceInfo(in ComponentName className, int flags); + + int checkPermission(String permName, String pkgName); + + int checkUidPermission(String permName, int uid); + + boolean addPermission(in PermissionInfo info); + + void removePermission(String name); + + int checkSignatures(String pkg1, String pkg2); + + String[] getPackagesForUid(int uid); + + String getNameForUid(int uid); + + int getUidForSharedUser(String sharedUserName); + + ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags); + + List<ResolveInfo> queryIntentActivities(in Intent intent, + String resolvedType, int flags); + + List<ResolveInfo> queryIntentActivityOptions( + in ComponentName caller, in Intent[] specifics, + in String[] specificTypes, in Intent intent, + String resolvedType, int flags); + + List<ResolveInfo> queryIntentReceivers(in Intent intent, + String resolvedType, int flags); + + ResolveInfo resolveService(in Intent intent, + String resolvedType, int flags); + + List<ResolveInfo> queryIntentServices(in Intent intent, + String resolvedType, int flags); + + List<PackageInfo> getInstalledPackages(int flags); + + List<ApplicationInfo> getInstalledApplications(int flags); + + /** + * Retrieve all applications that are marked as persistent. + * + * @return A List<applicationInfo> containing one entry for each persistent + * application. + */ + List<ApplicationInfo> getPersistentApplications(int flags); + + ProviderInfo resolveContentProvider(String name, int flags); + + /** + * Retrieve sync information for all content providers. + * + * @param outNames Filled in with a list of the root names of the content + * providers that can sync. + * @param outInfo Filled in with a list of the ProviderInfo for each + * name in 'outNames'. + */ + void querySyncProviders(inout List<String> outNames, + inout List<ProviderInfo> outInfo); + + List<ProviderInfo> queryContentProviders( + String processName, int uid, int flags); + + InstrumentationInfo getInstrumentationInfo( + in ComponentName className, int flags); + + List<InstrumentationInfo> queryInstrumentation( + String targetPackage, int flags); + + /** + * Install a package. + * + * @param packageURI The location of the package file to install. + * @param observer a callback to use to notify when the package installation in finished. + * @param flags - possible values: {@link #FORWARD_LOCK_PACKAGE}, + * {@link #REPLACE_EXISITING_PACKAGE} + */ + void installPackage(in Uri packageURI, IPackageInstallObserver observer, int flags); + + /** + * Delete a package. + * + * @param packageName The fully qualified name of the package to delete. + * @param observer a callback to use to notify when the package deletion in finished. + * @param flags - possible values: {@link #DONT_DELETE_DATA} + */ + void deletePackage(in String packageName, IPackageDeleteObserver observer, int flags); + + void addPackageToPreferred(String packageName); + + void removePackageFromPreferred(String packageName); + + List<PackageInfo> getPreferredPackages(int flags); + + void addPreferredActivity(in IntentFilter filter, int match, + in ComponentName[] set, in ComponentName activity); + void clearPackagePreferredActivities(String packageName); + int getPreferredActivities(out List<IntentFilter> outFilters, + out List<ComponentName> outActivities, String packageName); + + /** + * As per {@link android.content.pm.PackageManager#setComponentEnabledSetting}. + */ + void setComponentEnabledSetting(in ComponentName componentName, + in int newState, in int flags); + + /** + * As per {@link android.content.pm.PackageManager#getComponentEnabledSetting}. + */ + int getComponentEnabledSetting(in ComponentName componentName); + + /** + * As per {@link android.content.pm.PackageManager#setApplicationEnabledSetting}. + */ + void setApplicationEnabledSetting(in String packageName, in int newState, int flags); + + /** + * As per {@link android.content.pm.PackageManager#getApplicationEnabledSetting}. + */ + int getApplicationEnabledSetting(in String packageName); + + /** + * Free storage by deleting LRU sorted list of cache files across + * all applications. If the currently available free storage + * on the device is greater than or equal to the requested + * free storage, no cache files are cleared. If the currently + * available storage on the device is less than the requested + * free storage, some or all of the cache files across + * all applications are deleted (based on last accessed time) + * to increase the free storage space on the device to + * the requested value. There is no guarantee that clearing all + * the cache files from all applications will clear up + * enough storage to achieve the desired value. + * @param freeStorageSize The number of bytes of storage to be + * freed by the system. Say if freeStorageSize is XX, + * and the current free storage is YY, + * if XX is less than YY, just return. if not free XX-YY number + * of bytes if possible. + * @param observer call back used to notify when + * the operation is completed + */ + void freeStorageAndNotify(in long freeStorageSize, + IPackageDataObserver observer); + + /** + * Free storage by deleting LRU sorted list of cache files across + * all applications. If the currently available free storage + * on the device is greater than or equal to the requested + * free storage, no cache files are cleared. If the currently + * available storage on the device is less than the requested + * free storage, some or all of the cache files across + * all applications are deleted (based on last accessed time) + * to increase the free storage space on the device to + * the requested value. There is no guarantee that clearing all + * the cache files from all applications will clear up + * enough storage to achieve the desired value. + * @param freeStorageSize The number of bytes of storage to be + * freed by the system. Say if freeStorageSize is XX, + * and the current free storage is YY, + * if XX is less than YY, just return. if not free XX-YY number + * of bytes if possible. + * @param opFinishedIntent PendingIntent call back used to + * notify when the operation is completed.May be null + * to indicate that no call back is desired. + */ + void freeStorage(in long freeStorageSize, + in PendingIntent opFinishedIntent); + + /** + * Delete all the cache files in an applications cache directory + * @param packageName The package name of the application whose cache + * files need to be deleted + * @param observer a callback used to notify when the deletion is finished. + */ + void deleteApplicationCacheFiles(in String packageName, IPackageDataObserver observer); + + /** + * Clear the user data directory of an application. + * @param packageName The package name of the application whose cache + * files need to be deleted + * @param observer a callback used to notify when the operation is completed. + */ + void clearApplicationUserData(in String packageName, IPackageDataObserver observer); + + /** + * Get package statistics including the code, data and cache size for + * an already installed package + * @param packageName The package name of the application + * @param observer a callback to use to notify when the asynchronous + * retrieval of information is complete. + */ + void getPackageSizeInfo(in String packageName, IPackageStatsObserver observer); + + /** + * Get a list of shared libraries that are available on the + * system. + */ + String[] getSystemSharedLibraryNames(); + + void enterSafeMode(); + boolean isSafeMode(); + void systemReady(); + boolean hasSystemUidErrors(); +} diff --git a/core/java/android/content/pm/IPackageStatsObserver.aidl b/core/java/android/content/pm/IPackageStatsObserver.aidl new file mode 100755 index 0000000..ede4d1d --- /dev/null +++ b/core/java/android/content/pm/IPackageStatsObserver.aidl @@ -0,0 +1,30 @@ +/* +** +** 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.pm; + +import android.content.pm.PackageStats; +/** + * API for package data change related callbacks from the Package Manager. + * Some usage scenarios include deletion of cache directory, generate + * statistics related to code, data, cache usage(TODO) + * {@hide} + */ +oneway interface IPackageStatsObserver { + + void onGetStatsCompleted(in PackageStats pStats, boolean succeeded); +} diff --git a/core/java/android/content/pm/InstrumentationInfo.aidl b/core/java/android/content/pm/InstrumentationInfo.aidl new file mode 100755 index 0000000..3d847ae --- /dev/null +++ b/core/java/android/content/pm/InstrumentationInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable InstrumentationInfo; diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java new file mode 100644 index 0000000..30ca002 --- /dev/null +++ b/core/java/android/content/pm/InstrumentationInfo.java @@ -0,0 +1,98 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Information you can retrieve about a particular piece of test + * instrumentation. This corresponds to information collected + * from the AndroidManifest.xml's <instrumentation> tag. + */ +public class InstrumentationInfo extends PackageItemInfo implements Parcelable { + /** + * The name of the application package being instrumented. From the + * "package" attribute. + */ + public String targetPackage; + + /** + * Full path to the location of this package. + */ + public String sourceDir; + + /** + * Full path to the location of the publicly available parts of this package (i.e. the resources + * and manifest). For non-forward-locked apps this will be the same as {@link #sourceDir). + */ + public String publicSourceDir; + /** + * Full path to a directory assigned to the package for its persistent + * data. + */ + public String dataDir; + + /** + * Specifies whether or not this instrumentation will handle profiling. + */ + public boolean handleProfiling; + + /** Specifies whether or not to run this instrumentation as a functional test */ + public boolean functionalTest; + + public InstrumentationInfo() { + } + + public InstrumentationInfo(InstrumentationInfo orig) { + super(orig); + targetPackage = orig.targetPackage; + sourceDir = orig.sourceDir; + publicSourceDir = orig.publicSourceDir; + dataDir = orig.dataDir; + handleProfiling = orig.handleProfiling; + functionalTest = orig.functionalTest; + } + + public String toString() { + return "InstrumentationInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(targetPackage); + dest.writeString(sourceDir); + dest.writeString(publicSourceDir); + dest.writeString(dataDir); + dest.writeInt((handleProfiling == false) ? 0 : 1); + dest.writeInt((functionalTest == false) ? 0 : 1); + } + + public static final Parcelable.Creator<InstrumentationInfo> CREATOR + = new Parcelable.Creator<InstrumentationInfo>() { + public InstrumentationInfo createFromParcel(Parcel source) { + return new InstrumentationInfo(source); + } + public InstrumentationInfo[] newArray(int size) { + return new InstrumentationInfo[size]; + } + }; + + private InstrumentationInfo(Parcel source) { + super(source); + targetPackage = source.readString(); + sourceDir = source.readString(); + publicSourceDir = source.readString(); + dataDir = source.readString(); + handleProfiling = source.readInt() != 0; + functionalTest = source.readInt() != 0; + } +} diff --git a/core/java/android/content/pm/PackageInfo.aidl b/core/java/android/content/pm/PackageInfo.aidl new file mode 100755 index 0000000..35e2322 --- /dev/null +++ b/core/java/android/content/pm/PackageInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable PackageInfo; diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java new file mode 100644 index 0000000..d9326f2 --- /dev/null +++ b/core/java/android/content/pm/PackageInfo.java @@ -0,0 +1,199 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Overall information about the contents of a package. This corresponds + * to all of the information collected from AndroidManifest.xml. + */ +public class PackageInfo implements Parcelable { + /** + * The name of this package. From the <manifest> tag's "name" + * attribute. + */ + public String packageName; + + /** + * The version number of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_versionCode versionCode} + * attribute. + */ + public int versionCode; + + /** + * The version name of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_versionName versionName} + * attribute. + */ + public String versionName; + + /** + * The shared user ID name of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_sharedUserId sharedUserId} + * attribute. + */ + public String sharedUserId; + + /** + * The shared user ID label of this package, as specified by the <manifest> + * tag's {@link android.R.styleable#AndroidManifest_sharedUserLabel sharedUserLabel} + * attribute. + */ + public int sharedUserLabel; + + /** + * Information collected from the <application> tag, or null if + * there was none. + */ + public ApplicationInfo applicationInfo; + + /** + * All kernel group-IDs that have been assigned to this package. + * This is only filled in if the flag {@link PackageManager#GET_GIDS} was set. + */ + public int[] gids; + + /** + * Array of all {@link android.R.styleable#AndroidManifestActivity + * <activity>} tags included under <application>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_ACTIVITIES} was set. + */ + public ActivityInfo[] activities; + + /** + * Array of all {@link android.R.styleable#AndroidManifestReceiver + * <receiver>} tags included under <application>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_RECEIVERS} was set. + */ + public ActivityInfo[] receivers; + + /** + * Array of all {@link android.R.styleable#AndroidManifestService + * <service>} tags included under <application>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_SERVICES} was set. + */ + public ServiceInfo[] services; + + /** + * Array of all {@link android.R.styleable#AndroidManifestProvider + * <provider>} tags included under <application>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_PROVIDERS} was set. + */ + public ProviderInfo[] providers; + + /** + * Array of all {@link android.R.styleable#AndroidManifestInstrumentation + * <instrumentation>} tags included under <manifest>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_INSTRUMENTATION} was set. + */ + public InstrumentationInfo[] instrumentation; + + /** + * Array of all {@link android.R.styleable#AndroidManifestPermission + * <permission>} tags included under <manifest>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_PERMISSIONS} was set. + */ + public PermissionInfo[] permissions; + + /** + * Array of all {@link android.R.styleable#AndroidManifestUsesPermission + * <uses-permission>} tags included under <manifest>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_PERMISSIONS} was set. This list includes + * all permissions requested, even those that were not granted or known + * by the system at install time. + */ + public String[] requestedPermissions; + + /** + * Array of all signatures read from the package file. This is only filled + * in if the flag {@link PackageManager#GET_SIGNATURES} was set. + */ + public Signature[] signatures; + + /** + * Application specified preferred configuration + * {@link android.R.styleable#AndroidManifestUsesConfiguration + * <uses-configuration>} tags included under <manifest>, + * or null if there were none. This is only filled in if the flag + * {@link PackageManager#GET_CONFIGURATIONS} was set. + */ + public ConfigurationInfo[] configPreferences; + + public PackageInfo() { + } + + public String toString() { + return "PackageInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeString(packageName); + dest.writeInt(versionCode); + dest.writeString(versionName); + dest.writeString(sharedUserId); + dest.writeInt(sharedUserLabel); + if (applicationInfo != null) { + dest.writeInt(1); + applicationInfo.writeToParcel(dest, parcelableFlags); + } else { + dest.writeInt(0); + } + dest.writeIntArray(gids); + dest.writeTypedArray(activities, parcelableFlags); + dest.writeTypedArray(receivers, parcelableFlags); + dest.writeTypedArray(services, parcelableFlags); + dest.writeTypedArray(providers, parcelableFlags); + dest.writeTypedArray(instrumentation, parcelableFlags); + dest.writeTypedArray(permissions, parcelableFlags); + dest.writeStringArray(requestedPermissions); + dest.writeTypedArray(signatures, parcelableFlags); + dest.writeTypedArray(configPreferences, parcelableFlags); + } + + public static final Parcelable.Creator<PackageInfo> CREATOR + = new Parcelable.Creator<PackageInfo>() { + public PackageInfo createFromParcel(Parcel source) { + return new PackageInfo(source); + } + + public PackageInfo[] newArray(int size) { + return new PackageInfo[size]; + } + }; + + private PackageInfo(Parcel source) { + packageName = source.readString(); + versionCode = source.readInt(); + versionName = source.readString(); + sharedUserId = source.readString(); + sharedUserLabel = source.readInt(); + int hasApp = source.readInt(); + if (hasApp != 0) { + applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source); + } + gids = source.createIntArray(); + activities = source.createTypedArray(ActivityInfo.CREATOR); + receivers = source.createTypedArray(ActivityInfo.CREATOR); + services = source.createTypedArray(ServiceInfo.CREATOR); + providers = source.createTypedArray(ProviderInfo.CREATOR); + instrumentation = source.createTypedArray(InstrumentationInfo.CREATOR); + permissions = source.createTypedArray(PermissionInfo.CREATOR); + requestedPermissions = source.createStringArray(); + signatures = source.createTypedArray(Signature.CREATOR); + configPreferences = source.createTypedArray(ConfigurationInfo.CREATOR); + } +} diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java new file mode 100644 index 0000000..46e7ca4 --- /dev/null +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -0,0 +1,191 @@ +package android.content.pm; + +import android.content.res.XmlResourceParser; + +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.text.TextUtils; +import android.util.Printer; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Base class containing information common to all package items held by + * the package manager. This provides a very common basic set of attributes: + * a label, icon, and meta-data. This class is not intended + * to be used by itself; it is simply here to share common definitions + * between all items returned by the package manager. As such, it does not + * itself implement Parcelable, but does provide convenience methods to assist + * in the implementation of Parcelable in subclasses. + */ +public class PackageItemInfo { + /** + * Public name of this item. From the "android:name" attribute. + */ + public String name; + + /** + * Name of the package that this item is in. + */ + public String packageName; + + /** + * A string resource identifier (in the package's resources) of this + * component's label. From the "label" attribute or, if not set, 0. + */ + public int labelRes; + + /** + * The string provided in the AndroidManifest file, if any. You + * probably don't want to use this. You probably want + * {@link PackageManager#getApplicationLabel} + */ + public CharSequence nonLocalizedLabel; + + /** + * A drawable resource identifier (in the package's resources) of this + * component's icon. From the "icon" attribute or, if not set, 0. + */ + public int icon; + + /** + * Additional meta-data associated with this component. This field + * will only be filled in if you set the + * {@link PackageManager#GET_META_DATA} flag when requesting the info. + */ + public Bundle metaData; + + public PackageItemInfo() { + } + + public PackageItemInfo(PackageItemInfo orig) { + name = orig.name; + packageName = orig.packageName; + labelRes = orig.labelRes; + nonLocalizedLabel = orig.nonLocalizedLabel; + icon = orig.icon; + metaData = orig.metaData; + } + + /** + * Retrieve the current textual label associated with this item. This + * will call back on the given PackageManager to load the label from + * the application. + * + * @param pm A PackageManager from which the label can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a CharSequence containing the item's label. If the + * item does not have a label, its name is returned. + */ + public CharSequence loadLabel(PackageManager pm) { + if (nonLocalizedLabel != null) { + return nonLocalizedLabel; + } + if (labelRes != 0) { + CharSequence label = pm.getText(packageName, labelRes, null); + if (label != null) { + return label; + } + } + if(name != null) { + return name; + } + return packageName; + } + + /** + * Retrieve the current graphical icon associated with this item. This + * will call back on the given PackageManager to load the icon from + * the application. + * + * @param pm A PackageManager from which the icon can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a Drawable containing the item's icon. If the + * item does not have an icon, the default activity icon is returned. + */ + public Drawable loadIcon(PackageManager pm) { + if (icon != 0) { + Drawable dr = pm.getDrawable(packageName, icon, null); + if (dr != null) { + return dr; + } + } + return pm.getDefaultActivityIcon(); + } + + /** + * Load an XML resource attached to the meta-data of this item. This will + * retrieved the name meta-data entry, and if defined call back on the + * given PackageManager to load its XML file from the application. + * + * @param pm A PackageManager from which the XML can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * @param name Name of the meta-date you would like to load. + * + * @return Returns an XmlPullParser you can use to parse the XML file + * assigned as the given meta-data. If the meta-data name is not defined + * or the XML resource could not be found, null is returned. + */ + public XmlResourceParser loadXmlMetaData(PackageManager pm, String name) { + if (metaData != null) { + int resid = metaData.getInt(name); + if (resid != 0) { + return pm.getXml(packageName, resid, null); + } + } + return null; + } + + protected void dumpFront(Printer pw, String prefix) { + pw.println(prefix + "name=" + name); + pw.println(prefix + "packageName=" + packageName); + pw.println(prefix + "labelRes=0x" + Integer.toHexString(labelRes) + + " nonLocalizedLabel=" + nonLocalizedLabel + + " icon=0x" + Integer.toHexString(icon)); + } + + protected void dumpBack(Printer pw, String prefix) { + // no back here + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeString(name); + dest.writeString(packageName); + dest.writeInt(labelRes); + TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags); + dest.writeInt(icon); + dest.writeBundle(metaData); + } + + protected PackageItemInfo(Parcel source) { + name = source.readString(); + packageName = source.readString(); + labelRes = source.readInt(); + nonLocalizedLabel + = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + icon = source.readInt(); + metaData = source.readBundle(); + } + + public static class DisplayNameComparator + implements Comparator<PackageItemInfo> { + public DisplayNameComparator(PackageManager pm) { + mPM = pm; + } + + public final int compare(PackageItemInfo aa, PackageItemInfo ab) { + CharSequence sa = aa.loadLabel(mPM); + if (sa == null) sa = aa.name; + CharSequence sb = ab.loadLabel(mPM); + if (sb == null) sb = ab.name; + return sCollator.compare(sa.toString(), sb.toString()); + } + + private final Collator sCollator = Collator.getInstance(); + private PackageManager mPM; + } +} diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java new file mode 100644 index 0000000..7287d9c --- /dev/null +++ b/core/java/android/content/pm/PackageManager.java @@ -0,0 +1,1646 @@ +/* + * Copyright (C) 2006 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.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.AndroidException; +import android.util.DisplayMetrics; + +import java.io.File; +import java.util.List; + +/** + * Class for retrieving various kinds of information related to the application + * packages that are currently installed on the device. + * + * You can find this class through {@link Context#getPackageManager}. + */ +public abstract class PackageManager { + + /** + * This exception is thrown when a given package, application, or component + * name can not be found. + */ + public static class NameNotFoundException extends AndroidException { + public NameNotFoundException() { + } + + public NameNotFoundException(String name) { + super(name); + } + } + + /** + * {@link PackageInfo} flag: return information about + * activities in the package in {@link PackageInfo#activities}. + */ + public static final int GET_ACTIVITIES = 0x00000001; + + /** + * {@link PackageInfo} flag: return information about + * intent receivers in the package in + * {@link PackageInfo#receivers}. + */ + public static final int GET_RECEIVERS = 0x00000002; + + /** + * {@link PackageInfo} flag: return information about + * services in the package in {@link PackageInfo#services}. + */ + public static final int GET_SERVICES = 0x00000004; + + /** + * {@link PackageInfo} flag: return information about + * content providers in the package in + * {@link PackageInfo#providers}. + */ + public static final int GET_PROVIDERS = 0x00000008; + + /** + * {@link PackageInfo} flag: return information about + * instrumentation in the package in + * {@link PackageInfo#instrumentation}. + */ + public static final int GET_INSTRUMENTATION = 0x00000010; + + /** + * {@link PackageInfo} flag: return information about the + * intent filters supported by the activity. + */ + public static final int GET_INTENT_FILTERS = 0x00000020; + + /** + * {@link PackageInfo} flag: return information about the + * signatures included in the package. + */ + public static final int GET_SIGNATURES = 0x00000040; + + /** + * {@link ResolveInfo} flag: return the IntentFilter that + * was matched for a particular ResolveInfo in + * {@link ResolveInfo#filter}. + */ + public static final int GET_RESOLVED_FILTER = 0x00000040; + + /** + * {@link ComponentInfo} flag: return the {@link ComponentInfo#metaData} + * data {@link android.os.Bundle}s that are associated with a component. + * This applies for any API returning a ComponentInfo subclass. + */ + public static final int GET_META_DATA = 0x00000080; + + /** + * {@link PackageInfo} flag: return the + * {@link PackageInfo#gids group ids} that are associated with an + * application. + * This applies for any API returning an PackageInfo class, either + * directly or nested inside of another. + */ + public static final int GET_GIDS = 0x00000100; + + /** + * {@link PackageInfo} flag: include disabled components in the returned info. + */ + public static final int GET_DISABLED_COMPONENTS = 0x00000200; + + /** + * {@link ApplicationInfo} flag: return the + * {@link ApplicationInfo#sharedLibraryFiles paths to the shared libraries} + * that are associated with an application. + * This applies for any API returning an ApplicationInfo class, either + * directly or nested inside of another. + */ + public static final int GET_SHARED_LIBRARY_FILES = 0x00000400; + + /** + * {@link ProviderInfo} flag: return the + * {@link ProviderInfo#uriPermissionPatterns URI permission patterns} + * that are associated with a content provider. + * This applies for any API returning an ProviderInfo class, either + * directly or nested inside of another. + */ + public static final int GET_URI_PERMISSION_PATTERNS = 0x00000800; + /** + * {@link PackageInfo} flag: return information about + * permissions in the package in + * {@link PackageInfo#permissions}. + */ + public static final int GET_PERMISSIONS = 0x00001000; + + /** + * Flag parameter to retrieve all applications(even uninstalled ones) with data directories. + * This state could have resulted if applications have been deleted with flag + * DONT_DELETE_DATA + * with a possibility of being replaced or reinstalled in future + */ + public static final int GET_UNINSTALLED_PACKAGES = 0x00002000; + + /** + * {@link PackageInfo} flag: return information about + * hardware preferences + * {@link PackageInfo#configPreferences} + */ + public static final int GET_CONFIGURATIONS = 0x00004000; + + /** + * Permission check result: this is returned by {@link #checkPermission} + * if the permission has been granted to the given package. + */ + public static final int PERMISSION_GRANTED = 0; + + /** + * Permission check result: this is returned by {@link #checkPermission} + * if the permission has not been granted to the given package. + */ + public static final int PERMISSION_DENIED = -1; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if the two packages have a matching signature. + */ + public static final int SIGNATURE_MATCH = 0; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if neither of the two packages is signed. + */ + public static final int SIGNATURE_NEITHER_SIGNED = 1; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if the first package is not signed, but the second is. + */ + public static final int SIGNATURE_FIRST_NOT_SIGNED = -1; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if the second package is not signed, but the first is. + */ + public static final int SIGNATURE_SECOND_NOT_SIGNED = -2; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if both packages are signed but there is no matching signature. + */ + public static final int SIGNATURE_NO_MATCH = -3; + + /** + * Signature check result: this is returned by {@link #checkSignatures} + * if either of the given package names are not valid. + */ + public static final int SIGNATURE_UNKNOWN_PACKAGE = -4; + + /** + * Resolution and querying flag: if set, only filters that support the + * {@link android.content.Intent#CATEGORY_DEFAULT} will be considered for + * matching. This is a synonym for including the CATEGORY_DEFAULT in your + * supplied Intent. + */ + public static final int MATCH_DEFAULT_ONLY = 0x00010000; + + public static final int COMPONENT_ENABLED_STATE_DEFAULT = 0; + public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; + public static final int COMPONENT_ENABLED_STATE_DISABLED = 2; + + /** + * Flag parameter for {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} to + * indicate that this package should be installed as forward locked, i.e. only the app itself + * should have access to it's code and non-resource assets. + */ + public static final int FORWARD_LOCK_PACKAGE = 0x00000001; + + /** + * Flag parameter for {@link #installPackage} to indicate that you want to replace an already + * installed package, if one exists + */ + public static final int REPLACE_EXISTING_PACKAGE = 0x00000002; + + /** + * Flag parameter for + * {@link #setComponentEnabledSetting(android.content.ComponentName, int, int)} to indicate + * that you don't want to kill the app containing the component. Be careful when you set this + * since changing component states can make the containing application's behavior unpredictable. + */ + public static final int DONT_KILL_APP = 0x00000001; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success. + */ + public static final int INSTALL_SUCCEEDED = 1; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package is + * already installed. + */ + public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package archive + * file is invalid. + */ + public static final int INSTALL_FAILED_INVALID_APK = -2; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the URI passed in + * is invalid. + */ + public static final int INSTALL_FAILED_INVALID_URI = -3; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if the package manager + * service found that the device didn't have enough storage space to install the app + */ + public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if a + * package is already installed with the same name. + */ + public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the requested shared user does not exist. + */ + public static final int INSTALL_FAILED_NO_SHARED_USER = -6; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * a previously installed package of the same name has a different signature + * than the new package (and the old package's data was not removed). + */ + public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package is requested a shared user which is already installed on the + * device and does not have matching signature. + */ + public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a shared library that is not available. + */ + public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package uses a shared library that is not available. + */ + public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed while optimizing and validating its dex files, + * either because there was not enough storage or the validation failed. + */ + public static final int INSTALL_FAILED_DEXOPT = -11; + + /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package failed because the current SDK version is older than + * that required by the package. + */ + public static final int INSTALL_FAILED_OLDER_SDK = -12; + + /** + * 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 + * '.apk' extension. + */ + public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser was unable to retrieve the AndroidManifest.xml file. + */ + public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser encountered an unexpected exception. + */ + public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser did not find any certificates in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser found inconsistent certificates on the files in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser encountered a CertificateEncodingException in one of the + * files in the .apk. + */ + public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser encountered a bad or missing package name in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser encountered a bad shared user id name in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser encountered some structural problem in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; + + /** + * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the parser did not find any actionable tags (instrumentation or application) + * in the manifest. + */ + public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; + + /** + * Indicates the state of installation. Used by PackageManager to + * figure out incomplete installations. Say a package is being installed + * (the state is set to PKG_INSTALL_INCOMPLETE) and remains so till + * the package installation is successful or unsuccesful lin which case + * the PackageManager will no longer maintain state information associated + * with the package. If some exception(like device freeze or battery being + * pulled out) occurs during installation of a package, the PackageManager + * needs this information to clean up the previously failed installation. + */ + public static final int PKG_INSTALL_INCOMPLETE = 0; + public static final int PKG_INSTALL_COMPLETE = 1; + + /** + * Flag parameter for {@link #deletePackage} to indicate that you don't want to delete the + * package's data directory. + * + * @hide + */ + public static final int DONT_DELETE_DATA = 0x00000001; + + /** + * Retrieve overall information about an application package that is + * installed on the system. + * + * <p>Throws {@link NameNotFoundException} if a package with the given + * name can not be found on the system. + * + * @param packageName The full name (i.e. com.google.apps.contacts) of the + * desired package. + + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return Returns a PackageInfo object containing information about the package. + * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not + * found in the list of installed applications, the package information is + * retrieved from the list of uninstalled applications(which includes + * installed applications as well as applications + * with data directory ie applications which had been + * deleted with DONT_DELTE_DATA flag set). + * + * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS + * @see #GET_RECEIVERS + * @see #GET_SERVICES + * @see #GET_SIGNATURES + * @see #GET_UNINSTALLED_PACKAGES + * + */ + public abstract PackageInfo getPackageInfo(String packageName, int flags) + throws NameNotFoundException; + + /** + * Return a "good" intent to launch a front-door activity in a package, + * for use for example to implement an "open" button when browsing through + * packages. The current implementation will look first for a main + * activity in the category {@link Intent#CATEGORY_INFO}, next for a + * main activity in the category {@link Intent#CATEGORY_LAUNCHER}, or return + * null if neither are found. + * + * <p>Throws {@link NameNotFoundException} if a package with the given + * name can not be found on the system. + * + * @param packageName The name of the package to inspect. + * + * @return Returns either a fully-qualified Intent that can be used to + * launch the main activity in the package, or null if the package does + * not contain such an activity. + */ + public abstract Intent getLaunchIntentForPackage(String packageName) + throws NameNotFoundException; + + /** + * Return an array of all of the secondary group-ids that have been + * assigned to a package. + * + * <p>Throws {@link NameNotFoundException} if a package with the given + * name can not be found on the system. + * + * @param packageName The full name (i.e. com.google.apps.contacts) of the + * desired package. + * + * @return Returns an int array of the assigned gids, or null if there + * are none. + */ + public abstract int[] getPackageGids(String packageName) + throws NameNotFoundException; + + /** + * Retrieve all of the information we know about a particular permission. + * + * <p>Throws {@link NameNotFoundException} if a permission with the given + * name can not be found on the system. + * + * @param name The fully qualified name (i.e. com.google.permission.LOGIN) + * of the permission you are interested in. + * @param flags Additional option flags. Use {@link #GET_META_DATA} to + * retrieve any meta-data associated with the permission. + * + * @return Returns a {@link PermissionInfo} containing information about the + * permission. + */ + public abstract PermissionInfo getPermissionInfo(String name, int flags) + throws NameNotFoundException; + + /** + * Query for all of the permissions associated with a particular group. + * + * <p>Throws {@link NameNotFoundException} if the given group does not + * exist. + * + * @param group The fully qualified name (i.e. com.google.permission.LOGIN) + * of the permission group you are interested in. Use null to + * find all of the permissions not associated with a group. + * @param flags Additional option flags. Use {@link #GET_META_DATA} to + * retrieve any meta-data associated with the permissions. + * + * @return Returns a list of {@link PermissionInfo} containing information + * about all of the permissions in the given group. + */ + public abstract List<PermissionInfo> queryPermissionsByGroup(String group, + int flags) throws NameNotFoundException; + + /** + * Retrieve all of the information we know about a particular group of + * permissions. + * + * <p>Throws {@link NameNotFoundException} if a permission group with the given + * name can not be found on the system. + * + * @param name The fully qualified name (i.e. com.google.permission_group.APPS) + * of the permission you are interested in. + * @param flags Additional option flags. Use {@link #GET_META_DATA} to + * retrieve any meta-data associated with the permission group. + * + * @return Returns a {@link PermissionGroupInfo} containing information + * about the permission. + */ + public abstract PermissionGroupInfo getPermissionGroupInfo(String name, + int flags) throws NameNotFoundException; + + /** + * Retrieve all of the known permission groups in the system. + * + * @param flags Additional option flags. Use {@link #GET_META_DATA} to + * retrieve any meta-data associated with the permission group. + * + * @return Returns a list of {@link PermissionGroupInfo} containing + * information about all of the known permission groups. + */ + public abstract List<PermissionGroupInfo> getAllPermissionGroups(int flags); + + /** + * Retrieve all of the information we know about a particular + * package/application. + * + * <p>Throws {@link NameNotFoundException} if an application with the given + * package name can not be found on the system. + * + * @param packageName The full name (i.e. com.google.apps.contacts) of an + * application. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return {@link ApplicationInfo} Returns ApplicationInfo object containing + * information about the package. + * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not + * found in the list of installed applications, + * the application information is retrieved from the + * list of uninstalled applications(which includes + * installed applications as well as applications + * with data directory ie applications which had been + * deleted with DONT_DELTE_DATA flag set). + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + * @see #GET_UNINSTALLED_PACKAGES + */ + public abstract ApplicationInfo getApplicationInfo(String packageName, + int flags) throws NameNotFoundException; + + /** + * Retrieve all of the information we know about a particular activity + * class. + * + * <p>Throws {@link NameNotFoundException} if an activity with the given + * class name can not be found on the system. + * + * @param className The full name (i.e. + * com.google.apps.contacts.ContactsList) of an Activity + * class. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data (in ApplicationInfo) returned. + * + * @return {@link ActivityInfo} containing information about the activity. + * + * @see #GET_INTENT_FILTERS + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + */ + public abstract ActivityInfo getActivityInfo(ComponentName className, + int flags) throws NameNotFoundException; + + /** + * Retrieve all of the information we know about a particular receiver + * class. + * + * <p>Throws {@link NameNotFoundException} if a receiver with the given + * class name can not be found on the system. + * + * @param className The full name (i.e. + * com.google.apps.contacts.CalendarAlarm) of a Receiver + * class. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data returned. + * + * @return {@link ActivityInfo} containing information about the receiver. + * + * @see #GET_INTENT_FILTERS + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + */ + public abstract ActivityInfo getReceiverInfo(ComponentName className, + int flags) throws NameNotFoundException; + + /** + * Retrieve all of the information we know about a particular service + * class. + * + * <p>Throws {@link NameNotFoundException} if a service with the given + * class name can not be found on the system. + * + * @param className The full name (i.e. + * com.google.apps.media.BackgroundPlayback) of a Service + * class. + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * to modify the data returned. + * + * @return ServiceInfo containing information about the service. + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + */ + public abstract ServiceInfo getServiceInfo(ComponentName className, + int flags) throws NameNotFoundException; + + /** + * Return a List of all packages that are installed + * on the device. + * + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, + * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return A List of PackageInfo objects, one for each package that is + * installed on the device. In the unlikely case of there being no + * installed packages, an empty list is returned. + * If flag GET_UNINSTALLED_PACKAGES is set, a list of all + * applications including those deleted with DONT_DELETE_DATA + * (partially installed apps with data directory) will be returned. + * + * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS + * @see #GET_RECEIVERS + * @see #GET_SERVICES + * @see #GET_SIGNATURES + * @see #GET_UNINSTALLED_PACKAGES + * + */ + public abstract List<PackageInfo> getInstalledPackages(int flags); + + /** + * Check whether a particular package has been granted a particular + * permission. + * + * @param permName The name of the permission you are checking for, + * @param pkgName The name of the package you are checking against. + * + * @return If the package has the permission, PERMISSION_GRANTED is + * returned. If it does not have the permission, PERMISSION_DENIED + * is returned. + * + * @see #PERMISSION_GRANTED + * @see #PERMISSION_DENIED + */ + public abstract int checkPermission(String permName, String pkgName); + + /** + * Add a new dynamic permission to the system. For this to work, your + * package must have defined a permission tree through the + * {@link android.R.styleable#AndroidManifestPermissionTree + * <permission-tree>} tag in its manifest. A package can only add + * permissions to trees that were defined by either its own package or + * another with the same user id; a permission is in a tree if it + * matches the name of the permission tree + ".": for example, + * "com.foo.bar" is a member of the permission tree "com.foo". + * + * <p>It is good to make your permission tree name descriptive, because you + * are taking possession of that entire set of permission names. Thus, it + * must be under a domain you control, with a suffix that will not match + * any normal permissions that may be declared in any applications that + * are part of that domain. + * + * <p>New permissions must be added before + * any .apks are installed that use those permissions. Permissions you + * add through this method are remembered across reboots of the device. + * If the given permission already exists, the info you supply here + * will be used to update it. + * + * @param info Description of the permission to be added. + * + * @return Returns true if a new permission was created, false if an + * existing one was updated. + * + * @throws SecurityException if you are not allowed to add the + * given permission name. + * + * @see #removePermission(String) + */ + public abstract boolean addPermission(PermissionInfo info); + + /** + * Removes a permission that was previously added with + * {@link #addPermission(PermissionInfo)}. The same ownership rules apply + * -- you are only allowed to remove permissions that you are allowed + * to add. + * + * @param name The name of the permission to remove. + * + * @throws SecurityException if you are not allowed to remove the + * given permission name. + * + * @see #addPermission(PermissionInfo) + */ + public abstract void removePermission(String name); + + /** + * Compare the signatures of two packages to determine if the same + * signature appears in both of them. If they do contain the same + * signature, then they are allowed special privileges when working + * with each other: they can share the same user-id, run instrumentation + * against each other, etc. + * + * @param pkg1 First package name whose signature will be compared. + * @param pkg2 Second package name 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 #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(String pkg1, String pkg2); + + /** + * 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 + * sharing the same user id through the "sharedUserId" mechanism, all + * packages with that id will be returned. + * + * @param uid The user id for which you would like to retrieve the + * associated packages. + * + * @return Returns an array of one or more packages assigned to the user + * id, or null if there are no known packages with the given id. + */ + public abstract String[] getPackagesForUid(int uid); + + /** + * Retrieve the official name associated with a user id. This name is + * guaranteed to never change, though it is possibly for the underlying + * user id to be changed. That is, if you are storing information about + * user ids in persistent storage, you should use the string returned + * by this function instead of the raw user-id. + * + * @param uid The user id for which you would like to retrieve a name. + * @return Returns a unique name for the given user id, or null if the + * user id is not currently assigned. + */ + public abstract String getNameForUid(int uid); + + /** + * Return the user id associated with a shared user name. Multiple + * applications can specify a shared user name in their manifest and thus + * end up using a common uid. This might be used for new applications + * that use an existing shared user name and need to know the uid of the + * shared user. + * + * @param sharedUserName The shared user name whose uid is to be retrieved. + * @return Returns the uid associated with the shared user, or NameNotFoundException + * if the shared user name is not being used by any installed packages + * @hide + */ + public abstract int getUidForSharedUser(String sharedUserName) + throws NameNotFoundException; + + /** + * Return a List of all application packages that are installed on the + * device. If flag GET_UNINSTALLED_PACKAGES has been set, a list of all + * applications including those deleted with DONT_DELETE_DATA(partially + * installed apps with data directory) will be returned. + * + * @param flags Additional option flags. Use any combination of + * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, + * {link #GET_UNINSTALLED_PACKAGES} to modify the data returned. + * + * @return A List of ApplicationInfo objects, one for each application that + * is installed on the device. In the unlikely case of there being + * no installed applications, an empty list is returned. + * If flag GET_UNINSTALLED_PACKAGES is set, a list of all + * applications including those deleted with DONT_DELETE_DATA + * (partially installed apps with data directory) will be returned. + * + * @see #GET_META_DATA + * @see #GET_SHARED_LIBRARY_FILES + * @see #GET_UNINSTALLED_PACKAGES + */ + public abstract List<ApplicationInfo> getInstalledApplications(int flags); + + /** + * Get a list of shared libraries that are available on the + * system. + * + * @return An array of shared library names that are + * available on the system, or null if none are installed. + * + */ + public abstract String[] getSystemSharedLibraryNames(); + + /** + * 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. + * + * @param intent An intent containing all of the desired specification + * (action, data, type, category, and/or component). + * @param flags Additional option flags. The most important is + * MATCH_DEFAULT_ONLY, to limit the resolution to only + * those activities that support the CATEGORY_DEFAULT. + * + * @return Returns a ResolveInfo containing the final activity intent that + * was determined to be the best action. Returns null if no + * matching activity was found. + * + * @see #MATCH_DEFAULT_ONLY + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract ResolveInfo resolveActivity(Intent intent, int flags); + + /** + * Retrieve all activities that can be performed for the given intent. + * + * @param intent The desired intent as per resolveActivity(). + * @param flags Additional option flags. The most important is + * MATCH_DEFAULT_ONLY, to limit the resolution to only + * those activities that support the CATEGORY_DEFAULT. + * + * @return A List<ResolveInfo> containing one entry for each matching + * Activity. These are ordered from best to worst match -- that + * is, the first item in the list is what is returned by + * resolveActivity(). If there are no matching activities, an empty + * list is returned. + * + * @see #MATCH_DEFAULT_ONLY + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract List<ResolveInfo> queryIntentActivities(Intent intent, + int flags); + + /** + * Retrieve a set of activities that should be presented to the user as + * similar options. This is like {@link #queryIntentActivities}, except it + * also allows you to supply a list of more explicit Intents that you would + * like to resolve to particular options, and takes care of returning the + * final ResolveInfo list in a reasonable order, with no duplicates, based + * on those inputs. + * + * @param caller The class name of the activity that is making the + * request. This activity will never appear in the output + * list. Can be null. + * @param specifics An array of Intents that should be resolved to the + * first specific results. Can be null. + * @param intent The desired intent as per resolveActivity(). + * @param flags Additional option flags. The most important is + * MATCH_DEFAULT_ONLY, to limit the resolution to only + * those activities that support the CATEGORY_DEFAULT. + * + * @return A List<ResolveInfo> containing one entry for each matching + * Activity. These are ordered first by all of the intents resolved + * in <var>specifics</var> and then any additional activities that + * can handle <var>intent</var> but did not get included by one of + * the <var>specifics</var> intents. If there are no matching + * activities, an empty list is returned. + * + * @see #MATCH_DEFAULT_ONLY + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract List<ResolveInfo> queryIntentActivityOptions( + ComponentName caller, Intent[] specifics, Intent intent, int flags); + + /** + * Retrieve all receivers that can handle a broadcast of the given intent. + * + * @param intent The desired intent as per resolveActivity(). + * @param flags Additional option flags. The most important is + * MATCH_DEFAULT_ONLY, to limit the resolution to only + * those activities that support the CATEGORY_DEFAULT. + * + * @return A List<ResolveInfo> containing one entry for each matching + * Receiver. These are ordered from first to last in priority. If + * there are no matching receivers, an empty list is returned. + * + * @see #MATCH_DEFAULT_ONLY + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract List<ResolveInfo> queryBroadcastReceivers(Intent intent, + int flags); + + /** + * Determine the best service to handle for a given Intent. + * + * @param intent An intent containing all of the desired specification + * (action, data, type, category, and/or component). + * @param flags Additional option flags. + * + * @return Returns a ResolveInfo containing the final service intent that + * was determined to be the best action. Returns null if no + * matching service was found. + * + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract ResolveInfo resolveService(Intent intent, int flags); + + /** + * Retrieve all services that can match the given intent. + * + * @param intent The desired intent as per resolveService(). + * @param flags Additional option flags. + * + * @return A List<ResolveInfo> containing one entry for each matching + * ServiceInfo. These are ordered from best to worst match -- that + * is, the first item in the list is what is returned by + * resolveService(). If there are no matching services, an empty + * list is returned. + * + * @see #GET_INTENT_FILTERS + * @see #GET_RESOLVED_FILTER + */ + public abstract List<ResolveInfo> queryIntentServices(Intent intent, + int flags); + + /** + * Find a single content provider by its base path name. + * + * @param name The name of the provider to find. + * @param flags Additional option flags. Currently should always be 0. + * + * @return ContentProviderInfo Information about the provider, if found, + * else null. + */ + public abstract ProviderInfo resolveContentProvider(String name, + int flags); + + /** + * Retrieve content provider information. + * + * <p><em>Note: unlike most other methods, an empty result set is indicated + * by a null return instead of an empty list.</em> + * + * @param processName If non-null, limits the returned providers to only + * those that are hosted by the given process. If null, + * all content providers are returned. + * @param uid If <var>processName</var> is non-null, this is the required + * uid owning the requested content providers. + * @param flags Additional option flags. Currently should always be 0. + * + * @return A List<ContentProviderInfo> containing one entry for each + * content provider either patching <var>processName</var> or, if + * <var>processName</var> is null, all known content providers. + * <em>If there are no matching providers, null is returned.</em> + */ + public abstract List<ProviderInfo> queryContentProviders( + String processName, int uid, int flags); + + /** + * Retrieve all of the information we know about a particular + * instrumentation class. + * + * <p>Throws {@link NameNotFoundException} if instrumentation with the + * given class name can not be found on the system. + * + * @param className The full name (i.e. + * com.google.apps.contacts.InstrumentList) of an + * Instrumentation class. + * @param flags Additional option flags. Currently should always be 0. + * + * @return InstrumentationInfo containing information about the + * instrumentation. + */ + public abstract InstrumentationInfo getInstrumentationInfo( + ComponentName className, int flags) throws NameNotFoundException; + + /** + * Retrieve information about available instrumentation code. May be used + * to retrieve either all instrumentation code, or only the code targeting + * a particular package. + * + * @param targetPackage If null, all instrumentation is returned; only the + * instrumentation targeting this package name is + * returned. + * @param flags Additional option flags. Currently should always be 0. + * + * @return A List<InstrumentationInfo> containing one entry for each + * matching available Instrumentation. Returns an empty list if + * there is no instrumentation available for the given package. + */ + public abstract List<InstrumentationInfo> queryInstrumentation( + String targetPackage, int flags); + + /** + * Retrieve an image from a package. This is a low-level API used by + * the various package manager info structures (such as + * {@link ComponentInfo} to implement retrieval of their associated + * icon. + * + * @param packageName The name of the package that this icon is coming from. + * Can not be null. + * @param resid The resource identifier of the desired image. Can not be 0. + * @param appInfo Overall information about <var>packageName</var>. This + * may be null, in which case the application information will be retrieved + * for you if needed; if you already have this information around, it can + * be much more efficient to supply it here. + * + * @return Returns a Drawable holding the requested image. Returns null if + * an image could not be found for any reason. + */ + public abstract Drawable getDrawable(String packageName, int resid, + ApplicationInfo appInfo); + + /** + * Retrieve the icon associated with an activity. Given the full name of + * an activity, retrieves the information about it and calls + * {@link ComponentInfo#loadIcon ComponentInfo.loadIcon()} to return its icon. + * If the activity can not be found, NameNotFoundException is thrown. + * + * @param activityName Name of the activity whose icon is to be retrieved. + * + * @return Returns the image of the icon, or the default activity icon if + * it could not be found. Does not return null. + * @throws NameNotFoundException Thrown if the resources for the given + * activity could not be loaded. + * + * @see #getActivityIcon(Intent) + */ + public abstract Drawable getActivityIcon(ComponentName activityName) + throws NameNotFoundException; + + /** + * Retrieve the icon associated with an Intent. If intent.getClassName() is + * set, this simply returns the result of + * getActivityIcon(intent.getClassName()). Otherwise it resolves the intent's + * component and returns the icon associated with the resolved component. + * If intent.getClassName() can not be found or the Intent can not be resolved + * to a component, NameNotFoundException is thrown. + * + * @param intent The intent for which you would like to retrieve an icon. + * + * @return Returns the image of the icon, or the default activity icon if + * it could not be found. Does not return null. + * @throws NameNotFoundException Thrown if the resources for application + * matching the given intent could not be loaded. + * + * @see #getActivityIcon(ComponentName) + */ + public abstract Drawable getActivityIcon(Intent intent) + throws NameNotFoundException; + + /** + * Return the generic icon for an activity that is used when no specific + * icon is defined. + * + * @return Drawable Image of the icon. + */ + public abstract Drawable getDefaultActivityIcon(); + + /** + * Retrieve the icon associated with an application. If it has not defined + * an icon, the default app icon is returned. Does not return null. + * + * @param info Information about application being queried. + * + * @return Returns the image of the icon, or the default application icon + * if it could not be found. + * + * @see #getApplicationIcon(String) + */ + public abstract Drawable getApplicationIcon(ApplicationInfo info); + + /** + * Retrieve the icon associated with an application. Given the name of the + * application's package, retrieves the information about it and calls + * getApplicationIcon() to return its icon. If the application can not be + * found, NameNotFoundException is thrown. + * + * @param packageName Name of the package whose application icon is to be + * retrieved. + * + * @return Returns the image of the icon, or the default application icon + * if it could not be found. Does not return null. + * @throws NameNotFoundException Thrown if the resources for the given + * application could not be loaded. + * + * @see #getApplicationIcon(ApplicationInfo) + */ + public abstract Drawable getApplicationIcon(String packageName) + throws NameNotFoundException; + + /** + * Retrieve text from a package. This is a low-level API used by + * the various package manager info structures (such as + * {@link ComponentInfo} to implement retrieval of their associated + * labels and other text. + * + * @param packageName The name of the package that this text is coming from. + * Can not be null. + * @param resid The resource identifier of the desired text. Can not be 0. + * @param appInfo Overall information about <var>packageName</var>. This + * may be null, in which case the application information will be retrieved + * for you if needed; if you already have this information around, it can + * be much more efficient to supply it here. + * + * @return Returns a CharSequence holding the requested text. Returns null + * if the text could not be found for any reason. + */ + public abstract CharSequence getText(String packageName, int resid, + ApplicationInfo appInfo); + + /** + * Retrieve an XML file from a package. This is a low-level API used to + * retrieve XML meta data. + * + * @param packageName The name of the package that this xml is coming from. + * Can not be null. + * @param resid The resource identifier of the desired xml. Can not be 0. + * @param appInfo Overall information about <var>packageName</var>. This + * may be null, in which case the application information will be retrieved + * for you if needed; if you already have this information around, it can + * be much more efficient to supply it here. + * + * @return Returns an XmlPullParser allowing you to parse out the XML + * data. Returns null if the xml resource could not be found for any + * reason. + */ + public abstract XmlResourceParser getXml(String packageName, int resid, + ApplicationInfo appInfo); + + /** + * Return the label to use for this application. + * + * @return Returns the label associated with this application, or null if + * it could not be found for any reason. + * @param info The application to get the label of + */ + public abstract CharSequence getApplicationLabel(ApplicationInfo info); + + /** + * Retrieve the resources associated with an activity. Given the full + * name of an activity, retrieves the information about it and calls + * getResources() to return its application's resources. If the activity + * can not be found, NameNotFoundException is thrown. + * + * @param activityName Name of the activity whose resources are to be + * retrieved. + * + * @return Returns the application's Resources. + * @throws NameNotFoundException Thrown if the resources for the given + * application could not be loaded. + * + * @see #getResourcesForApplication(ApplicationInfo) + */ + public abstract Resources getResourcesForActivity(ComponentName activityName) + throws NameNotFoundException; + + /** + * Retrieve the resources for an application. Throws NameNotFoundException + * if the package is no longer installed. + * + * @param app Information about the desired application. + * + * @return Returns the application's Resources. + * @throws NameNotFoundException Thrown if the resources for the given + * application could not be loaded (most likely because it was uninstalled). + */ + public abstract Resources getResourcesForApplication(ApplicationInfo app) + throws NameNotFoundException; + + /** + * Retrieve the resources associated with an application. Given the full + * package name of an application, retrieves the information about it and + * calls getResources() to return its application's resources. If the + * appPackageName can not be found, NameNotFoundException is thrown. + * + * @param appPackageName Package name of the application whose resources + * are to be retrieved. + * + * @return Returns the application's Resources. + * @throws NameNotFoundException Thrown if the resources for the given + * application could not be loaded. + * + * @see #getResourcesForApplication(ApplicationInfo) + */ + public abstract Resources getResourcesForApplication(String appPackageName) + throws NameNotFoundException; + + /** + * Retrieve overall information about an application package defined + * in a package archive file + * + * @param archiveFilePath The path to the archive file + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, to modify the data returned. + * + * @return Returns the information about the package. Returns + * null if the package could not be successfully parsed. + * + * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS + * @see #GET_RECEIVERS + * @see #GET_SERVICES + * @see #GET_SIGNATURES + * + */ + public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) { + PackageParser packageParser = new PackageParser(archiveFilePath); + DisplayMetrics metrics = new DisplayMetrics(); + metrics.setToDefaults(); + final File sourceFile = new File(archiveFilePath); + PackageParser.Package pkg = packageParser.parsePackage( + sourceFile, archiveFilePath, metrics, 0); + if (pkg == null) { + return null; + } + return PackageParser.generatePackageInfo(pkg, null, flags); + } + + /** + * Install a package. Since this may take a little while, the result will + * be posted back to the given observer. An installation will fail if the calling context + * lacks the {@link android.Manifest.permission#INSTALL_PACKAGES} permission, if the + * package named in the package file's manifest is already installed, or if there's no space + * available on the device. + * + * @param packageURI The location of the package file to install. This can be a 'file:' or a + * 'content:' URI. + * @param observer An observer callback to get notified when the package installation is + * complete. {@link IPackageInstallObserver#packageInstalled(String, int)} will be + * called when that happens. observer may be null to indicate that no callback is desired. + * @param flags - possible values: {@link #FORWARD_LOCK_PACKAGE}, + * {@link #REPLACE_EXISTING_PACKAGE} + * + * @see #installPackage(android.net.Uri) + */ + public abstract void installPackage( + Uri packageURI, IPackageInstallObserver observer, int flags); + + /** + * Attempts to delete a package. Since this may take a little while, the result will + * be posted back to the given observer. A deletion will fail if the calling context + * lacks the {@link android.Manifest.permission#DELETE_PACKAGES} permission, if the + * named package cannot be found, or if the named package is a "system package". + * (TODO: include pointer to documentation on "system packages") + * + * @param packageName The name of the package to delete + * @param observer An observer callback to get notified when the package deletion is + * complete. {@link android.content.pm.IPackageDeleteObserver#packageDeleted(boolean)} will be + * called when that happens. observer may be null to indicate that no callback is desired. + * @param flags - possible values: {@link #DONT_DELETE_DATA} + * + * @hide + */ + public abstract void deletePackage( + String packageName, IPackageDeleteObserver observer, int flags); + /** + * Attempts to clear the user data directory of an application. + * Since this may take a little while, the result will + * be posted back to the given observer. A deletion will fail if the + * named package cannot be found, or if the named package is a "system package". + * + * @param packageName The name of the package + * @param observer An observer callback to get notified when the operation is finished + * {@link android.content.pm.IPackageDataObserver#onRemoveCompleted(String, boolean)} + * will be called when that happens. observer may be null to indicate that + * no callback is desired. + * + * @hide + */ + public abstract void clearApplicationUserData(String packageName, + IPackageDataObserver observer); + /** + * Attempts to delete the cache files associated with an application. + * Since this may take a little while, the result will + * be posted back to the given observer. A deletion will fail if the calling context + * lacks the {@link android.Manifest.permission#DELETE_CACHE_FILES} permission, if the + * named package cannot be found, or if the named package is a "system package". + * + * @param packageName The name of the package to delete + * @param observer An observer callback to get notified when the cache file deletion + * is complete. + * {@link android.content.pm.IPackageDataObserver#onRemoveCompleted(String, boolean)} + * will be called when that happens. observer may be null to indicate that + * no callback is desired. + * + * @hide + */ + public abstract void deleteApplicationCacheFiles(String packageName, + IPackageDataObserver observer); + + /** + * Free storage by deleting LRU sorted list of cache files across + * all applications. If the currently available free storage + * on the device is greater than or equal to the requested + * free storage, no cache files are cleared. If the currently + * available storage on the device is less than the requested + * free storage, some or all of the cache files across + * all applications are deleted (based on last accessed time) + * to increase the free storage space on the device to + * the requested value. There is no guarantee that clearing all + * the cache files from all applications will clear up + * enough storage to achieve the desired value. + * @param freeStorageSize The number of bytes of storage to be + * freed by the system. Say if freeStorageSize is XX, + * and the current free storage is YY, + * if XX is less than YY, just return. if not free XX-YY number + * of bytes if possible. + * @param observer call back used to notify when + * the operation is completed + * + * @hide + */ + public abstract void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer); + + /** + * Free storage by deleting LRU sorted list of cache files across + * all applications. If the currently available free storage + * on the device is greater than or equal to the requested + * free storage, no cache files are cleared. If the currently + * available storage on the device is less than the requested + * free storage, some or all of the cache files across + * all applications are deleted (based on last accessed time) + * to increase the free storage space on the device to + * the requested value. There is no guarantee that clearing all + * the cache files from all applications will clear up + * enough storage to achieve the desired value. + * @param freeStorageSize The number of bytes of storage to be + * freed by the system. Say if freeStorageSize is XX, + * and the current free storage is YY, + * if XX is less than YY, just return. if not free XX-YY number + * of bytes if possible. + * @param opFinishedIntent PendingIntent call back used to + * notify when the operation is completed.May be null + * to indicate that no call back is desired. + * + * @hide + */ + public abstract void freeStorage(long freeStorageSize, PendingIntent opFinishedIntent); + + /** + * Retrieve the size information for a package. + * Since this may take a little while, the result will + * be posted back to the given observer. The calling context + * should have the {@link android.Manifest.permission#GET_PACKAGE_SIZE} permission. + * + * @param packageName The name of the package whose size information is to be retrieved + * @param observer An observer callback to get notified when the operation + * is complete. + * {@link android.content.pm.IPackageStatsObserver#onGetStatsCompleted(PackageStats, boolean)} + * The observer's callback is invoked with a PackageStats object(containing the + * code, data and cache sizes of the package) and a boolean value representing + * the status of the operation. observer may be null to indicate that + * no callback is desired. + * + * @hide + */ + public abstract void getPackageSizeInfo(String packageName, + IPackageStatsObserver observer); + + /** + * Install a package. + * + * @param packageURI The location of the package file to install + * + * @see #installPackage(android.net.Uri, IPackageInstallObserver, int) + */ + public void installPackage(Uri packageURI) { + installPackage(packageURI, null, 0); + } + + /** + * Add a new package to the list of preferred packages. This new package + * will be added to the front of the list (removed from its current location + * if already listed), meaning it will now be preferred over all other + * packages when resolving conflicts. + * + * @param packageName The package name of the new package to make preferred. + */ + public abstract void addPackageToPreferred(String packageName); + + /** + * Remove a package from the list of preferred packages. If it was on + * the list, it will no longer be preferred over other packages. + * + * @param packageName The package name to remove. + */ + public abstract void removePackageFromPreferred(String packageName); + + /** + * Retrieve the list of all currently configured preferred packages. The + * first package on the list is the most preferred, the last is the + * least preferred. + * + * @param flags Additional option flags. Use any combination of + * {@link #GET_ACTIVITIES}, + * {@link #GET_GIDS}, + * {@link #GET_CONFIGURATIONS}, + * {@link #GET_INSTRUMENTATION}, + * {@link #GET_PERMISSIONS}, + * {@link #GET_PROVIDERS}, + * {@link #GET_RECEIVERS}, + * {@link #GET_SERVICES}, + * {@link #GET_SIGNATURES}, to modify the data returned. + * + * @return Returns a list of PackageInfo objects describing each + * preferred application, in order of preference. + * + * @see #GET_ACTIVITIES + * @see #GET_GIDS + * @see #GET_CONFIGURATIONS + * @see #GET_INSTRUMENTATION + * @see #GET_PERMISSIONS + * @see #GET_PROVIDERS + * @see #GET_RECEIVERS + * @see #GET_SERVICES + * @see #GET_SIGNATURES + */ + public abstract List<PackageInfo> getPreferredPackages(int flags); + + /** + * Add a new preferred activity mapping to the system. This will be used + * to automatically select the given activity component when + * {@link Context#startActivity(Intent) Context.startActivity()} finds + * multiple matching activities and also matches the given filter. + * + * @param filter The set of intents under which this activity will be + * made preferred. + * @param match The IntentFilter match category that this preference + * applies to. + * @param set The set of activities that the user was picking from when + * this preference was made. + * @param activity The component name of the activity that is to be + * preferred. + */ + public abstract void addPreferredActivity(IntentFilter filter, int match, + ComponentName[] set, ComponentName activity); + + /** + * Remove all preferred activity mappings, previously added with + * {@link #addPreferredActivity}, from the + * system whose activities are implemented in the given package name. + * + * @param packageName The name of the package whose preferred activity + * mappings are to be removed. + */ + public abstract void clearPackagePreferredActivities(String packageName); + + /** + * Retrieve all preferred activities, previously added with + * {@link #addPreferredActivity}, that are + * currently registered with the system. + * + * @param outFilters A list in which to place the filters of all of the + * preferred activities, or null for none. + * @param outActivities A list in which to place the component names of + * all of the preferred activities, or null for none. + * @param packageName An option package in which you would like to limit + * the list. If null, all activities will be returned; if non-null, only + * those activities in the given package are returned. + * + * @return Returns the total number of registered preferred activities + * (the number of distinct IntentFilter records, not the number of unique + * activity components) that were found. + */ + public abstract int getPreferredActivities(List<IntentFilter> outFilters, + List<ComponentName> outActivities, String packageName); + + /** + * Set the enabled setting for a package component (activity, receiver, service, provider). + * This setting will override any enabled state which may have been set by the component in its + * manifest. + * + * @param componentName The component to enable + * @param newState The new enabled state for the component. The legal values for this state + * are: + * {@link #COMPONENT_ENABLED_STATE_ENABLED}, + * {@link #COMPONENT_ENABLED_STATE_DISABLED} + * and + * {@link #COMPONENT_ENABLED_STATE_DEFAULT} + * The last one removes the setting, thereby restoring the component's state to + * whatever was set in it's manifest (or enabled, by default). + * @param flags Optional behavior flags: {@link #DONT_KILL_APP} or 0. + */ + public abstract void setComponentEnabledSetting(ComponentName componentName, + int newState, int flags); + + + /** + * Return the the enabled setting for a package component (activity, + * receiver, service, provider). This returns the last value set by + * {@link #setComponentEnabledSetting(ComponentName, int, int)}; in most + * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since + * the value originally specified in the manifest has not been modified. + * + * @param componentName The component to retrieve. + * @return Returns the current enabled state for the component. May + * be one of {@link #COMPONENT_ENABLED_STATE_ENABLED}, + * {@link #COMPONENT_ENABLED_STATE_DISABLED}, or + * {@link #COMPONENT_ENABLED_STATE_DEFAULT}. The last one means the + * component's enabled state is based on the original information in + * the manifest as found in {@link ComponentInfo}. + */ + public abstract int getComponentEnabledSetting(ComponentName componentName); + + /** + * Set the enabled setting for an application + * This setting will override any enabled state which may have been set by the application in + * its manifest. It also overrides the enabled state set in the manifest for any of the + * application's components. It does not override any enabled state set by + * {@link #setComponentEnabledSetting} for any of the application's components. + * + * @param packageName The package name of the application to enable + * @param newState The new enabled state for the component. The legal values for this state + * are: + * {@link #COMPONENT_ENABLED_STATE_ENABLED}, + * {@link #COMPONENT_ENABLED_STATE_DISABLED} + * and + * {@link #COMPONENT_ENABLED_STATE_DEFAULT} + * The last one removes the setting, thereby restoring the applications's state to + * whatever was set in its manifest (or enabled, by default). + * @param flags Optional behavior flags: {@link #DONT_KILL_APP} or 0. + */ + public abstract void setApplicationEnabledSetting(String packageName, + int newState, int flags); + + /** + * Return the the enabled setting for an application. This returns + * the last value set by + * {@link #setApplicationEnabledSetting(String, int, int)}; in most + * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since + * the value originally specified in the manifest has not been modified. + * + * @param packageName The component to retrieve. + * @return Returns the current enabled state for the component. May + * be one of {@link #COMPONENT_ENABLED_STATE_ENABLED}, + * {@link #COMPONENT_ENABLED_STATE_DISABLED}, or + * {@link #COMPONENT_ENABLED_STATE_DEFAULT}. The last one means the + * application's enabled state is based on the original information in + * the manifest as found in {@link ComponentInfo}. + */ + public abstract int getApplicationEnabledSetting(String packageName); + + /** + * Return whether the device has been booted into safe mode. + */ + public abstract boolean isSafeMode(); +} diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java new file mode 100644 index 0000000..4ae8b08 --- /dev/null +++ b/core/java/android/content/pm/PackageParser.java @@ -0,0 +1,2352 @@ +/* + * Copyright (C) 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.pm; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.os.PatternMatcher; +import android.util.AttributeSet; +import android.util.Config; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import com.android.internal.util.XmlUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Package archive parsing + * + * {@hide} + */ +public class PackageParser { + + private String mArchiveSourcePath; + private String[] mSeparateProcesses; + private int mSdkVersion; + + private int mParseError = PackageManager.INSTALL_SUCCEEDED; + + private static final Object mSync = new Object(); + private static WeakReference<byte[]> mReadBuffer; + + /** If set to true, we will only allow package files that exactly match + * the DTD. Otherwise, we try to get as much from the package as we + * can without failing. This should normally be set to false, to + * support extensions to the DTD in future versions. */ + private static final boolean RIGID_PARSER = false; + + private static final String TAG = "PackageParser"; + + public PackageParser(String archiveSourcePath) { + mArchiveSourcePath = archiveSourcePath; + } + + public void setSeparateProcesses(String[] procs) { + mSeparateProcesses = procs; + } + + public void setSdkVersion(int sdkVersion) { + mSdkVersion = sdkVersion; + } + + private static final boolean isPackageFilename(String name) { + return name.endsWith(".apk"); + } + + /** + * Generate and return the {@link PackageInfo} for a parsed package. + * + * @param p the parsed package. + * @param flags indicating which optional information is included. + */ + public static PackageInfo generatePackageInfo(PackageParser.Package p, + int gids[], int flags) { + + PackageInfo pi = new PackageInfo(); + pi.packageName = p.packageName; + pi.versionCode = p.mVersionCode; + pi.versionName = p.mVersionName; + pi.sharedUserId = p.mSharedUserId; + pi.sharedUserLabel = p.mSharedUserLabel; + pi.applicationInfo = p.applicationInfo; + if ((flags&PackageManager.GET_GIDS) != 0) { + pi.gids = gids; + } + if ((flags&PackageManager.GET_CONFIGURATIONS) != 0) { + 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); + } + } + } + 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++) { + 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); + } + } + } + } + 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++) { + 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); + } + } + } + } + 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++) { + 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); + } + } + } + } + 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++) { + 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); + } + } + } + } + if ((flags&PackageManager.GET_INSTRUMENTATION) != 0) { + int N = p.instrumentation.size(); + if (N > 0) { + pi.instrumentation = new InstrumentationInfo[N]; + for (int i=0; i<N; i++) { + pi.instrumentation[i] = generateInstrumentationInfo( + p.instrumentation.get(i), flags); + } + } + } + if ((flags&PackageManager.GET_PERMISSIONS) != 0) { + int N = p.permissions.size(); + if (N > 0) { + pi.permissions = new PermissionInfo[N]; + for (int i=0; i<N; i++) { + pi.permissions[i] = generatePermissionInfo(p.permissions.get(i), flags); + } + } + N = p.requestedPermissions.size(); + if (N > 0) { + pi.requestedPermissions = new String[N]; + for (int i=0; i<N; i++) { + pi.requestedPermissions[i] = p.requestedPermissions.get(i); + } + } + } + if ((flags&PackageManager.GET_SIGNATURES) != 0) { + int N = p.mSignatures.length; + if (N > 0) { + pi.signatures = new Signature[N]; + System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N); + } + } + return pi; + } + + private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, + byte[] readBuffer) { + try { + // We must read the stream for the JarEntry to retrieve + // its certificates. + InputStream is = jarFile.getInputStream(je); + while (is.read(readBuffer, 0, readBuffer.length) != -1) { + // not using + } + is.close(); + return je != null ? je.getCertificates() : null; + } catch (IOException e) { + Log.w(TAG, "Exception reading " + je.getName() + " in " + + jarFile.getName(), e); + } + return null; + } + + public final static int PARSE_IS_SYSTEM = 0x0001; + public final static int PARSE_CHATTY = 0x0002; + public final static int PARSE_MUST_BE_APK = 0x0004; + public final static int PARSE_IGNORE_PROCESSES = 0x0008; + + public int getParseError() { + return mParseError; + } + + public Package parsePackage(File sourceFile, String destFileName, + DisplayMetrics metrics, int flags) { + mParseError = PackageManager.INSTALL_SUCCEEDED; + + mArchiveSourcePath = sourceFile.getPath(); + if (!sourceFile.isFile()) { + Log.w(TAG, "Skipping dir: " + mArchiveSourcePath); + mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK; + return null; + } + if (!isPackageFilename(sourceFile.getName()) + && (flags&PARSE_MUST_BE_APK) != 0) { + if ((flags&PARSE_IS_SYSTEM) == 0) { + // We expect to have non-.apk files in the system dir, + // so don't warn about them. + Log.w(TAG, "Skipping non-package file: " + mArchiveSourcePath); + } + mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK; + return null; + } + + if ((flags&PARSE_CHATTY) != 0 && Config.LOGD) Log.d( + TAG, "Scanning package: " + mArchiveSourcePath); + + XmlResourceParser parser = null; + AssetManager assmgr = null; + boolean assetError = true; + try { + assmgr = new AssetManager(); + if(assmgr.addAssetPath(mArchiveSourcePath) != 0) { + parser = assmgr.openXmlResourceParser("AndroidManifest.xml"); + assetError = false; + } else { + Log.w(TAG, "Failed adding asset path:"+mArchiveSourcePath); + } + } catch (Exception e) { + Log.w(TAG, "Unable to read AndroidManifest.xml of " + + mArchiveSourcePath, e); + } + if(assetError) { + if (assmgr != null) assmgr.close(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; + return null; + } + String[] errorText = new String[1]; + Package pkg = null; + Exception errorException = null; + try { + // XXXX todo: need to figure out correct configuration. + Resources res = new Resources(assmgr, metrics, null); + pkg = parsePackage(res, parser, flags, errorText); + } catch (Exception e) { + errorException = e; + mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION; + } + + + if (pkg == null) { + if (errorException != null) { + Log.w(TAG, mArchiveSourcePath, errorException); + } else { + Log.w(TAG, mArchiveSourcePath + " (at " + + parser.getPositionDescription() + + "): " + errorText[0]); + } + parser.close(); + assmgr.close(); + if (mParseError == PackageManager.INSTALL_SUCCEEDED) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + } + return null; + } + + parser.close(); + assmgr.close(); + + pkg.applicationInfo.sourceDir = destFileName; + pkg.applicationInfo.publicSourceDir = destFileName; + pkg.mSignatures = null; + + return pkg; + } + + public boolean collectCertificates(Package pkg, int flags) { + pkg.mSignatures = null; + + WeakReference<byte[]> readBufferRef; + byte[] readBuffer = null; + synchronized (mSync) { + readBufferRef = mReadBuffer; + if (readBufferRef != null) { + mReadBuffer = null; + readBuffer = readBufferRef.get(); + } + if (readBuffer == null) { + readBuffer = new byte[8192]; + readBufferRef = new WeakReference<byte[]>(readBuffer); + } + } + + try { + JarFile jarFile = new JarFile(mArchiveSourcePath); + + Certificate[] certs = null; + + if ((flags&PARSE_IS_SYSTEM) != 0) { + // If this package comes from the system image, then we + // can trust it... we'll just use the AndroidManifest.xml + // to retrieve its signatures, not validating all of the + // files. + JarEntry jarEntry = jarFile.getJarEntry("AndroidManifest.xml"); + certs = loadCertificates(jarFile, jarEntry, readBuffer); + if (certs == null) { + Log.e(TAG, "Package " + pkg.packageName + + " has no certificates at entry " + + jarEntry.getName() + "; ignoring!"); + jarFile.close(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; + return false; + } + if (false) { + Log.i(TAG, "File " + mArchiveSourcePath + ": entry=" + jarEntry + + " certs=" + (certs != null ? certs.length : 0)); + if (certs != null) { + final int N = certs.length; + for (int i=0; i<N; i++) { + Log.i(TAG, " Public key: " + + certs[i].getPublicKey().getEncoded() + + " " + certs[i].getPublicKey()); + } + } + } + + } else { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry je = (JarEntry)entries.nextElement(); + if (je.isDirectory()) continue; + if (je.getName().startsWith("META-INF/")) continue; + Certificate[] localCerts = loadCertificates(jarFile, je, + readBuffer); + if (false) { + Log.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName() + + ": certs=" + certs + " (" + + (certs != null ? certs.length : 0) + ")"); + } + if (localCerts == null) { + Log.e(TAG, "Package " + pkg.packageName + + " has no certificates at entry " + + je.getName() + "; ignoring!"); + jarFile.close(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; + return false; + } else if (certs == null) { + certs = localCerts; + } else { + // Ensure all certificates match. + for (int i=0; i<certs.length; i++) { + boolean found = false; + for (int j=0; j<localCerts.length; j++) { + if (certs[i] != null && + certs[i].equals(localCerts[j])) { + found = true; + break; + } + } + if (!found || certs.length != localCerts.length) { + Log.e(TAG, "Package " + pkg.packageName + + " has mismatched certificates at entry " + + je.getName() + "; ignoring!"); + jarFile.close(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; + return false; + } + } + } + } + } + jarFile.close(); + + synchronized (mSync) { + mReadBuffer = readBufferRef; + } + + if (certs != null && certs.length > 0) { + final int N = certs.length; + pkg.mSignatures = new Signature[certs.length]; + for (int i=0; i<N; i++) { + pkg.mSignatures[i] = new Signature( + certs[i].getEncoded()); + } + } else { + Log.e(TAG, "Package " + pkg.packageName + + " has no certificates; ignoring!"); + mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; + return false; + } + } catch (CertificateEncodingException e) { + Log.w(TAG, "Exception reading " + mArchiveSourcePath, e); + mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; + return false; + } catch (IOException e) { + Log.w(TAG, "Exception reading " + mArchiveSourcePath, e); + mParseError = PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; + return false; + } catch (RuntimeException e) { + Log.w(TAG, "Exception reading " + mArchiveSourcePath, e); + mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION; + return false; + } + + return true; + } + + public static String parsePackageName(String packageFilePath, int flags) { + XmlResourceParser parser = null; + AssetManager assmgr = null; + try { + assmgr = new AssetManager(); + int cookie = assmgr.addAssetPath(packageFilePath); + parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml"); + } catch (Exception e) { + if (assmgr != null) assmgr.close(); + Log.w(TAG, "Unable to read AndroidManifest.xml of " + + packageFilePath, e); + return null; + } + AttributeSet attrs = parser; + String errors[] = new String[1]; + String packageName = null; + try { + packageName = parsePackageName(parser, attrs, flags, errors); + } catch (IOException e) { + Log.w(TAG, packageFilePath, e); + } catch (XmlPullParserException e) { + Log.w(TAG, packageFilePath, e); + } finally { + if (parser != null) parser.close(); + if (assmgr != null) assmgr.close(); + } + if (packageName == null) { + Log.e(TAG, "parsePackageName error: " + errors[0]); + return null; + } + return packageName; + } + + private static String validateName(String name, boolean requiresSeparator) { + final int N = name.length(); + boolean hasSep = false; + boolean front = true; + for (int i=0; i<N; i++) { + final char c = name.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + front = false; + continue; + } + if (!front) { + if ((c >= '0' && c <= '9') || c == '_') { + continue; + } + } + if (c == '.') { + hasSep = true; + front = true; + continue; + } + return "bad character '" + c + "'"; + } + return hasSep || !requiresSeparator + ? null : "must have at least one '.' separator"; + } + + private static String parsePackageName(XmlPullParser parser, + AttributeSet attrs, int flags, String[] outError) + throws IOException, XmlPullParserException { + + int type; + while ((type=parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + ; + } + + if (type != parser.START_TAG) { + outError[0] = "No start tag found"; + return null; + } + if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v( + TAG, "Root element name: '" + parser.getName() + "'"); + if (!parser.getName().equals("manifest")) { + outError[0] = "No <manifest> tag"; + return null; + } + String pkgName = attrs.getAttributeValue(null, "package"); + if (pkgName == null || pkgName.length() == 0) { + outError[0] = "<manifest> does not specify package"; + return null; + } + String nameError = validateName(pkgName, true); + if (nameError != null && !"android".equals(pkgName)) { + outError[0] = "<manifest> specifies bad package name \"" + + pkgName + "\": " + nameError; + return null; + } + + return pkgName.intern(); + } + + /** + * Temporary. + */ + static public Signature stringToSignature(String str) { + final int N = str.length(); + byte[] sig = new byte[N]; + for (int i=0; i<N; i++) { + sig[i] = (byte)str.charAt(i); + } + return new Signature(sig); + } + + private Package parsePackage( + Resources res, XmlResourceParser parser, int flags, String[] outError) + throws XmlPullParserException, IOException { + AttributeSet attrs = parser; + + String pkgName = parsePackageName(parser, attrs, flags, outError); + if (pkgName == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; + return null; + } + int type; + + final Package pkg = new Package(pkgName); + pkg.mSystem = (flags&PARSE_IS_SYSTEM) != 0; + boolean foundApp = false; + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifest); + pkg.mVersionCode = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_versionCode, 0); + pkg.mVersionName = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifest_versionName); + if (pkg.mVersionName != null) { + pkg.mVersionName = pkg.mVersionName.intern(); + } + String str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifest_sharedUserId); + if (str != null) { + String nameError = validateName(str, true); + if (nameError != null && !"android".equals(pkgName)) { + outError[0] = "<manifest> specifies bad sharedUserId name \"" + + str + "\": " + nameError; + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID; + return null; + } + pkg.mSharedUserId = str.intern(); + pkg.mSharedUserLabel = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); + } + sa.recycle(); + + final int innerDepth = parser.getDepth(); + + int outerDepth = parser.getDepth(); + while ((type=parser.next()) != parser.END_DOCUMENT + && (type != parser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == parser.END_TAG || type == parser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("application")) { + if (foundApp) { + if (RIGID_PARSER) { + outError[0] = "<manifest> has more than one <application>"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } else { + Log.w(TAG, "<manifest> has more than one <application>"); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + foundApp = true; + if (!parseApplication(pkg, res, parser, attrs, flags, outError)) { + return null; + } + } else if (tagName.equals("permission-group")) { + if (parsePermissionGroup(pkg, res, parser, attrs, outError) == null) { + return null; + } + } else if (tagName.equals("permission")) { + if (parsePermission(pkg, res, parser, attrs, outError) == null) { + return null; + } + } else if (tagName.equals("permission-tree")) { + if (parsePermissionTree(pkg, res, parser, attrs, outError) == null) { + return null; + } + } else if (tagName.equals("uses-permission")) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesPermission); + + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesPermission_name); + + sa.recycle(); + + if (name != null && !pkg.requestedPermissions.contains(name)) { + pkg.requestedPermissions.add(name); + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-configuration")) { + ConfigurationInfo cPref = new ConfigurationInfo(); + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesConfiguration); + cPref.reqTouchScreen = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen, + Configuration.TOUCHSCREEN_UNDEFINED); + cPref.reqKeyboardType = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType, + Configuration.KEYBOARD_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD; + } + cPref.reqNavigation = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqNavigation, + Configuration.NAVIGATION_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV; + } + sa.recycle(); + pkg.configPreferences.add(cPref); + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-sdk")) { + if (mSdkVersion > 0) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesSdk); + + int vers = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesSdk_minSdkVersion, 0); + + sa.recycle(); + + if (vers > mSdkVersion) { + outError[0] = "Requires newer sdk version #" + vers + + " (current version is #" + mSdkVersion + ")"; + mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; + return null; + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("instrumentation")) { + if (parseInstrumentation(pkg, res, parser, attrs, outError) == null) { + return null; + } + } else if (tagName.equals("eat-comment")) { + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + continue; + } else if (RIGID_PARSER) { + outError[0] = "Bad element under <manifest>: " + + parser.getName(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } else { + Log.w(TAG, "Bad element under <manifest>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + if (!foundApp && pkg.instrumentation.size() == 0) { + outError[0] = "<manifest> does not contain an <application> or <instrumentation>"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY; + } + + if (pkg.usesLibraries.size() > 0) { + pkg.usesLibraryFiles = new String[pkg.usesLibraries.size()]; + pkg.usesLibraries.toArray(pkg.usesLibraryFiles); + } + + return pkg; + } + + private static String buildClassName(String pkg, CharSequence clsSeq, + String[] outError) { + if (clsSeq == null || clsSeq.length() <= 0) { + outError[0] = "Empty class name in package " + pkg; + return null; + } + String cls = clsSeq.toString(); + char c = cls.charAt(0); + if (c == '.') { + return (pkg + cls).intern(); + } + if (cls.indexOf('.') < 0) { + StringBuilder b = new StringBuilder(pkg); + b.append('.'); + b.append(cls); + return b.toString().intern(); + } + if (c >= 'a' && c <= 'z') { + return cls.intern(); + } + outError[0] = "Bad class name " + cls + " in package " + pkg; + return null; + } + + private static String buildCompoundName(String pkg, + CharSequence procSeq, String type, String[] outError) { + String proc = procSeq.toString(); + char c = proc.charAt(0); + if (pkg != null && c == ':') { + if (proc.length() < 2) { + outError[0] = "Bad " + type + " name " + proc + " in package " + pkg + + ": must be at least two characters"; + return null; + } + String subName = proc.substring(1); + String nameError = validateName(subName, false); + if (nameError != null) { + outError[0] = "Invalid " + type + " name " + proc + " in package " + + pkg + ": " + nameError; + return null; + } + return (pkg + proc).intern(); + } + String nameError = validateName(proc, true); + if (nameError != null && !"system".equals(proc)) { + outError[0] = "Invalid " + type + " name " + proc + " in package " + + pkg + ": " + nameError; + return null; + } + return proc.intern(); + } + + private static String buildProcessName(String pkg, String defProc, + CharSequence procSeq, int flags, String[] separateProcesses, + String[] outError) { + if ((flags&PARSE_IGNORE_PROCESSES) != 0 && !"system".equals(procSeq)) { + return defProc != null ? defProc : pkg; + } + if (separateProcesses != null) { + for (int i=separateProcesses.length-1; i>=0; i--) { + String sp = separateProcesses[i]; + if (sp.equals(pkg) || sp.equals(defProc) || sp.equals(procSeq)) { + return pkg; + } + } + } + if (procSeq == null || procSeq.length() <= 0) { + return defProc; + } + return buildCompoundName(pkg, procSeq, "package", outError); + } + + private static String buildTaskAffinityName(String pkg, String defProc, + CharSequence procSeq, String[] outError) { + if (procSeq == null) { + return defProc; + } + if (procSeq.length() <= 0) { + return null; + } + return buildCompoundName(pkg, procSeq, "taskAffinity", outError); + } + + private PermissionGroup parsePermissionGroup(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, String[] outError) + throws XmlPullParserException, IOException { + PermissionGroup perm = new PermissionGroup(owner); + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestPermissionGroup); + + if (!parsePackageItemInfo(owner, perm.info, outError, + "<permission-group>", sa, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_name, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_label, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_icon)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + perm.info.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_description, + 0); + + sa.recycle(); + + if (!parseAllMetaData(res, parser, attrs, "<permission-group>", perm, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + owner.permissionGroups.add(perm); + + return perm; + } + + private Permission parsePermission(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, String[] outError) + throws XmlPullParserException, IOException { + Permission perm = new Permission(owner); + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestPermission); + + if (!parsePackageItemInfo(owner, perm.info, outError, + "<permission>", sa, + com.android.internal.R.styleable.AndroidManifestPermission_name, + com.android.internal.R.styleable.AndroidManifestPermission_label, + com.android.internal.R.styleable.AndroidManifestPermission_icon)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + perm.info.group = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestPermission_permissionGroup); + if (perm.info.group != null) { + perm.info.group = perm.info.group.intern(); + } + + perm.info.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermission_description, + 0); + + perm.info.protectionLevel = sa.getInt( + com.android.internal.R.styleable.AndroidManifestPermission_protectionLevel, + PermissionInfo.PROTECTION_NORMAL); + + sa.recycle(); + + if (perm.info.protectionLevel == -1) { + outError[0] = "<permission> does not specify protectionLevel"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + if (!parseAllMetaData(res, parser, attrs, "<permission>", perm, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + owner.permissions.add(perm); + + return perm; + } + + private Permission parsePermissionTree(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, String[] outError) + throws XmlPullParserException, IOException { + Permission perm = new Permission(owner); + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestPermissionTree); + + if (!parsePackageItemInfo(owner, perm.info, outError, + "<permission-tree>", sa, + com.android.internal.R.styleable.AndroidManifestPermissionTree_name, + com.android.internal.R.styleable.AndroidManifestPermissionTree_label, + com.android.internal.R.styleable.AndroidManifestPermissionTree_icon)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + sa.recycle(); + + int index = perm.info.name.indexOf('.'); + if (index > 0) { + index = perm.info.name.indexOf('.', index+1); + } + if (index < 0) { + outError[0] = "<permission-tree> name has less than three segments: " + + perm.info.name; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + perm.info.descriptionRes = 0; + perm.info.protectionLevel = PermissionInfo.PROTECTION_NORMAL; + perm.tree = true; + + if (!parseAllMetaData(res, parser, attrs, "<permission-tree>", perm, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + owner.permissions.add(perm); + + return perm; + } + + private Instrumentation parseInstrumentation(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestInstrumentation); + + Instrumentation a = new Instrumentation(owner); + + if (!parsePackageItemInfo(owner, a.info, outError, "<instrumentation>", sa, + com.android.internal.R.styleable.AndroidManifestInstrumentation_name, + com.android.internal.R.styleable.AndroidManifestInstrumentation_label, + com.android.internal.R.styleable.AndroidManifestInstrumentation_icon)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + a.component = new ComponentName(owner.applicationInfo.packageName, + a.info.name); + + String str; + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestInstrumentation_targetPackage); + a.info.targetPackage = str != null ? str.intern() : null; + + a.info.handleProfiling = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestInstrumentation_handleProfiling, + false); + + a.info.functionalTest = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestInstrumentation_functionalTest, + false); + + sa.recycle(); + + if (a.info.targetPackage == null) { + outError[0] = "<instrumentation> does not specify targetPackage"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + if (!parseAllMetaData(res, parser, attrs, "<instrumentation>", a, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + owner.instrumentation.add(a); + + return a; + } + + private boolean parseApplication(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, int flags, String[] outError) + throws XmlPullParserException, IOException { + final ApplicationInfo ai = owner.applicationInfo; + final String pkgName = owner.applicationInfo.packageName; + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestApplication); + + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_name); + if (name != null) { + ai.className = buildClassName(pkgName, name, outError); + if (ai.className == null) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } + + String manageSpaceActivity = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_manageSpaceActivity); + if (manageSpaceActivity != null) { + ai.manageSpaceActivityName = buildClassName(pkgName, manageSpaceActivity, + outError); + } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestApplication_label); + if (v != null && (ai.labelRes=v.resourceId) == 0) { + ai.nonLocalizedLabel = v.coerceToString(); + } + + ai.icon = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_icon, 0); + ai.theme = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_theme, 0); + ai.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_description, 0); + + if ((flags&PARSE_IS_SYSTEM) != 0) { + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_persistent, + false)) { + ai.flags |= ApplicationInfo.FLAG_PERSISTENT; + } + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_debuggable, + false)) { + ai.flags |= ApplicationInfo.FLAG_DEBUGGABLE; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hasCode, + true)) { + ai.flags |= ApplicationInfo.FLAG_HAS_CODE; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowTaskReparenting, + false)) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowClearUserData, + true)) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; + } + + String str; + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_permission); + ai.permission = (str != null && str.length() > 0) ? str.intern() : null; + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_taskAffinity); + ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName, + str, outError); + + if (outError[0] == null) { + ai.processName = buildProcessName(ai.packageName, null, sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_process), + flags, mSeparateProcesses, outError); + + ai.enabled = sa.getBoolean(com.android.internal.R.styleable.AndroidManifestApplication_enabled, true); + } + + sa.recycle(); + + if (outError[0] != null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + final int innerDepth = parser.getDepth(); + + int type; + while ((type=parser.next()) != parser.END_DOCUMENT + && (type != parser.END_TAG || parser.getDepth() > innerDepth)) { + if (type == parser.END_TAG || type == parser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("activity")) { + Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.activities.add(a); + + } else if (tagName.equals("receiver")) { + Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.receivers.add(a); + + } else if (tagName.equals("service")) { + Service s = parseService(owner, res, parser, attrs, flags, outError); + if (s == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.services.add(s); + + } else if (tagName.equals("provider")) { + Provider p = parseProvider(owner, res, parser, attrs, flags, outError); + if (p == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.providers.add(p); + + } else if (tagName.equals("activity-alias")) { + Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError, false); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.activities.add(a); + + } else if (parser.getName().equals("meta-data")) { + // note: application meta-data is stored off to the side, so it can + // remain null in the primary copy (we like to avoid extra copies because + // it can be large) + if ((owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData, + outError)) == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + } else if (tagName.equals("uses-library")) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesLibrary); + + String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + + sa.recycle(); + + if (lname != null && !owner.usesLibraries.contains(lname)) { + owner.usesLibraries.add(lname); + } + + XmlUtils.skipCurrentTag(parser); + + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + Log.w(TAG, "Unknown element under <application>: " + tagName); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under <application>: " + tagName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } + } + + return true; + } + + private boolean parsePackageItemInfo(Package owner, PackageItemInfo outInfo, + String[] outError, String tag, TypedArray sa, + int nameRes, int labelRes, int iconRes) { + String name = sa.getNonResourceString(nameRes); + if (name == null) { + outError[0] = tag + " does not specify android:name"; + return false; + } + + outInfo.name + = buildClassName(owner.applicationInfo.packageName, name, outError); + if (outInfo.name == null) { + return false; + } + + int iconVal = sa.getResourceId(iconRes, 0); + if (iconVal != 0) { + outInfo.icon = iconVal; + outInfo.nonLocalizedLabel = null; + } + + TypedValue v = sa.peekValue(labelRes); + if (v != null && (outInfo.labelRes=v.resourceId) == 0) { + outInfo.nonLocalizedLabel = v.coerceToString(); + } + + outInfo.packageName = owner.packageName; + + 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 { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestActivity); + + Activity a = new Activity(owner); + + if (!parseComponentInfo(owner, flags, a.info, outError, + receiver ? "<receiver>" : "<activity>", sa, + com.android.internal.R.styleable.AndroidManifestActivity_name, + com.android.internal.R.styleable.AndroidManifestActivity_label, + com.android.internal.R.styleable.AndroidManifestActivity_icon, + com.android.internal.R.styleable.AndroidManifestActivity_process, + com.android.internal.R.styleable.AndroidManifestActivity_enabled)) { + sa.recycle(); + return null; + } + + final boolean setExported = sa.hasValue( + com.android.internal.R.styleable.AndroidManifestActivity_exported); + if (setExported) { + a.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_exported, false); + } + + a.component = new ComponentName(owner.applicationInfo.packageName, + a.info.name); + + a.info.theme = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestActivity_theme, 0); + + String str; + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestActivity_permission); + if (str == null) { + a.info.permission = owner.applicationInfo.permission; + } else { + a.info.permission = str.length() > 0 ? str.toString().intern() : null; + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestActivity_taskAffinity); + a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName, + owner.applicationInfo.taskAffinity, str, outError); + + a.info.flags = 0; + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_multiprocess, + false)) { + a.info.flags |= ActivityInfo.FLAG_MULTIPROCESS; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_finishOnTaskLaunch, + false)) { + a.info.flags |= ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_clearTaskOnLaunch, + false)) { + a.info.flags |= ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_noHistory, + false)) { + a.info.flags |= ActivityInfo.FLAG_NO_HISTORY; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_alwaysRetainTaskState, + false)) { + a.info.flags |= ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_stateNotNeeded, + false)) { + a.info.flags |= ActivityInfo.FLAG_STATE_NOT_NEEDED; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_excludeFromRecents, + false)) { + a.info.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivity_allowTaskReparenting, + (owner.applicationInfo.flags&ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING) != 0)) { + a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING; + } + + if (!receiver) { + a.info.launchMode = sa.getInt( + com.android.internal.R.styleable.AndroidManifestActivity_launchMode, + ActivityInfo.LAUNCH_MULTIPLE); + a.info.screenOrientation = sa.getInt( + com.android.internal.R.styleable.AndroidManifestActivity_screenOrientation, + ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + a.info.configChanges = sa.getInt( + com.android.internal.R.styleable.AndroidManifestActivity_configChanges, + 0); + a.info.softInputMode = sa.getInt( + com.android.internal.R.styleable.AndroidManifestActivity_windowSoftInputMode, + 0); + } else { + a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE; + a.info.configChanges = 0; + } + + sa.recycle(); + + if (outError[0] != null) { + return null; + } + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, attrs, flags, intent, outError, !receiver)) { + return null; + } + if (intent.countActions() == 0) { + Log.w(TAG, "Intent filter for activity " + intent + + " defines no actions"); + } else { + a.intents.add(intent); + } + } else if (parser.getName().equals("meta-data")) { + if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData, + outError)) == null) { + return null; + } + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + if (receiver) { + Log.w(TAG, "Unknown element under <receiver>: " + parser.getName()); + } else { + Log.w(TAG, "Unknown element under <activity>: " + parser.getName()); + } + XmlUtils.skipCurrentTag(parser); + continue; + } + if (receiver) { + outError[0] = "Bad element under <receiver>: " + parser.getName(); + } else { + outError[0] = "Bad element under <activity>: " + parser.getName(); + } + return null; + } + } + + if (!setExported) { + a.info.exported = a.intents.size() > 0; + } + + return a; + } + + private Activity parseActivityAlias(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, int flags, String[] outError, + boolean receiver) throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestActivityAlias); + + String targetActivity = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestActivityAlias_targetActivity); + if (targetActivity == null) { + outError[0] = "<activity-alias> does not specify android:targetActivity"; + sa.recycle(); + return null; + } + + targetActivity = buildClassName(owner.applicationInfo.packageName, + targetActivity, outError); + if (targetActivity == null) { + sa.recycle(); + return null; + } + + Activity a = new Activity(owner); + Activity target = null; + + final int NA = owner.activities.size(); + for (int i=0; i<NA; i++) { + Activity t = owner.activities.get(i); + if (targetActivity.equals(t.info.name)) { + target = t; + break; + } + } + + if (target == null) { + outError[0] = "<activity-alias> target activity " + targetActivity + + " not found in manifest"; + sa.recycle(); + return null; + } + + a.info.targetActivity = targetActivity; + + a.info.configChanges = target.info.configChanges; + a.info.flags = target.info.flags; + a.info.icon = target.info.icon; + a.info.labelRes = target.info.labelRes; + a.info.launchMode = target.info.launchMode; + a.info.nonLocalizedLabel = target.info.nonLocalizedLabel; + a.info.processName = target.info.processName; + a.info.screenOrientation = target.info.screenOrientation; + a.info.taskAffinity = target.info.taskAffinity; + a.info.theme = target.info.theme; + + if (!parseComponentInfo(owner, flags, a.info, outError, + receiver ? "<receiver>" : "<activity>", sa, + com.android.internal.R.styleable.AndroidManifestActivityAlias_name, + com.android.internal.R.styleable.AndroidManifestActivityAlias_label, + com.android.internal.R.styleable.AndroidManifestActivityAlias_icon, + 0, + com.android.internal.R.styleable.AndroidManifestActivityAlias_enabled)) { + sa.recycle(); + return null; + } + + final boolean setExported = sa.hasValue( + com.android.internal.R.styleable.AndroidManifestActivityAlias_exported); + if (setExported) { + a.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestActivityAlias_exported, false); + } + + a.component = new ComponentName(owner.applicationInfo.packageName, + a.info.name); + + String str; + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestActivityAlias_permission); + if (str != null) { + a.info.permission = str.length() > 0 ? str.toString().intern() : null; + } + + sa.recycle(); + + if (outError[0] != null) { + return null; + } + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, attrs, flags, intent, outError, true)) { + return null; + } + if (intent.countActions() == 0) { + Log.w(TAG, "Intent filter for activity alias " + intent + + " defines no actions"); + } else { + a.intents.add(intent); + } + } else if (parser.getName().equals("meta-data")) { + if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData, + outError)) == null) { + return null; + } + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + Log.w(TAG, "Unknown element under <activity-alias>: " + parser.getName()); + XmlUtils.skipCurrentTag(parser); + continue; + } + outError[0] = "Bad element under <activity-alias>: " + parser.getName(); + return null; + } + } + + if (!setExported) { + a.info.exported = a.intents.size() > 0; + } + + return a; + } + + private Provider parseProvider(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, int flags, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestProvider); + + Provider p = new Provider(owner); + + if (!parseComponentInfo(owner, flags, p.info, outError, "<provider>", sa, + com.android.internal.R.styleable.AndroidManifestProvider_name, + com.android.internal.R.styleable.AndroidManifestProvider_label, + com.android.internal.R.styleable.AndroidManifestProvider_icon, + com.android.internal.R.styleable.AndroidManifestProvider_process, + com.android.internal.R.styleable.AndroidManifestProvider_enabled)) { + sa.recycle(); + return null; + } + + p.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_exported, true); + + p.component = new ComponentName(owner.applicationInfo.packageName, + p.info.name); + + String cpname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestProvider_authorities); + + p.info.isSyncable = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_syncable, + false); + + String permission = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestProvider_permission); + String str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestProvider_readPermission); + if (str == null) { + str = permission; + } + if (str == null) { + p.info.readPermission = owner.applicationInfo.permission; + } else { + p.info.readPermission = + str.length() > 0 ? str.toString().intern() : null; + } + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestProvider_writePermission); + if (str == null) { + str = permission; + } + if (str == null) { + p.info.writePermission = owner.applicationInfo.permission; + } else { + p.info.writePermission = + str.length() > 0 ? str.toString().intern() : null; + } + + p.info.grantUriPermissions = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_grantUriPermissions, + false); + + p.info.multiprocess = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_multiprocess, + false); + + p.info.initOrder = sa.getInt( + com.android.internal.R.styleable.AndroidManifestProvider_initOrder, + 0); + + sa.recycle(); + + if (cpname == null) { + outError[0] = "<provider> does not incude authorities attribute"; + return null; + } + p.info.authority = cpname.intern(); + + if (!parseProviderTags(res, parser, attrs, p, outError)) { + return null; + } + + return p; + } + + private boolean parseProviderTags(Resources res, + XmlPullParser parser, AttributeSet attrs, + Provider outInfo, String[] outError) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("meta-data")) { + if ((outInfo.metaData=parseMetaData(res, parser, attrs, + outInfo.metaData, outError)) == null) { + return false; + } + } else if (parser.getName().equals("grant-uri-permission")) { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestGrantUriPermission); + + PatternMatcher pa = null; + + String str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_path); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_LITERAL); + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPrefix); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_PREFIX); + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPattern); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + + sa.recycle(); + + if (pa != null) { + if (outInfo.info.uriPermissionPatterns == null) { + outInfo.info.uriPermissionPatterns = new PatternMatcher[1]; + outInfo.info.uriPermissionPatterns[0] = pa; + } else { + final int N = outInfo.info.uriPermissionPatterns.length; + PatternMatcher[] newp = new PatternMatcher[N+1]; + System.arraycopy(outInfo.info.uriPermissionPatterns, 0, newp, 0, N); + newp[N] = pa; + outInfo.info.uriPermissionPatterns = newp; + } + outInfo.info.grantUriPermissions = true; + } + XmlUtils.skipCurrentTag(parser); + + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + Log.w(TAG, "Unknown element under <provider>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + continue; + } + outError[0] = "Bad element under <provider>: " + + parser.getName(); + return false; + } + } + return true; + } + + private Service parseService(Package owner, Resources res, + XmlPullParser parser, AttributeSet attrs, int flags, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestService); + + Service s = new Service(owner); + + if (!parseComponentInfo(owner, flags, s.info, outError, "<service>", sa, + com.android.internal.R.styleable.AndroidManifestService_name, + com.android.internal.R.styleable.AndroidManifestService_label, + com.android.internal.R.styleable.AndroidManifestService_icon, + com.android.internal.R.styleable.AndroidManifestService_process, + com.android.internal.R.styleable.AndroidManifestService_enabled)) { + sa.recycle(); + return null; + } + + final boolean setExported = sa.hasValue( + com.android.internal.R.styleable.AndroidManifestService_exported); + if (setExported) { + s.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_exported, false); + } + + s.component = new ComponentName(owner.applicationInfo.packageName, + s.info.name); + + String str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestService_permission); + if (str == null) { + s.info.permission = owner.applicationInfo.permission; + } else { + s.info.permission = str.length() > 0 ? str.toString().intern() : null; + } + + sa.recycle(); + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ServiceIntentInfo intent = new ServiceIntentInfo(s); + if (!parseIntent(res, parser, attrs, flags, intent, outError, false)) { + return null; + } + + s.intents.add(intent); + } else if (parser.getName().equals("meta-data")) { + if ((s.metaData=parseMetaData(res, parser, attrs, s.metaData, + outError)) == null) { + return null; + } + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + Log.w(TAG, "Unknown element under <service>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + continue; + } + outError[0] = "Bad element under <service>: " + + parser.getName(); + return null; + } + } + + if (!setExported) { + s.info.exported = s.intents.size() > 0; + } + + return s; + } + + private boolean parseAllMetaData(Resources res, + XmlPullParser parser, AttributeSet attrs, String tag, + Component outInfo, String[] outError) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("meta-data")) { + if ((outInfo.metaData=parseMetaData(res, parser, attrs, + outInfo.metaData, outError)) == null) { + return false; + } + } else { + if (!RIGID_PARSER) { + Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + Log.w(TAG, "Unknown element under " + tag + ": " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + continue; + } + outError[0] = "Bad element under " + tag + ": " + + parser.getName(); + return false; + } + } + return true; + } + + private Bundle parseMetaData(Resources res, + XmlPullParser parser, AttributeSet attrs, + Bundle data, String[] outError) + throws XmlPullParserException, IOException { + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestMetaData); + + if (data == null) { + data = new Bundle(); + } + + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestMetaData_name); + if (name == null) { + outError[0] = "<meta-data> requires an android:name attribute"; + sa.recycle(); + return null; + } + + boolean success = true; + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestMetaData_resource); + if (v != null && v.resourceId != 0) { + //Log.i(TAG, "Meta data ref " + name + ": " + v); + data.putInt(name, v.resourceId); + } else { + v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestMetaData_value); + //Log.i(TAG, "Meta data " + name + ": " + v); + if (v != null) { + if (v.type == TypedValue.TYPE_STRING) { + CharSequence cs = v.coerceToString(); + data.putString(name, cs != null ? cs.toString() : null); + } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { + data.putBoolean(name, v.data != 0); + } else if (v.type >= TypedValue.TYPE_FIRST_INT + && v.type <= TypedValue.TYPE_LAST_INT) { + data.putInt(name, v.data); + } else if (v.type == TypedValue.TYPE_FLOAT) { + 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"); + } else { + outError[0] = "<meta-data> only supports string, integer, float, color, boolean, and resource reference types"; + data = null; + } + } + } else { + outError[0] = "<meta-data> requires an android:value or android:resource attribute"; + data = null; + } + } + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); + + return data; + } + + private static final String ANDROID_RESOURCES + = "http://schemas.android.com/apk/res/android"; + + private boolean parseIntent(Resources res, + XmlPullParser parser, AttributeSet attrs, int flags, + IntentInfo outInfo, String[] outError, boolean isActivity) + throws XmlPullParserException, IOException { + + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestIntentFilter); + + int priority = sa.getInt( + 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 " + + parser.getPositionDescription()); + priority = 0; + } + outInfo.setPriority(priority); + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestIntentFilter_label); + if (v != null && (outInfo.labelRes=v.resourceId) == 0) { + outInfo.nonLocalizedLabel = v.coerceToString(); + } + + outInfo.icon = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestIntentFilter_icon, 0); + + sa.recycle(); + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != parser.END_DOCUMENT + && (type != parser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == parser.END_TAG || type == parser.TEXT) { + continue; + } + + String nodeName = parser.getName(); + if (nodeName.equals("action")) { + String value = attrs.getAttributeValue( + ANDROID_RESOURCES, "name"); + if (value == null || value == "") { + outError[0] = "No value supplied for <android:name>"; + return false; + } + XmlUtils.skipCurrentTag(parser); + + outInfo.addAction(value); + } else if (nodeName.equals("category")) { + String value = attrs.getAttributeValue( + ANDROID_RESOURCES, "name"); + if (value == null || value == "") { + outError[0] = "No value supplied for <android:name>"; + return false; + } + XmlUtils.skipCurrentTag(parser); + + outInfo.addCategory(value); + + } else if (nodeName.equals("data")) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestData); + + String str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_mimeType); + if (str != null) { + try { + outInfo.addDataType(str); + } catch (IntentFilter.MalformedMimeTypeException e) { + outError[0] = e.toString(); + sa.recycle(); + return false; + } + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_scheme); + if (str != null) { + outInfo.addDataScheme(str); + } + + String host = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_host); + String port = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_port); + if (host != null) { + outInfo.addDataAuthority(host, port); + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_path); + if (str != null) { + outInfo.addDataPath(str, PatternMatcher.PATTERN_LITERAL); + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_pathPrefix); + if (str != null) { + outInfo.addDataPath(str, PatternMatcher.PATTERN_PREFIX); + } + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestData_pathPattern); + if (str != null) { + outInfo.addDataPath(str, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + + 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()); + XmlUtils.skipCurrentTag(parser); + } else { + outError[0] = "Bad element under <intent-filter>: " + parser.getName(); + return false; + } + } + + outInfo.hasDefault = outInfo.hasCategory(Intent.CATEGORY_DEFAULT); + if (false) { + String cats = ""; + Iterator<String> it = outInfo.categoriesIterator(); + while (it != null && it.hasNext()) { + cats += " " + it.next(); + } + System.out.println("Intent d=" + + outInfo.hasDefault + ", cat=" + cats); + } + + return true; + } + + public final static class Package { + public final String packageName; + + // For now we only support one application per package. + public final ApplicationInfo applicationInfo = new ApplicationInfo(); + + public final ArrayList<Permission> permissions = new ArrayList<Permission>(0); + public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0); + public final ArrayList<Activity> activities = new ArrayList<Activity>(0); + public final ArrayList<Activity> receivers = new ArrayList<Activity>(0); + public final ArrayList<Provider> providers = new ArrayList<Provider>(0); + public final ArrayList<Service> services = new ArrayList<Service>(0); + public final ArrayList<Instrumentation> instrumentation = new ArrayList<Instrumentation>(0); + + public final ArrayList<String> requestedPermissions = new ArrayList<String>(); + + public final ArrayList<String> usesLibraries = new ArrayList<String>(); + public String[] usesLibraryFiles = null; + + // We store the application meta-data independently to avoid multiple unwanted references + public Bundle mAppMetaData = null; + + // If this is a 3rd party app, this is the path of the zip file. + public String mPath; + + // True if this package is part of the system image. + public boolean mSystem; + + // The version code declared for this package. + public int mVersionCode; + + // The version name declared for this package. + public String mVersionName; + + // The shared user id that this package wants to use. + public String mSharedUserId; + + // The shared user label that this package wants to use. + public int mSharedUserLabel; + + // Signatures that were read from the package. + public Signature mSignatures[]; + + // For use by package manager service for quick lookup of + // preferred up order. + public int mPreferredOrder = 0; + + // Additional data supplied by callers. + public Object mExtras; + + /* + * Applications hardware preferences + */ + public final ArrayList<ConfigurationInfo> configPreferences = + new ArrayList<ConfigurationInfo>(); + + public Package(String _name) { + packageName = _name; + applicationInfo.packageName = _name; + applicationInfo.uid = -1; + } + + public String toString() { + return "Package{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + } + + public static class Component<II extends IntentInfo> { + public final Package owner; + public final ArrayList<II> intents = new ArrayList<II>(0); + public ComponentName component; + public Bundle metaData; + + public Component(Package _owner) { + owner = _owner; + } + + public Component(Component<II> clone) { + owner = clone.owner; + metaData = clone.metaData; + } + } + + public final static class Permission extends Component<IntentInfo> { + public final PermissionInfo info; + public boolean tree; + public PermissionGroup group; + + public Permission(Package _owner) { + super(_owner); + info = new PermissionInfo(); + } + + public Permission(Package _owner, PermissionInfo _info) { + super(_owner); + info = _info; + } + + public String toString() { + return "Permission{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + info.name + "}"; + } + } + + public final static class PermissionGroup extends Component<IntentInfo> { + public final PermissionGroupInfo info; + + public PermissionGroup(Package _owner) { + super(_owner); + info = new PermissionGroupInfo(); + } + + public PermissionGroup(Package _owner, PermissionGroupInfo _info) { + super(_owner); + info = _info; + } + + public String toString() { + return "PermissionGroup{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + info.name + "}"; + } + } + + private static boolean copyNeeded(int flags, Package p, Bundle metaData) { + if ((flags & PackageManager.GET_META_DATA) != 0 + && (metaData != null || p.mAppMetaData != null)) { + return true; + } + if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0 + && p.usesLibraryFiles != null) { + return true; + } + return false; + } + + public static ApplicationInfo generateApplicationInfo(Package p, int flags) { + if (p == null) return null; + if (!copyNeeded(flags, p, null)) { + return p.applicationInfo; + } + + // Make shallow copy so we can store the metadata/libraries safely + ApplicationInfo ai = new ApplicationInfo(p.applicationInfo); + if ((flags & PackageManager.GET_META_DATA) != 0) { + ai.metaData = p.mAppMetaData; + } + if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) { + ai.sharedLibraryFiles = p.usesLibraryFiles; + } + return ai; + } + + public static final PermissionInfo generatePermissionInfo( + Permission p, int flags) { + if (p == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return p.info; + } + PermissionInfo pi = new PermissionInfo(p.info); + pi.metaData = p.metaData; + return pi; + } + + public static final PermissionGroupInfo generatePermissionGroupInfo( + PermissionGroup pg, int flags) { + if (pg == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return pg.info; + } + PermissionGroupInfo pgi = new PermissionGroupInfo(pg.info); + pgi.metaData = pg.metaData; + return pgi; + } + + public final static class Activity extends Component<ActivityIntentInfo> { + public final ActivityInfo info = + new ActivityInfo(); + + public Activity(Package _owner) { + super(_owner); + info.applicationInfo = owner.applicationInfo; + } + + public String toString() { + return "Activity{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + component.flattenToString() + "}"; + } + } + + public static final ActivityInfo generateActivityInfo(Activity a, + int flags) { + if (a == null) return null; + if (!copyNeeded(flags, a.owner, a.metaData)) { + return a.info; + } + // Make shallow copies so we can store the metadata safely + ActivityInfo ai = new ActivityInfo(a.info); + ai.metaData = a.metaData; + ai.applicationInfo = generateApplicationInfo(a.owner, flags); + return ai; + } + + public final static class Service extends Component<ServiceIntentInfo> { + public final ServiceInfo info = + new ServiceInfo(); + + public Service(Package _owner) { + super(_owner); + info.applicationInfo = owner.applicationInfo; + } + + public String toString() { + return "Service{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + component.flattenToString() + "}"; + } + } + + public static final ServiceInfo generateServiceInfo(Service s, int flags) { + if (s == null) return null; + if (!copyNeeded(flags, s.owner, s.metaData)) { + return s.info; + } + // Make shallow copies so we can store the metadata safely + ServiceInfo si = new ServiceInfo(s.info); + si.metaData = s.metaData; + si.applicationInfo = generateApplicationInfo(s.owner, flags); + return si; + } + + public final static class Provider extends Component { + public final ProviderInfo info; + public boolean syncable; + + public Provider(Package _owner) { + super(_owner); + info = new ProviderInfo(); + info.applicationInfo = owner.applicationInfo; + syncable = false; + } + + public Provider(Provider existingProvider) { + super(existingProvider); + this.info = existingProvider.info; + this.syncable = existingProvider.syncable; + } + + public String toString() { + return "Provider{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + info.name + "}"; + } + } + + public static final ProviderInfo generateProviderInfo(Provider p, + int flags) { + if (p == null) return null; + if (!copyNeeded(flags, p.owner, p.metaData) + && ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) != 0 + || p.info.uriPermissionPatterns == null)) { + return p.info; + } + // Make shallow copies so we can store the metadata safely + ProviderInfo pi = new ProviderInfo(p.info); + pi.metaData = p.metaData; + if ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) == 0) { + pi.uriPermissionPatterns = null; + } + pi.applicationInfo = generateApplicationInfo(p.owner, flags); + return pi; + } + + public final static class Instrumentation extends Component { + public final InstrumentationInfo info = + new InstrumentationInfo(); + + public Instrumentation(Package _owner) { + super(_owner); + } + + public String toString() { + return "Instrumentation{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + component.flattenToString() + "}"; + } + } + + public static final InstrumentationInfo generateInstrumentationInfo( + Instrumentation i, int flags) { + if (i == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return i.info; + } + InstrumentationInfo ii = new InstrumentationInfo(i.info); + ii.metaData = i.metaData; + return ii; + } + + public static class IntentInfo extends IntentFilter { + public boolean hasDefault; + public int labelRes; + public CharSequence nonLocalizedLabel; + public int icon; + } + + public final static class ActivityIntentInfo extends IntentInfo { + public final Activity activity; + + public ActivityIntentInfo(Activity _activity) { + activity = _activity; + } + + public String toString() { + return "ActivityIntentInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + activity.info.name + "}"; + } + } + + public final static class ServiceIntentInfo extends IntentInfo { + public final Service service; + + public ServiceIntentInfo(Service _service) { + service = _service; + } + + public String toString() { + return "ServiceIntentInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + service.info.name + "}"; + } + } +} diff --git a/core/java/android/content/pm/PackageStats.aidl b/core/java/android/content/pm/PackageStats.aidl new file mode 100755 index 0000000..8c9786f --- /dev/null +++ b/core/java/android/content/pm/PackageStats.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable PackageStats; diff --git a/core/java/android/content/pm/PackageStats.java b/core/java/android/content/pm/PackageStats.java new file mode 100755 index 0000000..66c6efd --- /dev/null +++ b/core/java/android/content/pm/PackageStats.java @@ -0,0 +1,63 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; + +/** + * implementation of PackageStats associated with a + * application package. + */ +public class PackageStats implements Parcelable { + public String packageName; + public long codeSize; + public long dataSize; + public long cacheSize; + + public static final Parcelable.Creator<PackageStats> CREATOR + = new Parcelable.Creator<PackageStats>() { + public PackageStats createFromParcel(Parcel in) { + return new PackageStats(in); + } + + public PackageStats[] newArray(int size) { + return new PackageStats[size]; + } + }; + + public String toString() { + return "PackageStats{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + + public PackageStats(String pkgName) { + packageName = pkgName; + } + + public PackageStats(Parcel source) { + packageName = source.readString(); + codeSize = source.readLong(); + dataSize = source.readLong(); + cacheSize = source.readLong(); + } + + public PackageStats(PackageStats pStats) { + packageName = pStats.packageName; + codeSize = pStats.codeSize; + dataSize = pStats.dataSize; + cacheSize = pStats.cacheSize; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags){ + dest.writeString(packageName); + dest.writeLong(codeSize); + dest.writeLong(dataSize); + dest.writeLong(cacheSize); + } +} diff --git a/core/java/android/content/pm/PermissionGroupInfo.aidl b/core/java/android/content/pm/PermissionGroupInfo.aidl new file mode 100755 index 0000000..9f215f1 --- /dev/null +++ b/core/java/android/content/pm/PermissionGroupInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.aidl +** +** Copyright 2008, 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 PermissionGroupInfo; diff --git a/core/java/android/content/pm/PermissionGroupInfo.java b/core/java/android/content/pm/PermissionGroupInfo.java new file mode 100644 index 0000000..02eb816 --- /dev/null +++ b/core/java/android/content/pm/PermissionGroupInfo.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2008 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.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information you can retrieve about a particular security permission + * group known to the system. This corresponds to information collected from the + * AndroidManifest.xml's <permission-group> tags. + */ +public class PermissionGroupInfo extends PackageItemInfo implements Parcelable { + /** + * A string resource identifier (in the package's resources) of this + * permission's description. From the "description" attribute or, + * if not set, 0. + */ + public int descriptionRes; + + /** + * The description string provided in the AndroidManifest file, if any. You + * probably don't want to use this, since it will be null if the description + * is in a resource. You probably want + * {@link PermissionInfo#loadDescription} instead. + */ + public CharSequence nonLocalizedDescription; + + public PermissionGroupInfo() { + } + + public PermissionGroupInfo(PermissionGroupInfo orig) { + super(orig); + descriptionRes = orig.descriptionRes; + nonLocalizedDescription = orig.nonLocalizedDescription; + } + + /** + * Retrieve the textual description of this permission. This + * will call back on the given PackageManager to load the description from + * the application. + * + * @param pm A PackageManager from which the label can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a CharSequence containing the permission's description. + * If there is no description, null is returned. + */ + public CharSequence loadDescription(PackageManager pm) { + if (nonLocalizedDescription != null) { + return nonLocalizedDescription; + } + if (descriptionRes != 0) { + CharSequence label = pm.getText(packageName, descriptionRes, null); + if (label != null) { + return label; + } + } + return null; + } + + public String toString() { + return "PermissionGroupInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeInt(descriptionRes); + TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); + } + + public static final Creator<PermissionGroupInfo> CREATOR = + new Creator<PermissionGroupInfo>() { + public PermissionGroupInfo createFromParcel(Parcel source) { + return new PermissionGroupInfo(source); + } + public PermissionGroupInfo[] newArray(int size) { + return new PermissionGroupInfo[size]; + } + }; + + private PermissionGroupInfo(Parcel source) { + super(source); + descriptionRes = source.readInt(); + nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + } +} diff --git a/core/java/android/content/pm/PermissionInfo.aidl b/core/java/android/content/pm/PermissionInfo.aidl new file mode 100755 index 0000000..5a7d4f4 --- /dev/null +++ b/core/java/android/content/pm/PermissionInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable PermissionInfo; diff --git a/core/java/android/content/pm/PermissionInfo.java b/core/java/android/content/pm/PermissionInfo.java new file mode 100644 index 0000000..3cc884b --- /dev/null +++ b/core/java/android/content/pm/PermissionInfo.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2008 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.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information you can retrieve about a particular security permission + * known to the system. This corresponds to information collected from the + * AndroidManifest.xml's <permission> tags. + */ +public class PermissionInfo extends PackageItemInfo implements Parcelable { + /** + * A normal application value for {@link #protectionLevel}, corresponding + * to the <code>normal</code> value of + * {@link android.R.attr#protectionLevel}. + */ + public static final int PROTECTION_NORMAL = 0; + + /** + * Dangerous value for {@link #protectionLevel}, corresponding + * to the <code>dangerous</code> value of + * {@link android.R.attr#protectionLevel}. + */ + public static final int PROTECTION_DANGEROUS = 1; + + /** + * System-level value for {@link #protectionLevel}, corresponding + * to the <code>signature</code> value of + * {@link android.R.attr#protectionLevel}. + */ + public static final int PROTECTION_SIGNATURE = 2; + + /** + * System-level value for {@link #protectionLevel}, corresponding + * to the <code>signatureOrSystem</code> value of + * {@link android.R.attr#protectionLevel}. + */ + public static final int PROTECTION_SIGNATURE_OR_SYSTEM = 3; + + /** + * The group this permission is a part of, as per + * {@link android.R.attr#permissionGroup}. + */ + public String group; + + /** + * A string resource identifier (in the package's resources) of this + * permission's description. From the "description" attribute or, + * if not set, 0. + */ + public int descriptionRes; + + /** + * The description string provided in the AndroidManifest file, if any. You + * probably don't want to use this, since it will be null if the description + * is in a resource. You probably want + * {@link PermissionInfo#loadDescription} instead. + */ + public CharSequence nonLocalizedDescription; + + /** + * The level of access this permission is protecting, as per + * {@link android.R.attr#protectionLevel}. Values may be + * {@link #PROTECTION_NORMAL}, {@link #PROTECTION_DANGEROUS}, or + * {@link #PROTECTION_SIGNATURE}. + */ + public int protectionLevel; + + public PermissionInfo() { + } + + public PermissionInfo(PermissionInfo orig) { + super(orig); + group = orig.group; + descriptionRes = orig.descriptionRes; + protectionLevel = orig.protectionLevel; + nonLocalizedDescription = orig.nonLocalizedDescription; + } + + /** + * Retrieve the textual description of this permission. This + * will call back on the given PackageManager to load the description from + * the application. + * + * @param pm A PackageManager from which the label can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a CharSequence containing the permission's description. + * If there is no description, null is returned. + */ + public CharSequence loadDescription(PackageManager pm) { + if (nonLocalizedDescription != null) { + return nonLocalizedDescription; + } + if (descriptionRes != 0) { + CharSequence label = pm.getText(packageName, descriptionRes, null); + if (label != null) { + return label; + } + } + return null; + } + + public String toString() { + return "PermissionInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(group); + dest.writeInt(descriptionRes); + dest.writeInt(protectionLevel); + TextUtils.writeToParcel(nonLocalizedDescription, dest, parcelableFlags); + } + + public static final Creator<PermissionInfo> CREATOR = + new Creator<PermissionInfo>() { + public PermissionInfo createFromParcel(Parcel source) { + return new PermissionInfo(source); + } + public PermissionInfo[] newArray(int size) { + return new PermissionInfo[size]; + } + }; + + private PermissionInfo(Parcel source) { + super(source); + group = source.readString(); + descriptionRes = source.readInt(); + protectionLevel = source.readInt(); + nonLocalizedDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + } +} diff --git a/core/java/android/content/pm/ProviderInfo.aidl b/core/java/android/content/pm/ProviderInfo.aidl new file mode 100755 index 0000000..18fbc8a --- /dev/null +++ b/core/java/android/content/pm/ProviderInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable ProviderInfo; diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java new file mode 100644 index 0000000..b67ddf6 --- /dev/null +++ b/core/java/android/content/pm/ProviderInfo.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2006 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.os.Parcel; +import android.os.Parcelable; +import android.os.PatternMatcher; + +/** + * Holds information about a specific + * {@link android.content.ContentProvider content provider}. This is returned by + * {@link android.content.pm.PackageManager#resolveContentProvider(java.lang.String, int) + * PackageManager.resolveContentProvider()}. + */ +public final class ProviderInfo extends ComponentInfo + implements Parcelable { + /** The name provider is published under content:// */ + public String authority = null; + + /** Optional permission required for read-only access this content + * provider. */ + public String readPermission = null; + + /** Optional permission required for read/write access this content + * provider. */ + public String writePermission = null; + + /** If true, additional permissions to specific Uris in this content + * provider can be granted, as per the + * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions + * grantUriPermissions} attribute. + */ + public boolean grantUriPermissions = false; + + /** + * If non-null, these are the patterns that are allowed for granting URI + * permissions. Any URI that does not match one of these patterns will not + * allowed to be granted. If null, all URIs are allowed. The + * {@link PackageManager#GET_URI_PERMISSION_PATTERNS + * PackageManager.GET_URI_PERMISSION_PATTERNS} flag must be specified for + * this field to be filled in. + */ + public PatternMatcher[] uriPermissionPatterns = null; + + /** If true, this content provider allows multiple instances of itself + * to run in different process. If false, a single instances is always + * run in {@link #processName}. */ + public boolean multiprocess = false; + + /** Used to control initialization order of single-process providers + * running in the same process. Higher goes first. */ + public int initOrder = 0; + + /** Whether or not this provider is syncable. */ + public boolean isSyncable = false; + + public ProviderInfo() { + } + + public ProviderInfo(ProviderInfo orig) { + super(orig); + authority = orig.authority; + readPermission = orig.readPermission; + writePermission = orig.writePermission; + grantUriPermissions = orig.grantUriPermissions; + uriPermissionPatterns = orig.uriPermissionPatterns; + multiprocess = orig.multiprocess; + initOrder = orig.initOrder; + isSyncable = orig.isSyncable; + } + + public int describeContents() { + return 0; + } + + @Override public void writeToParcel(Parcel out, int parcelableFlags) { + super.writeToParcel(out, parcelableFlags); + out.writeString(authority); + out.writeString(readPermission); + out.writeString(writePermission); + out.writeInt(grantUriPermissions ? 1 : 0); + out.writeTypedArray(uriPermissionPatterns, parcelableFlags); + out.writeInt(multiprocess ? 1 : 0); + out.writeInt(initOrder); + out.writeInt(isSyncable ? 1 : 0); + } + + public static final Parcelable.Creator<ProviderInfo> CREATOR + = new Parcelable.Creator<ProviderInfo>() { + public ProviderInfo createFromParcel(Parcel in) { + return new ProviderInfo(in); + } + + public ProviderInfo[] newArray(int size) { + return new ProviderInfo[size]; + } + }; + + public String toString() { + return "ContentProviderInfo{name=" + authority + " className=" + name + + " isSyncable=" + (isSyncable ? "true" : "false") + "}"; + } + + private ProviderInfo(Parcel in) { + super(in); + authority = in.readString(); + readPermission = in.readString(); + writePermission = in.readString(); + grantUriPermissions = in.readInt() != 0; + uriPermissionPatterns = in.createTypedArray(PatternMatcher.CREATOR); + multiprocess = in.readInt() != 0; + initOrder = in.readInt(); + isSyncable = in.readInt() != 0; + } +} diff --git a/core/java/android/content/pm/ResolveInfo.aidl b/core/java/android/content/pm/ResolveInfo.aidl new file mode 100755 index 0000000..b4e7f8b --- /dev/null +++ b/core/java/android/content/pm/ResolveInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable ResolveInfo; diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java new file mode 100644 index 0000000..ee49c02 --- /dev/null +++ b/core/java/android/content/pm/ResolveInfo.java @@ -0,0 +1,280 @@ +package android.content.pm; + +import android.content.IntentFilter; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Printer; + +import java.text.Collator; +import java.util.Comparator; + +/** + * Information that is returned from resolving an intent + * against an IntentFilter. This partially corresponds to + * information collected from the AndroidManifest.xml's + * <intent> tags. + */ +public class ResolveInfo implements Parcelable { + /** + * The activity that corresponds to this resolution match, if this + * resolution is for an activity. One and only one of this and + * serviceInfo must be non-null. + */ + public ActivityInfo activityInfo; + + /** + * The service that corresponds to this resolution match, if this + * resolution is for a service. One and only one of this and + * activityInfo must be non-null. + */ + public ServiceInfo serviceInfo; + + /** + * The IntentFilter that was matched for this ResolveInfo. + */ + public IntentFilter filter; + + /** + * The declared priority of this match. Comes from the "priority" + * attribute or, if not set, defaults to 0. Higher values are a higher + * priority. + */ + public int priority; + + /** + * Order of result according to the user's preference. If the user + * has not set a preference for this result, the value is 0; higher + * values are a higher priority. + */ + public int preferredOrder; + + /** + * The system's evaluation of how well the activity matches the + * IntentFilter. This is a match constant, a combination of + * {@link IntentFilter#MATCH_CATEGORY_MASK IntentFilter.MATCH_CATEGORY_MASK} + * and {@link IntentFilter#MATCH_ADJUSTMENT_MASK IntentFiler.MATCH_ADJUSTMENT_MASK}. + */ + public int match; + + /** + * Only set when returned by + * {@link PackageManager#queryIntentActivityOptions}, this tells you + * which of the given specific intents this result came from. 0 is the + * first in the list, < 0 means it came from the generic Intent query. + */ + public int specificIndex = -1; + + /** + * This filter has specified the Intent.CATEGORY_DEFAULT, meaning it + * would like to be considered a default action that the user can + * perform on this data. + */ + public boolean isDefault; + + /** + * A string resource identifier (in the package's resources) of this + * match's label. From the "label" attribute or, if not set, 0. + */ + public int labelRes; + + /** + * The actual string retrieve from <var>labelRes</var> or null if none + * was provided. + */ + public CharSequence nonLocalizedLabel; + + /** + * A drawable resource identifier (in the package's resources) of this + * match's icon. From the "icon" attribute or, if not set, 0. + */ + public int icon; + + /** + * Retrieve the current textual label associated with this resolution. This + * will call back on the given PackageManager to load the label from + * the application. + * + * @param pm A PackageManager from which the label can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a CharSequence containing the resolutions's label. If the + * item does not have a label, its name is returned. + */ + public CharSequence loadLabel(PackageManager pm) { + if (nonLocalizedLabel != null) { + return nonLocalizedLabel; + } + 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) { + return label; + } + } + return ci.loadLabel(pm); + } + + /** + * Retrieve the current graphical icon associated with this resolution. This + * will call back on the given PackageManager to load the icon from + * the application. + * + * @param pm A PackageManager from which the icon can be loaded; usually + * the PackageManager from which you originally retrieved this item. + * + * @return Returns a Drawable containing the resolution's icon. If the + * item does not have an icon, the default activity icon is returned. + */ + public Drawable loadIcon(PackageManager pm) { + ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; + ApplicationInfo ai = ci.applicationInfo; + Drawable dr; + if (icon != 0) { + dr = pm.getDrawable(ci.packageName, icon, ai); + if (dr != null) { + return dr; + } + } + return ci.loadIcon(pm); + } + + /** + * Return the icon resource identifier to use for this match. If the + * match defines an icon, that is used; else if the activity defines + * an icon, that is used; else, the application icon is used. + * + * @return The icon associated with this match. + */ + public final int getIconResource() { + if (icon != 0) return icon; + if (activityInfo != null) return activityInfo.getIconResource(); + if (serviceInfo != null) return serviceInfo.getIconResource(); + return 0; + } + + public void dump(Printer pw, String prefix) { + 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 (activityInfo != null) { + pw.println(prefix + "ActivityInfo:"); + activityInfo.dump(pw, prefix + " "); + } else if (serviceInfo != null) { + pw.println(prefix + "ServiceInfo:"); + // TODO + //serviceInfo.dump(pw, prefix + " "); + } + } + + public ResolveInfo() { + } + + public String toString() { + ComponentInfo ci = activityInfo != null ? activityInfo : serviceInfo; + return "ResolveInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + ci.name + " p=" + priority + " o=" + + preferredOrder + " m=0x" + Integer.toHexString(match) + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + if (activityInfo != null) { + dest.writeInt(1); + activityInfo.writeToParcel(dest, parcelableFlags); + } else if (serviceInfo != null) { + dest.writeInt(2); + serviceInfo.writeToParcel(dest, parcelableFlags); + } else { + dest.writeInt(0); + } + if (filter != null) { + dest.writeInt(1); + filter.writeToParcel(dest, parcelableFlags); + } else { + dest.writeInt(0); + } + dest.writeInt(priority); + dest.writeInt(preferredOrder); + dest.writeInt(match); + dest.writeInt(specificIndex); + dest.writeInt(labelRes); + TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags); + dest.writeInt(icon); + } + + public static final Creator<ResolveInfo> CREATOR + = new Creator<ResolveInfo>() { + public ResolveInfo createFromParcel(Parcel source) { + return new ResolveInfo(source); + } + public ResolveInfo[] newArray(int size) { + return new ResolveInfo[size]; + } + }; + + private ResolveInfo(Parcel source) { + switch (source.readInt()) { + case 1: + activityInfo = ActivityInfo.CREATOR.createFromParcel(source); + serviceInfo = null; + break; + case 2: + serviceInfo = ServiceInfo.CREATOR.createFromParcel(source); + activityInfo = null; + break; + default: + activityInfo = null; + serviceInfo = null; + break; + } + if (source.readInt() != 0) { + filter = IntentFilter.CREATOR.createFromParcel(source); + } + priority = source.readInt(); + preferredOrder = source.readInt(); + match = source.readInt(); + specificIndex = source.readInt(); + labelRes = source.readInt(); + nonLocalizedLabel + = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + icon = source.readInt(); + } + + public static class DisplayNameComparator + implements Comparator<ResolveInfo> { + public DisplayNameComparator(PackageManager pm) { + mPM = pm; + } + + public final int compare(ResolveInfo a, ResolveInfo b) { + CharSequence sa = a.loadLabel(mPM); + if (sa == null) sa = a.activityInfo.name; + CharSequence sb = b.loadLabel(mPM); + if (sb == null) sb = b.activityInfo.name; + + return sCollator.compare(sa.toString(), sb.toString()); + } + + private final Collator sCollator = Collator.getInstance(); + private PackageManager mPM; + } +} diff --git a/core/java/android/content/pm/ServiceInfo.aidl b/core/java/android/content/pm/ServiceInfo.aidl new file mode 100755 index 0000000..5ddae1a --- /dev/null +++ b/core/java/android/content/pm/ServiceInfo.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable ServiceInfo; diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java new file mode 100644 index 0000000..b60650c --- /dev/null +++ b/core/java/android/content/pm/ServiceInfo.java @@ -0,0 +1,56 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Information you can retrieve about a particular application + * service. This corresponds to information collected from the + * AndroidManifest.xml's <service> tags. + */ +public class ServiceInfo extends ComponentInfo + implements Parcelable { + /** + * Optional name of a permission required to be able to access this + * Service. From the "permission" attribute. + */ + public String permission; + + public ServiceInfo() { + } + + public ServiceInfo(ServiceInfo orig) { + super(orig); + permission = orig.permission; + } + + public String toString() { + return "ServiceInfo{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + name + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + super.writeToParcel(dest, parcelableFlags); + dest.writeString(permission); + } + + public static final Creator<ServiceInfo> CREATOR = + new Creator<ServiceInfo>() { + public ServiceInfo createFromParcel(Parcel source) { + return new ServiceInfo(source); + } + public ServiceInfo[] newArray(int size) { + return new ServiceInfo[size]; + } + }; + + private ServiceInfo(Parcel source) { + super(source); + permission = source.readString(); + } +} diff --git a/core/java/android/content/pm/Signature.aidl b/core/java/android/content/pm/Signature.aidl new file mode 100755 index 0000000..3a0d775 --- /dev/null +++ b/core/java/android/content/pm/Signature.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/view/WindowManager.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.pm; + +parcelable Signature; diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java new file mode 100644 index 0000000..1bb3857 --- /dev/null +++ b/core/java/android/content/pm/Signature.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2008 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.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; + +/** + * Opaque, immutable representation of a signature associated with an + * application package. + */ +public class Signature implements Parcelable { + private final byte[] mSignature; + private int mHashCode; + private boolean mHaveHashCode; + private String mString; + + /** + * Create Signature from an existing raw byte array. + */ + public Signature(byte[] signature) { + mSignature = signature.clone(); + } + + /** + * Create Signature from a text representation previously returned by + * {@link #toChars} or {@link #toCharsString()}. + */ + public Signature(String text) { + final int N = text.length()/2; + byte[] sig = new byte[N]; + for (int i=0; i<N; i++) { + char c = text.charAt(i*2); + byte b = (byte)( + (c >= 'a' ? (c - 'a' + 10) : (c - '0'))<<4); + c = text.charAt(i*2 + 1); + b |= (byte)(c >= 'a' ? (c - 'a' + 10) : (c - '0')); + sig[i] = b; + } + mSignature = sig; + } + + /** + * Encode the Signature as ASCII text. + */ + public char[] toChars() { + return toChars(null, null); + } + + /** + * Encode the Signature as ASCII text in to an existing array. + * + * @param existingArray Existing char array or null. + * @param outLen Output parameter for the number of characters written in + * to the array. + * @return Returns either <var>existingArray</var> if it was large enough + * to hold the ASCII representation, or a newly created char[] array if + * needed. + */ + public char[] toChars(char[] existingArray, int[] outLen) { + byte[] sig = mSignature; + final int N = sig.length; + final int N2 = N*2; + char[] text = existingArray == null || N2 > existingArray.length + ? new char[N2] : existingArray; + for (int j=0; j<N; j++) { + byte v = sig[j]; + int d = (v>>4)&0xf; + text[j*2] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d)); + d = v&0xf; + text[j*2+1] = (char)(d >= 10 ? ('a' + d - 10) : ('0' + d)); + } + if (outLen != null) outLen[0] = N; + return text; + } + + /** + * Return the result of {@link #toChars()} as a String. This result is + * cached so future calls will return the same String. + */ + public String toCharsString() { + if (mString != null) return mString; + String str = new String(toChars()); + mString = str; + return mString; + } + + /** + * @return the contents of this signature as a byte array. + */ + public byte[] toByteArray() { + byte[] bytes = new byte[mSignature.length]; + System.arraycopy(mSignature, 0, bytes, 0, mSignature.length); + return bytes; + } + + @Override + public boolean equals(Object obj) { + try { + if (obj != null) { + Signature other = (Signature)obj; + return Arrays.equals(mSignature, other.mSignature); + } + } catch (ClassCastException e) { + } + return false; + } + + @Override + public int hashCode() { + if (mHaveHashCode) { + return mHashCode; + } + mHashCode = Arrays.hashCode(mSignature); + mHaveHashCode = true; + return mHashCode; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeByteArray(mSignature); + } + + public static final Parcelable.Creator<Signature> CREATOR + = new Parcelable.Creator<Signature>() { + public Signature createFromParcel(Parcel source) { + return new Signature(source); + } + + public Signature[] newArray(int size) { + return new Signature[size]; + } + }; + + private Signature(Parcel source) { + mSignature = source.createByteArray(); + } +} diff --git a/core/java/android/content/pm/package.html b/core/java/android/content/pm/package.html new file mode 100644 index 0000000..766b7dd --- /dev/null +++ b/core/java/android/content/pm/package.html @@ -0,0 +1,7 @@ +<HTML> +<BODY> +Contains classes for accessing information about an +application package, including information about its activities, +permissions, services, signatures, and providers. +</BODY> +</HTML>
\ No newline at end of file diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java new file mode 100644 index 0000000..231e3e2 --- /dev/null +++ b/core/java/android/content/res/AssetFileDescriptor.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2006 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.res; + +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * File descriptor of an entry in the AssetManager. This provides your own + * opened FileDescriptor that can be used to read the data, as well as the + * offset and length of that entry's data in the file. + */ +public class AssetFileDescriptor implements Parcelable { + /** + * Length used with {@link #AssetFileDescriptor(ParcelFileDescriptor, long, long)} + * and {@link #getDeclaredLength} when a length has not been declared. This means + * the data extends to the end of the file. + */ + public static final long UNKNOWN_LENGTH = -1; + + private final ParcelFileDescriptor mFd; + private final long mStartOffset; + private final long mLength; + + /** + * Create a new AssetFileDescriptor from the given values. + * @param fd The underlying file descriptor. + * @param startOffset The location within the file that the asset starts. + * This must be 0 if length is UNKNOWN_LENGTH. + * @param length The number of bytes of the asset, or + * {@link #UNKNOWN_LENGTH if it extends to the end of the file. + */ + public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset, + long length) { + if (length < 0 && startOffset != 0) { + throw new IllegalArgumentException( + "startOffset must be 0 when using UNKNOWN_LENGTH"); + } + mFd = fd; + mStartOffset = startOffset; + mLength = length; + } + + /** + * The AssetFileDescriptor contains its own ParcelFileDescriptor, which + * in addition to the normal FileDescriptor object also allows you to close + * the descriptor when you are done with it. + */ + public ParcelFileDescriptor getParcelFileDescriptor() { + return mFd; + } + + /** + * Returns the FileDescriptor that can be used to read the data in the + * file. + */ + public FileDescriptor getFileDescriptor() { + return mFd.getFileDescriptor(); + } + + /** + * Returns the byte offset where this asset entry's data starts. + */ + public long getStartOffset() { + return mStartOffset; + } + + /** + * Returns the total number of bytes of this asset entry's data. May be + * {@link #UNKNOWN_LENGTH} if the asset extends to the end of the file. + * If the AssetFileDescriptor was constructed with {@link #UNKNOWN_LENGTH}, + * this will use {@link ParcelFileDescriptor#getStatSize() + * ParcelFileDescriptor.getStatSize()} to find the total size of the file, + * returning that number if found or {@link #UNKNOWN_LENGTH} if it could + * not be determined. + * + * @see #getDeclaredLength() + */ + public long getLength() { + if (mLength >= 0) { + return mLength; + } + long len = mFd.getStatSize(); + return len >= 0 ? len : UNKNOWN_LENGTH; + } + + /** + * Return the actual number of bytes that were declared when the + * AssetFileDescriptor was constructed. Will be + * {@link #UNKNOWN_LENGTH} if the length was not declared, meaning data + * should be read to the end of the file. + * + * @see #getDeclaredLength() + */ + public long getDeclaredLength() { + return mLength; + } + + /** + * Convenience for calling <code>getParcelFileDescriptor().close()</code>. + */ + public void close() throws IOException { + mFd.close(); + } + + /** + * Create and return a new auto-close input stream for this asset. This + * will either return a full asset {@link AutoCloseInputStream}, or + * an underlying {@link ParcelFileDescriptor.AutoCloseInputStream + * ParcelFileDescriptor.AutoCloseInputStream} depending on whether the + * the object represents a complete file or sub-section of a file. You + * should only call this once for a particular asset. + */ + public FileInputStream createInputStream() throws IOException { + if (mLength < 0) { + return new ParcelFileDescriptor.AutoCloseInputStream(mFd); + } + return new AutoCloseInputStream(this); + } + + /** + * Create and return a new auto-close output stream for this asset. This + * will either return a full asset {@link AutoCloseOutputStream}, or + * an underlying {@link ParcelFileDescriptor.AutoCloseOutputStream + * ParcelFileDescriptor.AutoCloseOutputStream} depending on whether the + * the object represents a complete file or sub-section of a file. You + * should only call this once for a particular asset. + */ + public FileOutputStream createOutputStream() throws IOException { + if (mLength < 0) { + return new ParcelFileDescriptor.AutoCloseOutputStream(mFd); + } + return new AutoCloseOutputStream(this); + } + + @Override + public String toString() { + return "{AssetFileDescriptor: " + mFd + + " start=" + mStartOffset + " len=" + mLength + "}"; + } + + /** + * An InputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescritor.close()} for you when the stream is closed. + */ + public static class AutoCloseInputStream + extends ParcelFileDescriptor.AutoCloseInputStream { + private long mRemaining; + + public AutoCloseInputStream(AssetFileDescriptor fd) throws IOException { + super(fd.getParcelFileDescriptor()); + super.skip(fd.getStartOffset()); + mRemaining = (int)fd.getLength(); + } + + @Override + public int available() throws IOException { + return mRemaining >= 0 + ? (mRemaining < 0x7fffffff ? (int)mRemaining : 0x7fffffff) + : super.available(); + } + + @Override + public int read() throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + int res = super.read(); + if (res >= 0) mRemaining--; + return res; + } + + return super.read(); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + if (count > mRemaining) count = (int)mRemaining; + int res = super.read(buffer, offset, count); + if (res >= 0) mRemaining -= res; + return res; + } + + return super.read(buffer, offset, count); + } + + @Override + public int read(byte[] buffer) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + int count = buffer.length; + if (count > mRemaining) count = (int)mRemaining; + int res = super.read(buffer, 0, count); + if (res >= 0) mRemaining -= res; + return res; + } + + return super.read(buffer); + } + + @Override + public long skip(long count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return -1; + if (count > mRemaining) count = mRemaining; + long res = super.skip(count); + if (res >= 0) mRemaining -= res; + return res; + } + + // TODO Auto-generated method stub + return super.skip(count); + } + + @Override + public void mark(int readlimit) { + if (mRemaining >= 0) { + // Not supported. + return; + } + super.mark(readlimit); + } + + @Override + public boolean markSupported() { + if (mRemaining >= 0) { + return false; + } + return super.markSupported(); + } + + @Override + public synchronized void reset() throws IOException { + if (mRemaining >= 0) { + // Not supported. + return; + } + super.reset(); + } + } + + /** + * An OutputStream you can create on a ParcelFileDescriptor, which will + * take care of calling {@link ParcelFileDescriptor#close + * ParcelFileDescritor.close()} for you when the stream is closed. + */ + public static class AutoCloseOutputStream + extends ParcelFileDescriptor.AutoCloseOutputStream { + private long mRemaining; + + public AutoCloseOutputStream(AssetFileDescriptor fd) throws IOException { + super(fd.getParcelFileDescriptor()); + if (fd.getParcelFileDescriptor().seekTo(fd.getStartOffset()) < 0) { + throw new IOException("Unable to seek"); + } + mRemaining = (int)fd.getLength(); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + if (count > mRemaining) count = (int)mRemaining; + super.write(buffer, offset, count); + mRemaining -= count; + return; + } + + super.write(buffer, offset, count); + } + + @Override + public void write(byte[] buffer) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + int count = buffer.length; + if (count > mRemaining) count = (int)mRemaining; + super.write(buffer); + mRemaining -= count; + return; + } + + super.write(buffer); + } + + @Override + public void write(int oneByte) throws IOException { + if (mRemaining >= 0) { + if (mRemaining == 0) return; + super.write(oneByte); + mRemaining--; + return; + } + + super.write(oneByte); + } + } + + + /* Parcelable interface */ + public int describeContents() { + return mFd.describeContents(); + } + + public void writeToParcel(Parcel out, int flags) { + mFd.writeToParcel(out, flags); + out.writeLong(mStartOffset); + out.writeLong(mLength); + } + + AssetFileDescriptor(Parcel src) { + mFd = ParcelFileDescriptor.CREATOR.createFromParcel(src); + mStartOffset = src.readLong(); + mLength = src.readLong(); + } + + public static final Parcelable.Creator<AssetFileDescriptor> CREATOR + = new Parcelable.Creator<AssetFileDescriptor>() { + public AssetFileDescriptor createFromParcel(Parcel in) { + return new AssetFileDescriptor(in); + } + public AssetFileDescriptor[] newArray(int size) { + return new AssetFileDescriptor[size]; + } + }; +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java new file mode 100644 index 0000000..fadcb35 --- /dev/null +++ b/core/java/android/content/res/AssetManager.java @@ -0,0 +1,697 @@ +/* + * Copyright (C) 2006 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.res; + +import android.os.ParcelFileDescriptor; +import android.util.Config; +import android.util.Log; +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} + * for the way most applications will want to retrieve their resource data. + * This class presents a lower-level API that allows you to open and read raw + * files that have been bundled with the application as a simple stream of + * bytes. + */ +public final class AssetManager { + /* modes used when opening an asset */ + + /** + * Mode for {@link #open(String, int)}: no specific information about how + * data will be accessed. + */ + public static final int ACCESS_UNKNOWN = 0; + /** + * Mode for {@link #open(String, int)}: Read chunks, and seek forward and + * backward. + */ + public static final int ACCESS_RANDOM = 1; + /** + * Mode for {@link #open(String, int)}: Read sequentially, with an + * occasional forward seek. + */ + public static final int ACCESS_STREAMING = 2; + /** + * Mode for {@link #open(String, int)}: Attempt to load contents into + * memory, for fast small reads. + */ + public static final int ACCESS_BUFFER = 3; + + private static final String TAG = "AssetManager"; + private static final boolean localLOGV = Config.LOGV || false; + + private static final Object mSync = new Object(); + private static final TypedValue mValue = new TypedValue(); + private static final long[] mOffsets = new long[2]; + private static AssetManager mSystem = null; + + // For communication with native code. + private int mObject; + + private StringBlock mStringBlocks[] = null; + + private int mNumRefs = 1; + private boolean mOpen = true; + private String mAssetDir; + private String mAppName; + + /** + * Create a new AssetManager containing only the basic system assets. + * Applications will not generally use this method, instead retrieving the + * appropriate asset manager with {@link Resources#getAssets}. Not for + * use by applications. + * {@hide} + */ + public AssetManager() { + synchronized (mSync) { + init(); + if (localLOGV) Log.v(TAG, "New asset manager: " + this); + ensureSystemAssets(); + } + } + + private static void ensureSystemAssets() { + synchronized (mSync) { + if (mSystem == null) { + AssetManager system = new AssetManager(true); + system.makeStringBlocks(false); + mSystem = system; + } + } + } + + private AssetManager(boolean isSystem) { + init(); + if (localLOGV) Log.v(TAG, "New asset manager: " + this); + } + + /** + * Return a global shared asset manager that provides access to only + * system assets (no application assets). + * {@hide} + */ + public static AssetManager getSystem() { + ensureSystemAssets(); + return mSystem; + } + + /** + * Close this asset manager. + */ + public void close() { + synchronized(mSync) { + //System.out.println("Release: num=" + mNumRefs + // + ", released=" + mReleased); + if (mOpen) { + mOpen = false; + decRefsLocked(); + } + } + } + + /** + * Retrieve the string value associated with a particular resource + * identifier for the current configuration / skin. + */ + /*package*/ final CharSequence getResourceText(int ident) { + synchronized (mSync) { + TypedValue tmpValue = mValue; + int block = loadResourceValue(ident, tmpValue, true); + if (block >= 0) { + if (tmpValue.type == TypedValue.TYPE_STRING) { + return mStringBlocks[block].get(tmpValue.data); + } + return tmpValue.coerceToString(); + } + } + return null; + } + + /** + * Retrieve the string value associated with a particular resource + * identifier for the current configuration / skin. + */ + /*package*/ final CharSequence getResourceBagText(int ident, int bagEntryId) { + synchronized (mSync) { + TypedValue tmpValue = mValue; + int block = loadResourceBagValue(ident, bagEntryId, tmpValue, true); + if (block >= 0) { + if (tmpValue.type == TypedValue.TYPE_STRING) { + return mStringBlocks[block].get(tmpValue.data); + } + return tmpValue.coerceToString(); + } + } + return null; + } + + /** + * Retrieve the string array associated with a particular resource + * identifier. + * @param id Resource id of the string array + */ + /*package*/ final String[] getResourceStringArray(final int id) { + String[] retArray = getArrayStringResource(id); + return retArray; + } + + + /*package*/ final boolean getResourceValue(int ident, + TypedValue outValue, + boolean resolveRefs) + { + int block = loadResourceValue(ident, outValue, resolveRefs); + if (block >= 0) { + if (outValue.type != TypedValue.TYPE_STRING) { + return true; + } + outValue.string = mStringBlocks[block].get(outValue.data); + return true; + } + return false; + } + + /** + * Retrieve the text array associated with a particular resource + * identifier. + * @param id Resource id of the string array + */ + /*package*/ final CharSequence[] getResourceTextArray(final int id) { + int[] rawInfoArray = getArrayStringInfo(id); + int rawInfoArrayLen = rawInfoArray.length; + final int infoArrayLen = rawInfoArrayLen / 2; + int block; + int index; + CharSequence[] retArray = new CharSequence[infoArrayLen]; + for (int i = 0, j = 0; i < rawInfoArrayLen; i = i + 2, j++) { + block = rawInfoArray[i]; + index = rawInfoArray[i + 1]; + retArray[j] = index >= 0 ? mStringBlocks[block].get(index) : null; + } + return retArray; + } + + /*package*/ final boolean getThemeValue(int theme, int ident, + TypedValue outValue, boolean resolveRefs) { + int block = loadThemeAttributeValue(theme, ident, outValue, resolveRefs); + if (block >= 0) { + if (outValue.type != TypedValue.TYPE_STRING) { + return true; + } + StringBlock[] blocks = mStringBlocks; + if (blocks == null) { + ensureStringBlocks(); + } + outValue.string = blocks[block].get(outValue.data); + return true; + } + return false; + } + + /*package*/ final void ensureStringBlocks() { + if (mStringBlocks == null) { + synchronized (mSync) { + if (mStringBlocks == null) { + makeStringBlocks(true); + } + } + } + } + + private final void makeStringBlocks(boolean copyFromSystem) { + final int sysNum = copyFromSystem ? mSystem.mStringBlocks.length : 0; + final int num = getStringBlockCount(); + mStringBlocks = new StringBlock[num]; + if (localLOGV) Log.v(TAG, "Making string blocks for " + this + + ": " + num); + for (int i=0; i<num; i++) { + if (i < sysNum) { + mStringBlocks[i] = mSystem.mStringBlocks[i]; + } else { + mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true); + } + } + } + + /*package*/ final CharSequence getPooledString(int block, int id) { + //System.out.println("Get pooled: block=" + block + // + ", id=#" + Integer.toHexString(id) + // + ", blocks=" + mStringBlocks); + return mStringBlocks[block-1].get(id); + } + + /** + * Open an asset using ACCESS_STREAMING mode. This provides access to + * files that have been bundled with an application as assets -- that is, + * files placed in to the "assets" directory. + * + * @param fileName The name of the asset to open. This name can be + * hierarchical. + * + * @see #open(String, int) + * @see #list + */ + public final InputStream open(String fileName) throws IOException { + return open(fileName, ACCESS_STREAMING); + } + + /** + * Open an asset using an explicit access mode, returning an InputStream to + * read its contents. This provides access to files that have been bundled + * with an application as assets -- that is, files placed in to the + * "assets" directory. + * + * @param fileName The name of the asset to open. This name can be + * hierarchical. + * @param accessMode Desired access mode for retrieving the data. + * + * @see #ACCESS_UNKNOWN + * @see #ACCESS_STREAMING + * @see #ACCESS_RANDOM + * @see #ACCESS_BUFFER + * @see #open(String) + * @see #list + */ + public final InputStream open(String fileName, int accessMode) + throws IOException { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + int asset = openAsset(fileName, accessMode); + if (asset != 0) { + mNumRefs++; + return new AssetInputStream(asset); + } + } + throw new FileNotFoundException("Asset file: " + fileName); + } + + public final AssetFileDescriptor openFd(String fileName) + throws IOException { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + ParcelFileDescriptor pfd = openAssetFd(fileName, mOffsets); + if (pfd != null) { + return new AssetFileDescriptor(pfd, mOffsets[0], mOffsets[1]); + } + } + throw new FileNotFoundException("Asset file: " + fileName); + } + + /** + * Return a String array of all the assets at the given path. + * + * @param path A relative path within the assets, i.e., "docs/home.html". + * + * @return String[] Array of strings, one for each asset. These file + * names are relative to 'path'. You can open the file by + * concatenating 'path' and a name in the returned string (via + * File) and passing that to open(). + * + * @see #open + */ + public native final String[] list(String path) + throws IOException; + + /** + * {@hide} + * Open a non-asset file as an asset using ACCESS_STREAMING mode. This + * provides direct access to all of the files included in an application + * package (not only its assets). Applications should not normally use + * this. + * + * @see #open(String) + */ + public final InputStream openNonAsset(String fileName) throws IOException { + return openNonAsset(0, fileName, ACCESS_STREAMING); + } + + /** + * {@hide} + * Open a non-asset file as an asset using a specific access mode. This + * provides direct access to all of the files included in an application + * package (not only its assets). Applications should not normally use + * this. + * + * @see #open(String, int) + */ + public final InputStream openNonAsset(String fileName, int accessMode) + throws IOException { + return openNonAsset(0, fileName, accessMode); + } + + /** + * {@hide} + * Open a non-asset in a specified package. Not for use by applications. + * + * @param cookie Identifier of the package to be opened. + * @param fileName Name of the asset to retrieve. + */ + public final InputStream openNonAsset(int cookie, String fileName) + throws IOException { + return openNonAsset(cookie, fileName, ACCESS_STREAMING); + } + + /** + * {@hide} + * Open a non-asset in a specified package. Not for use by applications. + * + * @param cookie Identifier of the package to be opened. + * @param fileName Name of the asset to retrieve. + * @param accessMode Desired access mode for retrieving the data. + */ + public final InputStream openNonAsset(int cookie, String fileName, int accessMode) + throws IOException { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + int asset = openNonAssetNative(cookie, fileName, accessMode); + if (asset != 0) { + mNumRefs++; + return new AssetInputStream(asset); + } + } + throw new FileNotFoundException("Asset absolute file: " + fileName); + } + + public final AssetFileDescriptor openNonAssetFd(String fileName) + throws IOException { + return openNonAssetFd(0, fileName); + } + + public final AssetFileDescriptor openNonAssetFd(int cookie, + String fileName) throws IOException { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + ParcelFileDescriptor pfd = openNonAssetFdNative(cookie, + fileName, mOffsets); + if (pfd != null) { + return new AssetFileDescriptor(pfd, mOffsets[0], mOffsets[1]); + } + } + throw new FileNotFoundException("Asset absolute file: " + fileName); + } + + /** + * Retrieve a parser for a compiled XML file. + * + * @param fileName The name of the file to retrieve. + */ + public final XmlResourceParser openXmlResourceParser(String fileName) + throws IOException { + return openXmlResourceParser(0, fileName); + } + + /** + * Retrieve a parser for a compiled XML file. + * + * @param cookie Identifier of the package to be opened. + * @param fileName The name of the file to retrieve. + */ + public final XmlResourceParser openXmlResourceParser(int cookie, + String fileName) throws IOException { + XmlBlock block = openXmlBlockAsset(cookie, fileName); + XmlResourceParser rp = block.newParser(); + block.close(); + return rp; + } + + /** + * {@hide} + * Retrieve a non-asset as a compiled XML file. Not for use by + * applications. + * + * @param fileName The name of the file to retrieve. + */ + /*package*/ final XmlBlock openXmlBlockAsset(String fileName) + throws IOException { + return openXmlBlockAsset(0, fileName); + } + + /** + * {@hide} + * Retrieve a non-asset as a compiled XML file. Not for use by + * applications. + * + * @param cookie Identifier of the package to be opened. + * @param fileName Name of the asset to retrieve. + */ + /*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName) + throws IOException { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + int xmlBlock = openXmlAssetNative(cookie, fileName); + if (xmlBlock != 0) { + mNumRefs++; + return new XmlBlock(this, xmlBlock); + } + } + throw new FileNotFoundException("Asset XML file: " + fileName); + } + + /*package*/ void xmlBlockGone() { + synchronized (mSync) { + decRefsLocked(); + } + } + + /*package*/ final int createTheme() { + synchronized (mSync) { + if (!mOpen) { + throw new RuntimeException("Assetmanager has been closed"); + } + mNumRefs++; + return newTheme(); + } + } + + /*package*/ final void releaseTheme(int theme) { + synchronized (mSync) { + deleteTheme(theme); + decRefsLocked(); + } + } + + protected void finalize() throws Throwable { + destroy(); + } + + public final class AssetInputStream extends InputStream { + public final int getAssetInt() { + return mAsset; + } + private AssetInputStream(int asset) + { + mAsset = asset; + mLength = getAssetLength(asset); + } + public final int read() throws IOException { + return readAssetChar(mAsset); + } + public final boolean markSupported() { + return true; + } + public final int available() throws IOException { + long len = getAssetRemainingLength(mAsset); + return len > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)len; + } + public final void close() throws IOException { + synchronized (AssetManager.mSync) { + if (mAsset != 0) { + destroyAsset(mAsset); + mAsset = 0; + decRefsLocked(); + } + } + } + public final void mark(int readlimit) { + mMarkPos = seekAsset(mAsset, 0, 0); + } + public final void reset() throws IOException { + seekAsset(mAsset, mMarkPos, -1); + } + public final int read(byte[] b) throws IOException { + return readAsset(mAsset, b, 0, b.length); + } + public final int read(byte[] b, int off, int len) throws IOException { + return readAsset(mAsset, b, off, len); + } + public final long skip(long n) throws IOException { + long pos = seekAsset(mAsset, 0, 0); + if ((pos+n) > mLength) { + n = mLength-pos; + } + if (n > 0) { + seekAsset(mAsset, n, 0); + } + return n; + } + + protected void finalize() throws Throwable + { + close(); + } + + private int mAsset; + private long mLength; + private long mMarkPos; + } + + /** + * Add an additional set of assets to the asset manager. This can be + * either a directory or ZIP file. Not for use by applications. A + * zero return value indicates failure. + * {@hide} + */ + public native final int addAssetPath(String path); + + /** + * Determine whether the state in this asset manager is up-to-date with + * the files on the filesystem. If false is returned, you need to + * instantiate a new AssetManager class to see the new data. + * {@hide} + */ + public native final boolean isUpToDate(); + + /** + * Change the locale being used by this asset manager. Not for use by + * applications. + * {@hide} + */ + public native final void setLocale(String locale); + + /** + * Get the locales that this asset manager contains data for. + */ + public native final String[] getLocales(); + + /** + * Change the configuation used when retrieving resources. Not for use by + * applications. + * {@hide} + */ + public native final void setConfiguration(int mcc, int mnc, String locale, + int orientation, int touchscreen, int density, int keyboard, + int keyboardHidden, int navigation, int screenWidth, int screenHeight, + int majorVersion); + + /** + * Retrieve the resource identifier for the given resource name. + */ + /*package*/ native final int getResourceIdentifier(String type, + String name, + String defPackage); + + /*package*/ native final String getResourceName(int resid); + /*package*/ native final String getResourcePackageName(int resid); + /*package*/ native final String getResourceTypeName(int resid); + /*package*/ native final String getResourceEntryName(int resid); + + private native final int openAsset(String fileName, int accessMode); + private final native ParcelFileDescriptor openAssetFd(String fileName, + long[] outOffsets) throws IOException; + private native final int openNonAssetNative(int cookie, String fileName, + int accessMode); + private native ParcelFileDescriptor openNonAssetFdNative(int cookie, + String fileName, long[] outOffsets) throws IOException; + private native final void destroyAsset(int asset); + private native final int readAssetChar(int asset); + private native final int readAsset(int asset, byte[] b, int off, int len); + private native final long seekAsset(int asset, long offset, int whence); + private native final long getAssetLength(int asset); + private native final long getAssetRemainingLength(int asset); + + /** Returns true if the resource was found, filling in mRetStringBlock and + * mRetData. */ + private native final int loadResourceValue(int ident, TypedValue outValue, + boolean resolve); + /** Returns true if the resource was found, filling in mRetStringBlock and + * mRetData. */ + private native final int loadResourceBagValue(int ident, int bagEntryId, TypedValue outValue, + boolean resolve); + /*package*/ static final int STYLE_NUM_ENTRIES = 5; + /*package*/ static final int STYLE_TYPE = 0; + /*package*/ static final int STYLE_DATA = 1; + /*package*/ static final int STYLE_ASSET_COOKIE = 2; + /*package*/ static final int STYLE_RESOURCE_ID = 3; + /*package*/ static final int STYLE_CHANGING_CONFIGURATIONS = 4; + /*package*/ native static final boolean applyStyle(int theme, + int defStyleAttr, int defStyleRes, int xmlParser, + int[] inAttrs, int[] outValues, int[] outIndices); + /*package*/ native final boolean retrieveAttributes( + int xmlParser, int[] inAttrs, int[] outValues, int[] outIndices); + /*package*/ native final int getArraySize(int resource); + /*package*/ native final int retrieveArray(int resource, int[] outValues); + private native final int getStringBlockCount(); + private native final int getNativeStringBlock(int block); + + /** + * {@hide} + */ + public native final String getCookieName(int cookie); + + /** + * {@hide} + */ + public native static final int getGlobalAssetCount(); + + /** + * {@hide} + */ + public native static final int getGlobalAssetManagerCount(); + + private native final int newTheme(); + private native final void deleteTheme(int theme); + /*package*/ native static final void applyThemeStyle(int theme, int styleRes, boolean force); + /*package*/ native static final void copyTheme(int dest, int source); + /*package*/ native static final int loadThemeAttributeValue(int theme, int ident, + TypedValue outValue, + boolean resolve); + /*package*/ native static final void dumpTheme(int theme, int priority, String tag, String prefix); + + private native final int openXmlAssetNative(int cookie, String fileName); + + private native final String[] getArrayStringResource(int arrayRes); + private native final int[] getArrayStringInfo(int arrayRes); + /*package*/ native final int[] getArrayIntResource(int arrayRes); + + private native final void init(); + private native final void destroy(); + + private final void decRefsLocked() { + mNumRefs--; + //System.out.println("Dec streams: mNumRefs=" + mNumRefs + // + " mReleased=" + mReleased); + if (mNumRefs == 0) { + destroy(); + } + } +} diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java new file mode 100644 index 0000000..0f3f270 --- /dev/null +++ b/core/java/android/content/res/ColorStateList.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 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.res; + +import com.android.internal.util.ArrayUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.StateSet; +import android.util.Xml; +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Arrays; + +/** + * + * Lets you map {@link android.view.View} state sets to colors. + * + * {@link android.content.res.ColorStateList}s are created from XML resource files defined in the + * "color" subdirectory directory of an application's resource directory. The XML file contains + * a single "selector" element with a number of "item" elements inside. For example: + * + * <pre> + * <selector xmlns:android="http://schemas.android.com/apk/res/android"> + * <item android:state_focused="true" android:color="@color/testcolor1"/> + * <item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /> + * <item android:state_enabled="false" android:colore="@color/testcolor3" /> + * <item android:state_active="true" android:color="@color/testcolor4" /> + * <item android:color="@color/testcolor5"/> + * </selector> + * </pre> + * + * This defines a set of state spec / color pairs where each state spec specifies a set of + * states that a view must either be in or not be in and the color specifies the color associated + * with that spec. The list of state specs will be processed in order of the items in the XML file. + * An item with no state spec is considered to match any set of states and is generally useful as + * a final item to be used as a default. Note that if you have such an item before any other items + * in the list then any subsequent items will end up being ignored. + */ +public class ColorStateList implements Parcelable { + + private int[][] mStateSpecs; // must be parallel to mColors + private int[] mColors; // must be parallel to mStateSpecs + private int mDefaultColor = 0xffff0000; + + private static final int[][] EMPTY = new int[][] { new int[0] }; + private static final SparseArray<WeakReference<ColorStateList>> sCache = + new SparseArray<WeakReference<ColorStateList>>(); + + private ColorStateList() { } + + /** + * Creates a ColorStateList that returns the specified mapping from + * states to colors. + */ + public ColorStateList(int[][] states, int[] colors) { + mStateSpecs = states; + mColors = colors; + + if (states.length > 0) { + mDefaultColor = colors[0]; + + for (int i = 0; i < states.length; i++) { + if (states[i].length == 0) { + mDefaultColor = colors[i]; + } + } + } + } + + /** + * Creates or retrieves a ColorStateList that always returns a single color. + */ + public static ColorStateList valueOf(int color) { + // TODO: should we collect these eventually? + synchronized (sCache) { + WeakReference<ColorStateList> ref = sCache.get(color); + ColorStateList csl = ref != null ? ref.get() : null; + + if (csl != null) { + return csl; + } + + csl = new ColorStateList(EMPTY, new int[] { color }); + sCache.put(color, new WeakReference<ColorStateList>(csl)); + return csl; + } + } + + /** + * Create a ColorStateList from an XML document, given a set of {@link Resources}. + */ + public static ColorStateList createFromXml(Resources r, XmlPullParser parser) + throws XmlPullParserException, IOException { + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + } + + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + return createFromXmlInner(r, parser, attrs); + } + + /* Create from inside an XML document. Called on a parser positioned at + * a tag in an XML document, tries to create a ColorStateList from that tag. + * Returns null if the tag is not a valid ColorStateList. + */ + private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, + AttributeSet attrs) throws XmlPullParserException, IOException { + + ColorStateList colorStateList; + + final String name = parser.getName(); + + if (name.equals("selector")) { + colorStateList = new ColorStateList(); + } else { + throw new XmlPullParserException( + parser.getPositionDescription() + ": invalid drawable tag " + name); + } + + colorStateList.inflate(r, parser, attrs); + return colorStateList; + } + + /** + * Creates a new ColorStateList that has the same states and + * colors as this one but where each color has the specified alpha value + * (0-255). + */ + public ColorStateList withAlpha(int alpha) { + int[] colors = new int[mColors.length]; + + int len = colors.length; + for (int i = 0; i < len; i++) { + colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24); + } + + return new ColorStateList(mStateSpecs, colors); + } + + /** + * Fill in this object based on the contents of an XML "selector" element. + */ + private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + + int type; + + final int innerDepth = parser.getDepth()+1; + int depth; + + int listAllocated = 20; + int listSize = 0; + int[] colorList = new int[listAllocated]; + int[][] stateSpecList = new int[listAllocated][]; + + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && ((depth=parser.getDepth()) >= innerDepth + || type != XmlPullParser.END_TAG)) { + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (depth > innerDepth || !parser.getName().equals("item")) { + continue; + } + + int colorRes = 0; + int color = 0xffff0000; + boolean haveColor = false; + + int i; + int j = 0; + final int numAttrs = attrs.getAttributeCount(); + int[] stateSpec = new int[numAttrs]; + for (i = 0; i < numAttrs; i++) { + final int stateResId = attrs.getAttributeNameResource(i); + if (stateResId == 0) break; + if (stateResId == com.android.internal.R.attr.color) { + colorRes = attrs.getAttributeResourceValue(i, 0); + + if (colorRes == 0) { + color = attrs.getAttributeIntValue(i, color); + haveColor = true; + } + } else { + stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) + ? stateResId + : -stateResId; + } + } + stateSpec = StateSet.trimStateSet(stateSpec, j); + + if (colorRes != 0) { + color = r.getColor(colorRes); + } else if (!haveColor) { + throw new XmlPullParserException( + parser.getPositionDescription() + + ": <item> tag requires a 'android:color' attribute."); + } + + if (listSize == 0 || stateSpec.length == 0) { + mDefaultColor = color; + } + + if (listSize + 1 >= listAllocated) { + listAllocated = ArrayUtils.idealIntArraySize(listSize + 1); + + int[] ncolor = new int[listAllocated]; + System.arraycopy(colorList, 0, ncolor, 0, listSize); + + int[][] nstate = new int[listAllocated][]; + System.arraycopy(stateSpecList, 0, nstate, 0, listSize); + + colorList = ncolor; + stateSpecList = nstate; + } + + colorList[listSize] = color; + stateSpecList[listSize] = stateSpec; + listSize++; + } + + mColors = new int[listSize]; + mStateSpecs = new int[listSize][]; + System.arraycopy(colorList, 0, mColors, 0, listSize); + System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); + } + + public boolean isStateful() { + return mStateSpecs.length > 1; + } + + /** + * Return the color associated with the given set of {@link android.view.View} states. + * + * @param stateSet an array of {@link android.view.View} states + * @param defaultColor the color to return if there's not state spec in this + * {@link ColorStateList} that matches the stateSet. + * + * @return the color associated with that set of states in this {@link ColorStateList}. + */ + public int getColorForState(int[] stateSet, int defaultColor) { + final int setLength = mStateSpecs.length; + for (int i = 0; i < setLength; i++) { + int[] stateSpec = mStateSpecs[i]; + if (StateSet.stateSetMatches(stateSpec, stateSet)) { + return mColors[i]; + } + } + return defaultColor; + } + + /** + * Return the default color in this {@link ColorStateList}. + * + * @return the default color in this {@link ColorStateList}. + */ + public int getDefaultColor() { + return mDefaultColor; + } + + public String toString() { + return "ColorStateList{" + + "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + + "mColors=" + Arrays.toString(mColors) + + "mDefaultColor=" + mDefaultColor + '}'; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + final int N = mStateSpecs.length; + dest.writeInt(N); + for (int i=0; i<N; i++) { + dest.writeIntArray(mStateSpecs[i]); + } + dest.writeArray(mStateSpecs); + dest.writeIntArray(mColors); + } + + public static final Parcelable.Creator<ColorStateList> CREATOR = + new Parcelable.Creator<ColorStateList>() { + public ColorStateList[] newArray(int size) { + return new ColorStateList[size]; + } + + public ColorStateList createFromParcel(Parcel source) { + final int N = source.readInt(); + int[][] stateSpecs = new int[N][]; + for (int i=0; i<N; i++) { + stateSpecs[i] = source.createIntArray(); + } + int[] colors = source.createIntArray(); + return new ColorStateList(stateSpecs, colors); + } + }; +} diff --git a/core/java/android/content/res/Configuration.aidl b/core/java/android/content/res/Configuration.aidl new file mode 100755 index 0000000..bb7f2dd --- /dev/null +++ b/core/java/android/content/res/Configuration.aidl @@ -0,0 +1,21 @@ +/* //device/java/android/android/view/WindowManager.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.res; + +parcelable Configuration; + diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java new file mode 100644 index 0000000..7e4b7ac --- /dev/null +++ b/core/java/android/content/res/Configuration.java @@ -0,0 +1,436 @@ +package android.content.res; + +import android.content.pm.ActivityInfo; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Locale; + +/** + * This class describes all device configuration information that can + * impact the resources the application retrieves. This includes both + * user-specified configuration options (locale and scaling) as well + * as dynamic device configuration (various types of input devices). + */ +public final class Configuration implements Parcelable, Comparable<Configuration> { + /** + * Current user preference for the scaling factor for fonts, relative + * to the base density scaling. + */ + public float fontScale; + + /** + * IMSI MCC (Mobile Country Code). 0 if undefined. + */ + public int mcc; + + /** + * IMSI MNC (Mobile Network Code). 0 if undefined. + */ + public int mnc; + + /** + * Current user preference for the locale. + */ + public Locale locale; + + /** + * Locale should persist on setting + * @hide pending API council approval + */ + public boolean userSetLocale; + + public static final int TOUCHSCREEN_UNDEFINED = 0; + public static final int TOUCHSCREEN_NOTOUCH = 1; + public static final int TOUCHSCREEN_STYLUS = 2; + public static final int TOUCHSCREEN_FINGER = 3; + + /** + * The kind of touch screen attached to the device. + * One of: {@link #TOUCHSCREEN_NOTOUCH}, {@link #TOUCHSCREEN_STYLUS}, + * {@link #TOUCHSCREEN_FINGER}. + */ + public int touchscreen; + + public static final int KEYBOARD_UNDEFINED = 0; + public static final int KEYBOARD_NOKEYS = 1; + public static final int KEYBOARD_QWERTY = 2; + public static final int KEYBOARD_12KEY = 3; + + /** + * The kind of keyboard attached to the device. + * One of: {@link #KEYBOARD_QWERTY}, {@link #KEYBOARD_12KEY}. + */ + public int keyboard; + + public static final int KEYBOARDHIDDEN_UNDEFINED = 0; + public static final int KEYBOARDHIDDEN_NO = 1; + public static final int KEYBOARDHIDDEN_YES = 2; + /** Constant matching actual resource implementation. {@hide} */ + public static final int KEYBOARDHIDDEN_SOFT = 3; + + /** + * A flag indicating whether any keyboard is available. Unlike + * {@link #hardKeyboardHidden}, this also takes into account a soft + * keyboard, so if the hard keyboard is hidden but there is soft + * keyboard available, it will be set to NO. Value is one of: + * {@link #KEYBOARDHIDDEN_NO}, {@link #KEYBOARDHIDDEN_YES}. + */ + public int keyboardHidden; + + public static final int HARDKEYBOARDHIDDEN_UNDEFINED = 0; + public static final int HARDKEYBOARDHIDDEN_NO = 1; + public static final int HARDKEYBOARDHIDDEN_YES = 2; + + /** + * A flag indicating whether the hard keyboard has been hidden. This will + * be set on a device with a mechanism to hide the keyboard from the + * user, when that mechanism is closed. One of: + * {@link #HARDKEYBOARDHIDDEN_NO}, {@link #HARDKEYBOARDHIDDEN_YES}. + */ + public int hardKeyboardHidden; + + public static final int NAVIGATION_UNDEFINED = 0; + public static final int NAVIGATION_NONAV = 1; + public static final int NAVIGATION_DPAD = 2; + public static final int NAVIGATION_TRACKBALL = 3; + public static final int NAVIGATION_WHEEL = 4; + + /** + * The kind of navigation method available on the device. + * One of: {@link #NAVIGATION_DPAD}, {@link #NAVIGATION_TRACKBALL}, + * {@link #NAVIGATION_WHEEL}. + */ + public int navigation; + + public static final int ORIENTATION_UNDEFINED = 0; + public static final int ORIENTATION_PORTRAIT = 1; + public static final int ORIENTATION_LANDSCAPE = 2; + public static final int ORIENTATION_SQUARE = 3; + + /** + * Overall orientation of the screen. May be one of + * {@link #ORIENTATION_LANDSCAPE}, {@link #ORIENTATION_PORTRAIT}, + * or {@link #ORIENTATION_SQUARE}. + */ + public int orientation; + + /** + * Construct an invalid Configuration. You must call {@link #setToDefaults} + * for this object to be valid. {@more} + */ + public Configuration() { + setToDefaults(); + } + + /** + * Makes a deep copy suitable for modification. + */ + public Configuration(Configuration o) { + fontScale = o.fontScale; + mcc = o.mcc; + mnc = o.mnc; + if (o.locale != null) { + locale = (Locale) o.locale.clone(); + } + userSetLocale = o.userSetLocale; + touchscreen = o.touchscreen; + keyboard = o.keyboard; + keyboardHidden = o.keyboardHidden; + hardKeyboardHidden = o.hardKeyboardHidden; + navigation = o.navigation; + orientation = o.orientation; + } + + public String toString() { + return "{ scale=" + fontScale + " imsi=" + mcc + "/" + mnc + + " locale=" + locale + + " touch=" + touchscreen + " key=" + keyboard + "/" + + keyboardHidden + "/" + hardKeyboardHidden + + " nav=" + navigation + " orien=" + orientation + " }"; + } + + /** + * Set this object to the system defaults. + */ + public void setToDefaults() { + fontScale = 1; + mcc = mnc = 0; + locale = Locale.getDefault(); + userSetLocale = false; + touchscreen = TOUCHSCREEN_UNDEFINED; + keyboard = KEYBOARD_UNDEFINED; + keyboardHidden = KEYBOARDHIDDEN_UNDEFINED; + hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED; + navigation = NAVIGATION_UNDEFINED; + orientation = ORIENTATION_UNDEFINED; + } + + /** {@hide} */ + @Deprecated public void makeDefault() { + setToDefaults(); + } + + /** + * Copy the fields from delta into this Configuration object, keeping + * track of which ones have changed. Any undefined fields in + * <var>delta</var> are ignored and not copied in to the current + * Configuration. + * @return Returns a bit mask of the changed fields, as per + * {@link #diff}. + */ + public int updateFrom(Configuration delta) { + int changed = 0; + if (delta.fontScale > 0 && fontScale != delta.fontScale) { + changed |= ActivityInfo.CONFIG_FONT_SCALE; + fontScale = delta.fontScale; + } + if (delta.mcc != 0 && mcc != delta.mcc) { + changed |= ActivityInfo.CONFIG_MCC; + mcc = delta.mcc; + } + if (delta.mnc != 0 && mnc != delta.mnc) { + changed |= ActivityInfo.CONFIG_MNC; + mnc = delta.mnc; + } + if (delta.locale != null + && (locale == null || !locale.equals(delta.locale))) { + changed |= ActivityInfo.CONFIG_LOCALE; + locale = delta.locale != null + ? (Locale) delta.locale.clone() : null; + } + if (delta.userSetLocale && (!userSetLocale || ((changed & ActivityInfo.CONFIG_LOCALE) != 0))) + { + userSetLocale = true; + changed |= ActivityInfo.CONFIG_LOCALE; + } + if (delta.touchscreen != TOUCHSCREEN_UNDEFINED + && touchscreen != delta.touchscreen) { + changed |= ActivityInfo.CONFIG_TOUCHSCREEN; + touchscreen = delta.touchscreen; + } + if (delta.keyboard != KEYBOARD_UNDEFINED + && keyboard != delta.keyboard) { + changed |= ActivityInfo.CONFIG_KEYBOARD; + keyboard = delta.keyboard; + } + if (delta.keyboardHidden != KEYBOARDHIDDEN_UNDEFINED + && keyboardHidden != delta.keyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + keyboardHidden = delta.keyboardHidden; + } + if (delta.hardKeyboardHidden != HARDKEYBOARDHIDDEN_UNDEFINED + && hardKeyboardHidden != delta.hardKeyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + hardKeyboardHidden = delta.hardKeyboardHidden; + } + if (delta.navigation != NAVIGATION_UNDEFINED + && navigation != delta.navigation) { + changed |= ActivityInfo.CONFIG_NAVIGATION; + navigation = delta.navigation; + } + if (delta.orientation != ORIENTATION_UNDEFINED + && orientation != delta.orientation) { + changed |= ActivityInfo.CONFIG_ORIENTATION; + orientation = delta.orientation; + } + + return changed; + } + + /** + * Return a bit mask of the differences between this Configuration + * object and the given one. Does not change the values of either. Any + * undefined fields in <var>delta</var> are ignored. + * @return Returns a bit mask indicating which configuration + * values has changed, containing any combination of + * {@link android.content.pm.ActivityInfo#CONFIG_FONT_SCALE + * PackageManager.ActivityInfo.CONFIG_FONT_SCALE}, + * {@link android.content.pm.ActivityInfo#CONFIG_MCC + * PackageManager.ActivityInfo.CONFIG_MCC}, + * {@link android.content.pm.ActivityInfo#CONFIG_MNC + * PackageManager.ActivityInfo.CONFIG_MNC}, + * {@link android.content.pm.ActivityInfo#CONFIG_LOCALE + * PackageManager.ActivityInfo.CONFIG_LOCALE}, + * {@link android.content.pm.ActivityInfo#CONFIG_TOUCHSCREEN + * PackageManager.ActivityInfo.CONFIG_TOUCHSCREEN}, + * {@link android.content.pm.ActivityInfo#CONFIG_KEYBOARD + * PackageManager.ActivityInfo.CONFIG_KEYBOARD}, + * {@link android.content.pm.ActivityInfo#CONFIG_NAVIGATION + * PackageManager.ActivityInfo.CONFIG_NAVIGATION}, or + * {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION + * PackageManager.ActivityInfo.CONFIG_ORIENTATION}. + */ + public int diff(Configuration delta) { + int changed = 0; + if (delta.fontScale > 0 && fontScale != delta.fontScale) { + changed |= ActivityInfo.CONFIG_FONT_SCALE; + } + if (delta.mcc != 0 && mcc != delta.mcc) { + changed |= ActivityInfo.CONFIG_MCC; + } + if (delta.mnc != 0 && mnc != delta.mnc) { + changed |= ActivityInfo.CONFIG_MNC; + } + if (delta.locale != null + && (locale == null || !locale.equals(delta.locale))) { + changed |= ActivityInfo.CONFIG_LOCALE; + } + if (delta.touchscreen != TOUCHSCREEN_UNDEFINED + && touchscreen != delta.touchscreen) { + changed |= ActivityInfo.CONFIG_TOUCHSCREEN; + } + if (delta.keyboard != KEYBOARD_UNDEFINED + && keyboard != delta.keyboard) { + changed |= ActivityInfo.CONFIG_KEYBOARD; + } + if (delta.keyboardHidden != KEYBOARDHIDDEN_UNDEFINED + && keyboardHidden != delta.keyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + } + if (delta.hardKeyboardHidden != HARDKEYBOARDHIDDEN_UNDEFINED + && hardKeyboardHidden != delta.hardKeyboardHidden) { + changed |= ActivityInfo.CONFIG_KEYBOARD_HIDDEN; + } + if (delta.navigation != NAVIGATION_UNDEFINED + && navigation != delta.navigation) { + changed |= ActivityInfo.CONFIG_NAVIGATION; + } + if (delta.orientation != ORIENTATION_UNDEFINED + && orientation != delta.orientation) { + changed |= ActivityInfo.CONFIG_ORIENTATION; + } + + return changed; + } + + /** + * Determine if a new resource needs to be loaded from the bit set of + * configuration changes returned by {@link #updateFrom(Configuration)}. + * + * @param configChanges The mask of changes configurations as returned by + * {@link #updateFrom(Configuration)}. + * @param interestingChanges The configuration changes that the resource + * can handled, as given in {@link android.util.TypedValue#changingConfigurations}. + * + * @return Return true if the resource needs to be loaded, else false. + */ + public static boolean needNewResources(int configChanges, int interestingChanges) { + return (configChanges & (interestingChanges|ActivityInfo.CONFIG_FONT_SCALE)) != 0; + } + + /** + * Parcelable methods + */ + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(fontScale); + dest.writeInt(mcc); + dest.writeInt(mnc); + if (locale == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + dest.writeString(locale.getLanguage()); + dest.writeString(locale.getCountry()); + dest.writeString(locale.getVariant()); + } + if(userSetLocale) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } + dest.writeInt(touchscreen); + dest.writeInt(keyboard); + dest.writeInt(keyboardHidden); + dest.writeInt(hardKeyboardHidden); + dest.writeInt(navigation); + dest.writeInt(orientation); + } + + public static final Parcelable.Creator<Configuration> CREATOR + = new Parcelable.Creator<Configuration>() { + public Configuration createFromParcel(Parcel source) { + return new Configuration(source); + } + + public Configuration[] newArray(int size) { + return new Configuration[size]; + } + }; + + /** + * Construct this Configuration object, reading from the Parcel. + */ + private Configuration(Parcel source) { + fontScale = source.readFloat(); + mcc = source.readInt(); + mnc = source.readInt(); + if (source.readInt() != 0) { + locale = new Locale(source.readString(), source.readString(), + source.readString()); + } + userSetLocale = (source.readInt()==1); + touchscreen = source.readInt(); + keyboard = source.readInt(); + keyboardHidden = source.readInt(); + hardKeyboardHidden = source.readInt(); + navigation = source.readInt(); + orientation = source.readInt(); + } + + public int compareTo(Configuration that) { + int n; + float a = this.fontScale; + float b = that.fontScale; + if (a < b) return -1; + if (a > b) return 1; + n = this.mcc - that.mcc; + if (n != 0) return n; + n = this.mnc - that.mnc; + if (n != 0) return n; + n = this.locale.getLanguage().compareTo(that.locale.getLanguage()); + if (n != 0) return n; + n = this.locale.getCountry().compareTo(that.locale.getCountry()); + if (n != 0) return n; + n = this.locale.getVariant().compareTo(that.locale.getVariant()); + if (n != 0) return n; + n = this.touchscreen - that.touchscreen; + if (n != 0) return n; + n = this.keyboard - that.keyboard; + if (n != 0) return n; + n = this.keyboardHidden - that.keyboardHidden; + if (n != 0) return n; + n = this.hardKeyboardHidden - that.hardKeyboardHidden; + if (n != 0) return n; + n = this.navigation - that.navigation; + if (n != 0) return n; + n = this.orientation - that.orientation; + //if (n != 0) return n; + return n; + } + + public boolean equals(Configuration that) { + if (that == null) return false; + if (that == this) return true; + return this.compareTo(that) == 0; + } + + public boolean equals(Object that) { + try { + return equals((Configuration)that); + } catch (ClassCastException e) { + } + return false; + } + + public int hashCode() { + return ((int)this.fontScale) + this.mcc + this.mnc + + this.locale.hashCode() + this.touchscreen + + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden + + this.navigation + this.orientation; + } +} diff --git a/core/java/android/content/res/PluralRules.java b/core/java/android/content/res/PluralRules.java new file mode 100644 index 0000000..2dce3c1 --- /dev/null +++ b/core/java/android/content/res/PluralRules.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 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.res; + +import java.util.Locale; + +/* + * Yuck-o. This is not the right way to implement this. When the ICU PluralRules + * object has been integrated to android, we should switch to that. For now, yuck-o. + */ + +abstract class PluralRules { + + static final int QUANTITY_OTHER = 0x0000; + static final int QUANTITY_ZERO = 0x0001; + static final int QUANTITY_ONE = 0x0002; + static final int QUANTITY_TWO = 0x0004; + static final int QUANTITY_FEW = 0x0008; + static final int QUANTITY_MANY = 0x0010; + + static final int ID_OTHER = 0x01000004; + + abstract int quantityForNumber(int n); + + final int attrForNumber(int n) { + return PluralRules.attrForQuantity(quantityForNumber(n)); + } + + static final int attrForQuantity(int quantity) { + // see include/utils/ResourceTypes.h + switch (quantity) { + case QUANTITY_ZERO: return 0x01000005; + case QUANTITY_ONE: return 0x01000006; + case QUANTITY_TWO: return 0x01000007; + case QUANTITY_FEW: return 0x01000008; + case QUANTITY_MANY: return 0x01000009; + default: return ID_OTHER; + } + } + + static final String stringForQuantity(int quantity) { + switch (quantity) { + case QUANTITY_ZERO: + return "zero"; + case QUANTITY_ONE: + return "one"; + case QUANTITY_TWO: + return "two"; + case QUANTITY_FEW: + return "few"; + case QUANTITY_MANY: + return "many"; + default: + return "other"; + } + } + + static final PluralRules ruleForLocale(Locale locale) { + String lang = locale.getLanguage(); + if ("cs".equals(lang)) { + if (cs == null) cs = new cs(); + return cs; + } + else { + if (en == null) en = new en(); + return en; + } + } + + private static PluralRules cs; + private static class cs extends PluralRules { + int quantityForNumber(int n) { + if (n == 1) { + return QUANTITY_ONE; + } + else if (n >= 2 && n <= 4) { + return QUANTITY_FEW; + } + else { + return QUANTITY_OTHER; + } + } + } + + private static PluralRules en; + private static class en extends PluralRules { + int quantityForNumber(int n) { + if (n == 1) { + return QUANTITY_ONE; + } + else { + return QUANTITY_OTHER; + } + } + } +} + diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java new file mode 100644 index 0000000..1a963f6 --- /dev/null +++ b/core/java/android/content/res/Resources.java @@ -0,0 +1,1890 @@ +/* + * Copyright (C) 2006 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.res; + + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.graphics.Movie; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.SystemProperties; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; + +/** + * Class for accessing an application's resources. This sits on top of the + * asset manager of the application (accessible through getAssets()) and + * provides a higher-level API for getting typed data from the assets. + */ +public class Resources { + static final String TAG = "Resources"; + private static final boolean DEBUG_LOAD = false; + 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); + private static final Object mSync = new Object(); + private static Resources mSystem = null; + + // Information about preloaded resources. Note that they are not + // protected by a lock, because while preloading in zygote we are all + // single-threaded, and after that these are immutable. + private static final SparseArray<Drawable.ConstantState> mPreloadedDrawables + = new SparseArray<Drawable.ConstantState>(); + private static final SparseArray<ColorStateList> mPreloadedColorStateLists + = new SparseArray<ColorStateList>(); + private static boolean mPreloaded; + + /*package*/ final TypedValue mTmpValue = new TypedValue(); + + // These are protected by the mTmpValue lock. + private final SparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache + = new SparseArray<WeakReference<Drawable.ConstantState> >(); + private final SparseArray<WeakReference<ColorStateList> > mColorStateListCache + = new SparseArray<WeakReference<ColorStateList> >(); + private boolean mPreloading; + + /*package*/ TypedArray mCachedStyledAttributes = null; + + private int mLastCachedXmlBlockIndex = -1; + private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 }; + private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4]; + + /*package*/ final AssetManager mAssets; + private final Configuration mConfiguration = new Configuration(); + /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); + PluralRules mPluralRule; + + /** + * This exception is thrown by the resource APIs when a requested resource + * can not be found. + */ + public static class NotFoundException extends RuntimeException { + public NotFoundException() { + } + + public NotFoundException(String name) { + super(name); + } + } + + /** + * Create a new Resources object on top of an existing set of assets in an + * AssetManager. + * + * @param assets Previously created AssetManager. + * @param metrics Current display metrics to consider when + * selecting/computing resource values. + * @param config Desired device configuration to consider when + * selecting/computing resource values (optional). + */ + public Resources(AssetManager assets, DisplayMetrics metrics, + Configuration config) { + mAssets = assets; + mConfiguration.setToDefaults(); + mMetrics.setToDefaults(); + updateConfiguration(config, metrics); + assets.ensureStringBlocks(); + } + + /** + * Return a global shared Resources object that provides access to only + * system resources (no application resources), and is not configured for + * the current screen (can not use dimension units, does not change based + * on orientation, etc). + */ + public static Resources getSystem() { + synchronized (mSync) { + Resources ret = mSystem; + if (ret == null) { + ret = new Resources(); + mSystem = ret; + } + + return ret; + } + } + + /** + * Return the string value associated with a particular resource ID. The + * returned object will be a String if this is a plain string; it will be + * some other type of CharSequence if it is styled. + * {@more} + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return CharSequence The string data associated with the resource, plus + * possibly styled text information. + */ + public CharSequence getText(int id) throws NotFoundException { + CharSequence res = mAssets.getResourceText(id); + if (res != null) { + return res; + } + throw new NotFoundException("String resource ID #0x" + + Integer.toHexString(id)); + } + + /** + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return CharSequence The string data associated with the resource, plus + * possibly styled text information. + */ + public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { + PluralRules rule = getPluralRule(); + CharSequence res = mAssets.getResourceBagText(id, rule.attrForNumber(quantity)); + if (res != null) { + return res; + } + res = mAssets.getResourceBagText(id, PluralRules.ID_OTHER); + if (res != null) { + return res; + } + throw new NotFoundException("Plural resource ID #0x" + Integer.toHexString(id) + + " quantity=" + quantity + + " item=" + PluralRules.stringForQuantity(rule.quantityForNumber(quantity))); + } + + private PluralRules getPluralRule() { + synchronized (mSync) { + if (mPluralRule == null) { + mPluralRule = PluralRules.ruleForLocale(mConfiguration.locale); + } + return mPluralRule; + } + } + + /** + * Return the string value associated with a particular resource ID. It + * will be stripped of any styled text information. + * {@more} + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return String The string data associated with the resource, + * stripped of styled text information. + */ + public String getString(int id) throws NotFoundException { + CharSequence res = getText(id); + if (res != null) { + return res.toString(); + } + throw new NotFoundException("String resource ID #0x" + + Integer.toHexString(id)); + } + + + /** + * Return the string value associated with a particular resource ID, + * substituting the format arguments as defined in {@link java.util.Formatter} + * and {@link java.lang.String#format}. It will be stripped of any styled text + * information. + * {@more} + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @param formatArgs The format arguments that will be used for substitution. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return String The string data associated with the resource, + * stripped of styled text information. + */ + public String getString(int id, Object... formatArgs) throws NotFoundException { + String raw = getString(id); + return String.format(mConfiguration.locale, raw, formatArgs); + } + + /** + * Return the string value associated with a particular resource ID for a particular + * numerical quantity, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. It will be + * stripped of any styled text information. + * {@more} + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules. + * @param formatArgs The format arguments that will be used for substitution. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return String The string data associated with the resource, + * stripped of styled text information. + */ + public String getQuantityString(int id, int quantity, Object... formatArgs) + throws NotFoundException { + String raw = getQuantityText(id, quantity).toString(); + return String.format(mConfiguration.locale, raw, formatArgs); + } + + /** + * Return the string value associated with a particular resource ID for a particular + * numerical quantity. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param quantity The number used to get the correct string for the current language's + * plural rules. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return String The string data associated with the resource, + * stripped of styled text information. + */ + public String getQuantityString(int id, int quantity) throws NotFoundException { + return getQuantityText(id, quantity).toString(); + } + + /** + * Return the string value associated with a particular resource ID. The + * returned object will be a String if this is a plain string; it will be + * some other type of CharSequence if it is styled. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @param def The default CharSequence to return. + * + * @return CharSequence The string data associated with the resource, plus + * possibly styled text information, or def if id is 0 or not found. + */ + public CharSequence getText(int id, CharSequence def) { + CharSequence res = id != 0 ? mAssets.getResourceText(id) : null; + return res != null ? res : def; + } + + /** + * Return the styled text array associated with a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return The styled text array associated with the resource. + */ + public CharSequence[] getTextArray(int id) throws NotFoundException { + CharSequence[] res = mAssets.getResourceTextArray(id); + if (res != null) { + return res; + } + throw new NotFoundException("Text array resource ID #0x" + + Integer.toHexString(id)); + } + + /** + * Return the string array associated with a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return The string array associated with the resource. + */ + public String[] getStringArray(int id) throws NotFoundException { + String[] res = mAssets.getResourceStringArray(id); + if (res != null) { + return res; + } + throw new NotFoundException("String array resource ID #0x" + + Integer.toHexString(id)); + } + + /** + * Return the int array associated with a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return The int array associated with the resource. + */ + public int[] getIntArray(int id) throws NotFoundException { + int[] res = mAssets.getArrayIntResource(id); + if (res != null) { + return res; + } + throw new NotFoundException("Int array resource ID #0x" + + Integer.toHexString(id)); + } + + /** + * Return an array of heterogeneous values. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns a TypedArray holding an array of the array values. + * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} + * when done with it. + */ + public TypedArray obtainTypedArray(int id) throws NotFoundException { + int len = mAssets.getArraySize(id); + if (len < 0) { + throw new NotFoundException("Array resource ID #0x" + + Integer.toHexString(id)); + } + + TypedArray array = getCachedStyledAttributes(len); + array.mLength = mAssets.retrieveArray(id, array.mData); + array.mIndices[0] = 0; + + return array; + } + + /** + * Retrieve a dimensional for a particular resource ID. Unit + * conversions are based on the current {@link DisplayMetrics} associated + * with the resources. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @return Resource dimension value multiplied by the appropriate + * metric. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getDimensionPixelOffset + * @see #getDimensionPixelSize + */ + public float getDimension(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimension(value.data, mMetrics); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Retrieve a dimensional for a particular resource ID for use + * as an offset in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for you. An offset conversion involves simply + * truncating the base value to an integer. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @return Resource dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getDimension + * @see #getDimensionPixelSize + */ + public int getDimensionPixelOffset(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelOffset( + value.data, mMetrics); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Retrieve a dimensional for a particular resource ID for use + * as a size in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for use as a size. A size conversion involves + * rounding the base value, and ensuring that a non-zero base value + * is at least one pixel in size. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @return Resource dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getDimension + * @see #getDimensionPixelOffset + */ + public int getDimensionPixelSize(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize( + value.data, mMetrics); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Retrieve a fractional unit for a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * + * @return Attribute fractional value multiplied by the appropriate + * base value. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + */ + public float getFraction(int id, int base, int pbase) { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type == TypedValue.TYPE_FRACTION) { + return TypedValue.complexToFraction(value.data, base, pbase); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Return a drawable object associated with a particular resource ID. + * Various types of objects will be returned depending on the underlying + * resource -- for example, a solid color, PNG image, scalable image, etc. + * The Drawable API hides these implementation details. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Drawable An object that can be used to draw this resource. + */ + public Drawable getDrawable(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + return loadDrawable(value, id); + } + } + + /** + * Return a movie object associated with the particular resource ID. + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + */ + public Movie getMovie(int id) throws NotFoundException { + InputStream is = openRawResource(id); + Movie movie = Movie.decodeStream(is); + try { + is.close(); + } + catch (java.io.IOException e) { + // don't care, since the return value is valid + } + return movie; + } + + /** + * Return a color integer associated with a particular resource ID. + * If the resource holds a complex + * {@link android.content.res.ColorStateList}, then the default color from + * the set is returned. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns a single color value in the form 0xAARRGGBB. + */ + public int getColor(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type >= TypedValue.TYPE_FIRST_INT + && value.type <= TypedValue.TYPE_LAST_INT) { + return value.data; + } else if (value.type == TypedValue.TYPE_STRING) { + ColorStateList csl = loadColorStateList(mTmpValue, id); + return csl.getDefaultColor(); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Return a color state list associated with a particular resource ID. The + * resource may contain either a single raw color value, or a complex + * {@link android.content.res.ColorStateList} holding multiple possible colors. + * + * @param id The desired resource identifier of a {@link ColorStateList}, + * as generated by the aapt tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns a ColorStateList object containing either a single + * solid color or multiple colors that can be selected based on a state. + */ + public ColorStateList getColorStateList(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + return loadColorStateList(value, id); + } + } + + /** + * Return a boolean associated with a particular resource ID. This can be + * used with any integral resource value, and will return true if it is + * non-zero. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns the boolean value contained in the resource. + */ + public boolean getBoolean(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type >= TypedValue.TYPE_FIRST_INT + && value.type <= TypedValue.TYPE_LAST_INT) { + return value.data != 0; + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Return an integer associated with a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns the integer value contained in the resource. + */ + public int getInteger(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type >= TypedValue.TYPE_FIRST_INT + && value.type <= TypedValue.TYPE_LAST_INT) { + return value.data; + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /** + * Return an XmlResourceParser through which you can read a view layout + * description for the given resource ID. This parser has limited + * functionality -- in particular, you can't change its input, and only + * the high-level events are available. + * + * <p>This function is really a simple wrapper for calling + * {@link #getXml} with a layout resource. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return A new parser object through which you can read + * the XML data. + * + * @see #getXml + */ + public XmlResourceParser getLayout(int id) throws NotFoundException { + return loadXmlResourceParser(id, "layout"); + } + + /** + * Return an XmlResourceParser through which you can read an animation + * description for the given resource ID. This parser has limited + * functionality -- in particular, you can't change its input, and only + * the high-level events are available. + * + * <p>This function is really a simple wrapper for calling + * {@link #getXml} with an animation resource. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return A new parser object through which you can read + * the XML data. + * + * @see #getXml + */ + public XmlResourceParser getAnimation(int id) throws NotFoundException { + return loadXmlResourceParser(id, "anim"); + } + + /** + * Return an XmlResourceParser through which you can read a generic XML + * resource for the given resource ID. + * + * <p>The XmlPullParser implementation returned here has some limited + * functionality. In particular, you can't change its input, and only + * high-level parsing events are available (since the document was + * pre-parsed for you at build time, which involved merging text and + * stripping comments). + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return A new parser object through which you can read + * the XML data. + * + * @see android.util.AttributeSet + */ + public XmlResourceParser getXml(int id) throws NotFoundException { + return loadXmlResourceParser(id, "xml"); + } + + /** + * Open a data stream for reading a raw resource. This can only be used + * with resources whose value is the name of an asset files -- that is, it can be + * used to open drawable, sound, and raw resources; it will fail on string + * and color resources. + * + * @param id The resource identifier to open, as generated by the appt + * tool. + * + * @return InputStream Access to the resource data. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + */ + public InputStream openRawResource(int id) throws NotFoundException { + synchronized (mTmpValue) { + return openRawResource(id, mTmpValue); + } + } + + /** + * Open a data stream for reading a raw resource. This can only be used + * with resources whose value is the name of an asset files -- that is, it can be + * used to open drawable, sound, and raw resources; it will fail on string + * and color resources. + * + * @param id The resource identifier to open, as generated by the appt tool. + * @param value The TypedValue object to hold the resource information. + * + * @return InputStream Access to the resource data. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @hide Pending API council approval + */ + public InputStream openRawResource(int id, TypedValue value) throws NotFoundException { + getValue(id, value, true); + + try { + return mAssets.openNonAsset(value.assetCookie, value.string.toString(), + AssetManager.ACCESS_STREAMING); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException("File " + value.string.toString() + + " from drawable resource ID #0x" + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } + + /** + * Open a file descriptor for reading a raw resource. This can only be used + * with resources whose value is the name of an asset files -- that is, it can be + * used to open drawable, sound, and raw resources; it will fail on string + * and color resources. + * + * <p>This function only works for resources that are stored in the package + * as uncompressed data, which typically includes things like mp3 files + * and png images. + * + * @param id The resource identifier to open, as generated by the appt + * tool. + * + * @return AssetFileDescriptor A new file descriptor you can use to read + * the resource. This includes the file descriptor itself, as well as the + * offset and length of data where the resource appears in the file. A + * null is returned if the file exists but is compressed. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + */ + public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + + try { + return mAssets.openNonAssetFd( + value.assetCookie, value.string.toString()); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + value.string.toString() + + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + + } + } + + /** + * Return the raw data associated with a particular resource ID. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param outValue Object in which to place the resource data. + * @param resolveRefs If true, a resource that is a reference to another + * resource will be followed so that you receive the + * actual final resource data. If false, the TypedValue + * will be filled in with the reference itself. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + */ + public void getValue(int id, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + boolean found = mAssets.getResourceValue(id, outValue, resolveRefs); + if (found) { + return; + } + throw new NotFoundException("Resource ID #0x" + + Integer.toHexString(id)); + } + + /** + * Return the raw data associated with a particular resource ID. + * See getIdentifier() for information on how names are mapped to resource + * IDs, and getString(int) for information on how string resources are + * retrieved. + * + * <p>Note: use of this function is discouraged. It is much more + * efficient to retrieve resources by identifier than by name. + * + * @param name The name of the desired resource. This is passed to + * getIdentifier() with a default type of "string". + * @param outValue Object in which to place the resource data. + * @param resolveRefs If true, a resource that is a reference to another + * resource will be followed so that you receive the + * actual final resource data. If false, the TypedValue + * will be filled in with the reference itself. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + */ + public void getValue(String name, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + int id = getIdentifier(name, "string", null); + if (id != 0) { + getValue(id, outValue, resolveRefs); + return; + } + throw new NotFoundException("String resource name " + name); + } + + /** + * This class holds the current attribute values for a particular theme. + * In other words, a Theme is a set of values for resource attributes; + * these are used in conjunction with {@link TypedArray} + * to resolve the final value for an attribute. + * + * <p>The Theme's attributes come into play in two ways: (1) a styled + * attribute can explicit reference a value in the theme through the + * "?themeAttribute" syntax; (2) if no value has been defined for a + * particular styled attribute, as a last resort we will try to find that + * attribute's value in the Theme. + * + * <p>You will normally use the {@link #obtainStyledAttributes} APIs to + * retrieve XML attributes with style and theme information applied. + */ + public final class Theme { + /** + * Place new attribute values into the theme. The style resource + * specified by <var>resid</var> will be retrieved from this Theme's + * resources, its values placed into the Theme object. + * + * <p>The semantics of this function depends on the <var>force</var> + * argument: If false, only values that are not already defined in + * the theme will be copied from the system resource; otherwise, if + * any of the style's attributes are already defined in the theme, the + * current values in the theme will be overwritten. + * + * @param resid The resource ID of a style resource from which to + * obtain attribute values. + * @param force If true, values in the style resource will always be + * used in the theme; otherwise, they will only be used + * if not already defined in the theme. + */ + public void applyStyle(int resid, boolean force) { + AssetManager.applyThemeStyle(mTheme, resid, force); + } + + /** + * Set this theme to hold the same contents as the theme + * <var>other</var>. If both of these themes are from the same + * Resources object, they will be identical after this function + * returns. If they are from different Resources, only the resources + * they have in common will be set in this theme. + * + * @param other The existing Theme to copy from. + */ + public void setTo(Theme other) { + AssetManager.copyTheme(mTheme, other.mTheme); + } + + /** + * Return a StyledAttributes holding the values defined by + * <var>Theme</var> which are listed in <var>attrs</var>. + * + * <p>Be sure to call StyledAttributes.recycle() when you are done with + * the array. + * + * @param attrs The desired attributes. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns a TypedArray holding an array of the attribute values. + * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} + * when done with it. + * + * @see Resources#obtainAttributes + * @see #obtainStyledAttributes(int, int[]) + * @see #obtainStyledAttributes(AttributeSet, int[], int, int) + */ + public TypedArray obtainStyledAttributes(int[] attrs) { + int len = attrs.length; + TypedArray array = getCachedStyledAttributes(len); + array.mRsrcs = attrs; + AssetManager.applyStyle(mTheme, 0, 0, 0, attrs, + array.mData, array.mIndices); + return array; + } + + /** + * Return a StyledAttributes holding the values defined by the style + * resource <var>resid</var> which are listed in <var>attrs</var>. + * + * <p>Be sure to call StyledAttributes.recycle() when you are done with + * the array. + * + * @param resid The desired style resource. + * @param attrs The desired attributes in the style. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @return Returns a TypedArray holding an array of the attribute values. + * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} + * when done with it. + * + * @see Resources#obtainAttributes + * @see #obtainStyledAttributes(int[]) + * @see #obtainStyledAttributes(AttributeSet, int[], int, int) + */ + public TypedArray obtainStyledAttributes(int resid, int[] attrs) + throws NotFoundException { + int len = attrs.length; + TypedArray array = getCachedStyledAttributes(len); + array.mRsrcs = attrs; + + AssetManager.applyStyle(mTheme, 0, resid, 0, attrs, + array.mData, array.mIndices); + if (false) { + int[] data = array.mData; + + System.out.println("**********************************************************"); + System.out.println("**********************************************************"); + System.out.println("**********************************************************"); + System.out.println("Attributes:"); + String s = " Attrs:"; + int i; + for (i=0; i<attrs.length; i++) { + s = s + " 0x" + Integer.toHexString(attrs[i]); + } + System.out.println(s); + s = " Found:"; + TypedValue value = new TypedValue(); + for (i=0; i<attrs.length; i++) { + int d = i*AssetManager.STYLE_NUM_ENTRIES; + value.type = data[d+AssetManager.STYLE_TYPE]; + value.data = data[d+AssetManager.STYLE_DATA]; + value.assetCookie = data[d+AssetManager.STYLE_ASSET_COOKIE]; + value.resourceId = data[d+AssetManager.STYLE_RESOURCE_ID]; + s = s + " 0x" + Integer.toHexString(attrs[i]) + + "=" + value; + } + System.out.println(s); + } + return array; + } + + /** + * Return a StyledAttributes holding the attribute values in + * <var>set</var> + * that are listed in <var>attrs</var>. In addition, if the given + * AttributeSet specifies a style class (through the "style" attribute), + * that style will be applied on top of the base attributes it defines. + * + * <p>Be sure to call StyledAttributes.recycle() when you are done with + * the array. + * + * <p>When determining the final value of a particular attribute, there + * are four inputs that come into play:</p> + * + * <ol> + * <li> Any attribute values in the given AttributeSet. + * <li> The style resource specified in the AttributeSet (named + * "style"). + * <li> The default style specified by <var>defStyleAttr</var> and + * <var>defStyleRes</var> + * <li> The base values in this theme. + * </ol> + * + * <p>Each of these inputs is considered in-order, with the first listed + * taking precedence over the following ones. In other words, if in the + * AttributeSet you have supplied <code><Button + * textColor="#ff000000"></code>, then the button's text will + * <em>always</em> be black, regardless of what is specified in any of + * the styles. + * + * @param set The base set of attribute values. May be null. + * @param attrs The desired attributes to be retrieved. + * @param defStyleAttr An attribute in the current theme that contains a + * reference to a style resource that supplies + * defaults values for the StyledAttributes. Can be + * 0 to not look for defaults. + * @param defStyleRes A resource identifier of a style resource that + * supplies default values for the StyledAttributes, + * used only if defStyleAttr is 0 or can not be found + * in the theme. Can be 0 to not look for defaults. + * + * @return Returns a TypedArray holding an array of the attribute values. + * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} + * when done with it. + * + * @see Resources#obtainAttributes + * @see #obtainStyledAttributes(int[]) + * @see #obtainStyledAttributes(int, int[]) + */ + public TypedArray obtainStyledAttributes(AttributeSet set, + int[] attrs, int defStyleAttr, int defStyleRes) { + int len = attrs.length; + TypedArray array = getCachedStyledAttributes(len); + + // XXX note that for now we only work with compiled XML files. + // To support generic XML files we will need to manually parse + // out the attributes from the XML file (applying type information + // contained in the resources and such). + XmlBlock.Parser parser = (XmlBlock.Parser)set; + AssetManager.applyStyle( + mTheme, defStyleAttr, defStyleRes, + parser != null ? parser.mParseState : 0, attrs, + array.mData, array.mIndices); + + array.mRsrcs = attrs; + array.mXml = parser; + + if (false) { + int[] data = array.mData; + + System.out.println("Attributes:"); + String s = " Attrs:"; + int i; + for (i=0; i<set.getAttributeCount(); i++) { + s = s + " " + set.getAttributeName(i); + int id = set.getAttributeNameResource(i); + if (id != 0) { + s = s + "(0x" + Integer.toHexString(id) + ")"; + } + s = s + "=" + set.getAttributeValue(i); + } + System.out.println(s); + s = " Found:"; + TypedValue value = new TypedValue(); + for (i=0; i<attrs.length; i++) { + int d = i*AssetManager.STYLE_NUM_ENTRIES; + value.type = data[d+AssetManager.STYLE_TYPE]; + value.data = data[d+AssetManager.STYLE_DATA]; + value.assetCookie = data[d+AssetManager.STYLE_ASSET_COOKIE]; + value.resourceId = data[d+AssetManager.STYLE_RESOURCE_ID]; + s = s + " 0x" + Integer.toHexString(attrs[i]) + + "=" + value; + } + System.out.println(s); + } + + return array; + } + + /** + * Retrieve the value of an attribute in the Theme. The contents of + * <var>outValue</var> are ultimately filled in by + * {@link Resources#getValue}. + * + * @param resid The resource identifier of the desired theme + * attribute. + * @param outValue Filled in with the ultimate resource value supplied + * by the attribute. + * @param resolveRefs If true, resource references will be walked; if + * false, <var>outValue</var> may be a + * TYPE_REFERENCE. In either case, it will never + * be a TYPE_ATTRIBUTE. + * + * @return boolean Returns true if the attribute was found and + * <var>outValue</var> is valid, else false. + */ + public boolean resolveAttribute(int resid, TypedValue outValue, + boolean resolveRefs) { + boolean got = mAssets.getThemeValue(mTheme, resid, outValue, resolveRefs); + if (false) { + System.out.println( + "resolveAttribute #" + Integer.toHexString(resid) + + " got=" + got + ", type=0x" + Integer.toHexString(outValue.type) + + ", data=0x" + Integer.toHexString(outValue.data)); + } + return got; + } + + /** + * Print contents of this theme out to the log. For debugging only. + * + * @param priority The log priority to use. + * @param tag The log tag to use. + * @param prefix Text to prefix each line printed. + */ + public void dump(int priority, String tag, String prefix) { + AssetManager.dumpTheme(mTheme, priority, tag, prefix); + } + + protected void finalize() throws Throwable { + super.finalize(); + mAssets.releaseTheme(mTheme); + } + + /*package*/ Theme() { + mAssets = Resources.this.mAssets; + mTheme = mAssets.createTheme(); + } + + private final AssetManager mAssets; + private final int mTheme; + } + + /** + * Generate a new Theme object for this set of Resources. It initially + * starts out empty. + * + * @return Theme The newly created Theme container. + */ + public final Theme newTheme() { + return new Theme(); + } + + /** + * Retrieve a set of basic attribute values from an AttributeSet, not + * performing styling of them using a theme and/or style resources. + * + * @param set The current attribute values to retrieve. + * @param attrs The specific attributes to be retrieved. + * @return Returns a TypedArray holding an array of the attribute values. + * Be sure to call {@link TypedArray#recycle() TypedArray.recycle()} + * when done with it. + * + * @see Theme#obtainStyledAttributes(AttributeSet, int[], int, int) + */ + public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { + int len = attrs.length; + TypedArray array = getCachedStyledAttributes(len); + + // XXX note that for now we only work with compiled XML files. + // To support generic XML files we will need to manually parse + // out the attributes from the XML file (applying type information + // contained in the resources and such). + XmlBlock.Parser parser = (XmlBlock.Parser)set; + mAssets.retrieveAttributes(parser.mParseState, attrs, + array.mData, array.mIndices); + + array.mRsrcs = attrs; + array.mXml = parser; + + return array; + } + + /** + * Store the newly updated configuration. + */ + public void updateConfiguration(Configuration config, + DisplayMetrics metrics) { + synchronized (mTmpValue) { + int configChanges = 0xfffffff; + if (config != null) { + configChanges = mConfiguration.updateFrom(config); + } + if (metrics != null) { + mMetrics.setTo(metrics); + } + mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale; + String locale = null; + if (mConfiguration.locale != null) { + locale = mConfiguration.locale.getLanguage(); + if (mConfiguration.locale.getCountry() != null) { + locale += "-" + mConfiguration.locale.getCountry(); + } + } + int width, height; + if (mMetrics.widthPixels >= mMetrics.heightPixels) { + width = mMetrics.widthPixels; + height = mMetrics.heightPixels; + } else { + //noinspection SuspiciousNameCombination + width = mMetrics.heightPixels; + //noinspection SuspiciousNameCombination + height = mMetrics.widthPixels; + } + int keyboardHidden = mConfiguration.keyboardHidden; + if (keyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO + && mConfiguration.hardKeyboardHidden + == Configuration.HARDKEYBOARDHIDDEN_YES) { + keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT; + } + mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, + locale, mConfiguration.orientation, + mConfiguration.touchscreen, + (int)(mMetrics.density*160), mConfiguration.keyboard, + keyboardHidden, mConfiguration.navigation, width, height, + sSdkVersion); + int N = mDrawableCache.size(); + if (DEBUG_CONFIG) { + Log.d(TAG, "Cleaning up drawables config changes: 0x" + + Integer.toHexString(configChanges)); + } + for (int i=0; i<N; i++) { + WeakReference<Drawable.ConstantState> ref = mDrawableCache.valueAt(i); + if (ref != null) { + Drawable.ConstantState cs = ref.get(); + if (cs != null) { + if (Configuration.needNewResources( + configChanges, cs.getChangingConfigurations())) { + if (DEBUG_CONFIG) { + Log.d(TAG, "FLUSHING #0x" + + Integer.toHexString(mDrawableCache.keyAt(i)) + + " / " + cs + " with changes: 0x" + + Integer.toHexString(cs.getChangingConfigurations())); + } + mDrawableCache.setValueAt(i, null); + } else if (DEBUG_CONFIG) { + Log.d(TAG, "(Keeping #0x" + + Integer.toHexString(mDrawableCache.keyAt(i)) + + " / " + cs + " with changes: 0x" + + Integer.toHexString(cs.getChangingConfigurations()) + + ")"); + } + } + } + } + mDrawableCache.clear(); + mColorStateListCache.clear(); + flushLayoutCache(); + } + synchronized (mSync) { + if (mPluralRule != null) { + mPluralRule = PluralRules.ruleForLocale(config.locale); + } + } + } + + /** + * Update the system resources configuration if they have previously + * been initialized. + * + * @hide + */ + public static void updateSystemConfiguration(Configuration config, DisplayMetrics metrics) { + if (mSystem != null) { + mSystem.updateConfiguration(config, metrics); + //Log.i(TAG, "Updated system resources " + mSystem + // + ": " + mSystem.getConfiguration()); + } + } + + /** + * Return the current display metrics that are in effect for this resource + * object. The returned object should be treated as read-only. + * + * @return The resource's current display metrics. + */ + public DisplayMetrics getDisplayMetrics() { + return mMetrics; + } + + /** + * Return the current configuration that is in effect for this resource + * object. The returned object should be treated as read-only. + * + * @return The resource's current configuration. + */ + public Configuration getConfiguration() { + return mConfiguration; + } + + /** + * Return a resource identifier for the given resource name. A fully + * qualified resource name is of the form "package:type/entry". The first + * two components (package and type) are optional if defType and + * defPackage, respectively, are specified here. + * + * <p>Note: use of this function is discouraged. It is much more + * efficient to retrieve resources by identifier than by name. + * + * @param name The name of the desired resource. + * @param defType Optional default resource type to find, if "type/" is + * not included in the name. Can be null to require an + * explicit type. + * @param defPackage Optional default package to find, if "package:" is + * not included in the name. Can be null to require an + * explicit package. + * + * @return int The associated resource identifier. Returns 0 if no such + * resource was found. (0 is not a valid resource ID.) + */ + public int getIdentifier(String name, String defType, String defPackage) { + try { + return Integer.parseInt(name); + } catch (Exception e) { + // Ignore + } + return mAssets.getResourceIdentifier(name, defType, defPackage); + } + + /** + * Return the full name for a given resource identifier. This name is + * a single string of the form "package:type/entry". + * + * @param resid The resource identifier whose name is to be retrieved. + * + * @return A string holding the name of the resource. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getResourcePackageName + * @see #getResourceTypeName + * @see #getResourceEntryName + */ + public String getResourceName(int resid) throws NotFoundException { + String str = mAssets.getResourceName(resid); + if (str != null) return str; + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + + /** + * Return the package name for a given resource identifier. + * + * @param resid The resource identifier whose package name is to be + * retrieved. + * + * @return A string holding the package name of the resource. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getResourceName + */ + public String getResourcePackageName(int resid) throws NotFoundException { + String str = mAssets.getResourcePackageName(resid); + if (str != null) return str; + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + + /** + * Return the type name for a given resource identifier. + * + * @param resid The resource identifier whose type name is to be + * retrieved. + * + * @return A string holding the type name of the resource. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getResourceName + */ + public String getResourceTypeName(int resid) throws NotFoundException { + String str = mAssets.getResourceTypeName(resid); + if (str != null) return str; + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + + /** + * Return the entry name for a given resource identifier. + * + * @param resid The resource identifier whose entry name is to be + * retrieved. + * + * @return A string holding the entry name of the resource. + * + * @throws NotFoundException Throws NotFoundException if the given ID does not exist. + * + * @see #getResourceName + */ + public String getResourceEntryName(int resid) throws NotFoundException { + String str = mAssets.getResourceEntryName(resid); + if (str != null) return str; + throw new NotFoundException("Unable to find resource ID #0x" + + Integer.toHexString(resid)); + } + + /** + * Parse a series of {@link android.R.styleable#Extra <extra>} tags from + * an XML file. You call this when you are at the parent tag of the + * extra tags, and it return once all of the child tags have been parsed. + * This will call {@link #parseBundleExtra} for each extra tag encountered. + * + * @param parser The parser from which to retrieve the extras. + * @param outBundle A Bundle in which to place all parsed extras. + * @throws XmlPullParserException + * @throws IOException + */ + public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String nodeName = parser.getName(); + if (nodeName.equals("extra")) { + parseBundleExtra("extra", parser, outBundle); + XmlUtils.skipCurrentTag(parser); + + } else { + XmlUtils.skipCurrentTag(parser); + } + } + } + + /** + * Parse a name/value pair out of an XML tag holding that data. The + * AttributeSet must be holding the data defined by + * {@link android.R.styleable#Extra}. The following value types are supported: + * <ul> + * <li> {@link TypedValue#TYPE_STRING}: + * {@link Bundle#putCharSequence Bundle.putCharSequence()} + * <li> {@link TypedValue#TYPE_INT_BOOLEAN}: + * {@link Bundle#putCharSequence Bundle.putBoolean()} + * <li> {@link TypedValue#TYPE_FIRST_INT}-{@link TypedValue#TYPE_LAST_INT}: + * {@link Bundle#putCharSequence Bundle.putBoolean()} + * <li> {@link TypedValue#TYPE_FLOAT}: + * {@link Bundle#putCharSequence Bundle.putFloat()} + * </ul> + * + * @param tagName The name of the tag these attributes come from; this is + * only used for reporting error messages. + * @param attrs The attributes from which to retrieve the name/value pair. + * @param outBundle The Bundle in which to place the parsed value. + * @throws XmlPullParserException If the attributes are not valid. + */ + public void parseBundleExtra(String tagName, AttributeSet attrs, + Bundle outBundle) throws XmlPullParserException { + TypedArray sa = obtainAttributes(attrs, + com.android.internal.R.styleable.Extra); + + String name = sa.getString( + com.android.internal.R.styleable.Extra_name); + if (name == null) { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> requires an android:name attribute at " + + attrs.getPositionDescription()); + } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.Extra_value); + if (v != null) { + if (v.type == TypedValue.TYPE_STRING) { + CharSequence cs = v.coerceToString(); + outBundle.putCharSequence(name, cs); + } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { + outBundle.putBoolean(name, v.data != 0); + } else if (v.type >= TypedValue.TYPE_FIRST_INT + && v.type <= TypedValue.TYPE_LAST_INT) { + outBundle.putInt(name, v.data); + } else if (v.type == TypedValue.TYPE_FLOAT) { + outBundle.putFloat(name, v.getFloat()); + } else { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> only supports string, integer, float, color, and boolean at " + + attrs.getPositionDescription()); + } + } else { + sa.recycle(); + throw new XmlPullParserException("<" + tagName + + "> requires an android:value or android:resource attribute at " + + attrs.getPositionDescription()); + } + + sa.recycle(); + } + + /** + * Retrieve underlying AssetManager storage for these resources. + */ + public final AssetManager getAssets() { + return mAssets; + } + + /** + * Call this to remove all cached loaded layout resources from the + * Resources object. Only intended for use with performance testing + * tools. + */ + public final void flushLayoutCache() { + synchronized (mCachedXmlBlockIds) { + // First see if this block is in our cache. + final int num = mCachedXmlBlockIds.length; + for (int i=0; i<num; i++) { + mCachedXmlBlockIds[i] = -0; + XmlBlock oldBlock = mCachedXmlBlocks[i]; + if (oldBlock != null) { + oldBlock.close(); + } + mCachedXmlBlocks[i] = null; + } + } + } + + /** + * Start preloading of resource data using this Resources object. Only + * for use by the zygote process for loading common system resources. + * {@hide} + */ + public final void startPreloading() { + synchronized (mSync) { + if (mPreloaded) { + throw new IllegalStateException("Resources already preloaded"); + } + mPreloaded = true; + mPreloading = true; + } + } + + /** + * Called by zygote when it is done preloading resources, to change back + * to normal Resources operation. + */ + public final void finishPreloading() { + if (mPreloading) { + mPreloading = false; + flushLayoutCache(); + } + } + + /*package*/ Drawable loadDrawable(TypedValue value, int id) + throws NotFoundException { + + if (TRACE_FOR_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) android.util.Log.d("PreloadDrawable", name); + } + } + + final int key = (value.assetCookie << 24) | value.data; + Drawable dr = getCachedDrawable(key); + + if (dr != null) { + return dr; + } + + Drawable.ConstantState cs = mPreloadedDrawables.get(key); + if (cs != null) { + dr = cs.newDrawable(); + } else { + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + dr = new ColorDrawable(value.data); + } + + if (dr == null) { + if (value.string == null) { + throw new NotFoundException( + "Resource is not a Drawable (color or path): " + value); + } + + String file = value.string.toString(); + + if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie " + + value.assetCookie + ": " + file); + + if (file.endsWith(".xml")) { + try { + XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "drawable"); + dr = Drawable.createFromXml(this, rp); + rp.close(); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + + } else { + try { + InputStream is = mAssets.openNonAsset( + value.assetCookie, file, AssetManager.ACCESS_BUFFER); + // System.out.println("Opened file " + file + ": " + is); + dr = Drawable.createFromResourceStream(this, value, is, file); + is.close(); + // System.out.println("Created stream: " + dr); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } + } + } + + if (dr != null) { + dr.setChangingConfigurations(value.changingConfigurations); + cs = dr.getConstantState(); + if (cs != null) { + if (mPreloading) { + mPreloadedDrawables.put(key, cs); + } else { + synchronized (mTmpValue) { + //Log.i(TAG, "Saving cached drawable @ #" + + // Integer.toHexString(key.intValue()) + // + " in " + this + ": " + cs); + mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs)); + } + } + } + } + + return dr; + } + + private Drawable getCachedDrawable(int key) { + synchronized (mTmpValue) { + WeakReference<Drawable.ConstantState> wr = mDrawableCache.get(key); + if (wr != null) { // we have the key + Drawable.ConstantState entry = wr.get(); + if (entry != null) { + //Log.i(TAG, "Returning cached drawable @ #" + + // Integer.toHexString(((Integer)key).intValue()) + // + " in " + this + ": " + entry); + return entry.newDrawable(); + } + else { // our entry has been purged + mDrawableCache.delete(key); + } + } + } + return null; + } + + /*package*/ ColorStateList loadColorStateList(TypedValue value, int id) + throws NotFoundException { + if (TRACE_FOR_PRELOAD) { + // Log only framework resources + if ((id >>> 24) == 0x1) { + final String name = getResourceName(id); + if (name != null) android.util.Log.d("PreloadColorStateList", name); + } + } + + final int key = (value.assetCookie << 24) | value.data; + + ColorStateList csl; + + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && + value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + + csl = mPreloadedColorStateLists.get(key); + if (csl != null) { + return csl; + } + + csl = ColorStateList.valueOf(value.data); + if (mPreloading) { + mPreloadedColorStateLists.put(key, csl); + } + + return csl; + } + + csl = getCachedColorStateList(key); + if (csl != null) { + return csl; + } + + csl = mPreloadedColorStateLists.get(key); + if (csl != null) { + return csl; + } + + if (value.string == null) { + throw new NotFoundException( + "Resource is not a ColorStateList (color or path): " + value); + } + + String file = value.string.toString(); + + if (file.endsWith(".xml")) { + try { + XmlResourceParser rp = loadXmlResourceParser( + file, id, value.assetCookie, "colorstatelist"); + csl = ColorStateList.createFromXml(this, rp); + rp.close(); + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from color state list resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } else { + throw new NotFoundException( + "File " + file + " from drawable resource ID #0x" + + Integer.toHexString(id) + ": .xml extension required"); + } + + if (csl != null) { + if (mPreloading) { + mPreloadedColorStateLists.put(key, csl); + } else { + synchronized (mTmpValue) { + //Log.i(TAG, "Saving cached color state list @ #" + + // Integer.toHexString(key.intValue()) + // + " in " + this + ": " + csl); + mColorStateListCache.put( + key, new WeakReference<ColorStateList>(csl)); + } + } + } + + return csl; + } + + private ColorStateList getCachedColorStateList(int key) { + synchronized (mTmpValue) { + WeakReference<ColorStateList> wr = mColorStateListCache.get(key); + if (wr != null) { // we have the key + ColorStateList entry = wr.get(); + if (entry != null) { + //Log.i(TAG, "Returning cached color state list @ #" + + // Integer.toHexString(((Integer)key).intValue()) + // + " in " + this + ": " + entry); + return entry; + } + else { // our entry has been purged + mColorStateListCache.delete(key); + } + } + } + return null; + } + + /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type) + throws NotFoundException { + synchronized (mTmpValue) { + TypedValue value = mTmpValue; + getValue(id, value, true); + if (value.type == TypedValue.TYPE_STRING) { + return loadXmlResourceParser(value.string.toString(), id, + value.assetCookie, type); + } + throw new NotFoundException( + "Resource ID #0x" + Integer.toHexString(id) + " type #0x" + + Integer.toHexString(value.type) + " is not valid"); + } + } + + /*package*/ XmlResourceParser loadXmlResourceParser(String file, int id, + int assetCookie, String type) throws NotFoundException { + if (id != 0) { + try { + // These may be compiled... + synchronized (mCachedXmlBlockIds) { + // First see if this block is in our cache. + final int num = mCachedXmlBlockIds.length; + for (int i=0; i<num; i++) { + if (mCachedXmlBlockIds[i] == id) { + //System.out.println("**** REUSING XML BLOCK! id=" + // + id + ", index=" + i); + return mCachedXmlBlocks[i].newParser(); + } + } + + // Not in the cache, create a new block and put it at + // the next slot in the cache. + XmlBlock block = mAssets.openXmlBlockAsset( + assetCookie, file); + if (block != null) { + int pos = mLastCachedXmlBlockIndex+1; + if (pos >= num) pos = 0; + mLastCachedXmlBlockIndex = pos; + XmlBlock oldBlock = mCachedXmlBlocks[pos]; + if (oldBlock != null) { + oldBlock.close(); + } + mCachedXmlBlockIds[pos] = id; + mCachedXmlBlocks[pos] = block; + //System.out.println("**** CACHING NEW XML BLOCK! id=" + // + id + ", index=" + pos); + return block.newParser(); + } + } + } catch (Exception e) { + NotFoundException rnf = new NotFoundException( + "File " + file + " from xml type " + type + " resource ID #0x" + + Integer.toHexString(id)); + rnf.initCause(e); + throw rnf; + } + } + + throw new NotFoundException( + "File " + file + " from xml type " + type + " resource ID #0x" + + Integer.toHexString(id)); + } + + private TypedArray getCachedStyledAttributes(int len) { + synchronized (mTmpValue) { + TypedArray attrs = mCachedStyledAttributes; + if (attrs != null) { + mCachedStyledAttributes = null; + + attrs.mLength = len; + int fullLen = len * AssetManager.STYLE_NUM_ENTRIES; + if (attrs.mData.length >= fullLen) { + return attrs; + } + attrs.mData = new int[fullLen]; + attrs.mIndices = new int[1+len]; + return attrs; + } + return new TypedArray(this, + new int[len*AssetManager.STYLE_NUM_ENTRIES], + new int[1+len], len); + } + } + + private Resources() { + mAssets = AssetManager.getSystem(); + // NOTE: Intentionally leaving this uninitialized (all values set + // to zero), so that anyone who tries to do something that requires + // metrics will get a very wrong value. + mConfiguration.setToDefaults(); + mMetrics.setToDefaults(); + updateConfiguration(null, null); + mAssets.ensureStringBlocks(); + } +} + diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java new file mode 100644 index 0000000..e684cb8 --- /dev/null +++ b/core/java/android/content/res/StringBlock.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2006 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.res; + +import android.text.*; +import android.text.style.*; +import android.util.Config; +import android.util.Log; +import android.util.SparseArray; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import com.android.internal.util.XmlUtils; + +/** + * Conveniences for retrieving data out of a compiled string resource. + * + * {@hide} + */ +final class StringBlock { + private static final String TAG = "AssetManager"; + private static final boolean localLOGV = Config.LOGV || false; + + private final int mNative; + private final boolean mUseSparse; + private final boolean mOwnsNative; + private CharSequence[] mStrings; + private SparseArray<CharSequence> mSparseStrings; + StyleIDs mStyleIDs = null; + + public StringBlock(byte[] data, boolean useSparse) { + mNative = nativeCreate(data, 0, data.length); + mUseSparse = useSparse; + mOwnsNative = true; + if (localLOGV) Log.v(TAG, "Created string block " + this + + ": " + nativeGetSize(mNative)); + } + + public StringBlock(byte[] data, int offset, int size, boolean useSparse) { + mNative = nativeCreate(data, offset, size); + mUseSparse = useSparse; + mOwnsNative = true; + if (localLOGV) Log.v(TAG, "Created string block " + this + + ": " + nativeGetSize(mNative)); + } + + public CharSequence get(int idx) { + synchronized (this) { + if (mStrings != null) { + CharSequence res = mStrings[idx]; + if (res != null) { + return res; + } + } else if (mSparseStrings != null) { + CharSequence res = mSparseStrings.get(idx); + if (res != null) { + return res; + } + } else { + final int num = nativeGetSize(mNative); + if (mUseSparse && num > 250) { + mSparseStrings = new SparseArray<CharSequence>(); + } else { + mStrings = new CharSequence[num]; + } + } + String str = nativeGetString(mNative, idx); + CharSequence res = str; + int[] style = nativeGetStyle(mNative, idx); + if (localLOGV) Log.v(TAG, "Got string: " + str); + if (localLOGV) Log.v(TAG, "Got styles: " + style); + if (style != null) { + if (mStyleIDs == null) { + mStyleIDs = new StyleIDs(); + mStyleIDs.boldId = nativeIndexOfString(mNative, "b"); + mStyleIDs.italicId = nativeIndexOfString(mNative, "i"); + mStyleIDs.underlineId = nativeIndexOfString(mNative, "u"); + mStyleIDs.ttId = nativeIndexOfString(mNative, "tt"); + mStyleIDs.bigId = nativeIndexOfString(mNative, "big"); + mStyleIDs.smallId = nativeIndexOfString(mNative, "small"); + mStyleIDs.supId = nativeIndexOfString(mNative, "sup"); + mStyleIDs.subId = nativeIndexOfString(mNative, "sub"); + mStyleIDs.strikeId = nativeIndexOfString(mNative, "strike"); + mStyleIDs.listItemId = nativeIndexOfString(mNative, "li"); + mStyleIDs.marqueeId = nativeIndexOfString(mNative, "marquee"); + + if (localLOGV) Log.v(TAG, "BoldId=" + mStyleIDs.boldId + + ", ItalicId=" + mStyleIDs.italicId + + ", UnderlineId=" + mStyleIDs.underlineId); + } + + res = applyStyles(str, style, mStyleIDs); + } + if (mStrings != null) mStrings[idx] = res; + else mSparseStrings.put(idx, res); + return res; + } + } + + protected void finalize() throws Throwable { + if (mOwnsNative) { + nativeDestroy(mNative); + } + } + + static final class StyleIDs { + private int boldId; + private int italicId; + private int underlineId; + private int ttId; + private int bigId; + private int smallId; + private int subId; + private int supId; + private int strikeId; + private int listItemId; + private int marqueeId; + } + + private CharSequence applyStyles(String str, int[] style, StyleIDs ids) { + if (style.length == 0) + return str; + + SpannableString buffer = new SpannableString(str); + int i=0; + while (i < style.length) { + int type = style[i]; + if (localLOGV) Log.v(TAG, "Applying style span id=" + type + + ", start=" + style[i+1] + ", end=" + style[i+2]); + + + if (type == ids.boldId) { + buffer.setSpan(new StyleSpan(Typeface.BOLD), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.italicId) { + buffer.setSpan(new StyleSpan(Typeface.ITALIC), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.underlineId) { + buffer.setSpan(new UnderlineSpan(), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.ttId) { + buffer.setSpan(new TypefaceSpan("monospace"), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.bigId) { + buffer.setSpan(new RelativeSizeSpan(1.25f), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.smallId) { + buffer.setSpan(new RelativeSizeSpan(0.8f), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.subId) { + buffer.setSpan(new SubscriptSpan(), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.supId) { + buffer.setSpan(new SuperscriptSpan(), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.strikeId) { + buffer.setSpan(new StrikethroughSpan(), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else if (type == ids.listItemId) { + addParagraphSpan(buffer, new BulletSpan(10), + style[i+1], style[i+2]+1); + } else if (type == ids.marqueeId) { + buffer.setSpan(TextUtils.TruncateAt.MARQUEE, + style[i+1], style[i+2]+1, + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } else { + String tag = nativeGetString(mNative, type); + + if (tag.startsWith("font;")) { + String sub; + + sub = subtag(tag, ";height="); + if (sub != null) { + int size = Integer.parseInt(sub); + addParagraphSpan(buffer, new Height(size), + style[i+1], style[i+2]+1); + } + + sub = subtag(tag, ";size="); + if (sub != null) { + int size = Integer.parseInt(sub); + buffer.setSpan(new AbsoluteSizeSpan(size), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + sub = subtag(tag, ";fgcolor="); + if (sub != null) { + int color = XmlUtils.convertValueToUnsignedInt(sub, -1); + buffer.setSpan(new ForegroundColorSpan(color), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + sub = subtag(tag, ";bgcolor="); + if (sub != null) { + int color = XmlUtils.convertValueToUnsignedInt(sub, -1); + buffer.setSpan(new BackgroundColorSpan(color), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else if (tag.startsWith("a;")) { + String sub; + + sub = subtag(tag, ";href="); + if (sub != null) { + buffer.setSpan(new URLSpan(sub), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else if (tag.startsWith("annotation;")) { + int len = tag.length(); + int next; + + for (int t = tag.indexOf(';'); t < len; t = next) { + int eq = tag.indexOf('=', t); + if (eq < 0) { + break; + } + + next = tag.indexOf(';', eq); + if (next < 0) { + next = len; + } + + String key = tag.substring(t + 1, eq); + String value = tag.substring(eq + 1, next); + + buffer.setSpan(new Annotation(key, value), + style[i+1], style[i+2]+1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + + i += 3; + } + return new SpannedString(buffer); + } + + /** + * If a translator has messed up the edges of paragraph-level markup, + * fix it to actually cover the entire paragraph that it is attached to + * instead of just whatever range they put it on. + */ + private static void addParagraphSpan(Spannable buffer, Object what, + int start, int end) { + int len = buffer.length(); + + if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') { + for (start--; start > 0; start--) { + if (buffer.charAt(start - 1) == '\n') { + break; + } + } + } + + if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') { + for (end++; end < len; end++) { + if (buffer.charAt(end - 1) == '\n') { + break; + } + } + } + + buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH); + } + + private static String subtag(String full, String attribute) { + int start = full.indexOf(attribute); + if (start < 0) { + return null; + } + + start += attribute.length(); + int end = full.indexOf(';', start); + + if (end < 0) { + return full.substring(start); + } else { + return full.substring(start, end); + } + } + + /** + * Forces the text line to be the specified height, shrinking/stretching + * the ascent if possible, or the descent if shrinking the ascent further + * will make the text unreadable. + */ + private static class Height implements LineHeightSpan { + private int mSize; + private static float sProportion = 0; + + public Height(int size) { + mSize = size; + } + + 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; + } else { + if (sProportion == 0) { + /* + * Calculate what fraction of the nominal ascent + * the height of a capital letter actually is, + * so that we won't reduce the ascent to less than + * that unless we absolutely have to. + */ + + Paint p = new Paint(); + p.setTextSize(100); + Rect r = new Rect(); + p.getTextBounds("ABCDEFG", 0, 7, r); + + sProportion = (r.top) / p.ascent(); + } + + int need = (int) Math.ceil(-fm.top * sProportion); + + if (mSize - 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) { + /* + * 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; + } else { + /* + * Show as much of the ascent as we can, and no descent. + */ + + fm.top = fm.ascent = -mSize; + fm.bottom = fm.descent = 0; + } + } + } + } + + /** + * Create from an existing string block native object. This is + * -extremely- dangerous -- only use it if you absolutely know what you + * are doing! The given native object must exist for the entire lifetime + * of this newly creating StringBlock. + */ + StringBlock(int obj, boolean useSparse) { + mNative = obj; + mUseSparse = useSparse; + mOwnsNative = false; + if (localLOGV) Log.v(TAG, "Created string block " + this + + ": " + nativeGetSize(mNative)); + } + + private static final native int nativeCreate(byte[] data, + int offset, + int size); + private static final native int nativeGetSize(int obj); + private static final native String nativeGetString(int obj, int idx); + private static final native int[] nativeGetStyle(int obj, int idx); + private static final native int nativeIndexOfString(int obj, String str); + private static final native void nativeDestroy(int obj); +} diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java new file mode 100644 index 0000000..3a32c03 --- /dev/null +++ b/core/java/android/content/res/TypedArray.java @@ -0,0 +1,688 @@ +package android.content.res; + +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import com.android.internal.util.XmlUtils; + +import java.util.Arrays; + +/** + * Container for an array of values that were retrieved with + * {@link Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)} + * or {@link Resources#obtainAttributes}. Be + * sure to call {@link #recycle} when done with them. + * + * The indices used to retrieve values from this structure correspond to + * the positions of the attributes given to obtainStyledAttributes. + */ +public class TypedArray { + private final Resources mResources; + /*package*/ XmlBlock.Parser mXml; + /*package*/ int[] mRsrcs; + /*package*/ int[] mData; + /*package*/ int[] mIndices; + /*package*/ int mLength; + private TypedValue mValue = new TypedValue(); + + /** + * Return the number of values in this array. + */ + public int length() { + return mLength; + } + + /** + * Return the number of indices in the array that actually have data. + */ + public int getIndexCount() { + return mIndices[0]; + } + + /** + * Return an index in the array that has data. + * + * @param at The index you would like to returned, ranging from 0 to + * {@link #getIndexCount()}. + * + * @return The index at the given offset, which can be used with + * {@link #getValue} and related APIs. + */ + public int getIndex(int at) { + return mIndices[1+at]; + } + + /** + * Return the Resources object this array was loaded from. + */ + public Resources getResources() { + return mResources; + } + + /** + * Retrieve the styled string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence holding string data. May be styled. Returns + * null if the attribute is not defined. + */ + public CharSequence getText(int index) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return null; + } else if (type == TypedValue.TYPE_STRING) { + return loadStringValueAt(index); + } + + TypedValue v = mValue; + if (getValueAt(index, v)) { + Log.w(Resources.TAG, "Converting to string: " + v); + return v.coerceToString(); + } + Log.w(Resources.TAG, "getString of bad type: 0x" + + Integer.toHexString(type)); + return null; + } + + /** + * Retrieve the string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return String holding string data. Any styling information is + * removed. Returns null if the attribute is not defined. + */ + public String getString(int index) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return null; + } else if (type == TypedValue.TYPE_STRING) { + return loadStringValueAt(index).toString(); + } + + TypedValue v = mValue; + if (getValueAt(index, v)) { + Log.w(Resources.TAG, "Converting to string: " + v); + CharSequence cs = v.coerceToString(); + return cs != null ? cs.toString() : null; + } + Log.w(Resources.TAG, "getString of bad type: 0x" + + Integer.toHexString(type)); + return null; + } + + /** + * Retrieve the string value for the attribute at <var>index</var>, but + * only if that string comes from an immediate value in an XML file. That + * is, this does not allow references to string resources, string + * attributes, or conversions from other types. As such, this method + * will only return strings for TypedArray objects that come from + * attributes in an XML file. + * + * @param index Index of attribute to retrieve. + * + * @return String holding string data. Any styling information is + * removed. Returns null if the attribute is not defined or is not + * an immediate string value. + */ + public String getNonResourceString(int index) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_STRING) { + final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE]; + if (cookie < 0) { + return mXml.getPooledString( + data[index+AssetManager.STYLE_DATA]).toString(); + } + } + return null; + } + + /** + * Retrieve the boolean value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute boolean value, or defValue if not defined. + */ + public boolean getBoolean(int index, boolean defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA] != 0; + } + + TypedValue v = mValue; + if (getValueAt(index, v)) { + Log.w(Resources.TAG, "Converting to boolean: " + v); + return XmlUtils.convertValueToBoolean( + v.coerceToString(), defValue); + } + Log.w(Resources.TAG, "getBoolean of bad type: 0x" + + Integer.toHexString(type)); + return defValue; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute int value, or defValue if not defined. + */ + public int getInt(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } + + TypedValue v = mValue; + if (getValueAt(index, v)) { + Log.w(Resources.TAG, "Converting to int: " + v); + return XmlUtils.convertValueToInt( + v.coerceToString(), defValue); + } + Log.w(Resources.TAG, "getInt of bad type: 0x" + + Integer.toHexString(type)); + return defValue; + } + + /** + * Retrieve the float value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return Attribute float value, or defValue if not defined.. + */ + public float getFloat(int index, float defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type == TypedValue.TYPE_FLOAT) { + return Float.intBitsToFloat(data[index+AssetManager.STYLE_DATA]); + } else if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } + + TypedValue v = mValue; + if (getValueAt(index, v)) { + Log.w(Resources.TAG, "Converting to float: " + v); + CharSequence str = v.coerceToString(); + if (str != null) { + return Float.parseFloat(str.toString()); + } + } + Log.w(Resources.TAG, "getFloat of bad type: 0x" + + Integer.toHexString(type)); + return defValue; + } + + /** + * Retrieve the color value for the attribute at <var>index</var>. If + * the attribute references a color resource holding a complex + * {@link android.content.res.ColorStateList}, then the default color from + * the set is returned. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute color value, or defValue if not defined. + */ + public int getColor(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } else if (type == TypedValue.TYPE_STRING) { + final TypedValue value = mValue; + if (getValueAt(index, value)) { + ColorStateList csl = mResources.loadColorStateList( + value, value.resourceId); + return csl.getDefaultColor(); + } + return defValue; + } + + throw new UnsupportedOperationException("Can't convert to color: type=0x" + + Integer.toHexString(type)); + } + + /** + * Retrieve the ColorStateList for the attribute at <var>index</var>. + * The value may be either a single solid color or a reference to + * a color or complex {@link android.content.res.ColorStateList} description. + * + * @param index Index of attribute to retrieve. + * + * @return ColorStateList for the attribute, or null if not defined. + */ + public ColorStateList getColorStateList(int index) { + final TypedValue value = mValue; + if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { + return mResources.loadColorStateList(value, value.resourceId); + } + return null; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute integer value, or defValue if not defined. + */ + public int getInteger(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } + + throw new UnsupportedOperationException("Can't convert to integer: type=0x" + + Integer.toHexString(type)); + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var>. Unit + * conversions are based on the current {@link DisplayMetrics} + * associated with the resources this {@link TypedArray} object + * came from. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric, or defValue if not defined. + * + * @see #getDimensionPixelOffset + * @see #getDimensionPixelSize + */ + public float getDimension(int index, float defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimension( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + + Integer.toHexString(type)); + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as an offset in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for you. An offset conversion involves simply + * truncating the base value to an integer. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelSize + */ + public int getDimensionPixelOffset(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelOffset( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + + Integer.toHexString(type)); + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as a size in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for use as a size. A size conversion involves + * rounding the base value, and ensuring that a non-zero base value + * is at least one pixel in size. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelOffset + */ + public int getDimensionPixelSize(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + throw new UnsupportedOperationException("Can't convert to dimension: type=0x" + + Integer.toHexString(type)); + } + + /** + * Special version of {@link #getDimensionPixelSize} for retrieving + * {@link android.view.ViewGroup}'s layout_width and layout_height + * attributes. This is only here for performance reasons; applications + * should use {@link #getDimensionPixelSize}. + * + * @param index Index of the attribute to retrieve. + * @param name Textual name of attribute for error reporting. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + */ + public int getLayoutDimension(int index, String name) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + throw new RuntimeException(getPositionDescription() + + ": You must supply a " + name + " attribute."); + } + + /** + * Special version of {@link #getDimensionPixelSize} for retrieving + * {@link android.view.ViewGroup}'s layout_width and layout_height + * attributes. This is only here for performance reasons; applications + * should use {@link #getDimensionPixelSize}. + * + * @param index Index of the attribute to retrieve. + * @param defValue The default value to return if this attribute is not + * default or contains the wrong type of data. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + */ + public int getLayoutDimension(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type >= TypedValue.TYPE_FIRST_INT + && type <= TypedValue.TYPE_LAST_INT) { + return data[index+AssetManager.STYLE_DATA]; + } else if (type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize( + data[index+AssetManager.STYLE_DATA], mResources.mMetrics); + } + + return defValue; + } + + /** + * Retrieve a fractional unit attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute fractional value multiplied by the appropriate + * base value, or defValue if not defined. + */ + public float getFraction(int index, int base, int pbase, float defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return defValue; + } else if (type == TypedValue.TYPE_FRACTION) { + return TypedValue.complexToFraction( + data[index+AssetManager.STYLE_DATA], base, pbase); + } + + throw new UnsupportedOperationException("Can't convert to fraction: type=0x" + + Integer.toHexString(type)); + } + + /** + * Retrieve the resource identifier for the attribute at + * <var>index</var>. Note that attribute resource as resolved when + * the overall {@link TypedArray} object is retrieved. As a + * result, this function will return the resource identifier of the + * final resource value that was found, <em>not</em> necessarily the + * original resource that was specified by the attribute. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute resource identifier, or defValue if not defined. + */ + public int getResourceId(int index, int defValue) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + if (data[index+AssetManager.STYLE_TYPE] != TypedValue.TYPE_NULL) { + final int resid = data[index+AssetManager.STYLE_RESOURCE_ID]; + if (resid != 0) { + return resid; + } + } + return defValue; + } + + /** + * Retrieve the Drawable for the attribute at <var>index</var>. This + * gets the resource ID of the selected attribute, and uses + * {@link Resources#getDrawable Resources.getDrawable} of the owning + * Resources object to retrieve its Drawable. + * + * @param index Index of attribute to retrieve. + * + * @return Drawable for the attribute, or null if not defined. + */ + public Drawable getDrawable(int index) { + final TypedValue value = mValue; + if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { + if (false) { + System.out.println("******************************************************************"); + System.out.println("Got drawable resource: type=" + + value.type + + " str=" + value.string + + " int=0x" + Integer.toHexString(value.data) + + " cookie=" + value.assetCookie); + System.out.println("******************************************************************"); + } + return mResources.loadDrawable(value, value.resourceId); + } + return null; + } + + /** + * Retrieve the CharSequence[] for the attribute at <var>index</var>. + * This gets the resource ID of the selected attribute, and uses + * {@link Resources#getTextArray Resources.getTextArray} of the owning + * Resources object to retrieve its String[]. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence[] for the attribute, or null if not defined. + */ + public CharSequence[] getTextArray(int index) { + final TypedValue value = mValue; + if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { + if (false) { + System.out.println("******************************************************************"); + System.out.println("Got drawable resource: type=" + + value.type + + " str=" + value.string + + " int=0x" + Integer.toHexString(value.data) + + " cookie=" + value.assetCookie); + System.out.println("******************************************************************"); + } + return mResources.getTextArray(value.resourceId); + } + return null; + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param outValue TypedValue object in which to place the attribute's + * data. + * + * @return Returns true if the value was retrieved, else false. + */ + public boolean getValue(int index, TypedValue outValue) { + return getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, outValue); + } + + /** + * Determines whether there is an attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return True if the attribute has a value, false otherwise. + */ + public boolean hasValue(int index) { + index *= AssetManager.STYLE_NUM_ENTRIES; + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + return type != TypedValue.TYPE_NULL; + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var> + * and return a temporary object holding its data. This object is only + * valid until the next call on to {@link TypedArray}. + * + * @param index Index of attribute to retrieve. + * + * @return Returns a TypedValue object if the attribute is defined, + * containing its data; otherwise returns null. (You will not + * receive a TypedValue whose type is TYPE_NULL.) + */ + public TypedValue peekValue(int index) { + final TypedValue value = mValue; + if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) { + return value; + } + return null; + } + + /** + * Returns a message about the parser state suitable for printing error messages. + */ + public String getPositionDescription() { + return mXml != null ? mXml.getPositionDescription() : "<internal>"; + } + + /** + * Give back a previously retrieved StyledAttributes, for later re-use. + */ + public void recycle() { + synchronized (mResources.mTmpValue) { + TypedArray cached = mResources.mCachedStyledAttributes; + if (cached == null || cached.mData.length < mData.length) { + mXml = null; + mResources.mCachedStyledAttributes = this; + } + } + } + + private boolean getValueAt(int index, TypedValue outValue) { + final int[] data = mData; + final int type = data[index+AssetManager.STYLE_TYPE]; + if (type == TypedValue.TYPE_NULL) { + return false; + } + outValue.type = type; + outValue.data = data[index+AssetManager.STYLE_DATA]; + outValue.assetCookie = data[index+AssetManager.STYLE_ASSET_COOKIE]; + outValue.resourceId = data[index+AssetManager.STYLE_RESOURCE_ID]; + outValue.changingConfigurations = data[index+AssetManager.STYLE_CHANGING_CONFIGURATIONS]; + if (type == TypedValue.TYPE_STRING) { + outValue.string = loadStringValueAt(index); + } + return true; + } + + private CharSequence loadStringValueAt(int index) { + final int[] data = mData; + final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE]; + if (cookie < 0) { + if (mXml != null) { + return mXml.getPooledString( + data[index+AssetManager.STYLE_DATA]); + } + return null; + } + //System.out.println("Getting pooled from: " + v); + return mResources.mAssets.getPooledString( + cookie, data[index+AssetManager.STYLE_DATA]); + } + + /*package*/ TypedArray(Resources resources, int[] data, int[] indices, int len) { + mResources = resources; + mData = data; + mIndices = indices; + mLength = len; + } + + public String toString() { + return Arrays.toString(mData); + } +}
\ No newline at end of file diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java new file mode 100644 index 0000000..6336678 --- /dev/null +++ b/core/java/android/content/res/XmlBlock.java @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2006 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.res; + +import android.util.TypedValue; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/** + * Wrapper around a compiled XML file. + * + * {@hide} + */ +final class XmlBlock { + private static final boolean DEBUG=false; + + public XmlBlock(byte[] data) { + mAssets = null; + mNative = nativeCreate(data, 0, data.length); + mStrings = new StringBlock(nativeGetStringBlock(mNative), false); + } + + public XmlBlock(byte[] data, int offset, int size) { + mAssets = null; + mNative = nativeCreate(data, offset, size); + mStrings = new StringBlock(nativeGetStringBlock(mNative), false); + } + + public void close() { + synchronized (this) { + if (mOpen) { + mOpen = false; + decOpenCountLocked(); + } + } + } + + private void decOpenCountLocked() { + mOpenCount--; + if (mOpenCount == 0) { + nativeDestroy(mNative); + if (mAssets != null) { + mAssets.xmlBlockGone(); + } + } + } + + public XmlResourceParser newParser() { + synchronized (this) { + if (mNative != 0) { + return new Parser(nativeCreateParseState(mNative), this); + } + return null; + } + } + + /*package*/ final class Parser implements XmlResourceParser { + Parser(int parseState, XmlBlock block) { + mParseState = parseState; + mBlock = block; + block.mOpenCount++; + } + + public void setFeature(String name, boolean state) throws XmlPullParserException { + if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) { + return; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) { + return; + } + throw new XmlPullParserException("Unsupported feature: " + name); + } + public boolean getFeature(String name) { + if (FEATURE_PROCESS_NAMESPACES.equals(name)) { + return true; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return true; + } + return false; + } + public void setProperty(String name, Object value) throws XmlPullParserException { + throw new XmlPullParserException("setProperty() not supported"); + } + public Object getProperty(String name) { + return null; + } + public void setInput(Reader in) throws XmlPullParserException { + throw new XmlPullParserException("setInput() not supported"); + } + public void setInput(InputStream inputStream, String inputEncoding) throws XmlPullParserException { + throw new XmlPullParserException("setInput() not supported"); + } + public void defineEntityReplacementText(String entityName, String replacementText) throws XmlPullParserException { + throw new XmlPullParserException("defineEntityReplacementText() not supported"); + } + public String getNamespacePrefix(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespacePrefix() not supported"); + } + public String getInputEncoding() { + return null; + } + public String getNamespace(String prefix) { + throw new RuntimeException("getNamespace() not supported"); + } + public int getNamespaceCount(int depth) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceCount() not supported"); + } + public String getPositionDescription() { + return "Binary XML file line #" + getLineNumber(); + } + public String getNamespaceUri(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceUri() not supported"); + } + public int getColumnNumber() { + return -1; + } + public int getDepth() { + return mDepth; + } + public String getText() { + int id = nativeGetText(mParseState); + return id >= 0 ? mStrings.get(id).toString() : null; + } + public int getLineNumber() { + return nativeGetLineNumber(mParseState); + } + public int getEventType() throws XmlPullParserException { + return mEventType; + } + public boolean isWhitespace() throws XmlPullParserException { + // whitespace was stripped by aapt. + return false; + } + public String getPrefix() { + throw new RuntimeException("getPrefix not supported"); + } + public char[] getTextCharacters(int[] holderForStartAndLength) { + String txt = getText(); + char[] chars = null; + if (txt != null) { + holderForStartAndLength[0] = 0; + holderForStartAndLength[1] = txt.length(); + chars = new char[txt.length()]; + txt.getChars(0, txt.length(), chars, 0); + } + return chars; + } + public String getNamespace() { + int id = nativeGetNamespace(mParseState); + return id >= 0 ? mStrings.get(id).toString() : ""; + } + public String getName() { + int id = nativeGetName(mParseState); + return id >= 0 ? mStrings.get(id).toString() : null; + } + public String getAttributeNamespace(int index) { + int id = nativeGetAttributeNamespace(mParseState, index); + if (DEBUG) System.out.println("getAttributeNamespace of " + index + " = " + id); + if (id >= 0) return mStrings.get(id).toString(); + else if (id == -1) return ""; + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + public String getAttributeName(int index) { + int id = nativeGetAttributeName(mParseState, index); + if (DEBUG) System.out.println("getAttributeName of " + index + " = " + id); + if (id >= 0) return mStrings.get(id).toString(); + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + public String getAttributePrefix(int index) { + throw new RuntimeException("getAttributePrefix not supported"); + } + public boolean isEmptyElementTag() throws XmlPullParserException { + // XXX Need to detect this. + return false; + } + public int getAttributeCount() { + return mEventType == START_TAG ? nativeGetAttributeCount(mParseState) : -1; + } + public String getAttributeValue(int index) { + int id = nativeGetAttributeStringValue(mParseState, index); + if (DEBUG) System.out.println("getAttributeValue of " + index + " = " + id); + if (id >= 0) return mStrings.get(id).toString(); + + // May be some other type... check and try to convert if so. + int t = nativeGetAttributeDataType(mParseState, index); + if (t == TypedValue.TYPE_NULL) { + throw new IndexOutOfBoundsException(String.valueOf(index)); + } + + int v = nativeGetAttributeData(mParseState, index); + return TypedValue.coerceToString(t, v); + } + public String getAttributeType(int index) { + return "CDATA"; + } + public boolean isAttributeDefault(int index) { + return false; + } + public int nextToken() throws XmlPullParserException,IOException { + return next(); + } + public String getAttributeValue(String namespace, String name) { + int idx = nativeGetAttributeIndex(mParseState, namespace, name); + if (idx >= 0) { + if (DEBUG) System.out.println("getAttributeName of " + + namespace + ":" + name + " index = " + idx); + if (DEBUG) System.out.println( + "Namespace=" + getAttributeNamespace(idx) + + "Name=" + getAttributeName(idx) + + ", Value=" + getAttributeValue(idx)); + return getAttributeValue(idx); + } + return null; + } + public int next() throws XmlPullParserException,IOException { + if (!mStarted) { + mStarted = true; + return START_DOCUMENT; + } + if (mParseState == 0) { + return END_DOCUMENT; + } + int ev = nativeNext(mParseState); + if (mDecNextDepth) { + mDepth--; + mDecNextDepth = false; + } + switch (ev) { + case START_TAG: + mDepth++; + break; + case END_TAG: + mDecNextDepth = true; + break; + } + mEventType = ev; + if (ev == END_DOCUMENT) { + // Automatically close the parse when we reach the end of + // a document, since the standard XmlPullParser interface + // doesn't have such an API so most clients will leave us + // dangling. + close(); + } + return ev; + } + public void require(int type, String namespace, String name) throws XmlPullParserException,IOException { + if (type != getEventType() + || (namespace != null && !namespace.equals( getNamespace () ) ) + || (name != null && !name.equals( getName() ) ) ) + throw new XmlPullParserException( "expected "+ TYPES[ type ]+getPositionDescription()); + } + public String nextText() throws XmlPullParserException,IOException { + if(getEventType() != START_TAG) { + throw new XmlPullParserException( + getPositionDescription() + + ": parser must be on START_TAG to read next text", this, null); + } + int eventType = next(); + if(eventType == TEXT) { + String result = getText(); + eventType = next(); + if(eventType != END_TAG) { + throw new XmlPullParserException( + getPositionDescription() + + ": event TEXT it must be immediately followed by END_TAG", this, null); + } + return result; + } else if(eventType == END_TAG) { + return ""; + } else { + throw new XmlPullParserException( + getPositionDescription() + + ": parser must be on START_TAG or TEXT to read text", this, null); + } + } + public int nextTag() throws XmlPullParserException,IOException { + int eventType = next(); + if(eventType == TEXT && isWhitespace()) { // skip whitespace + eventType = next(); + } + if (eventType != START_TAG && eventType != END_TAG) { + throw new XmlPullParserException( + getPositionDescription() + + ": expected start or end tag", this, null); + } + return eventType; + } + + public int getAttributeNameResource(int index) { + return nativeGetAttributeResource(mParseState, index); + } + + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeListValue(idx, options, defaultValue); + } + return defaultValue; + } + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeBooleanValue(idx, defaultValue); + } + return defaultValue; + } + public int getAttributeResourceValue(String namespace, String attribute, + int defaultValue) { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeResourceValue(idx, defaultValue); + } + return defaultValue; + } + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue) { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeIntValue(idx, defaultValue); + } + return defaultValue; + } + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) + { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeUnsignedIntValue(idx, defaultValue); + } + return defaultValue; + } + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + int idx = nativeGetAttributeIndex(mParseState, namespace, attribute); + if (idx >= 0) { + return getAttributeFloatValue(idx, defaultValue); + } + return defaultValue; + } + + public int getAttributeListValue(int idx, + String[] options, int defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + int v = nativeGetAttributeData(mParseState, idx); + if (t == TypedValue.TYPE_STRING) { + return XmlUtils.convertValueToList( + mStrings.get(v), options, defaultValue); + } + return v; + } + public boolean getAttributeBooleanValue(int idx, + boolean defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + // Note: don't attempt to convert any other types, because + // we want to count on appt doing the conversion for us. + if (t >= TypedValue.TYPE_FIRST_INT && + t <= TypedValue.TYPE_LAST_INT) { + return nativeGetAttributeData(mParseState, idx) != 0; + } + return defaultValue; + } + public int getAttributeResourceValue(int idx, int defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + // Note: don't attempt to convert any other types, because + // we want to count on appt doing the conversion for us. + if (t == TypedValue.TYPE_REFERENCE) { + return nativeGetAttributeData(mParseState, idx); + } + return defaultValue; + } + public int getAttributeIntValue(int idx, int defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + // Note: don't attempt to convert any other types, because + // we want to count on appt doing the conversion for us. + if (t >= TypedValue.TYPE_FIRST_INT && + t <= TypedValue.TYPE_LAST_INT) { + return nativeGetAttributeData(mParseState, idx); + } + return defaultValue; + } + public int getAttributeUnsignedIntValue(int idx, int defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + // Note: don't attempt to convert any other types, because + // we want to count on appt doing the conversion for us. + if (t >= TypedValue.TYPE_FIRST_INT && + t <= TypedValue.TYPE_LAST_INT) { + return nativeGetAttributeData(mParseState, idx); + } + return defaultValue; + } + public float getAttributeFloatValue(int idx, float defaultValue) { + int t = nativeGetAttributeDataType(mParseState, idx); + // Note: don't attempt to convert any other types, because + // we want to count on appt doing the conversion for us. + if (t == TypedValue.TYPE_FLOAT) { + return Float.intBitsToFloat( + nativeGetAttributeData(mParseState, idx)); + } + throw new RuntimeException("not a float!"); + } + + public String getIdAttribute() { + int id = nativeGetIdAttribute(mParseState); + return id >= 0 ? mStrings.get(id).toString() : null; + } + public String getClassAttribute() { + int id = nativeGetClassAttribute(mParseState); + return id >= 0 ? mStrings.get(id).toString() : null; + } + + public int getIdAttributeResourceValue(int defaultValue) { + //todo: create and use native method + return getAttributeResourceValue(null, "id", defaultValue); + } + + public int getStyleAttribute() { + return nativeGetStyleAttribute(mParseState); + } + + public void close() { + synchronized (mBlock) { + if (mParseState != 0) { + nativeDestroyParseState(mParseState); + mParseState = 0; + mBlock.decOpenCountLocked(); + } + } + } + + protected void finalize() throws Throwable { + close(); + } + + /*package*/ final CharSequence getPooledString(int id) { + return mStrings.get(id); + } + + /*package*/ int mParseState; + private final XmlBlock mBlock; + private boolean mStarted = false; + private boolean mDecNextDepth = false; + private int mDepth = 0; + private int mEventType = START_DOCUMENT; + } + + protected void finalize() throws Throwable { + close(); + } + + /** + * Create from an existing xml block native object. This is + * -extremely- dangerous -- only use it if you absolutely know what you + * are doing! The given native object must exist for the entire lifetime + * of this newly creating XmlBlock. + */ + XmlBlock(AssetManager assets, int xmlBlock) { + mAssets = assets; + mNative = xmlBlock; + mStrings = new StringBlock(nativeGetStringBlock(xmlBlock), false); + } + + private final AssetManager mAssets; + private final int mNative; + private final StringBlock mStrings; + private boolean mOpen = true; + private int mOpenCount = 1; + + private static final native int nativeCreate(byte[] data, + int offset, + int size); + private static final native int nativeGetStringBlock(int obj); + + private static final native int nativeCreateParseState(int obj); + private static final native int nativeNext(int state); + private static final native int nativeGetNamespace(int state); + private static final native int nativeGetName(int state); + private static final native int nativeGetText(int state); + private static final native int nativeGetLineNumber(int state); + private static final native int nativeGetAttributeCount(int state); + private static final native int nativeGetAttributeNamespace(int state, int idx); + private static final native int nativeGetAttributeName(int state, int idx); + private static final native int nativeGetAttributeResource(int state, int idx); + private static final native int nativeGetAttributeDataType(int state, int idx); + private static final native int nativeGetAttributeData(int state, int idx); + private static final native int nativeGetAttributeStringValue(int state, int idx); + private static final native int nativeGetIdAttribute(int state); + private static final native int nativeGetClassAttribute(int state); + private static final native int nativeGetStyleAttribute(int state); + private static final native int nativeGetAttributeIndex(int state, String namespace, String name); + private static final native void nativeDestroyParseState(int state); + + private static final native void nativeDestroy(int obj); +} diff --git a/core/java/android/content/res/XmlResourceParser.java b/core/java/android/content/res/XmlResourceParser.java new file mode 100644 index 0000000..c59e6d4 --- /dev/null +++ b/core/java/android/content/res/XmlResourceParser.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2006 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.res; + +import org.xmlpull.v1.XmlPullParser; + +import android.util.AttributeSet; + +/** + * The XML parsing interface returned for an XML resource. This is a standard + * XmlPullParser interface, as well as an extended AttributeSet interface and + * an additional close() method on this interface for the client to indicate + * when it is done reading the resource. + */ +public interface XmlResourceParser extends XmlPullParser, AttributeSet { + /** + * Close this interface to the resource. Calls on the interface are no + * longer value after this call. + */ + public void close(); +} + diff --git a/core/java/android/content/res/package.html b/core/java/android/content/res/package.html new file mode 100644 index 0000000..bb09dc7 --- /dev/null +++ b/core/java/android/content/res/package.html @@ -0,0 +1,8 @@ +<HTML> +<BODY> +Contains classes for accessing application resources, +such as raw asset files, colors, drawables, media or other other files +in the package, plus important device configuration details +(orientation, input types, etc.) that affect how the application may behave. +</BODY> +</HTML>
\ No newline at end of file |