From c55e08e3fc14475b3b15b4c87aaef7e47ec8a172 Mon Sep 17 00:00:00 2001 From: Bjorn Bringert Date: Mon, 1 Jun 2009 21:50:09 +0100 Subject: [PATCH 1/7] Allow making AssetFileDescriptors from SQLite blobs. This change adds a new class SQLiteContentHelper, which contains a static method for creating an AssetFileDescriptor from an SQLite query that returns a blob. Internally, this uses a file descriptor for a MemoryFile. The implementation is temporary. Ideally, the data should be copied directly from SQLite to the MemoryFile ashmem region, using sqlite3_blob_read(). This is part of the implementation of http://b/issue?id=1871731 --- .../database/sqlite/SQLiteContentHelper.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 core/java/android/database/sqlite/SQLiteContentHelper.java diff --git a/core/java/android/database/sqlite/SQLiteContentHelper.java b/core/java/android/database/sqlite/SQLiteContentHelper.java new file mode 100644 index 0000000000000..2800d86279b1e --- /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(); + } + } + +} From 043dadc7516d20c3b3ccbcb20c53aaeef076a237 Mon Sep 17 00:00:00 2001 From: Christopher Tate Date: Tue, 2 Jun 2009 16:11:00 -0700 Subject: [PATCH 2/7] More backup work * Put in some permission enforcement around agent connection notification and full-backup scheduling. * Full backup now applies to any package, not just backup participants who have declared their own android:backupAgent * The process of running the backup operation on the set of apps who have been queued for it is now done in a separate thread, with a notification mechanism from the main Backup Manager service to pass along new-agent binding knowledge. There's no longer one do-backup message on the primary Handler per target application. * The new backup thread sets up the desired transport now and passes along the newly backed-up data to it for each backup target. Two transports have been defined so far, GoogleTransport and AdbTransport; both are stubs at present. Note that at present the backup data output file seems to be properly created, but after doBackup() is called on the test app's agent it's still zero size. --- core/java/android/backup/BackupManager.java | 6 + core/java/android/backup/IBackupManager.aidl | 16 +- .../android/internal/backup/AdbTransport.java | 28 ++ .../internal/backup/GoogleTransport.java | 28 ++ .../internal/backup/IBackupTransport.aidl | 1 + .../android/server/BackupManagerService.java | 291 ++++++++++++++---- .../server/am/ActivityManagerService.java | 3 + 7 files changed, 308 insertions(+), 65 deletions(-) create mode 100644 core/java/com/android/internal/backup/AdbTransport.java create mode 100644 core/java/com/android/internal/backup/GoogleTransport.java diff --git a/core/java/android/backup/BackupManager.java b/core/java/android/backup/BackupManager.java index 6f0b2eef603f7..30f781e659e50 100644 --- a/core/java/android/backup/BackupManager.java +++ b/core/java/android/backup/BackupManager.java @@ -41,6 +41,12 @@ public class BackupManager { private Context mContext; 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 3468d70962c20..f5b82fe578518 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/com/android/internal/backup/AdbTransport.java b/core/java/com/android/internal/backup/AdbTransport.java new file mode 100644 index 0000000000000..acb3273a77b4d --- /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 0000000000000..85ab21eac425a --- /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 ce39768137e28..2b44fe7c83935 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 82ed1e39e0e5f..b003e76c5cec3 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 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(); 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 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 allApps = null; if (packageName != null) { allApps = new ArrayList(); @@ -354,6 +364,13 @@ class BackupManagerService extends IBackupManager.Stub { private void removePackageParticipantsLockedInner(String packageName, List 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 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 mQueue; + + public PerformBackupThread(int transportId, ArrayList 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 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 5202b6f1d160d..736c0da67b40d 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); } } } From 90cb947c22f05f8ac68ceff1d74af15bf38268c6 Mon Sep 17 00:00:00 2001 From: Tammo Spalink Date: Wed, 13 May 2009 14:44:07 +0800 Subject: [PATCH 3/7] cdma getNeighboringCids to fail with an exception --- .../internal/telephony/cdma/CDMAPhone.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java b/telephony/java/com/android/internal/telephony/cdma/CDMAPhone.java index 7ba99519f2df5..0ebe5077ab981 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() { From 3a27803b29dc739685e1e2465a0ca697e4b60d14 Mon Sep 17 00:00:00 2001 From: Mike LeBeau Date: Thu, 4 Jun 2009 14:09:42 +0100 Subject: [PATCH 4/7] Add INTENT_ACTION_WEB_SEARCH_SETTINGS (thereby fixing the build too :)). --- core/java/android/app/SearchManager.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index b4a3a78eddeb2..f607db929289a 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -1415,6 +1415,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}, From 3250fa73711537d42a9711eba6315f24d89dee0f Mon Sep 17 00:00:00 2001 From: Bjorn Bringert Date: Wed, 3 Jun 2009 10:33:41 +0100 Subject: [PATCH 5/7] Remove support for bitmap icons in search suggestions. This is no longer needed, since content providers can now return AssetFileDescriptors for in-memory data. Bitmaps in the suggestion cursor was resopnsible for lots of unnecessary copying, since all rows are copied out of the database regardless of how many are displayed in the UI. --- core/java/android/app/SearchManager.java | 22 ------------- core/java/android/app/SuggestionsAdapter.java | 32 ++++--------------- 2 files changed, 7 insertions(+), 47 deletions(-) diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index f607db929289a..841e6aaffa99c 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -1291,28 +1291,6 @@ public class SearchManager * for more information on these schemes. */ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2"; - /** - * Column name for suggestions cursor. Optional. 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. Optional. 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. Optional. If this column exists and * this element exists at the given row, this is the action that will be used when diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 747bec9944dc1..9c824291124ec 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; @@ -71,8 +68,6 @@ 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. @@ -139,8 +134,6 @@ 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(); } @@ -215,8 +208,8 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } 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 +231,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); From 0f7e914e87070b8528892380f8316d2fe486f65e Mon Sep 17 00:00:00 2001 From: Mike LeBeau Date: Thu, 4 Jun 2009 15:07:24 +0100 Subject: [PATCH 6/7] Turn off verbose logging in SearchableInfo, which seems to have been left on by accident. --- core/java/android/server/search/SearchableInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java index 842fc7573aefd..c08314272d505 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. From 1c54cf0903cd81467ca41ec6733c459e10b22763 Mon Sep 17 00:00:00 2001 From: Karl Rosaen Date: Thu, 4 Jun 2009 15:58:36 +0100 Subject: [PATCH 7/7] Detect impressions, and cleanup the SearchDialog / SuggestionCursor communication. (framework portion) There are now 4 times the search dialog will check with the cursor: - after data set changed - when an item is clicked - when the cursor is about to be closed - when an item at a particular position is detected as showing these are now the points where we can add data in either direction, which we use to accomplish: - finding out whether there are any pending results - find out if there is a position at which to notify when it is displayed (the "more results" triggering) - toggling the "more results" button - sending the max position that was displayed when the cursor is done the new behavior (in addition to the refactoring) is improved detection of "more results" to trigger the additional sources (it is now precise), and detection of which items were displayed to the user. --- core/java/android/app/SearchDialog.java | 11 +- core/java/android/app/SearchManager.java | 66 +++++---- core/java/android/app/SuggestionsAdapter.java | 132 ++++++++++++------ 3 files changed, 129 insertions(+), 80 deletions(-) diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index bcb279151b354..0bb483b40d8bb 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 841e6aaffa99c..b0c248c52b118 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 diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 9c824291124ec..48d8d1235acef 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -33,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; @@ -44,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"; @@ -73,10 +67,16 @@ class SuggestionsAdapter extends ResourceCursorAdapter { // 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 outsideDrawablesCache, boolean globalSearchMode) { @@ -127,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); @@ -135,39 +140,69 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); } - updateWorking(); } - + + /** + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#PRE_CLOSE}. + * + * @param cursor The cursor to call. + */ + private void callCursorPreClose(Cursor cursor) { + 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. - */ - public void setListItemToSelect(int index) { - mListItemToSelect = index; - } - - /** - * Updates the search dialog according to the current working status of the cursor. - */ - 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)); + /** + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#POST_REFRESH}. + * + * @param cursor The cursor to call. + */ + private void callCursorPostRefresh(Cursor cursor) { + 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); + } + + /** + * 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. + */ + void callCursorOnClick(Cursor cursor, int position) { + 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); } /** @@ -179,7 +214,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. @@ -201,11 +236,22 @@ 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 (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, mIconName1Col); @@ -400,7 +446,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);