Merge "Add null checks for strings"
diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java
index 1ae9315..cb947b1 100644
--- a/core/java/android/app/SuggestionsAdapter.java
+++ b/core/java/android/app/SuggestionsAdapter.java
@@ -32,6 +32,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.StateListDrawable;
 import android.net.Uri;
+import android.os.Bundle;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -80,7 +81,7 @@
     private int mIconName1Col;
     private int mIconName2Col;
     private int mBackgroundColorCol;
-    
+
     static final int NONE = -1;
 
     private final Runnable mStartSpinnerRunnable;
@@ -121,7 +122,7 @@
                 mSearchDialog.setWorking(false);
             }
         };
-        
+
         // delay 500ms when deleting
         getFilter().setDelayer(new Filter.Delayer() {
 
@@ -129,7 +130,7 @@
 
             public long getPostingDelay(CharSequence constraint) {
                 if (constraint == null) return 0;
-                
+
                 long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0;
                 mPreviousLength = constraint.length();
                 return delay;
@@ -203,15 +204,16 @@
     }
 
     private void updateSpinnerState(Cursor cursor) {
+        Bundle extras = cursor != null ? cursor.getExtras() : null;
         if (DBG) {
             Log.d(LOG_TAG, "updateSpinnerState - extra = "
-                + (cursor != null
-                        ? cursor.getExtras().getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
+                + (extras != null
+                        ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
                         : null));
         }
         // Check if the Cursor indicates that the query is not complete and show the spinner
-        if (cursor != null
-                && cursor.getExtras().getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
+        if (extras != null
+                && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
             mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable);
             return;
         }
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/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index 6a95831..b8d71b9 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -24,12 +24,17 @@
 import android.content.pm.PackageManager;
 import android.database.DataSetObserver;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Interpolator;
+import android.graphics.Paint;
 import android.graphics.Picture;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.net.http.SslCertificate;
 import android.net.Uri;
@@ -70,7 +75,7 @@
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.ListView;
-import android.widget.Scroller;
+import android.widget.OverScroller;
 import android.widget.Toast;
 import android.widget.ZoomButtonsController;
 import android.widget.ZoomControls;
@@ -455,7 +460,9 @@
     // time for the longest scroll animation
     private static final int MAX_DURATION = 750;   // milliseconds
     private static final int SLIDE_TITLE_DURATION = 500;   // milliseconds
-    private Scroller mScroller;
+    private OverScroller mScroller;
+    private boolean mInOverScrollMode = false;
+    private static Paint mOverScrollBackground;
 
     private boolean mWrapContent;
     private static final int MOTIONLESS_FALSE           = 0;
@@ -810,7 +817,7 @@
         mViewManager = new ViewManager(this);
         mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces);
         mDatabase = WebViewDatabase.getInstance(context);
-        mScroller = new Scroller(context);
+        mScroller = new OverScroller(context);
 
         mZoomButtonsController = new ZoomButtonsController(this);
         mZoomButtonsController.setOnZoomListener(mZoomListener);
@@ -1024,7 +1031,8 @@
      * Return the amount of the titlebarview (if any) that is visible
      */
     private int getVisibleTitleHeight() {
-        return Math.max(getTitleHeight() - mScrollY, 0);
+        // need to restrict mScrollY due to over scroll
+        return Math.max(getTitleHeight() - Math.max(0, mScrollY), 0);
     }
 
     /*
@@ -1867,11 +1875,13 @@
 
     // Expects x in view coordinates
     private int pinLocX(int x) {
+        if (mInOverScrollMode) return x;
         return pinLoc(x, getViewWidth(), computeHorizontalScrollRange());
     }
 
     // Expects y in view coordinates
     private int pinLocY(int y) {
+        if (mInOverScrollMode) return y;
         int titleH = getTitleHeight();
         // if the titlebar is still visible, just pin against 0
         if (y <= titleH) {
@@ -2269,6 +2279,24 @@
         scrollBar.draw(canvas);
     }
 
+    @Override
+    protected void onOverscrolled(int scrollX, int scrollY, boolean clampedX,
+            boolean clampedY) {
+        mInOverScrollMode = false;
+        int maxX = computeMaxScrollX();
+        if (Math.abs(mMinZoomScale - mMaxZoomScale) < 0.01f && maxX == 0) {
+            // do not over scroll x if the page can't be zoomed and it just fits
+            // the screen
+            scrollX = pinLocX(scrollX);
+        } else if (scrollX < 0 || scrollX > maxX) {
+            mInOverScrollMode = true;
+        }
+        if (scrollY < 0 || scrollY > computeMaxScrollY()) {
+            mInOverScrollMode = true;
+        }
+        super.scrollTo(scrollX, scrollY);
+    }
+
     /**
      * Get the url for the current page. This is not always the same as the url
      * passed to WebViewClient.onPageStarted because although the load for
@@ -2611,13 +2639,14 @@
         if (mScroller.computeScrollOffset()) {
             int oldX = mScrollX;
             int oldY = mScrollY;
-            mScrollX = mScroller.getCurrX();
-            mScrollY = mScroller.getCurrY();
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
             postInvalidate();  // So we draw again
-            if (oldX != mScrollX || oldY != mScrollY) {
-                // as onScrollChanged() is not called, sendOurVisibleRect()
-                // needs to be call explicitly
-                sendOurVisibleRect();
+            if (oldX != x || oldY != y) {
+                overscrollBy(x - oldX, y - oldY, oldX, oldY,
+                        computeMaxScrollX(), computeMaxScrollY(),
+                        getViewWidth() / 3, getViewHeight() / 3);
+                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
             }
         } else {
             super.computeScroll();
@@ -3028,8 +3057,13 @@
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         if (child == mTitleBar) {
             // When drawing the title bar, move it horizontally to always show
-            // at the top of the WebView.
+            // at the top of the WebView. While overscroll, stick the title bar
+            // on the top otherwise we may have two during loading, one is drawn
+            // here, another is drawn by the Browser.
             mTitleBar.offsetLeftAndRight(mScrollX - mTitleBar.getLeft());
+            if (mScrollY <= 0) {
+                mTitleBar.offsetTopAndBottom(mScrollY - mTitleBar.getTop());
+            }
         }
         return super.drawChild(canvas, child, drawingTime);
     }
@@ -3057,6 +3091,29 @@
         }
 
         int saveCount = canvas.save();
+        if (mInOverScrollMode) {
+            if (mOverScrollBackground == null) {
+                mOverScrollBackground = new Paint();
+                Bitmap bm = BitmapFactory.decodeResource(
+                        mContext.getResources(),
+                        com.android.internal.R.drawable.pattern_underwear);
+                mOverScrollBackground.setShader(new BitmapShader(bm,
+                        Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
+            }
+            int top = getTitleHeight();
+            // first draw the background and anchor to the top of the view
+            canvas.save();
+            canvas.translate(mScrollX, mScrollY);
+            canvas.clipRect(-mScrollX, top - mScrollY,
+                    computeHorizontalScrollRange() - mScrollX, top
+                            + computeVerticalScrollRange() - mScrollY,
+                    Region.Op.DIFFERENCE);
+            canvas.drawPaint(mOverScrollBackground);
+            canvas.restore();
+            // next clip the region for the content
+            canvas.clipRect(0, top, computeHorizontalScrollRange(), top
+                    + computeVerticalScrollRange());
+        }
         if (mTitleBar != null) {
             canvas.translate(0, (int) mTitleBar.getHeight());
         }
@@ -3074,12 +3131,12 @@
         canvas.restoreToCount(saveCount);
 
         // Now draw the shadow.
-        if (mTitleBar != null) {
-            int y = mScrollY + getVisibleTitleHeight();
+        int titleH = getVisibleTitleHeight();
+        if (mTitleBar != null && titleH == 0) {
             int height = (int) (5f * getContext().getResources()
                     .getDisplayMetrics().density);
-            mTitleShadow.setBounds(mScrollX, y, mScrollX + getWidth(),
-                    y + height);
+            mTitleShadow.setBounds(mScrollX, mScrollY, mScrollX + getWidth(),
+                    mScrollY + height);
             mTitleShadow.draw(canvas);
         }
         if (AUTO_REDRAW_HACK && mAutoRedraw) {
@@ -4680,18 +4737,6 @@
                 }
 
                 // do pan
-                int newScrollX = pinLocX(mScrollX + deltaX);
-                int newDeltaX = newScrollX - mScrollX;
-                if (deltaX != newDeltaX) {
-                    deltaX = newDeltaX;
-                    fDeltaX = (float) newDeltaX;
-                }
-                int newScrollY = pinLocY(mScrollY + deltaY);
-                int newDeltaY = newScrollY - mScrollY;
-                if (deltaY != newDeltaY) {
-                    deltaY = newDeltaY;
-                    fDeltaY = (float) newDeltaY;
-                }
                 boolean done = false;
                 boolean keepScrollBarsVisible = false;
                 if (Math.abs(fDeltaX) < 1.0f && Math.abs(fDeltaY) < 1.0f) {
@@ -4736,7 +4781,9 @@
                         }
                     }
                     if ((deltaX | deltaY) != 0) {
-                        scrollBy(deltaX, deltaY);
+                        overscrollBy(deltaX, deltaY, mScrollX, mScrollY,
+                                computeMaxScrollX(), computeMaxScrollY(),
+                                getViewWidth() / 3, getViewHeight() / 3);
                         if (deltaX != 0) {
                             mLastTouchX = x;
                         }
@@ -4819,8 +4866,8 @@
                             Log.w(LOGTAG, "Miss a drag as we are waiting for" +
                                     " WebCore's response for touch down.");
                             if (mFullScreenHolder == null
-                                    && (computeHorizontalScrollExtent() < computeHorizontalScrollRange()
-                                    || computeVerticalScrollExtent() < computeVerticalScrollRange())) {
+                                    && (computeMaxScrollX() > 0
+                                            || computeMaxScrollY() > 0)) {
                                 // remove the pending TOUCH_EVENT and send a
                                 // cancel
                                 mWebViewCore
@@ -4866,6 +4913,12 @@
                             mVelocityTracker.addMovement(ev);
                             doFling();
                             break;
+                        } else {
+                            if (mScroller.springback(mScrollX, mScrollY, 0,
+                                    computeMaxScrollX(), 0,
+                                    computeMaxScrollY())) {
+                                invalidate();
+                            }
                         }
                         mLastVelocity = 0;
                         WebViewCore.resumePriority();
@@ -4886,6 +4939,12 @@
             }
             case MotionEvent.ACTION_CANCEL: {
                 cancelTouch();
+                if (mTouchMode == TOUCH_DRAG_MODE) {
+                    if (mScroller.springback(mScrollX, mScrollY, 0,
+                            computeMaxScrollX(), 0, computeMaxScrollY())) {
+                        invalidate();
+                    }
+                }
                 break;
             }
         }
@@ -5204,16 +5263,18 @@
         }
     }
 
+    private int computeMaxScrollX() {
+        return Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
+    }
+
     private int computeMaxScrollY() {
-        int maxContentH = computeVerticalScrollRange() + getTitleHeight();
-        return Math.max(maxContentH - getViewHeightWithTitle(), getTitleHeight());
+        return Math.max(computeVerticalScrollRange() + getTitleHeight()
+                - getViewHeightWithTitle(), getTitleHeight());
     }
 
     public void flingScroll(int vx, int vy) {
-        int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
-        int maxY = computeMaxScrollY();
-
-        mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY);
+        mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0,
+                computeMaxScrollY(), getViewWidth() / 3, getViewHeight() / 3);
         invalidate();
     }
 
@@ -5221,7 +5282,7 @@
         if (mVelocityTracker == null) {
             return;
         }
-        int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
+        int maxX = computeMaxScrollX();
         int maxY = computeMaxScrollY();
 
         mVelocityTracker.computeCurrentVelocity(1000, mMaximumFling);
@@ -5243,6 +5304,10 @@
         }
         if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) {
             WebViewCore.resumePriority();
+            if (mScroller.springback(mScrollX, mScrollY, 0, computeMaxScrollX(),
+                    0, computeMaxScrollY())) {
+                invalidate();
+            }
             return;
         }
         float currentVelocity = mScroller.getCurrVelocity();
@@ -5270,7 +5335,8 @@
         mLastVelY = vy;
         mLastVelocity = (float) Math.hypot(vx, vy);
 
-        mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY);
+        mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY,
+                getViewWidth() / 3, getViewHeight() / 3);
         // TODO: duration is calculated based on velocity, if the range is
         // small, the animation will stop before duration is up. We may
         // want to calculate how long the animation is going to run to precisely
diff --git a/core/res/res/drawable/pattern_underwear.png b/core/res/res/drawable/pattern_underwear.png
new file mode 100644
index 0000000..651212f
--- /dev/null
+++ b/core/res/res/drawable/pattern_underwear.png
Binary files differ
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/services/java/com/android/server/UiModeManagerService.java b/services/java/com/android/server/UiModeManagerService.java
index e225edb..71826ff 100644
--- a/services/java/com/android/server/UiModeManagerService.java
+++ b/services/java/com/android/server/UiModeManagerService.java
@@ -20,6 +20,7 @@
 import android.app.ActivityManagerNative;
 import android.app.AlarmManager;
 import android.app.IActivityManager;
+import android.app.KeyguardManager;
 import android.app.IUiModeManager;
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -37,10 +38,12 @@
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
+import android.os.BatteryManager;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.text.format.DateUtils;
@@ -79,6 +82,9 @@
     
     private int mNightMode = UiModeManager.MODE_NIGHT_NO;
     private boolean mCarModeEnabled = false;
+    private boolean mCharging = false;
+    private final boolean mCarModeKeepsScreenOn;
+    private final boolean mDeskModeKeepsScreenOn;
 
     private boolean mComputedNightMode;
     private int mCurUiMode = 0;
@@ -94,6 +100,8 @@
     private LocationManager mLocationManager;
     private Location mLocation;
     private StatusBarManager mStatusBarManager;
+    private KeyguardManager.KeyguardLock mKeyguardLock;
+    private final PowerManager.WakeLock mWakeLock;
 
     // The broadcast receiver which receives the result of the ordered broadcast sent when
     // the dock state changes. The original ordered broadcast is sent with an initial result
@@ -148,6 +156,18 @@
         }
     };
 
+    private final BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mCharging = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
+            synchronized (mLock) {
+                if (mSystemReady) {
+                    updateLocked();
+                }
+            }
+        }
+    };
+
     // A LocationListener to initialize the network location provider. The location updates
     // are handled through the passive location provider.
     private final LocationListener mEmptyLocationListener =  new LocationListener() {
@@ -235,13 +255,26 @@
                 new IntentFilter(ACTION_UPDATE_NIGHT_MODE));
         mContext.registerReceiver(mDockModeReceiver,
                 new IntentFilter(Intent.ACTION_DOCK_EVENT));
+        mContext.registerReceiver(mBatteryReceiver,
+                new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+
+        PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG);
 
         mConfiguration.setToDefaults();
+
+        mCarModeKeepsScreenOn = (context.getResources().getInteger(
+                com.android.internal.R.integer.config_carDockKeepsScreenOn) == 1);
+        mDeskModeKeepsScreenOn = (context.getResources().getInteger(
+                com.android.internal.R.integer.config_deskDockKeepsScreenOn) == 1);
     }
 
     public void disableCarMode() {
         synchronized (mLock) {
             setCarModeLocked(false);
+            if (mSystemReady) {
+                updateLocked();
+            }
         }
     }
 
@@ -251,6 +284,9 @@
                 "Need ENABLE_CAR_MODE permission");
         synchronized (mLock) {
             setCarModeLocked(true);
+            if (mSystemReady) {
+                updateLocked();
+            }
         }
     }
 
@@ -301,7 +337,22 @@
     void setCarModeLocked(boolean enabled) {
         if (mCarModeEnabled != enabled) {
             mCarModeEnabled = enabled;
-            updateLocked();
+
+            // Disable keyguard when in car mode
+            if (mKeyguardLock == null) {
+                KeyguardManager km =
+                        (KeyguardManager)mContext.getSystemService(Context.KEYGUARD_SERVICE);
+                if (km != null) {
+                    mKeyguardLock = km.newKeyguardLock(TAG);
+                }
+            }
+            if (mKeyguardLock != null) {
+                if (enabled) {
+                    mKeyguardLock.disableKeyguard();
+                } else {
+                    mKeyguardLock.reenableKeyguard();
+                }
+            }
         }
     }
 
@@ -309,14 +360,14 @@
         synchronized (mLock) {
             if (newState != mDockState) {
                 mDockState = newState;
-                mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR;
+                setCarModeLocked(mDockState == Intent.EXTRA_DOCK_STATE_CAR);
                 if (mSystemReady) {
                     updateLocked();
                 }
             }
         }
     }
-    
+
     final void updateLocked() {
         long ident = Binder.clearCallingIdentity();
         
@@ -398,6 +449,18 @@
                 mContext.sendOrderedBroadcast(new Intent(action), null,
                         mResultReceiver, null, Activity.RESULT_OK, null, null);
             }
+
+            // keep screen on when charging and in car mode
+            boolean keepScreenOn = mCharging &&
+                    ((mCarModeEnabled && mCarModeKeepsScreenOn) ||
+                     (mCurUiMode == Configuration.UI_MODE_TYPE_DESK && mDeskModeKeepsScreenOn));
+            if (keepScreenOn != mWakeLock.isHeld()) {
+                if (keepScreenOn) {
+                    mWakeLock.acquire();
+                } else {
+                    mWakeLock.release();
+                }
+            }
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
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");
     }
diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp
index 0b531c2..1f9d152 100644
--- a/tools/aapt/ResourceTable.cpp
+++ b/tools/aapt/ResourceTable.cpp
@@ -2369,7 +2369,7 @@
                         if (configSet.find(region) == configSet.end()) {
                             if (configSet.count(defaultLocale) == 0) {
                                 fprintf(stdout, "aapt: warning: "
-                                        "*** string '%s' has no default or required localization "
+                                        "**** string '%s' has no default or required localization "
                                         "for '%s' in %s\n",
                                         String8(nameIter->first).string(),
                                         config.string(),