Add a cache to LockPatternUtils

Caches responses from LockSettingsService in the client process.

Bug: 15088101
Change-Id: If77c5ec45f52a02c800d50cb8550bfcb180f301d
diff --git a/Android.mk b/Android.mk
index de93ca2..9c41f9f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -276,6 +276,7 @@
 	core/java/com/android/internal/view/IInputMethodSession.aidl \
 	core/java/com/android/internal/view/IInputSessionCallback.aidl \
 	core/java/com/android/internal/widget/ILockSettings.aidl \
+	core/java/com/android/internal/widget/ILockSettingsObserver.aidl \
 	core/java/com/android/internal/widget/IRemoteViewsFactory.aidl \
 	core/java/com/android/internal/widget/IRemoteViewsAdapterConnection.aidl \
 	keystore/java/android/security/IKeyChainAliasCallback.aidl \
diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl
index 9501f92..c70841b 100644
--- a/core/java/com/android/internal/widget/ILockSettings.aidl
+++ b/core/java/com/android/internal/widget/ILockSettings.aidl
@@ -16,6 +16,8 @@
 
 package com.android.internal.widget;
 
+import com.android.internal.widget.ILockSettingsObserver;
+
 /** {@hide} */
 interface ILockSettings {
     void setBoolean(in String key, in boolean value, in int userId);
@@ -32,4 +34,6 @@
     boolean havePattern(int userId);
     boolean havePassword(int userId);
     void removeUser(int userId);
+    void registerObserver(in ILockSettingsObserver observer);
+    void unregisterObserver(in ILockSettingsObserver observer);
 }
diff --git a/core/java/com/android/internal/widget/ILockSettingsObserver.aidl b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl
new file mode 100644
index 0000000..6c354d8
--- /dev/null
+++ b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 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 com.android.internal.widget;
+
+/** {@hide} */
+oneway interface ILockSettingsObserver {
+    void onLockSettingChanged(in String key, in int userId);
+}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 2882b54..25e3463 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -199,8 +199,8 @@
 
     private ILockSettings getLockSettings() {
         if (mLockSettingsService == null) {
-            mLockSettingsService = ILockSettings.Stub.asInterface(
-                (IBinder) ServiceManager.getService("lock_settings"));
+            mLockSettingsService = LockPatternUtilsCache.getInstance(
+                    ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings")));
         }
         return mLockSettingsService;
     }
diff --git a/core/java/com/android/internal/widget/LockPatternUtilsCache.java b/core/java/com/android/internal/widget/LockPatternUtilsCache.java
new file mode 100644
index 0000000..550aa6d
--- /dev/null
+++ b/core/java/com/android/internal/widget/LockPatternUtilsCache.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2014 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 com.android.internal.widget;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+
+/**
+ * A decorator for {@link ILockSettings} that caches the key-value responses in memory.
+ *
+ * Specifically, the return values of {@link #getString(String, String, int)},
+ * {@link #getLong(String, long, int)} and {@link #getBoolean(String, boolean, int)} are cached.
+ */
+public class LockPatternUtilsCache implements ILockSettings {
+
+    private static LockPatternUtilsCache sInstance;
+
+    private final ILockSettings mService;
+
+    /** Only access when holding {@code mCache} lock. */
+    private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
+
+    /** Only access when holding {@link #mCache} lock. */
+    private final CacheKey mCacheKey = new CacheKey();
+
+
+    public static synchronized LockPatternUtilsCache getInstance(ILockSettings service) {
+        if (sInstance == null) {
+            sInstance = new LockPatternUtilsCache(service);
+        }
+        return sInstance;
+    }
+
+    // ILockSettings
+
+    private LockPatternUtilsCache(ILockSettings service) {
+        mService = service;
+        try {
+            service.registerObserver(mObserver);
+        } catch (RemoteException e) {
+            // Not safe to do caching without the observer. System process has probably died
+            // anyway, so crashing here is fine.
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void setBoolean(String key, boolean value, int userId) throws RemoteException {
+        invalidateCache(key, userId);
+        mService.setBoolean(key, value, userId);
+        putCache(key, userId, value);
+    }
+
+    public void setLong(String key, long value, int userId) throws RemoteException {
+        invalidateCache(key, userId);
+        mService.setLong(key, value, userId);
+        putCache(key, userId, value);
+    }
+
+    public void setString(String key, String value, int userId) throws RemoteException {
+        invalidateCache(key, userId);
+        mService.setString(key, value, userId);
+        putCache(key, userId, value);
+    }
+
+    public long getLong(String key, long defaultValue, int userId) throws RemoteException {
+        Object value = peekCache(key, userId);
+        if (value instanceof Long) {
+            return (long) value;
+        }
+        long result = mService.getLong(key, defaultValue, userId);
+        putCache(key, userId, result);
+        return result;
+    }
+
+    public String getString(String key, String defaultValue, int userId) throws RemoteException {
+        Object value = peekCache(key, userId);
+        if (value instanceof String) {
+            return (String) value;
+        }
+        String result = mService.getString(key, defaultValue, userId);
+        putCache(key, userId, result);
+        return result;
+    }
+
+    public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException {
+        Object value = peekCache(key, userId);
+        if (value instanceof Boolean) {
+            return (boolean) value;
+        }
+        boolean result = mService.getBoolean(key, defaultValue, userId);
+        putCache(key, userId, result);
+        return result;
+    }
+
+    @Override
+    public void setLockPattern(String pattern, int userId) throws RemoteException {
+        mService.setLockPattern(pattern, userId);
+    }
+
+    @Override
+    public boolean checkPattern(String pattern, int userId) throws RemoteException {
+        return mService.checkPattern(pattern, userId);
+    }
+
+    @Override
+    public void setLockPassword(String password, int userId) throws RemoteException {
+        mService.setLockPassword(password, userId);
+    }
+
+    @Override
+    public boolean checkPassword(String password, int userId) throws RemoteException {
+        return mService.checkPassword(password, userId);
+    }
+
+    @Override
+    public boolean checkVoldPassword(int userId) throws RemoteException {
+        return mService.checkVoldPassword(userId);
+    }
+
+    @Override
+    public boolean havePattern(int userId) throws RemoteException {
+        return mService.havePattern(userId);
+    }
+
+    @Override
+    public boolean havePassword(int userId) throws RemoteException {
+        return mService.havePassword(userId);
+    }
+
+    @Override
+    public void removeUser(int userId) throws RemoteException {
+        mService.removeUser(userId);
+    }
+
+    @Override
+    public void registerObserver(ILockSettingsObserver observer) throws RemoteException {
+        mService.registerObserver(observer);
+    }
+
+    @Override
+    public void unregisterObserver(ILockSettingsObserver observer) throws RemoteException {
+        mService.unregisterObserver(observer);
+    }
+
+    @Override
+    public IBinder asBinder() {
+        return mService.asBinder();
+    }
+
+    // Caching
+
+    private Object peekCache(String key, int userId) {
+        synchronized (mCache) {
+            // Safe to reuse mCacheKey, because it is not stored in the map.
+            return mCache.get(mCacheKey.set(key, userId));
+        }
+    }
+
+    private void putCache(String key, int userId, Object value) {
+        synchronized (mCache) {
+            // Create a new key, because this will be stored in the map.
+            mCache.put(new CacheKey().set(key, userId), value);
+        }
+    }
+
+    private void invalidateCache(String key, int userId) {
+        synchronized (mCache) {
+            // Safe to reuse mCacheKey, because it is not stored in the map.
+            mCache.remove(mCacheKey.set(key, userId));
+        }
+    }
+
+    private final ILockSettingsObserver mObserver = new ILockSettingsObserver.Stub() {
+        @Override
+        public void onLockSettingChanged(String key, int userId) throws RemoteException {
+            invalidateCache(key, userId);
+        }
+    };
+
+    private static final class CacheKey {
+        String key;
+        int userId;
+
+        public CacheKey set(String key, int userId) {
+            this.key = key;
+            this.userId = userId;
+            return this;
+        }
+
+        public CacheKey copy() {
+            return new CacheKey().set(key, userId);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof CacheKey))
+                return false;
+            CacheKey o = (CacheKey) obj;
+            return userId == o.userId && key.equals(o.key);
+        }
+
+        @Override
+        public int hashCode() {
+            return key.hashCode() ^ userId;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index 0d2cee8..5cfc49c 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -47,12 +47,14 @@
 import android.util.Slog;
 
 import com.android.internal.widget.ILockSettings;
+import com.android.internal.widget.ILockSettingsObserver;
 import com.android.internal.widget.LockPatternUtils;
 
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
@@ -65,6 +67,9 @@
 public class LockSettingsService extends ILockSettings.Stub {
 
     private static final String PERMISSION = "android.permission.ACCESS_KEYGUARD_SECURE_STORAGE";
+
+    private static final String SYSTEM_DEBUGGABLE = "ro.debuggable";
+
     private final DatabaseHelper mOpenHelper;
     private static final String TAG = "LockSettingsService";
 
@@ -85,6 +90,8 @@
     private LockPatternUtils mLockPatternUtils;
     private boolean mFirstCallToVold;
 
+    private final ArrayList<LockSettingsObserver> mObservers = new ArrayList<>();
+
     public LockSettingsService(Context context) {
         mContext = context;
         // Open the database
@@ -222,6 +229,52 @@
         return readFromDb(key, defaultValue, userId);
     }
 
+    @Override
+    public void registerObserver(ILockSettingsObserver remote) throws RemoteException {
+        synchronized (mObservers) {
+            for (int i = 0; i < mObservers.size(); i++) {
+                if (mObservers.get(i).remote.asBinder() == remote.asBinder()) {
+                    boolean isDebuggable = "1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0"));
+                    if (isDebuggable) {
+                        throw new IllegalStateException("Observer was already registered.");
+                    } else {
+                        Log.e(TAG, "Observer was already registered.");
+                        return;
+                    }
+                }
+            }
+            LockSettingsObserver o = new LockSettingsObserver();
+            o.remote = remote;
+            o.remote.asBinder().linkToDeath(o, 0);
+            mObservers.add(o);
+        }
+    }
+
+    @Override
+    public void unregisterObserver(ILockSettingsObserver remote) throws RemoteException {
+        synchronized (mObservers) {
+            for (int i = 0; i < mObservers.size(); i++) {
+                if (mObservers.get(i).remote.asBinder() == remote.asBinder()) {
+                    mObservers.remove(i);
+                    return;
+                }
+            }
+        }
+    }
+
+    public void notifyObservers(String key, int userId) {
+        synchronized (mObservers) {
+            for (int i = 0; i < mObservers.size(); i++) {
+                try {
+                    mObservers.get(i).remote.onLockSettingChanged(key, userId);
+                } catch (RemoteException e) {
+                    // The stack trace is not really helpful here.
+                    Log.e(TAG, "Failed to notify ILockSettingsObserver: " + e);
+                }
+            }
+        }
+    }
+
     private String getLockPatternFilename(int userId) {
         String dataSystemDirectory =
                 android.os.Environment.getDataDirectory().getAbsolutePath() +
@@ -438,6 +491,7 @@
 
     private void writeToDb(String key, String value, int userId) {
         writeToDb(mOpenHelper.getWritableDatabase(), key, value, userId);
+        notifyObservers(key, userId);
     }
 
     private void writeToDb(SQLiteDatabase db, String key, String value, int userId) {
@@ -583,4 +637,13 @@
         }
         return null;
     }
+
+    private class LockSettingsObserver implements DeathRecipient {
+        ILockSettingsObserver remote;
+
+        @Override
+        public void binderDied() {
+            mObservers.remove(this);
+        }
+    }
 }