diff options
13 files changed, 561 insertions, 191 deletions
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index bcb279151b35..0bb483b40d8b 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -1202,16 +1202,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { Cursor c = mSuggestionsAdapter.getCursor(); if ((c != null) && c.moveToPosition(position)) { - // let the cursor know which position was clicked - final Bundle clickResponse = new Bundle(1); - clickResponse.putInt(SearchManager.RESPOND_EXTRA_POSITION_CLICKED, position); - final Bundle response = c.respond(clickResponse); - - // the convention is to send a position to select in response to a click (if applicable) - final int posToSelect = response.getInt( - SearchManager.RESPOND_EXTRA_POSITION_SELECTED, - SuggestionsAdapter.NO_ITEM_TO_SELECT); - mSuggestionsAdapter.setListItemToSelect(posToSelect); + mSuggestionsAdapter.callCursorOnClick(c, position); // launch the intent Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index b4a3a78eddeb..b0c248c52b11 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -1167,38 +1167,50 @@ public class SearchManager */ public final static String EXTRA_DATA_KEY = "intent_extra_data_key"; - /** - * Used by the search dialog to ask the global search provider whether there are any pending - * sources that have yet to respond. Specifically, the search dialog will call - * {@link Cursor#respond} with a bundle containing this extra as a key, and expect the same key - * to be in the response, with a boolean value indicating whether there are pending sources. + * Defines the constants used in the communication between {@link android.app.SearchDialog} and + * the global search provider via {@link Cursor#respond(android.os.Bundle)}. * - * {@hide} + * @hide */ - public final static String RESPOND_EXTRA_PENDING_SOURCES = "respond_extra_pending_sources"; + public static class DialogCursorProtocol { - /** - * Used by the search dialog to tell the cursor that supplied suggestions which item was clicked - * before launching the intent. The search dialog will call {@link Cursor#respond} with a - * bundle containing this extra as a key and the position that was clicked as the value. - * - * The response bundle will use {@link #RESPOND_EXTRA_POSITION_SELECTED} to return an int value - * of the index that should be selected, if applicable. - * - * {@hide} - */ - public final static String RESPOND_EXTRA_POSITION_CLICKED = "respond_extra_position_clicked"; + /** + * The sent bundle will contain this integer key, with a value set to one of the events + * below. + */ + public final static String METHOD = "DialogCursorProtocol.method"; - /** - * Used as a key in the response bundle from a call to {@link Cursor#respond} that sends the - * position that is clicked. - * - * @see #RESPOND_EXTRA_POSITION_CLICKED - * - * {@hide} - */ - public final static String RESPOND_EXTRA_POSITION_SELECTED = "respond_extra_position_selected"; + /** + * After data has been refreshed. + */ + public final static int POST_REFRESH = 0; + public final static String POST_REFRESH_RECEIVE_ISPENDING + = "DialogCursorProtocol.POST_REFRESH.isPending"; + public final static String POST_REFRESH_RECEIVE_DISPLAY_NOTIFY + = "DialogCursorProtocol.POST_REFRESH.displayNotify"; + + /** + * Just before closing the cursor. + */ + public final static int PRE_CLOSE = 1; + public final static String PRE_CLOSE_SEND_MAX_DISPLAY_POS + = "DialogCursorProtocol.PRE_CLOSE.sendDisplayPosition"; + + /** + * When a position has been clicked. + */ + public final static int CLICK = 2; + public final static String CLICK_SEND_POSITION + = "DialogCursorProtocol.CLICK.sendPosition"; + public final static String CLICK_RECEIVE_SELECTED_POS + = "DialogCursorProtocol.CLICK.receiveSelectedPosition"; + + /** + * When the threshold received in {@link #POST_REFRESH_RECEIVE_DISPLAY_NOTIFY} is displayed. + */ + public final static int THRESH_HIT = 3; + } /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and @@ -1292,28 +1304,6 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2"; /** - * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column, - * then all suggestions will be provided in a format that includes space for two small icons, - * one at the left and one at the right of each suggestion. The data in the column must - * be a blob that contains a bitmap. - * - * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_1} column. - * - * @hide - */ - public final static String SUGGEST_COLUMN_ICON_1_BITMAP = "suggest_icon_1_bitmap"; - /** - * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column, - * then all suggestions will be provided in a format that includes space for two small icons, - * one at the left and one at the right of each suggestion. The data in the column must - * be a blob that contains a bitmap. - * - * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_2} column. - * - * @hide - */ - public final static String SUGGEST_COLUMN_ICON_2_BITMAP = "suggest_icon_2_bitmap"; - /** * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i> * this element exists at the given row, this is the action that will be used when * forming the suggestion's intent. If the element is not provided, the action will be taken @@ -1415,6 +1405,16 @@ public class SearchManager */ public final static String INTENT_ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS"; + + /** + * Intent action for starting a web search provider's settings activity. + * Web search providers should handle this intent if they have provider-specific + * settings to implement. + * + * @hide Pending API council approval. + */ + public final static String INTENT_ACTION_WEB_SEARCH_SETTINGS + = "android.search.action.WEB_SEARCH_SETTINGS"; /** * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 747bec9944dc..e515594e5706 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -20,9 +20,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources.NotFoundException; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; @@ -36,6 +33,8 @@ import android.widget.ImageView; import android.widget.ResourceCursorAdapter; import android.widget.TextView; +import static android.app.SearchManager.DialogCursorProtocol; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -47,15 +46,7 @@ import java.util.WeakHashMap; * @hide */ class SuggestionsAdapter extends ResourceCursorAdapter { - // The value used to query a cursor whether it is still expecting more input, - // so we can correctly display (or not display) the 'working' spinner in the search dialog. - public static final String IS_WORKING = "isWorking"; - - // The value used to tell a cursor to display the corpus selectors, if this is global - // search. Also returns the index of the more results item to allow the SearchDialog - // to tell the ListView to scroll to that list item. - public static final String SHOW_CORPUS_SELECTORS = "showCorpusSelectors"; - + private static final boolean DBG = false; private static final String LOG_TAG = "SuggestionsAdapter"; @@ -71,17 +62,21 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private int mText2Col; private int mIconName1Col; private int mIconName2Col; - private int mIconBitmap1Col; - private int mIconBitmap2Col; // This value is stored in SuggestionsAdapter by the SearchDialog to indicate whether // a particular list item should be selected upon the next call to notifyDataSetChanged. // This is used to indicate the index of the "More results..." list item so that when // the data set changes after a click of "More results...", we can correctly tell the - // ListView to scroll to the right line item. It gets reset to NO_ITEM_TO_SELECT every time it + // ListView to scroll to the right line item. It gets reset to NONE every time it // is consumed. - private int mListItemToSelect = NO_ITEM_TO_SELECT; - static final int NO_ITEM_TO_SELECT = -1; + private int mListItemToSelect = NONE; + static final int NONE = -1; + + // holds the maximum position that has been displayed to the user + int mMaxDisplayed = NONE; + + // holds the position that, when displayed, should result in notifying the cursor + int mDisplayNotifyPos = NONE; public SuggestionsAdapter(Context context, SearchDialog searchDialog, SearchableInfo searchable, WeakHashMap<String, Drawable> outsideDrawablesCache, boolean globalSearchMode) { @@ -132,6 +127,11 @@ class SuggestionsAdapter extends ResourceCursorAdapter { @Override public void changeCursor(Cursor c) { if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); + + if (mCursor != null) { + callCursorPreClose(mCursor); + } + super.changeCursor(c); if (c != null) { mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT); @@ -139,42 +139,73 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); - mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP); - mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP); } - updateWorking(); } - + + /** + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#PRE_CLOSE}. + * + * @param cursor The cursor to call. + */ + private void callCursorPreClose(Cursor cursor) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.PRE_CLOSE); + request.putInt(DialogCursorProtocol.PRE_CLOSE_SEND_MAX_DISPLAY_POS, mMaxDisplayed); + final Bundle response = cursor.respond(request); + + mMaxDisplayed = -1; + } + @Override public void notifyDataSetChanged() { + if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); super.notifyDataSetChanged(); - updateWorking(); - if (mListItemToSelect != NO_ITEM_TO_SELECT) { + + callCursorPostRefresh(mCursor); + + // look out for the pending item we are supposed to scroll to + if (mListItemToSelect != NONE) { mSearchDialog.setListSelection(mListItemToSelect); - mListItemToSelect = NO_ITEM_TO_SELECT; + mListItemToSelect = NONE; } } - + /** - * Specifies the list item to select upon next call of {@link #notifyDataSetChanged()}, - * in order to let us scroll the "More results..." list item to the top of the screen - * (or as close as it can get) when clicked. + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#POST_REFRESH}. + * + * @param cursor The cursor to call. */ - public void setListItemToSelect(int index) { - mListItemToSelect = index; + private void callCursorPostRefresh(Cursor cursor) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.POST_REFRESH); + final Bundle response = cursor.respond(request); + + mSearchDialog.setWorking( + response.getBoolean(DialogCursorProtocol.POST_REFRESH_RECEIVE_ISPENDING, false)); + + mDisplayNotifyPos = + response.getInt(DialogCursorProtocol.POST_REFRESH_RECEIVE_DISPLAY_NOTIFY, -1); } - + /** - * Updates the search dialog according to the current working status of the cursor. + * Tell the cursor which position was clicked, handling sending and receiving information + * associated with {@link DialogCursorProtocol#CLICK}. + * + * @param cursor The cursor + * @param position The position that was clicked. */ - private void updateWorking() { - if (!mGlobalSearchMode || mCursor == null) return; - - Bundle request = new Bundle(); - request.putString(SearchManager.RESPOND_EXTRA_PENDING_SOURCES, "DUMMY"); - Bundle response = mCursor.respond(request); - - mSearchDialog.setWorking(response.getBoolean(SearchManager.RESPOND_EXTRA_PENDING_SOURCES)); + void callCursorOnClick(Cursor cursor, int position) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(1); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.CLICK); + request.putInt(DialogCursorProtocol.CLICK_SEND_POSITION, position); + final Bundle response = cursor.respond(request); + mListItemToSelect = response.getInt( + DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, SuggestionsAdapter.NONE); } /** @@ -186,7 +217,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { v.setTag(new ChildViewCache(v)); return v; } - + /** * Cache of the child views of drop-drown list items, to avoid looking up the children * each time the contents of a list item are changed. @@ -208,15 +239,26 @@ class SuggestionsAdapter extends ResourceCursorAdapter { @Override public void bindView(View view, Context context, Cursor cursor) { ChildViewCache views = (ChildViewCache) view.getTag(); - boolean isHtml = false; - if (mFormatCol >= 0) { - String format = cursor.getString(mFormatCol); - isHtml = "html".equals(format); + final int pos = cursor.getPosition(); + + // update the maximum position displayed since last refresh + if (pos > mMaxDisplayed) { + mMaxDisplayed = pos; } + + // if the cursor wishes to be notified about this position, send it + if (mGlobalSearchMode && mDisplayNotifyPos != NONE && pos == mDisplayNotifyPos) { + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.THRESH_HIT); + mCursor.respond(request); + mDisplayNotifyPos = NONE; // only notify the first time + } + + final boolean isHtml = mFormatCol > 0 && "html".equals(cursor.getString(mFormatCol)); setViewText(cursor, views.mText1, mText1Col, isHtml); setViewText(cursor, views.mText2, mText2Col, isHtml); - setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col); - setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col); + setViewIcon(cursor, views.mIcon1, mIconName1Col); + setViewIcon(cursor, views.mIcon2, mIconName2Col); } private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) { @@ -238,26 +280,15 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } } - private void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) { + private void setViewIcon(Cursor cursor, ImageView v, int iconNameCol) { if (v == null) { return; } - Drawable drawable = null; - // First try the bitmap column - if (iconBitmapCol >= 0) { - byte[] data = cursor.getBlob(iconBitmapCol); - if (data != null) { - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap != null) { - drawable = new BitmapDrawable(bitmap); - } - } - } - // If there was no bitmap, try the icon resource column. - if (drawable == null && iconNameCol >= 0) { - String value = cursor.getString(iconNameCol); - drawable = getDrawableFromResourceValue(value); + if (iconNameCol < 0) { + return; } + String value = cursor.getString(iconNameCol); + Drawable drawable = getDrawableFromResourceValue(value); // Set the icon even if the drawable is null, since we need to clear any // previous icon. v.setImageDrawable(drawable); @@ -418,7 +449,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { */ public static String getColumnString(Cursor cursor, String columnName) { int col = cursor.getColumnIndex(columnName); - if (col == NO_ITEM_TO_SELECT) { + if (col == NONE) { return null; } return cursor.getString(col); diff --git a/core/java/android/backup/BackupManager.java b/core/java/android/backup/BackupManager.java index 6f0b2eef603f..30f781e659e5 100644 --- a/core/java/android/backup/BackupManager.java +++ b/core/java/android/backup/BackupManager.java @@ -42,6 +42,12 @@ public class BackupManager { private IBackupManager mService; /** + * Defined backup transports understood by {@link IBackupManager.selectBackupTransport}. + */ + public static final int TRANSPORT_ADB = 1; + public static final int TRANSPORT_GOOGLE = 2; + + /** * Constructs a BackupManager object through which the application can * communicate with the Android backup system. * diff --git a/core/java/android/backup/IBackupManager.aidl b/core/java/android/backup/IBackupManager.aidl index 3468d70962c2..f5b82fe57851 100644 --- a/core/java/android/backup/IBackupManager.aidl +++ b/core/java/android/backup/IBackupManager.aidl @@ -36,20 +36,28 @@ interface IBackupManager { /** * Notifies the Backup Manager Service that an agent has become available. This * method is only invoked by the Activity Manager. - * !!! TODO: permission */ oneway void agentConnected(String packageName, IBinder agent); /** * Notify the Backup Manager Service that an agent has unexpectedly gone away. * This method is only invoked by the Activity Manager. - * !!! TODO: permission */ oneway void agentDisconnected(String packageName); /** - * Schedule a full backup of the given package. - * !!! TODO: permission + * Schedule a full backup of the given package. Callers must hold the + * android.permission.BACKUP permission to use this method. */ oneway void scheduleFullBackup(String packageName); + + /** + * Specify a default backup transport. Callers must hold the + * android.permission.BACKUP permission to use this method. + * + * @param transportID The ID of the transport to select. This should be one + * of {@link BackupManager.TRANSPORT_GOOGLE} or {@link BackupManager.TRANSPORT_ADB}. + * @return The ID of the previously selected transport. + */ + int selectBackupTransport(int transportID); } diff --git a/core/java/android/database/sqlite/SQLiteContentHelper.java b/core/java/android/database/sqlite/SQLiteContentHelper.java new file mode 100644 index 000000000000..2800d86279b1 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteContentHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.database.sqlite; + +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.os.MemoryFile; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Some helper functions for using SQLite database to implement content providers. + * + * @hide + */ +public class SQLiteContentHelper { + + /** + * Runs an SQLite query and returns an AssetFileDescriptor for the + * blob in column 0 of the first row. If the first column does + * not contain a blob, an unspecified exception is thrown. + * + * @param db Handle to a readable database. + * @param sql SQL query, possibly with query arguments. + * @param selectionArgs Query argument values, or {@code null} for no argument. + * @return If no exception is thrown, a non-null AssetFileDescriptor is returned. + * @throws FileNotFoundException If the query returns no results or the + * value of column 0 is NULL, or if there is an error creating the + * asset file descriptor. + */ + public static AssetFileDescriptor getBlobColumnAsAssetFile(SQLiteDatabase db, String sql, + String[] selectionArgs) throws FileNotFoundException { + try { + MemoryFile file = simpleQueryForBlobMemoryFile(db, sql, selectionArgs); + if (file == null) { + throw new FileNotFoundException("No results."); + } + return AssetFileDescriptor.fromMemoryFile(file); + } catch (IOException ex) { + throw new FileNotFoundException(ex.toString()); + } + } + + /** + * Runs an SQLite query and returns a MemoryFile for the + * blob in column 0 of the first row. If the first column does + * not contain a blob, an unspecified exception is thrown. + * + * @return A memory file, or {@code null} if the query returns no results + * or the value column 0 is NULL. + * @throws IOException If there is an error creating the memory file. + */ + // TODO: make this native and use the SQLite blob API to reduce copying + private static MemoryFile simpleQueryForBlobMemoryFile(SQLiteDatabase db, String sql, + String[] selectionArgs) throws IOException { + Cursor cursor = db.rawQuery(sql, selectionArgs); + if (cursor == null) { + return null; + } + try { + if (!cursor.moveToFirst()) { + return null; + } + byte[] bytes = cursor.getBlob(0); + if (bytes == null) { + return null; + } + MemoryFile file = new MemoryFile(null, bytes.length); + file.writeBytes(bytes, 0, 0, bytes.length); + file.deactivate(); + return file; + } finally { + cursor.close(); + } + } + +} diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java index 842fc7573aef..c08314272d50 100644 --- a/core/java/android/server/search/SearchableInfo.java +++ b/core/java/android/server/search/SearchableInfo.java @@ -40,7 +40,7 @@ import java.util.HashMap; public final class SearchableInfo implements Parcelable { // general debugging support - private static final boolean DBG = true; + private static final boolean DBG = false; private static final String LOG_TAG = "SearchableInfo"; // static strings used for XML lookups. diff --git a/core/java/com/android/internal/backup/AdbTransport.java b/core/java/com/android/internal/backup/AdbTransport.java new file mode 100644 index 000000000000..acb3273a77b4 --- /dev/null +++ b/core/java/com/android/internal/backup/AdbTransport.java @@ -0,0 +1,28 @@ +package com.android.internal.backup; + +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +/** + * Backup transport for full backup over adb. This transport pipes everything to + * a file in a known location in /cache, which 'adb backup' then pulls to the desktop + * (deleting it afterwards). + */ + +public class AdbTransport extends IBackupTransport.Stub { + + public int startSession() throws RemoteException { + return 0; + } + + public int endSession() throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + public int performBackup(String packageName, ParcelFileDescriptor data) + throws RemoteException { + // TODO Auto-generated method stub + return 0; + } +} diff --git a/core/java/com/android/internal/backup/GoogleTransport.java b/core/java/com/android/internal/backup/GoogleTransport.java new file mode 100644 index 000000000000..85ab21eac425 --- /dev/null +++ b/core/java/com/android/internal/backup/GoogleTransport.java @@ -0,0 +1,28 @@ +package com.android.internal.backup; + +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +/** + * Backup transport for saving data to Google cloud storage. + */ + +public class GoogleTransport extends IBackupTransport.Stub { + + public int endSession() throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + public int performBackup(String packageName, ParcelFileDescriptor data) + throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + public int startSession() throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + +} diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index ce39768137e2..2b44fe7c8393 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -16,6 +16,7 @@ package com.android.internal.backup; +import android.os.Bundle; import android.os.ParcelFileDescriptor; /** {@hide} */ diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index 82ed1e39e0e5..b003e76c5cec 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -34,11 +35,17 @@ import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.RemoteException; import android.util.Log; import android.util.SparseArray; import android.backup.IBackupManager; +import android.backup.BackupManager; + +import com.android.internal.backup.AdbTransport; +import com.android.internal.backup.GoogleTransport; +import com.android.internal.backup.IBackupTransport; import java.io.File; import java.io.FileDescriptor; @@ -59,6 +66,7 @@ class BackupManagerService extends IBackupManager.Stub { //private static final long COLLECTION_INTERVAL = 3 * 60 * 1000; private static final int MSG_RUN_BACKUP = 1; + private static final int MSG_RUN_FULL_BACKUP = 2; private Context mContext; private PackageManager mPackageManager; @@ -89,6 +97,16 @@ class BackupManagerService extends IBackupManager.Stub { private ArrayList<BackupRequest> mBackupQueue; private final Object mQueueLock = new Object(); + // The thread performing the sequence of queued backups binds to each app's agent + // in succession. Bind notifications are asynchronously delivered through the + // Activity Manager; use this lock object to signal when a requested binding has + // completed. + private final Object mAgentConnectLock = new Object(); + private IBackupAgent mConnectedAgent; + private volatile boolean mConnecting; + + private int mTransportId; + private File mStateDir; private File mDataDir; @@ -101,6 +119,7 @@ class BackupManagerService extends IBackupManager.Stub { mStateDir = new File(Environment.getDataDirectory(), "backup"); mStateDir.mkdirs(); mDataDir = Environment.getDownloadCacheDirectory(); + mTransportId = BackupManager.TRANSPORT_GOOGLE; // Build our mapping of uid to backup client services synchronized (mBackupParticipants) { @@ -130,6 +149,8 @@ class BackupManagerService extends IBackupManager.Stub { return; } + // !!! TODO: this is buggy right now; we wind up with duplicate participant entries + // after using 'adb install -r' of a participating app String action = intent.getAction(); if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { synchronized (mBackupParticipants) { @@ -166,7 +187,7 @@ class BackupManagerService extends IBackupManager.Stub { // snapshot the pending-backup set and work on that synchronized (mQueueLock) { if (mBackupQueue == null) { - mBackupQueue = new ArrayList(); + mBackupQueue = new ArrayList<BackupRequest>(); for (BackupRequest b: mPendingBackups.values()) { mBackupQueue.add(b); } @@ -176,7 +197,11 @@ class BackupManagerService extends IBackupManager.Stub { // WARNING: If we crash after this line, anything in mPendingBackups will // be lost. FIX THIS. } - startOneAgent(); + //startOneAgent(); + (new PerformBackupThread(mTransportId, mBackupQueue)).run(); + break; + + case MSG_RUN_FULL_BACKUP: break; } } @@ -221,23 +246,15 @@ class BackupManagerService extends IBackupManager.Stub { } } - void processOneBackup(String packageName, IBackupAgent bs) { + void processOneBackup(BackupRequest request, IBackupAgent agent, IBackupTransport transport) { + final String packageName = request.appInfo.packageName; Log.d(TAG, "processOneBackup doBackup() on " + packageName); - BackupRequest request; - synchronized (mQueueLock) { - if (mBackupQueue == null) { - Log.d(TAG, "mBackupQueue is null. WHY?"); - } - request = mBackupQueue.get(0); - } - try { - // !!! TODO right now these naming schemes limit applications to - // one backup service per package - File savedStateName = new File(mStateDir, request.appInfo.packageName); - File backupDataName = new File(mDataDir, request.appInfo.packageName + ".data"); - File newStateName = new File(mStateDir, request.appInfo.packageName + ".new"); + // !!! TODO: get the state file dir from the transport + File savedStateName = new File(mStateDir, packageName); + File backupDataName = new File(mDataDir, packageName + ".data"); + File newStateName = new File(mStateDir, packageName + ".new"); // In a full backup, we pass a null ParcelFileDescriptor as // the saved-state "file" @@ -259,9 +276,10 @@ class BackupManagerService extends IBackupManager.Stub { ParcelFileDescriptor.MODE_CREATE); // Run the target's backup pass + boolean success = false; try { - // TODO: Make this oneway - bs.doBackup(savedState, backupData, newState); + agent.doBackup(savedState, backupData, newState); + success = true; } finally { if (savedState != null) { savedState.close(); @@ -270,43 +288,34 @@ class BackupManagerService extends IBackupManager.Stub { newState.close(); } - // !!! TODO: Now propagate the newly-backed-up data to the transport - - // !!! TODO: After successful transport, delete the now-stale data - // and juggle the files so that next time the new state is passed - //backupDataName.delete(); - newStateName.renameTo(savedStateName); - + // Now propagate the newly-backed-up data to the transport + if (success) { + backupData = + ParcelFileDescriptor.open(backupDataName, ParcelFileDescriptor.MODE_READ_ONLY); + int error = transport.performBackup(packageName, backupData); + + // !!! TODO: After successful transport, delete the now-stale data + // and juggle the files so that next time the new state is passed + //backupDataName.delete(); + newStateName.renameTo(savedStateName); + } } catch (FileNotFoundException fnf) { Log.d(TAG, "File not found on backup: "); fnf.printStackTrace(); } catch (RemoteException e) { - Log.d(TAG, "Remote target " + packageName + " threw during backup:"); + Log.d(TAG, "Remote target " + request.appInfo.packageName + " threw during backup:"); e.printStackTrace(); } catch (Exception e) { Log.w(TAG, "Final exception guard in backup: "); e.printStackTrace(); } - synchronized (mQueueLock) { - mBackupQueue.remove(0); - } - - if (request != null) { - try { - mActivityManager.unbindBackupAgent(request.appInfo); - } catch (RemoteException e) { - // can't happen - } - } - - // start the next one - startOneAgent(); } // Add the backup agents in the given package to our set of known backup participants. // If 'packageName' is null, adds all backup agents in the whole system. void addPackageParticipantsLocked(String packageName) { // Look for apps that define the android:backupAgent attribute + if (DEBUG) Log.v(TAG, "addPackageParticipantsLocked: " + packageName); List<ApplicationInfo> targetApps = allAgentApps(); addPackageParticipantsLockedInner(packageName, targetApps); } @@ -336,6 +345,7 @@ class BackupManagerService extends IBackupManager.Stub { // Remove the given package's backup services from our known active set. If // 'packageName' is null, *all* backup services will be removed. void removePackageParticipantsLocked(String packageName) { + if (DEBUG) Log.v(TAG, "removePackageParticipantsLocked: " + packageName); List<ApplicationInfo> allApps = null; if (packageName != null) { allApps = new ArrayList<ApplicationInfo>(); @@ -354,6 +364,13 @@ class BackupManagerService extends IBackupManager.Stub { private void removePackageParticipantsLockedInner(String packageName, List<ApplicationInfo> agents) { + if (DEBUG) { + Log.v(TAG, "removePackageParticipantsLockedInner (" + packageName + + ") removing " + agents.size() + " entries"); + for (ApplicationInfo a : agents) { + Log.v(TAG, " - " + a); + } + } for (ApplicationInfo app : agents) { if (packageName == null || app.packageName.equals(packageName)) { int uid = app.uid; @@ -361,8 +378,7 @@ class BackupManagerService extends IBackupManager.Stub { if (set != null) { set.remove(app); if (set.size() == 0) { - mBackupParticipants.put(uid, null); - } + mBackupParticipants.delete(uid); } } } } @@ -390,6 +406,7 @@ class BackupManagerService extends IBackupManager.Stub { Log.e(TAG, "updatePackageParticipants called with null package name"); return; } + if (DEBUG) Log.v(TAG, "updatePackageParticipantsLocked: " + packageName); // brute force but small code size List<ApplicationInfo> allApps = allAgentApps(); @@ -397,6 +414,128 @@ class BackupManagerService extends IBackupManager.Stub { addPackageParticipantsLockedInner(packageName, allApps); } + // ----- Back up a set of applications via a worker thread ----- + + class PerformBackupThread extends Thread { + private static final String TAG = "PerformBackupThread"; + int mTransport; + ArrayList<BackupRequest> mQueue; + + public PerformBackupThread(int transportId, ArrayList<BackupRequest> queue) { + mTransport = transportId; + mQueue = queue; + } + + @Override + public void run() { + /* + * 1. start up the current transport + * 2. for each item in the queue: + * 2a. bind the agent [wait for async attach] + * 2b. set up the files and call doBackup() + * 2c. unbind the agent + * 3. tear down the transport + * 4. done! + */ + if (DEBUG) Log.v(TAG, "Beginning backup of " + mQueue.size() + " targets"); + + // stand up the current transport + IBackupTransport transport = null; + switch (mTransport) { + case BackupManager.TRANSPORT_ADB: + if (DEBUG) Log.v(TAG, "Initializing adb transport"); + transport = new AdbTransport(); + break; + + case BackupManager.TRANSPORT_GOOGLE: + if (DEBUG) Log.v(TAG, "Initializing Google transport"); + //!!! TODO: stand up the google backup transport here + transport = new GoogleTransport(); + break; + + default: + Log.e(TAG, "Perform backup with unknown transport " + mTransport); + // !!! TODO: re-enqueue the backup queue for later? + return; + } + + try { + transport.startSession(); + } catch (Exception e) { + Log.e(TAG, "Error starting backup session"); + e.printStackTrace(); + // !!! TODO: re-enqueue the backup queue for later? + return; + } + + // The transport is up and running; now run all the backups in our queue + doQueuedBackups(transport); + + // Finally, tear down the transport + try { + transport.endSession(); + } catch (Exception e) { + Log.e(TAG, "Error ending transport"); + e.printStackTrace(); + } + } + + private void doQueuedBackups(IBackupTransport transport) { + for (BackupRequest request : mQueue) { + Log.d(TAG, "starting agent for " + request); + // !!! TODO: need to handle the restore case? + + IBackupAgent agent = null; + int mode = (request.fullBackup) + ? IApplicationThread.BACKUP_MODE_FULL + : IApplicationThread.BACKUP_MODE_INCREMENTAL; + try { + synchronized(mAgentConnectLock) { + mConnecting = true; + mConnectedAgent = null; + if (mActivityManager.bindBackupAgent(request.appInfo, mode)) { + Log.d(TAG, "awaiting agent for " + request); + + // success; wait for the agent to arrive + while (mConnecting && mConnectedAgent == null) { + try { + mAgentConnectLock.wait(10000); + } catch (InterruptedException e) { + // just retry + continue; + } + } + + // if we timed out with no connect, abort and move on + if (mConnecting == true) { + Log.w(TAG, "Timeout waiting for agent " + request); + continue; + } + agent = mConnectedAgent; + } + } + } catch (RemoteException e) { + // can't happen; activity manager is local + } catch (SecurityException ex) { + // Try for the next one. + Log.d(TAG, "error in bind", ex); + } + + // successful bind? run the backup for this agent + if (agent != null) { + processOneBackup(request, agent, transport); + } + + // send the unbind even on timeout, just in case + try { + mActivityManager.unbindBackupAgent(request.appInfo); + } catch (RemoteException e) { + // can't happen + } + } + } + } + // ----- IBackupManager binder interface ----- public void dataChanged(String packageName) throws RemoteException { @@ -432,45 +571,75 @@ class BackupManagerService extends IBackupManager.Stub { // Schedule a backup pass in a few minutes. As backup-eligible data // keeps changing, continue to defer the backup pass until things // settle down, to avoid extra overhead. + mBackupHandler.removeMessages(MSG_RUN_BACKUP); mBackupHandler.sendEmptyMessageDelayed(MSG_RUN_BACKUP, COLLECTION_INTERVAL); } } } - // Schedule a backup pass for a given package, even if the caller is not part of - // that uid or package itself. + // Schedule a backup pass for a given package. This method will schedule a + // full backup even for apps that do not declare an android:backupAgent, so + // use with care. public void scheduleFullBackup(String packageName) throws RemoteException { - // !!! TODO: protect with a signature-or-system permission? + mContext.enforceCallingPermission("android.permission.BACKUP", "scheduleFullBackup"); + + if (DEBUG) Log.v(TAG, "Scheduling immediate full backup for " + packageName); synchronized (mQueueLock) { - int numKeys = mBackupParticipants.size(); - for (int index = 0; index < numKeys; index++) { - int uid = mBackupParticipants.keyAt(index); - HashSet<ApplicationInfo> servicesAtUid = mBackupParticipants.get(uid); - for (ApplicationInfo app: servicesAtUid) { - if (app.packageName.equals(packageName)) { - mPendingBackups.put(app, new BackupRequest(app, true)); - } - } + try { + ApplicationInfo app = mPackageManager.getApplicationInfo(packageName, 0); + mPendingBackups.put(app, new BackupRequest(app, true)); + mBackupHandler.sendEmptyMessage(MSG_RUN_FULL_BACKUP); + } catch (NameNotFoundException e) { + Log.w(TAG, "Could not find app for " + packageName + " to schedule full backup"); } } } - // Callback: a requested backup agent has been instantiated + // Select which transport to use for the next backup operation + public int selectBackupTransport(int transportId) { + mContext.enforceCallingPermission("android.permission.BACKUP", "selectBackupTransport"); + + int prevTransport = mTransportId; + mTransportId = transportId; + return prevTransport; + } + + // Callback: a requested backup agent has been instantiated. This should only + // be called from the Activity Manager. public void agentConnected(String packageName, IBinder agentBinder) { - Log.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); - IBackupAgent bs = IBackupAgent.Stub.asInterface(agentBinder); - processOneBackup(packageName, bs); + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + Log.d(TAG, "agentConnected pkg=" + packageName + " agent=" + agentBinder); + IBackupAgent agent = IBackupAgent.Stub.asInterface(agentBinder); + mConnectedAgent = agent; + mConnecting = false; + } else { + Log.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent connected"); + } + mAgentConnectLock.notifyAll(); + } } // Callback: a backup agent has failed to come up, or has unexpectedly quit. // If the agent failed to come up in the first place, the agentBinder argument - // will be null. + // will be null. This should only be called from the Activity Manager. public void agentDisconnected(String packageName) { // TODO: handle backup being interrupted + synchronized(mAgentConnectLock) { + if (Binder.getCallingUid() == Process.SYSTEM_UID) { + mConnectedAgent = null; + mConnecting = false; + } else { + Log.w(TAG, "Non-system process uid=" + Binder.getCallingUid() + + " claiming agent disconnected"); + } + mAgentConnectLock.notifyAll(); + } } - - + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { synchronized (mQueueLock) { diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index 5202b6f1d160..736c0da67b40 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -10377,6 +10377,7 @@ public final class ActivityManagerService extends ActivityManagerNative implemen return; } + long oldIdent = Binder.clearCallingIdentity(); try { IBackupManager bm = IBackupManager.Stub.asInterface( ServiceManager.getService(Context.BACKUP_SERVICE)); @@ -10386,6 +10387,8 @@ public final class ActivityManagerService extends ActivityManagerNative implemen } catch (Exception e) { Log.w(TAG, "Exception trying to deliver BackupAgent binding: "); e.printStackTrace(); + } finally { + Binder.restoreCallingIdentity(oldIdent); } } } diff --git a/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java b/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java index 7ba99519f2df..0ebe5077ab98 100755 --- a/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java +++ b/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java @@ -38,6 +38,7 @@ import static com.android.internal.telephony.TelephonyProperties.PROPERTY_BASEBA import static com.android.internal.telephony.TelephonyProperties.PROPERTY_INECM_MODE; import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.CommandException; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.Connection; import com.android.internal.telephony.DataConnection; @@ -525,8 +526,20 @@ public class CDMAPhone extends PhoneBase { public void getNeighboringCids(Message response) { - // WINK:TODO: implement after Cupcake merge - mCM.getNeighboringCids(response); // workaround. + /* + * This is currently not implemented. At least as of June + * 2009, there is no neighbor cell information available for + * CDMA because some party is resisting making this + * information readily available. Consequently, calling this + * function can have no useful effect. This situation may + * (and hopefully will) change in the future. + */ + if (response != null) { + CommandException ce = new CommandException( + CommandException.Error.REQUEST_NOT_SUPPORTED); + AsyncResult.forMessage(response).exception = ce; + response.sendToTarget(); + } } public DataState getDataConnectionState() { |