OSDN Git Service

7/n: Add enrollment animation
authorKevin Chyn <kchyn@google.com>
Thu, 9 Aug 2018 23:31:09 +0000 (16:31 -0700)
committerKevin Chyn <kchyn@google.com>
Wed, 15 Aug 2018 05:22:53 +0000 (22:22 -0700)
Fixes: 112005540

Test: Tested with ag/4749121

Change-Id: I7d51187f7b8b7a6c2c34c984740b76bc9fd89262

res/values/colors.xml
src/com/android/settings/biometrics/BiometricEnrollSidecar.java
src/com/android/settings/biometrics/face/AnimationParticle.java [new file with mode: 0644]
src/com/android/settings/biometrics/face/FaceEnrollAnimationDrawable.java
src/com/android/settings/biometrics/face/FaceEnrollEnrolling.java
src/com/android/settings/biometrics/face/FaceEnrollPreviewFragment.java
src/com/android/settings/biometrics/face/ParticleCollection.java [new file with mode: 0644]
src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java

index e5f7c27..f398d92 100644 (file)
     <color name="battery_maybe_color_dark">#fdd835</color> <!-- Material Yellow 600 -->
     <color name="battery_bad_color_dark">#f44336</color> <!-- Material Red 500 -->
 
+    <!-- TODO: Figure out colors -->
+    <color name="face_anim_particle_color_1">#ff00bcd4</color> <!-- Material Cyan 500 -->
+    <color name="face_anim_particle_color_2">#ffef6c00</color> <!-- Material Orange 800 -->
+    <color name="face_anim_particle_color_3">#ff4caf50</color> <!-- Material Green 500 -->
+    <color name="face_anim_particle_color_4">#fffdd835</color> <!-- Material Yellow 600 -->
+    <color name="face_anim_particle_error">#ff9e9e9e</color> <!-- Material Gray 500 -->
 </resources>
 
index 111fecd..cedbec1 100644 (file)
@@ -37,7 +37,7 @@ import java.util.ArrayList;
 public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
 
     public interface Listener {
-        void onEnrollmentHelp(CharSequence helpString);
+        void onEnrollmentHelp(int helpMsgId, CharSequence helpString);
         void onEnrollmentError(int errMsgId, CharSequence errString);
         void onEnrollmentProgressChange(int steps, int remaining);
     }
@@ -82,7 +82,7 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
 
         @Override
         public void send(Listener listener) {
-            listener.onEnrollmentHelp(helpString);
+            listener.onEnrollmentHelp(helpMsgId, helpString);
         }
     }
 
@@ -174,7 +174,7 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
 
     protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
         if (mListener != null) {
-            mListener.onEnrollmentHelp(helpString);
+            mListener.onEnrollmentHelp(helpMsgId, helpString);
         } else {
             mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
         }
diff --git a/src/com/android/settings/biometrics/face/AnimationParticle.java b/src/com/android/settings/biometrics/face/AnimationParticle.java
new file mode 100644 (file)
index 0000000..a192e9f
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2018 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.settings.biometrics.face;
+
+import android.animation.ArgbEvaluator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+
+import com.android.settings.R;
+
+import java.util.List;
+
+/**
+ * Class containing the state for an individual feedback dot / path. The dots are assigned colors
+ * based on their index.
+ */
+public class AnimationParticle {
+
+    private static final String TAG = "AnimationParticle";
+
+    private static final int MIN_STROKE_WIDTH = 10;
+    private static final int MAX_STROKE_WIDTH = 20; // Be careful that this doesn't get clipped
+    private static final int FINAL_RING_STROKE_WIDTH = 15;
+
+    private static final float ROTATION_SPEED_NORMAL = 0.8f; // radians per second, 1 = ~57 degrees
+    private static final float ROTATION_ACCELERATION_SPEED = 2.0f;
+    private static final float PULSE_SPEED_NORMAL = 1 * 2 * (float) Math.PI; // 1 cycle per second
+    private static final float RING_SWEEP_GROW_RATE_PRIMARY = 480; // degrees per second
+    private static final float RING_SWEEP_GROW_RATE_SECONDARY = 240; // degrees per second
+    private static final float RING_SIZE_FINALIZATION_TIME = 0.1f; // seconds
+
+    private final Rect mBounds; // bounds for the canvas
+    private final int mBorderWidth; // amount of padding from the edges
+    private final ArgbEvaluator mEvaluator;
+    private final int mErrorColor;
+    private final int mIndex;
+    private final Listener mListener;
+
+    private final Paint mPaint;
+    private final int mAssignedColor;
+    private final float mOffsetTimeSec; // stagger particle size to make a wave effect
+
+    private int mLastAnimationState;
+    private int mAnimationState;
+    private float mCurrentSize = MIN_STROKE_WIDTH;
+    private float mCurrentAngle; // 0 is to the right, in radians
+    private float mRotationSpeed = ROTATION_SPEED_NORMAL; // speed of dot rotation
+    private float mSweepAngle = 0; // ring sweep, degrees per second
+    private float mSweepRate = RING_SWEEP_GROW_RATE_SECONDARY; // acceleration
+    private float mRingAdjustRate; // rate at which ring should grow/shrink to final size
+    private float mRingCompletionTime; // time at which ring should be completed
+
+    public interface Listener {
+        void onRingCompleted(int index);
+    }
+
+    public AnimationParticle(Context context, Listener listener, Rect bounds, int borderWidth,
+            int index, int totalParticles, List<Integer> colors) {
+        mBounds = bounds;
+        mBorderWidth = borderWidth;
+        mEvaluator = new ArgbEvaluator();
+        mErrorColor = context.getResources()
+                .getColor(R.color.face_anim_particle_error, context.getTheme());
+        mIndex = index;
+        mListener = listener;
+
+        mCurrentAngle = (float) index / totalParticles * 2 * (float) Math.PI;
+        mOffsetTimeSec = (float) index / totalParticles
+                * (1 / ROTATION_SPEED_NORMAL) * 2 * (float) Math.PI;
+
+        mPaint = new Paint();
+        mAssignedColor = colors.get(index % colors.size());
+        mPaint.setColor(mAssignedColor);
+        mPaint.setAntiAlias(true);
+        mPaint.setStrokeWidth(mCurrentSize);
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+    }
+
+    public void updateState(int animationState) {
+        if (mAnimationState == animationState) {
+            Log.w(TAG, "Already in state " + animationState);
+            return;
+        }
+        if (animationState == ParticleCollection.STATE_COMPLETE) {
+            mPaint.setStyle(Paint.Style.STROKE);
+        }
+        mLastAnimationState = mAnimationState;
+        mAnimationState = animationState;
+    }
+
+    // There are two types of particles, secondary and primary. Primary particles accelerate faster
+    // during the "completed" animation. Particles are secondary by default.
+    public void setAsPrimary() {
+        mSweepRate = RING_SWEEP_GROW_RATE_PRIMARY;
+    }
+
+    public void update(long t, long dt) {
+        if (mAnimationState != ParticleCollection.STATE_COMPLETE) {
+            updateDot(t, dt);
+        } else {
+            updateRing(t, dt);
+        }
+    }
+
+    private void updateDot(long t, long dt) {
+        final float dtSec = 0.001f * dt;
+        final float tSec = 0.001f * t;
+
+        final float multiplier = mRotationSpeed / ROTATION_SPEED_NORMAL;
+
+        // Calculate rotation speed / angle
+        if ((mAnimationState == ParticleCollection.STATE_STOPPED_COLORFUL
+                || mAnimationState == ParticleCollection.STATE_STOPPED_GRAY)
+                && mRotationSpeed > 0) {
+            // Linear slow down for now
+            mRotationSpeed = Math.max(mRotationSpeed - ROTATION_ACCELERATION_SPEED * dtSec, 0);
+        } else if (mAnimationState == ParticleCollection.STATE_STARTED
+                && mRotationSpeed < ROTATION_SPEED_NORMAL) {
+            // Linear speed up for now
+            mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec;
+        }
+
+        mCurrentAngle += dtSec * mRotationSpeed;
+
+        // Calculate dot / ring size; linearly proportional with rotation speed
+        mCurrentSize =
+                (MAX_STROKE_WIDTH - MIN_STROKE_WIDTH) / 2
+                * (float) Math.sin(tSec * PULSE_SPEED_NORMAL + mOffsetTimeSec)
+                + (MAX_STROKE_WIDTH + MIN_STROKE_WIDTH) / 2;
+        mCurrentSize = (mCurrentSize - MIN_STROKE_WIDTH) * multiplier + MIN_STROKE_WIDTH;
+
+        // Calculate paint color; linearly proportional to rotation speed
+        int color = mAssignedColor;
+        if (mAnimationState == ParticleCollection.STATE_STOPPED_GRAY) {
+            color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor);
+        } else if (mLastAnimationState == ParticleCollection.STATE_STOPPED_GRAY) {
+            color = (int) mEvaluator.evaluate(1 - multiplier, mAssignedColor, mErrorColor);
+        }
+
+        mPaint.setColor(color);
+        mPaint.setStrokeWidth(mCurrentSize);
+    }
+
+    private void updateRing(long t, long dt) {
+        final float dtSec = 0.001f * dt;
+        final float tSec = 0.001f * t;
+
+        // Store the start time, since we need to guarantee all rings reach final size at same time
+        // independent of current size. The magic 0 check is safe.
+        if (mRingAdjustRate == 0) {
+            mRingAdjustRate =
+                    (FINAL_RING_STROKE_WIDTH - mCurrentSize) / RING_SIZE_FINALIZATION_TIME;
+            if (mRingCompletionTime == 0) {
+                mRingCompletionTime = tSec + RING_SIZE_FINALIZATION_TIME;
+            }
+        }
+
+        // Accelerate to attack speed.. jk, back to normal speed
+        if (mRotationSpeed < ROTATION_SPEED_NORMAL) {
+            mRotationSpeed += ROTATION_ACCELERATION_SPEED * dtSec;
+        }
+
+        // For arcs, this is the "start"
+        mCurrentAngle += dtSec * mRotationSpeed;
+
+        // Update the sweep angle until it fills entire circle
+        if (mSweepAngle < 360) {
+            final float sweepGrowth = mSweepRate * dtSec;
+            mSweepAngle = mSweepAngle + sweepGrowth;
+            mSweepRate = mSweepRate + sweepGrowth;
+        }
+        if (mSweepAngle > 360) {
+            mSweepAngle = 360;
+            mListener.onRingCompleted(mIndex);
+        }
+
+        // Animate stroke width to final size.
+        if (tSec < RING_SIZE_FINALIZATION_TIME) {
+            mCurrentSize = mCurrentSize + mRingAdjustRate * dtSec;
+            mPaint.setStrokeWidth(mCurrentSize);
+        } else {
+            // There should be small to no discontinuity in this if/else
+            mCurrentSize = FINAL_RING_STROKE_WIDTH;
+            mPaint.setStrokeWidth(mCurrentSize);
+        }
+
+    }
+
+    public void draw(Canvas canvas) {
+        if (mAnimationState != ParticleCollection.STATE_COMPLETE) {
+            drawDot(canvas);
+        } else {
+            drawRing(canvas);
+        }
+    }
+
+    // Draws a dot at the current position on the circumference of the path.
+    private void drawDot(Canvas canvas) {
+        final float w = mBounds.right - mBounds.exactCenterX() - mBorderWidth;
+        final float h = mBounds.bottom - mBounds.exactCenterY() - mBorderWidth;
+        canvas.drawCircle(
+                mBounds.exactCenterX() + w * (float) Math.cos(mCurrentAngle),
+                mBounds.exactCenterY() + h * (float) Math.sin(mCurrentAngle),
+                mCurrentSize,
+                mPaint);
+    }
+
+    private void drawRing(Canvas canvas) {
+        RectF arc = new RectF(
+                mBorderWidth, mBorderWidth,
+                mBounds.width() - mBorderWidth, mBounds.height() - mBorderWidth);
+        Path path = new Path();
+        path.arcTo(arc, (float) Math.toDegrees(mCurrentAngle), mSweepAngle);
+        canvas.drawPath(path, mPaint);
+    }
+}
index 0da666c..5be7c53 100644 (file)
@@ -16,6 +16,8 @@
 
 package com.android.settings.biometrics.face;
 
+import android.animation.TimeAnimator;
+import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
@@ -26,16 +28,43 @@ import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 
+import com.android.settings.biometrics.BiometricEnrollSidecar;
+
 /**
- * A drawable containing the circle cutout.
+ * A drawable containing the circle cutout as well as the animations.
  */
-public class FaceEnrollAnimationDrawable extends Drawable {
+public class FaceEnrollAnimationDrawable extends Drawable
+        implements BiometricEnrollSidecar.Listener {
+
+    // Tune this parameter so the UI looks nice - and so that we don't have to draw the animations
+    // outside our bounds. A fraction of each rotating dot should be overlapping the camera preview.
+    private static final int BORDER_BOUNDS = 20;
 
+    private final Context mContext;
+    private final ParticleCollection.Listener mListener;
     private Rect mBounds;
     private final Paint mSquarePaint;
     private final Paint mCircleCutoutPaint;
 
-    public FaceEnrollAnimationDrawable() {
+    private ParticleCollection mParticleCollection;
+
+    private TimeAnimator mTimeAnimator;
+
+    private final ParticleCollection.Listener mAnimationListener
+            = new ParticleCollection.Listener() {
+        @Override
+        public void onEnrolled() {
+            if (mTimeAnimator != null && mTimeAnimator.isStarted()) {
+                mTimeAnimator.end();
+                mListener.onEnrolled();
+            }
+        }
+    };
+
+    public FaceEnrollAnimationDrawable(Context context, ParticleCollection.Listener listener) {
+        mContext = context;
+        mListener = listener;
+
         mSquarePaint = new Paint();
         mSquarePaint.setColor(Color.WHITE);
         mSquarePaint.setAntiAlias(true);
@@ -47,8 +76,34 @@ public class FaceEnrollAnimationDrawable extends Drawable {
     }
 
     @Override
+    public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+        mParticleCollection.onEnrollmentHelp(helpMsgId, helpString);
+    }
+
+    @Override
+    public void onEnrollmentError(int errMsgId, CharSequence errString) {
+        mParticleCollection.onEnrollmentError(errMsgId, errString);
+    }
+
+    @Override
+    public void onEnrollmentProgressChange(int steps, int remaining) {
+        mParticleCollection.onEnrollmentProgressChange(steps, remaining);
+    }
+
+    @Override
     protected void onBoundsChange(Rect bounds) {
         mBounds = bounds;
+        mParticleCollection =
+                new ParticleCollection(mContext, mAnimationListener, bounds, BORDER_BOUNDS);
+
+        if (mTimeAnimator == null) {
+            mTimeAnimator = new TimeAnimator();
+            mTimeAnimator.setTimeListener((animation, totalTimeMs, deltaTimeMs) -> {
+                mParticleCollection.update(totalTimeMs, deltaTimeMs);
+                FaceEnrollAnimationDrawable.this.invalidateSelf();
+            });
+            mTimeAnimator.start();
+        }
     }
 
     @Override
@@ -63,7 +118,10 @@ public class FaceEnrollAnimationDrawable extends Drawable {
 
         // Clear a circle in the middle for the camera preview
         canvas.drawCircle(mBounds.exactCenterX(), mBounds.exactCenterY(),
-                mBounds.height() / 2, mCircleCutoutPaint);
+                mBounds.height() / 2 - BORDER_BOUNDS, mCircleCutoutPaint);
+
+        // Draw the animation
+        mParticleCollection.draw(canvas);
 
         canvas.restore();
     }
index 7fac9f6..fccb39a 100644 (file)
@@ -45,7 +45,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
     private TextView mErrorText;
     private Interpolator mLinearOutSlowInInterpolator;
     private boolean mShouldFinishOnStop = true;
-    private FaceEnrollPreviewFragment mFaceCameraPreview;
+    private FaceEnrollPreviewFragment mPreviewFragment;
+
+    private ParticleCollection.Listener mListener = new ParticleCollection.Listener() {
+        @Override
+        public void onEnrolled() {
+            FaceEnrollEnrolling.this.launchFinish(mToken);
+        }
+    };
 
     public static class FaceErrorDialog extends BiometricErrorDialog {
         static FaceErrorDialog newInstance(CharSequence msg, int msgId) {
@@ -87,7 +94,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
 
         if (shouldLaunchConfirmLock()) {
             launchConfirmLock(R.string.security_settings_face_preference_title,
-                    Utils.getFaceManagerOrNull(this).preEnroll());
+                    Utils.getFingerprintManagerOrNull(this).preEnroll());
             mShouldFinishOnStop = false;
         } else {
             startEnrollment();
@@ -97,13 +104,14 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
     @Override
     public void startEnrollment() {
         super.startEnrollment();
-        mFaceCameraPreview = (FaceEnrollPreviewFragment) getSupportFragmentManager()
+        mPreviewFragment = (FaceEnrollPreviewFragment) getSupportFragmentManager()
                 .findFragmentByTag(TAG_FACE_PREVIEW);
-        if (mFaceCameraPreview == null) {
-            mFaceCameraPreview = new FaceEnrollPreviewFragment();
-            getSupportFragmentManager().beginTransaction().add(mFaceCameraPreview, TAG_FACE_PREVIEW)
+        if (mPreviewFragment == null) {
+            mPreviewFragment = new FaceEnrollPreviewFragment();
+            getSupportFragmentManager().beginTransaction().add(mPreviewFragment, TAG_FACE_PREVIEW)
                     .commitAllowingStateLoss();
         }
+        mPreviewFragment.setListener(mListener);
     }
 
     @Override
@@ -132,10 +140,11 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
     }
 
     @Override
-    public void onEnrollmentHelp(CharSequence helpString) {
+    public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
         if (!TextUtils.isEmpty(helpString)) {
             showError(helpString);
         }
+        mPreviewFragment.onEnrollmentHelp(helpMsgId, helpString);
     }
 
     @Override
@@ -149,6 +158,7 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
                 msgId = R.string.security_settings_face_enroll_error_generic_dialog_message;
                 break;
         }
+        mPreviewFragment.onEnrollmentError(errMsgId, errString);
         showErrorDialog(getText(msgId), errMsgId);
     }
 
@@ -157,6 +167,8 @@ public class FaceEnrollEnrolling extends BiometricsEnrollEnrolling {
         if (DEBUG) {
             Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining);
         }
+        mPreviewFragment.onEnrollmentProgressChange(steps, remaining);
+
         // TODO: Update the actual animation
         showError("Steps: " + steps + " Remaining: " + remaining);
     }
index 8bb8b92..1861e10 100644 (file)
@@ -38,6 +38,7 @@ import android.widget.ImageView;
 
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.settings.R;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
 import com.android.settings.core.InstrumentedPreferenceFragment;
 
 import java.util.ArrayList;
@@ -50,7 +51,8 @@ import java.util.List;
  * Fragment that contains the logic for showing and controlling the camera preview, circular
  * overlay, as well as the enrollment animations.
  */
-public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
+public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment
+        implements BiometricEnrollSidecar.Listener {
 
     private static final String TAG = "FaceEnrollPreviewFragment";
 
@@ -65,6 +67,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
     private CameraCaptureSession mCaptureSession;
     private CaptureRequest mPreviewRequest;
     private Size mPreviewSize;
+    private ParticleCollection.Listener mListener;
 
     // View used to contain the circular cutout and enrollment animation drawable
     private ImageView mCircleView;
@@ -75,6 +78,15 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
     // Texture used for showing the camera preview
     private FaceSquareTextureView mTextureView;
 
+    // Listener sent to the animation drawable
+    private final ParticleCollection.Listener mAnimationListener
+            = new ParticleCollection.Listener() {
+        @Override
+        public void onEnrolled() {
+            mListener.onEnrolled();
+        }
+    };
+
     private final TextureView.SurfaceTextureListener mSurfaceTextureListener =
             new TextureView.SurfaceTextureListener() {
 
@@ -185,7 +197,7 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
         // Must disable hardware acceleration for this view, otherwise transparency breaks
         mCircleView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
 
-        mAnimationDrawable = new FaceEnrollAnimationDrawable();
+        mAnimationDrawable = new FaceEnrollAnimationDrawable(getContext(), mAnimationListener);
         mCircleView.setImageDrawable(mAnimationDrawable);
 
         mCameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE);
@@ -212,6 +224,25 @@ public class FaceEnrollPreviewFragment extends InstrumentedPreferenceFragment {
         closeCamera();
     }
 
+    @Override
+    public void onEnrollmentError(int errMsgId, CharSequence errString) {
+        mAnimationDrawable.onEnrollmentError(errMsgId, errString);
+    }
+
+    @Override
+    public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+        mAnimationDrawable.onEnrollmentHelp(helpMsgId, helpString);
+    }
+
+    @Override
+    public void onEnrollmentProgressChange(int steps, int remaining) {
+        mAnimationDrawable.onEnrollmentProgressChange(steps, remaining);
+    }
+
+    public void setListener(ParticleCollection.Listener listener) {
+        mListener = listener;
+    }
+
     /**
      * Sets up member variables related to camera.
      *
diff --git a/src/com/android/settings/biometrics/face/ParticleCollection.java b/src/com/android/settings/biometrics/face/ParticleCollection.java
new file mode 100644 (file)
index 0000000..399beec
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.settings.biometrics.face;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricEnrollSidecar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class that's used to create, maintain, and update the state of each animation particle. Particles
+ * should have their colors assigned based on their index. Particles are split into primary and
+ * secondary types - primary types animate twice as fast during the completion effect. The particles
+ * are updated/drawn in a special order so that the overlap is correct during the final completion
+ * effect.
+ */
+public class ParticleCollection implements BiometricEnrollSidecar.Listener {
+
+    private static final String TAG = "AnimationController";
+
+    private static final int NUM_PARTICLES = 12;
+
+    public static final int STATE_STARTED = 1; // dots are rotating
+    public static final int STATE_STOPPED_COLORFUL = 2; // dots are not rotating but colorful
+    public static final int STATE_STOPPED_GRAY = 3; // dots are not rotating and also gray (error)
+    public static final int STATE_COMPLETE = 4; // face is enrolled
+
+    private final List<AnimationParticle> mParticleList;
+    private final List<Integer> mPrimariesInProgress; // primary particles not done animating yet
+    private int mState;
+    private Listener mListener;
+
+    public interface Listener {
+        void onEnrolled();
+    }
+
+    private final AnimationParticle.Listener mParticleListener = new AnimationParticle.Listener() {
+        @Override
+        public void onRingCompleted(int index) {
+            final boolean wasEmpty = mPrimariesInProgress.isEmpty();
+            // We can stop the time animator once the three primary particles have finished
+            for (int i = 0; i < mPrimariesInProgress.size(); i++) {
+                if (mPrimariesInProgress.get(i).intValue() == index) {
+                    mPrimariesInProgress.remove(i);
+                    break;
+                }
+            }
+            if (mPrimariesInProgress.isEmpty() && !wasEmpty) {
+                mListener.onEnrolled();
+            }
+        }
+    };
+
+    public ParticleCollection(Context context, Listener listener, Rect bounds, int borderWidth) {
+        mParticleList = new ArrayList<>();
+        mListener = listener;
+
+        final List<Integer> colors = new ArrayList<>();
+        final Resources.Theme theme = context.getTheme();
+        final Resources resources = context.getResources();
+        colors.add(resources.getColor(R.color.face_anim_particle_color_1, theme));
+        colors.add(resources.getColor(R.color.face_anim_particle_color_2, theme));
+        colors.add(resources.getColor(R.color.face_anim_particle_color_3, theme));
+        colors.add(resources.getColor(R.color.face_anim_particle_color_4, theme));
+
+        // Primary particles expand faster during the completion animation
+        mPrimariesInProgress = new ArrayList<>(Arrays.asList(0, 4, 8));
+
+        // Order in which to draw the particles. This is so the final "completion" animation has
+        // the correct behavior.
+        final int[] order = {3, 7, 11, 2, 6, 10, 1, 5, 9, 0, 4, 8};
+
+        for (int i = 0; i < NUM_PARTICLES; i++) {
+            AnimationParticle particle = new AnimationParticle(context, mParticleListener, bounds,
+                    borderWidth, order[i], NUM_PARTICLES, colors);
+            if (mPrimariesInProgress.contains(order[i])) {
+                particle.setAsPrimary();
+            }
+            mParticleList.add(particle);
+        }
+
+        updateState(STATE_STARTED);
+    }
+
+    public void update(long t, long dt) {
+        for (int i = 0; i < mParticleList.size(); i++) {
+            mParticleList.get(i).update(t, dt);
+        }
+    }
+
+    public void draw(Canvas canvas) {
+        for (int i = 0; i < mParticleList.size(); i++) {
+            mParticleList.get(i).draw(canvas);
+        }
+    }
+
+    private void updateState(int state) {
+        if (mState != state) {
+            for (int i = 0; i < mParticleList.size(); i++) {
+                mParticleList.get(i).updateState(state);
+            }
+            mState = state;
+        }
+    }
+
+    @Override
+    public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
+
+    }
+
+    @Override
+    public void onEnrollmentError(int errMsgId, CharSequence errString) {
+
+    }
+
+    @Override
+    public void onEnrollmentProgressChange(int steps, int remaining) {
+        if (remaining == 0) {
+            updateState(STATE_COMPLETE);
+        }
+    }
+}
index 38ef2c1..e6f3b04 100644 (file)
@@ -245,7 +245,7 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
     }
 
     @Override
-    public void onEnrollmentHelp(CharSequence helpString) {
+    public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
         if (!TextUtils.isEmpty(helpString)) {
             mErrorText.removeCallbacks(mTouchAgainRunnable);
             showError(helpString);
index 927b5eb..c104eb3 100644 (file)
@@ -94,7 +94,7 @@ public class FingerprintEnrollFindSensor extends BiometricEnrollBase {
             }
 
             @Override
-            public void onEnrollmentHelp(CharSequence helpString) {
+            public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
             }
 
             @Override