Add "call" method on ContentProvider.

This permits implementing interfaces which are faster than using
remote Cursors.  It then uses it for Settings & SettingProvider, which
together account for ~50% of total ContentProvider event loop stalls
across Froyo dogfooders.

For fetching Settings this looks like it should reduce average
Settings lookup from 10 ms to 0.4 ms on Sholes, once the
SettingsProvider serves most gets from in-memory cache.  Currently it
brings the Sholes average down from 10ms to 2.5 ms while still using
SQLite queries on each get.
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index 91b1c4e..5fb2aae 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -29,6 +29,7 @@
 import android.database.SQLException;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
 
@@ -217,6 +218,13 @@
             return ContentProvider.this.openAssetFile(uri, mode);
         }
 
+        /**
+         * @hide
+         */
+        public Bundle call(String method, String request, Bundle args) {
+            return ContentProvider.this.call(method, request, args);
+        }
+
         private void enforceReadPermission(Uri uri) {
             final int uid = Binder.getCallingUid();
             if (uid == mMyUid) {
@@ -748,4 +756,18 @@
         }
         return results;
     }
-}
\ No newline at end of file
+
+    /**
+     * @hide -- until interface has proven itself
+     *
+     * Call an provider-defined method.  This can be used to implement
+     * interfaces that are cheaper than using a Cursor.
+     *
+     * @param method Method name to call.  Opaque to framework.
+     * @param request Nullable String argument passed to method.
+     * @param args Nullable Bundle argument passed to method.
+     */
+    public Bundle call(String method, String request, Bundle args) {
+        return null;
+    }
+}
diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java
index bacb684..81b8055 100644
--- a/core/java/android/content/ContentProviderNative.java
+++ b/core/java/android/content/ContentProviderNative.java
@@ -26,6 +26,7 @@
 import android.database.IContentObserver;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -222,6 +223,21 @@
                     }
                     return true;
                 }
+
+                case CALL_TRANSACTION:
+                {
+                    data.enforceInterface(IContentProvider.descriptor);
+
+                    String method = data.readString();
+                    String stringArg = data.readString();
+                    Bundle args = data.readBundle();
+
+                    Bundle responseBundle = call(method, stringArg, args);
+
+                    reply.writeNoException();
+                    reply.writeBundle(responseBundle);
+                    return true;
+                }
             }
         } catch (Exception e) {
             DatabaseUtils.writeExceptionToParcel(reply, e);
@@ -485,6 +501,22 @@
         return fd;
     }
 
+    public Bundle call(String method, String request, Bundle args)
+            throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+
+        data.writeInterfaceToken(IContentProvider.descriptor);
+
+        data.writeString(method);
+        data.writeString(request);
+        data.writeBundle(args);
+
+        mRemote.transact(IContentProvider.CALL_TRANSACTION, data, reply, 0);
+
+        DatabaseUtils.readExceptionFromParcel(reply);
+        return reply.readBundle();
+    }
+
     private IBinder mRemote;
 }
-
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 1b0ef34..bcef75e 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -736,7 +736,7 @@
      * @hide
      */
     public final IContentProvider acquireProvider(String name) {
-        if(name == null) {
+        if (name == null) {
             return null;
         }
         return acquireProvider(mContext, name);
diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java
index 1b0ca58..67e7581 100644
--- a/core/java/android/content/IContentProvider.java
+++ b/core/java/android/content/IContentProvider.java
@@ -22,10 +22,11 @@
 import android.database.IBulkCursor;
 import android.database.IContentObserver;
 import android.net.Uri;
-import android.os.RemoteException;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
 
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
@@ -58,6 +59,17 @@
             throws RemoteException, FileNotFoundException;
     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
             throws RemoteException, OperationApplicationException;
+    /**
+     * @hide -- until interface has proven itself
+     *
+     * Call an provider-defined method.  This can be used to implement
+     * interfaces that are cheaper than using a Cursor.
+     *
+     * @param method Method name to call.  Opaque to framework.
+     * @param request Nullable String argument passed to method.
+     * @param args Nullable Bundle argument passed to method.
+     */
+    public Bundle call(String method, String request, Bundle args) throws RemoteException;
 
     /* IPC constants */
     static final String descriptor = "android.content.IContentProvider";
@@ -71,4 +83,5 @@
     static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13;
     static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14;
     static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19;
+    static final int CALL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 20;
 }
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index d6fe107..0ec1c74 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -132,6 +132,45 @@
     }
 
     /**
+     * Make a Bundle for a single key/value pair.
+     *
+     * @hide
+     */
+    public static Bundle forPair(String key, String value) {
+        // TODO: optimize this case.
+        Bundle b = new Bundle(1);
+        b.putString(key, value);
+        return b;
+    }
+
+    /**
+     * TODO: optimize this later (getting just the value part of a Bundle
+     * with a single pair) once Bundle.forPair() above is implemented
+     * with a special single-value Map implementation/serialization.
+     *
+     * Note: value in single-pair Bundle may be null.
+     *
+     * @hide
+     */
+    public String getPairValue() {
+        unparcel();
+        int size = mMap.size();
+        if (size > 1) {
+            Log.w(LOG_TAG, "getPairValue() used on Bundle with multiple pairs.");
+        }
+        if (size == 0) {
+            return null;
+        }
+        Object o = mMap.values().iterator().next();
+        try {
+            return (String) o;
+        } catch (ClassCastException e) {
+            typeWarning("getPairValue()", o, "String", e);
+            return null;
+        }
+    }
+
+    /**
      * Changes the ClassLoader this Bundle uses when instantiating objects.
      *
      * @param loader An explicit ClassLoader to use when instantiating objects
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 7df509f..726f98a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -27,6 +27,7 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.IContentProvider;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -492,6 +493,16 @@
     // End of Intent actions for Settings
 
     /**
+     * @hide - Private call() method on SettingsProvider to read from 'system' table.
+     */
+    public static final String CALL_METHOD_GET_SYSTEM = "GET_system";
+
+    /**
+     * @hide - Private call() method on SettingsProvider to read from 'secure' table.
+     */
+    public static final String CALL_METHOD_GET_SECURE = "GET_secure";
+
+    /**
      * Activity Extra: Limit available options in launched activity based on the given authority.
      * <p>
      * This can be passed as an extra field in an Activity Intent with one or more syncable content
@@ -544,23 +555,36 @@
         }
     }
 
+    // Thread-safe.
     private static class NameValueCache {
         private final String mVersionSystemProperty;
         private final Uri mUri;
 
-        // Must synchronize(mValues) to access mValues and mValuesVersion.
+        private static final String[] SELECT_VALUE =
+            new String[] { Settings.NameValueTable.VALUE };
+        private static final String NAME_EQ_PLACEHOLDER = "name=?";
+
+        // Must synchronize on 'this' to access mValues and mValuesVersion.
         private final HashMap<String, String> mValues = new HashMap<String, String>();
         private long mValuesVersion = 0;
 
-        public NameValueCache(String versionSystemProperty, Uri uri) {
+        // Initially null; set lazily and held forever.  Synchronized on 'this'.
+        private IContentProvider mContentProvider = null;
+
+        // The method we'll call (or null, to not use) on the provider
+        // for the fast path of retrieving settings.
+        private final String mCallCommand;
+
+        public NameValueCache(String versionSystemProperty, Uri uri, String callCommand) {
             mVersionSystemProperty = versionSystemProperty;
             mUri = uri;
+            mCallCommand = callCommand;
         }
 
         public String getString(ContentResolver cr, String name) {
             long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0);
 
-            synchronized (mValues) {
+            synchronized (this) {
                 if (mValuesVersion != newValuesVersion) {
                     if (LOCAL_LOGV) {
                         Log.v(TAG, "invalidate [" + mUri.getLastPathSegment() + "]: current " +
@@ -576,17 +600,47 @@
                 }
             }
 
+            IContentProvider cp = null;
+            synchronized (this) {
+                cp = mContentProvider;
+                if (cp == null) {
+                    cp = mContentProvider = cr.acquireProvider(mUri.getAuthority());
+                }
+            }
+
+            // Try the fast path first, not using query().  If this
+            // fails (alternate Settings provider that doesn't support
+            // this interface?) then we fall back to the query/table
+            // interface.
+            if (mCallCommand != null) {
+                try {
+                    Bundle b = cp.call(mCallCommand, name, null);
+                    if (b != null) {
+                        String value = b.getPairValue();
+                        synchronized (this) {
+                            mValues.put(name, value);
+                        }
+                        return value;
+                    }
+                    // If the response Bundle is null, we fall through
+                    // to the query interface below.
+                } catch (RemoteException e) {
+                    // Not supported by the remote side?  Fall through
+                    // to query().
+                }
+            }
+
             Cursor c = null;
             try {
-                c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE },
-                        Settings.NameValueTable.NAME + "=?", new String[]{name}, null);
+                c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER,
+                             new String[]{name}, null);
                 if (c == null) {
                     Log.w(TAG, "Can't get key " + name + " from " + mUri);
                     return null;
                 }
 
                 String value = c.moveToNext() ? c.getString(0) : null;
-                synchronized (mValues) {
+                synchronized (this) {
                     mValues.put(name, value);
                 }
                 if (LOCAL_LOGV) {
@@ -594,7 +648,7 @@
                             name + " = " + (value == null ? "(null)" : value));
                 }
                 return value;
-            } catch (SQLException e) {
+            } catch (RemoteException e) {
                 Log.w(TAG, "Can't get key " + name + " from " + mUri, e);
                 return null;  // Return null, but don't cache it.
             } finally {
@@ -611,7 +665,8 @@
     public static final class System extends NameValueTable {
         public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version";
 
-        private static volatile NameValueCache mNameValueCache = null;
+        // Populated lazily, guarded by class object:
+        private static NameValueCache sNameValueCache = null;
 
         private static final HashSet<String> MOVED_TO_SECURE;
         static {
@@ -660,10 +715,11 @@
                         + " to android.provider.Settings.Secure, returning read-only value.");
                 return Secure.getString(resolver, name);
             }
-            if (mNameValueCache == null) {
-                mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI);
+            if (sNameValueCache == null) {
+                sNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI,
+                                                     CALL_METHOD_GET_SYSTEM);
             }
-            return mNameValueCache.getString(resolver, name);
+            return sNameValueCache.getString(resolver, name);
         }
 
         /**
@@ -1905,7 +1961,8 @@
     public static final class Secure extends NameValueTable {
         public static final String SYS_PROP_SETTING_VERSION = "sys.settings_secure_version";
 
-        private static volatile NameValueCache mNameValueCache = null;
+        // Populated lazily, guarded by class object:
+        private static NameValueCache sNameValueCache = null;
 
         /**
          * Look up a name in the database.
@@ -1914,10 +1971,11 @@
          * @return the corresponding value, or null if not present
          */
         public synchronized static String getString(ContentResolver resolver, String name) {
-            if (mNameValueCache == null) {
-                mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI);
+            if (sNameValueCache == null) {
+                sNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI,
+                                                     CALL_METHOD_GET_SECURE);
             }
-            return mNameValueCache.getString(resolver, name);
+            return sNameValueCache.getString(resolver, name);
         }
 
         /**
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index db802d3..4f1146b 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -30,9 +30,11 @@
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.media.RingtoneManager;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemProperties;
 import android.provider.DrmStore;
@@ -48,6 +50,8 @@
     private static final String TABLE_FAVORITES = "favorites";
     private static final String TABLE_OLD_FAVORITES = "old_favorites";
 
+    private static final String[] COLUMN_VALUE = new String[] { "value" };
+
     protected DatabaseHelper mOpenHelper;
     private BackupManager mBackupManager;
 
@@ -220,6 +224,44 @@
         }
     }
 
+    /**
+     * Fast path that avoids the use of chatty remoted Cursors.
+     */
+    @Override
+    public Bundle call(String method, String request, Bundle args) {
+        if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
+            return lookupValue("system", request);
+        }
+
+        if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
+            return lookupValue("secure", request);
+        }
+        return null;
+    }
+
+    // Looks up value 'key' in 'table' and returns either a single-pair Bundle,
+    // possibly with a null value, or null on failure.
+    private Bundle lookupValue(String table, String key) {
+        // TODO: avoid database lookup and serve from in-process cache.
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor cursor = null;
+        try {
+            cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
+                              null, null, null, null);
+            if (cursor != null && cursor.getCount() == 1) {
+                cursor.moveToFirst();
+                String value = cursor.getString(0);
+                return Bundle.forPair("value", value);
+            }
+        } catch (SQLiteException e) {
+            Log.w(TAG, "settings lookup error", e);
+            return null;
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        return Bundle.forPair("value", null);
+    }
+
     @Override
     public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
         SqlArguments args = new SqlArguments(url, where, whereArgs);
diff --git a/test-runner/src/android/test/mock/MockContentProvider.java b/test-runner/src/android/test/mock/MockContentProvider.java
index 4078622..3fd71c8 100644
--- a/test-runner/src/android/test/mock/MockContentProvider.java
+++ b/test-runner/src/android/test/mock/MockContentProvider.java
@@ -32,6 +32,7 @@
 import android.database.IBulkCursor;
 import android.database.IContentObserver;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -113,6 +114,15 @@
             return MockContentProvider.this.update(url, values, selection, selectionArgs);
         }
 
+        /**
+         * @hide
+         */
+        @SuppressWarnings("unused")
+        public Bundle call(String method, String request, Bundle args)
+                throws RemoteException {
+            return MockContentProvider.this.call(method, request, args);
+        }
+
         public IBinder asBinder() {
             throw new UnsupportedOperationException();
         }
@@ -205,6 +215,14 @@
     }
 
     /**
+     * @hide
+     */
+    @Override
+    public Bundle call(String method, String request, Bundle args) {
+        throw new UnsupportedOperationException("unimplemented mock method call");
+    }
+
+    /**
      * Returns IContentProvider which calls back same methods in this class.
      * By overriding this class, we avoid the mechanism hidden behind ContentProvider
      * (IPC, etc.)
diff --git a/test-runner/src/android/test/mock/MockIContentProvider.java b/test-runner/src/android/test/mock/MockIContentProvider.java
index 7c0a1e2..0be5bea 100644
--- a/test-runner/src/android/test/mock/MockIContentProvider.java
+++ b/test-runner/src/android/test/mock/MockIContentProvider.java
@@ -27,6 +27,7 @@
 import android.database.IBulkCursor;
 import android.database.IContentObserver;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -38,7 +39,7 @@
  * {@link java.lang.UnsupportedOperationException}.  Tests can extend this class to
  * implement behavior needed for tests.
  *
- * @hide - @hide because this exposes bulkQuery(), which must also be hidden.
+ * @hide - @hide because this exposes bulkQuery() and call(), which must also be hidden.
  */
 public class MockIContentProvider implements IContentProvider {
     public int bulkInsert(Uri url, ContentValues[] initialValues) {
@@ -93,6 +94,11 @@
         throw new UnsupportedOperationException("unimplemented mock method");
     }
 
+    public Bundle call(String method, String request, Bundle args)
+            throws RemoteException {
+        throw new UnsupportedOperationException("unimplemented mock method");
+    }
+
     public IBinder asBinder() {
         throw new UnsupportedOperationException("unimplemented mock method");
     }