<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>
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);
}
@Override
public void send(Listener listener) {
- listener.onEnrollmentHelp(helpString);
+ listener.onEnrollmentHelp(helpMsgId, helpString);
}
}
protected void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (mListener != null) {
- mListener.onEnrollmentHelp(helpString);
+ mListener.onEnrollmentHelp(helpMsgId, helpString);
} else {
mQueuedEvents.add(new QueuedEnrollmentHelp(helpMsgId, helpString));
}
--- /dev/null
+/*
+ * 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);
+ }
+}
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;
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);
}
@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
// 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();
}
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) {
if (shouldLaunchConfirmLock()) {
launchConfirmLock(R.string.security_settings_face_preference_title,
- Utils.getFaceManagerOrNull(this).preEnroll());
+ Utils.getFingerprintManagerOrNull(this).preEnroll());
mShouldFinishOnStop = false;
} else {
startEnrollment();
@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
}
@Override
- public void onEnrollmentHelp(CharSequence helpString) {
+ public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) {
showError(helpString);
}
+ mPreviewFragment.onEnrollmentHelp(helpMsgId, helpString);
}
@Override
msgId = R.string.security_settings_face_enroll_error_generic_dialog_message;
break;
}
+ mPreviewFragment.onEnrollmentError(errMsgId, errString);
showErrorDialog(getText(msgId), errMsgId);
}
if (DEBUG) {
Log.v(TAG, "Steps: " + steps + " Remaining: " + remaining);
}
+ mPreviewFragment.onEnrollmentProgressChange(steps, remaining);
+
// TODO: Update the actual animation
showError("Steps: " + steps + " Remaining: " + remaining);
}
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;
* 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";
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;
// 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() {
// 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);
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.
*
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
}
@Override
- public void onEnrollmentHelp(CharSequence helpString) {
+ public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (!TextUtils.isEmpty(helpString)) {
mErrorText.removeCallbacks(mTouchAgainRunnable);
showError(helpString);
}
@Override
- public void onEnrollmentHelp(CharSequence helpString) {
+ public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
}
@Override