Persist fingerprint names

Bug: 20469328
Change-Id: I8f4a988687bfb78c36cc7cf187103a9d93ed4535
diff --git a/services/core/java/com/android/server/fingerprint/FingerprintService.java b/services/core/java/com/android/server/fingerprint/FingerprintService.java
index 0faccc6..aa143e4 100644
--- a/services/core/java/com/android/server/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/fingerprint/FingerprintService.java
@@ -31,7 +31,6 @@
 
 import com.android.server.SystemService;
 
-import android.hardware.fingerprint.FingerprintUtils;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.IFingerprintService;
@@ -93,6 +92,7 @@
     private Context mContext;
     private int mHalDeviceId;
     private int mFailedAttempts;
+    private final FingerprintUtils mFingerprintUtils = FingerprintUtils.getInstance();
     private final Runnable mLockoutReset = new Runnable() {
         @Override
         public void run() {
@@ -172,7 +172,6 @@
      * @return true if the operation is done, i.e. authentication completed
      */
     boolean dispatchNotify(ClientMonitor clientMonitor, int type, int arg1, int arg2, int arg3) {
-        ContentResolver contentResolver = mContext.getContentResolver();
         boolean operationCompleted = false;
         int fpId;
         int groupId;
@@ -198,7 +197,7 @@
                 remaining = arg3;
                 operationCompleted = clientMonitor.sendEnrollResult(fpId, groupId, remaining);
                 if (remaining == 0) {
-                    addTemplateForUser(clientMonitor, contentResolver, fpId);
+                    addTemplateForUser(clientMonitor, fpId);
                     operationCompleted = true; // enroll completed
                 }
                 break;
@@ -207,7 +206,7 @@
                 groupId = arg2;
                 operationCompleted = clientMonitor.sendRemoved(fpId, groupId);
                 if (fpId != 0) {
-                    removeTemplateForUser(clientMonitor, contentResolver, fpId);
+                    removeTemplateForUser(clientMonitor, fpId);
                 }
                 break;
         }
@@ -252,16 +251,12 @@
         return false;
     }
 
-    private void removeTemplateForUser(ClientMonitor clientMonitor, ContentResolver contentResolver,
-            final int fingerId) {
-        FingerprintUtils.removeFingerprintIdForUser(fingerId, contentResolver,
-                clientMonitor.userId);
+    private void removeTemplateForUser(ClientMonitor clientMonitor, int fingerId) {
+        mFingerprintUtils.removeFingerprintIdForUser(mContext, fingerId, clientMonitor.userId);
     }
 
-    private void addTemplateForUser(ClientMonitor clientMonitor, ContentResolver contentResolver,
-            final int fingerId) {
-        FingerprintUtils.addFingerprintIdForUser(contentResolver, fingerId,
-                clientMonitor.userId);
+    private void addTemplateForUser(ClientMonitor clientMonitor, int fingerId) {
+        mFingerprintUtils.addFingerprintForUser(mContext, fingerId, clientMonitor.userId);
     }
 
     void startEnrollment(IBinder token, byte[] cryptoToken, int groupId,
@@ -345,24 +340,11 @@
     }
 
     public List<Fingerprint> getEnrolledFingerprints(int groupId) {
-        ContentResolver resolver = mContext.getContentResolver();
-        int[] ids = FingerprintUtils.getFingerprintIdsForUser(resolver, groupId);
-        List<Fingerprint> result = new ArrayList<Fingerprint>();
-        for (int i = 0; i < ids.length; i++) {
-            // TODO: persist names in Settings
-            CharSequence name = "Finger" + ids[i];
-            final int group = 0; // TODO
-            final int fingerId = ids[i];
-            final long deviceId = 0; // TODO
-            Fingerprint item = new Fingerprint(name, 0, ids[i], 0);
-            result.add(item);
-        }
-        return result;
+        return mFingerprintUtils.getFingerprintsForUser(mContext, groupId);
     }
 
     public boolean hasEnrolledFingerprints(int groupId) {
-        ContentResolver resolver = mContext.getContentResolver();
-        return FingerprintUtils.getFingerprintIdsForUser(resolver, groupId).length > 0;
+        return mFingerprintUtils.getFingerprintsForUser(mContext, groupId).size() > 0;
     }
 
     void checkPermission(String permission) {
@@ -596,7 +578,7 @@
             mHandler.post(new Runnable() {
                 @Override
                 public void run() {
-                    Slog.w(TAG, "rename id=" + fingerId + ",gid=" + groupId + ",name=" + name);
+                    mFingerprintUtils.renameFingerprintForUser(mContext, fingerId, groupId, name);
                 }
             });
         }
diff --git a/services/core/java/com/android/server/fingerprint/FingerprintUtils.java b/services/core/java/com/android/server/fingerprint/FingerprintUtils.java
new file mode 100644
index 0000000..1e6e105
--- /dev/null
+++ b/services/core/java/com/android/server/fingerprint/FingerprintUtils.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (C) 2015 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.server.fingerprint;
+
+import android.content.Context;
+import android.hardware.fingerprint.Fingerprint;
+import android.os.Vibrator;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.List;
+
+/**
+ * Utility class for dealing with fingerprints and fingerprint settings.
+ */
+public class FingerprintUtils {
+
+    private static final long[] FP_ERROR_VIBRATE_PATTERN = new long[] {0, 30, 100, 30};
+    private static final long[] FP_SUCCESS_VIBRATE_PATTERN = new long[] {0, 30};
+
+    private static final Object sInstanceLock = new Object();
+    private static FingerprintUtils sInstance;
+
+    @GuardedBy("this")
+    private final SparseArray<FingerprintsUserState> mUsers = new SparseArray<>();
+
+    public static FingerprintUtils getInstance() {
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                sInstance = new FingerprintUtils();
+            }
+        }
+        return sInstance;
+    }
+
+    private FingerprintUtils() {
+    }
+
+    public List<Fingerprint> getFingerprintsForUser(Context ctx, int userId) {
+        return getStateForUser(ctx, userId).getFingerprints();
+    }
+
+    public void addFingerprintForUser(Context ctx, int fingerId, int userId) {
+        getStateForUser(ctx, userId).addFingerprint(fingerId);
+    }
+
+    public void removeFingerprintIdForUser(Context ctx, int fingerId, int userId) {
+        getStateForUser(ctx, userId).removeFingerprint(fingerId);
+    }
+
+    public void renameFingerprintForUser(Context ctx, int fingerId, int userId, CharSequence name) {
+        getStateForUser(ctx, userId).renameFingerprint(fingerId, name);
+    }
+
+    public static void vibrateFingerprintError(Context context) {
+        Vibrator vibrator = context.getSystemService(Vibrator.class);
+        if (vibrator != null) {
+            vibrator.vibrate(FP_ERROR_VIBRATE_PATTERN, -1);
+        }
+    }
+
+    public static void vibrateFingerprintSuccess(Context context) {
+        Vibrator vibrator = context.getSystemService(Vibrator.class);
+        if (vibrator != null) {
+            vibrator.vibrate(FP_SUCCESS_VIBRATE_PATTERN, -1);
+        }
+    }
+
+    private FingerprintsUserState getStateForUser(Context ctx, int userId) {
+        synchronized (this) {
+            FingerprintsUserState state = mUsers.get(userId);
+            if (state == null) {
+                state = new FingerprintsUserState(ctx, userId);
+                mUsers.put(userId, state);
+            }
+            return state;
+        }
+    }
+}
+
diff --git a/services/core/java/com/android/server/fingerprint/FingerprintsUserState.java b/services/core/java/com/android/server/fingerprint/FingerprintsUserState.java
new file mode 100644
index 0000000..33177b4
--- /dev/null
+++ b/services/core/java/com/android/server/fingerprint/FingerprintsUserState.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2015 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.server.fingerprint;
+
+import android.content.Context;
+import android.hardware.fingerprint.Fingerprint;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+
+import libcore.io.IoUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class managing the set of fingerprint per user across device reboots.
+ */
+class FingerprintsUserState {
+
+    private static final String TAG = "FingerprintState";
+    private static final String FINGERPRINT_FILE = "settings_fingerprint.xml";
+
+    private static final String TAG_FINGERPRINTS = "fingerprints";
+    private static final String TAG_FINGERPRINT = "fingerprint";
+    private static final String ATTR_NAME = "name";
+    private static final String ATTR_GROUP_ID = "groupId";
+    private static final String ATTR_FINGER_ID = "fingerId";
+    private static final String ATTR_DEVICE_ID = "deviceId";
+
+    private final File mFile;
+
+    @GuardedBy("this")
+    private final ArrayList<Fingerprint> mFingerprints = new ArrayList<>();
+    private final Context mCtx;
+
+    public FingerprintsUserState(Context ctx, int userId) {
+        mFile = getFileForUser(userId);
+        mCtx = ctx;
+        synchronized (this) {
+            readStateSyncLocked();
+        }
+    }
+
+    public void addFingerprint(int fingerId) {
+        synchronized (this) {
+            mFingerprints.add(new Fingerprint(getDefaultFingerprintName(fingerId), 0, fingerId, 0));
+            scheduleWriteStateLocked();
+        }
+    }
+
+    public void removeFingerprint(int fingerId) {
+        synchronized (this) {
+            for (int i = 0; i < mFingerprints.size(); i++) {
+                if (mFingerprints.get(i).getFingerId() == fingerId) {
+                    mFingerprints.remove(i);
+                    scheduleWriteStateLocked();
+                    break;
+                }
+            }
+        }
+    }
+
+    public void renameFingerprint(int fingerId, CharSequence name) {
+        synchronized (this) {
+            for (int i = 0; i < mFingerprints.size(); i++) {
+                if (mFingerprints.get(i).getFingerId() == fingerId) {
+                    Fingerprint old = mFingerprints.get(i);
+                    mFingerprints.set(i, new Fingerprint(name, old.getGroupId(), old.getFingerId(),
+                            old.getDeviceId()));
+                    scheduleWriteStateLocked();
+                    break;
+                }
+            }
+        }
+    }
+
+    public List<Fingerprint> getFingerprints() {
+        synchronized (this) {
+            return getCopy(mFingerprints);
+        }
+    }
+
+    private String getDefaultFingerprintName(int fingerId) {
+        return mCtx.getString(com.android.internal.R.string.fingerprint_name_template, fingerId);
+    }
+
+    private static File getFileForUser(int userId) {
+        return new File(Environment.getUserSystemDirectory(userId), FINGERPRINT_FILE);
+    }
+
+    private final Runnable mWriteStateRunnable = new Runnable() {
+        @Override
+        public void run() {
+            doWriteState();
+        }
+    };
+
+    private void scheduleWriteStateLocked() {
+        AsyncTask.execute(mWriteStateRunnable);
+    }
+
+    private ArrayList<Fingerprint> getCopy(ArrayList<Fingerprint> array) {
+        ArrayList<Fingerprint> result = new ArrayList<>(array.size());
+        for (int i = 0; i < array.size(); i++) {
+            Fingerprint fp = array.get(i);
+            result.add(new Fingerprint(fp.getName(), fp.getGroupId(), fp.getFingerId(),
+                    fp.getDeviceId()));
+        }
+        return result;
+    }
+
+    private void doWriteState() {
+        AtomicFile destination = new AtomicFile(mFile);
+
+        ArrayList<Fingerprint> fingerprints;
+
+        synchronized (this) {
+            fingerprints = getCopy(mFingerprints);
+        }
+
+        FileOutputStream out = null;
+        try {
+            out = destination.startWrite();
+
+            XmlSerializer serializer = Xml.newSerializer();
+            serializer.setOutput(out, "utf-8");
+            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+            serializer.startDocument(null, true);
+            serializer.startTag(null, TAG_FINGERPRINTS);
+
+            final int count = fingerprints.size();
+            for (int i = 0; i < count; i++) {
+                Fingerprint fp = fingerprints.get(i);
+                serializer.startTag(null, TAG_FINGERPRINT);
+                serializer.attribute(null, ATTR_FINGER_ID, Integer.toString(fp.getFingerId()));
+                serializer.attribute(null, ATTR_NAME, fp.getName().toString());
+                serializer.attribute(null, ATTR_GROUP_ID, Integer.toString(fp.getGroupId()));
+                serializer.attribute(null, ATTR_DEVICE_ID, Long.toString(fp.getDeviceId()));
+                serializer.endTag(null, TAG_FINGERPRINT);
+            }
+
+            serializer.endTag(null, TAG_FINGERPRINTS);
+            serializer.endDocument();
+            destination.finishWrite(out);
+
+            // Any error while writing is fatal.
+        } catch (Throwable t) {
+            Slog.wtf(TAG, "Failed to write settings, restoring backup", t);
+            destination.failWrite(out);
+            throw new IllegalStateException("Failed to write fingerprints", t);
+        } finally {
+            IoUtils.closeQuietly(out);
+        }
+    }
+
+    private void readStateSyncLocked() {
+        FileInputStream in;
+        if (!mFile.exists()) {
+            return;
+        }
+        try {
+            in = new FileInputStream(mFile);
+        } catch (FileNotFoundException fnfe) {
+            Slog.i(TAG, "No fingerprint state");
+            return;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(in, null);
+            parseStateLocked(parser);
+
+        } catch (XmlPullParserException | IOException e) {
+            throw new IllegalStateException("Failed parsing settings file: "
+                    + mFile , e);
+        } finally {
+            IoUtils.closeQuietly(in);
+        }
+    }
+
+    private void parseStateLocked(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals(TAG_FINGERPRINTS)) {
+                parseFingerprintsLocked(parser);
+            }
+        }
+    }
+
+    private void parseFingerprintsLocked(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals(TAG_FINGERPRINT)) {
+                String name = parser.getAttributeValue(null, ATTR_NAME);
+                String groupId = parser.getAttributeValue(null, ATTR_GROUP_ID);
+                String fingerId = parser.getAttributeValue(null, ATTR_FINGER_ID);
+                String deviceId = parser.getAttributeValue(null, ATTR_DEVICE_ID);
+                mFingerprints.add(new Fingerprint(name, Integer.parseInt(groupId),
+                        Integer.parseInt(fingerId), Integer.parseInt(deviceId)));
+            }
+        }
+    }
+
+}