From 78adf767d803e7ab6cbee81a987ed1978a313638 Mon Sep 17 00:00:00 2001 From: Jacqueline Bronger Date: Thu, 3 Jun 2021 11:50:53 +0000 Subject: [PATCH] Add shrinking animation to the tv privacy chip. Updated to match ux design: icons shrinking to a dot design after a few seconds. Bug: 186718078 Test: manual + atest CameraMicindicatorsPermissionTest + atest MicIndicatorTest Change-Id: If032c235cb5ef124fb48523796ce09f5784e50e0 --- .../res/anim/tv_privacy_chip_collapse.xml | 41 ++ .../res/anim/tv_privacy_chip_expand.xml | 41 ++ .../res/anim/tv_privacy_chip_fade_in.xml | 25 ++ .../res/anim/tv_privacy_chip_fade_out.xml | 29 ++ ...tv_privacy_chip_collapse_interpolator.xml} | 15 +- .../tv_privacy_chip_expand_interpolator.xml | 22 + .../res/layout/tv_ongoing_privacy_chip.xml | 25 +- .../transition/tv_privacy_chip_collapse.xml | 20 + .../res/transition/tv_privacy_chip_expand.xml | 20 + .../SystemUI/res/values-television/colors.xml | 3 + .../SystemUI/res/values-television/dimens.xml | 20 +- .../res/values-television/integers.xml | 2 + .../television/PrivacyChipDrawable.java | 390 ++++++++++++++++++ .../television/TvOngoingPrivacyChip.java | 279 ++++++++++--- 14 files changed, 850 insertions(+), 82 deletions(-) create mode 100644 packages/SystemUI/res/anim/tv_privacy_chip_collapse.xml create mode 100644 packages/SystemUI/res/anim/tv_privacy_chip_expand.xml create mode 100644 packages/SystemUI/res/anim/tv_privacy_chip_fade_in.xml create mode 100644 packages/SystemUI/res/anim/tv_privacy_chip_fade_out.xml rename packages/SystemUI/res/{drawable/tv_rect_shadow_rounded.xml => interpolator/tv_privacy_chip_collapse_interpolator.xml} (62%) create mode 100644 packages/SystemUI/res/interpolator/tv_privacy_chip_expand_interpolator.xml create mode 100644 packages/SystemUI/res/transition/tv_privacy_chip_collapse.xml create mode 100644 packages/SystemUI/res/transition/tv_privacy_chip_expand.xml create mode 100644 packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java diff --git a/packages/SystemUI/res/anim/tv_privacy_chip_collapse.xml b/packages/SystemUI/res/anim/tv_privacy_chip_collapse.xml new file mode 100644 index 0000000000000..e6ceeb919232e --- /dev/null +++ b/packages/SystemUI/res/anim/tv_privacy_chip_collapse.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/anim/tv_privacy_chip_expand.xml b/packages/SystemUI/res/anim/tv_privacy_chip_expand.xml new file mode 100644 index 0000000000000..4a510ae6cab8c --- /dev/null +++ b/packages/SystemUI/res/anim/tv_privacy_chip_expand.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/anim/tv_privacy_chip_fade_in.xml b/packages/SystemUI/res/anim/tv_privacy_chip_fade_in.xml new file mode 100644 index 0000000000000..701489a284502 --- /dev/null +++ b/packages/SystemUI/res/anim/tv_privacy_chip_fade_in.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/anim/tv_privacy_chip_fade_out.xml b/packages/SystemUI/res/anim/tv_privacy_chip_fade_out.xml new file mode 100644 index 0000000000000..fa134717ba6ea --- /dev/null +++ b/packages/SystemUI/res/anim/tv_privacy_chip_fade_out.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/tv_rect_shadow_rounded.xml b/packages/SystemUI/res/interpolator/tv_privacy_chip_collapse_interpolator.xml similarity index 62% rename from packages/SystemUI/res/drawable/tv_rect_shadow_rounded.xml rename to packages/SystemUI/res/interpolator/tv_privacy_chip_collapse_interpolator.xml index 93f8724b22a99..4298124846741 100644 --- a/packages/SystemUI/res/drawable/tv_rect_shadow_rounded.xml +++ b/packages/SystemUI/res/interpolator/tv_privacy_chip_collapse_interpolator.xml @@ -1,6 +1,6 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/SystemUI/res/interpolator/tv_privacy_chip_expand_interpolator.xml b/packages/SystemUI/res/interpolator/tv_privacy_chip_expand_interpolator.xml new file mode 100644 index 0000000000000..ed44715130e9b --- /dev/null +++ b/packages/SystemUI/res/interpolator/tv_privacy_chip_expand_interpolator.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml index dff148b2c5705..6218a5e6ea820 100644 --- a/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/tv_ongoing_privacy_chip.xml @@ -1,6 +1,6 @@ + + + + diff --git a/packages/SystemUI/res/transition/tv_privacy_chip_expand.xml b/packages/SystemUI/res/transition/tv_privacy_chip_expand.xml new file mode 100644 index 0000000000000..059ebc84ea7e4 --- /dev/null +++ b/packages/SystemUI/res/transition/tv_privacy_chip_expand.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/packages/SystemUI/res/values-television/colors.xml b/packages/SystemUI/res/values-television/colors.xml index e13c42e9ca3d5..566f6b229bc95 100644 --- a/packages/SystemUI/res/values-television/colors.xml +++ b/packages/SystemUI/res/values-television/colors.xml @@ -34,4 +34,7 @@ #DB202124 #B5E8EAED + #5BB974 + #30302A + #66000000 diff --git a/packages/SystemUI/res/values-television/dimens.xml b/packages/SystemUI/res/values-television/dimens.xml index 3a1a3d923fcc1..c258fcc4273a1 100644 --- a/packages/SystemUI/res/values-television/dimens.xml +++ b/packages/SystemUI/res/values-television/dimens.xml @@ -18,10 +18,6 @@ 1.0 - 3dp - 8dp - 13dp - 32dp 24dp @@ -44,4 +40,20 @@ 24dp 120dp + 12dp + 9dp + 9dp + 12dp + 24dp + 30dp + 12dp + + 8dp + 4dp + 8dp + + 24dp + 18dp + 9dp + \ No newline at end of file diff --git a/packages/SystemUI/res/values-television/integers.xml b/packages/SystemUI/res/values-television/integers.xml index 587497edfdfb7..b265d7812229c 100644 --- a/packages/SystemUI/res/values-television/integers.xml +++ b/packages/SystemUI/res/values-television/integers.xml @@ -20,4 +20,6 @@ Value 81 corresponds to BOTTOM|CENTER_HORIZONTAL. Value 21 corresponds to RIGHT|CENTER_VERTICAL. --> 21 + + 300 diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java b/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java new file mode 100644 index 0000000000000..e5479badcb0af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/television/PrivacyChipDrawable.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2021 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.systemui.privacy.television; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.R; + +/** + * Drawable that can go from being the background of the privacy icons to a small dot. + * The icons are not included. + */ +public class PrivacyChipDrawable extends Drawable { + + private static final String TAG = PrivacyChipDrawable.class.getSimpleName(); + private static final boolean DEBUG = false; + + private float mWidth; + private float mHeight; + private float mMarginEnd; + private float mRadius; + private int mDotAlpha; + private int mBgAlpha; + + private float mTargetWidth; + private final int mMinWidth; + private final int mIconWidth; + private final int mIconPadding; + private final int mBgWidth; + private final int mBgHeight; + private final int mBgRadius; + private final int mDotSize; + + private final AnimatorSet mFadeIn; + private final AnimatorSet mFadeOut; + private final AnimatorSet mCollapse; + private final AnimatorSet mExpand; + private Animator mWidthAnimator; + + private final Paint mChipPaint; + private final Paint mBgPaint; + + private boolean mIsRtl; + + private boolean mIsExpanded = true; + + private PrivacyChipDrawableListener mListener; + + interface PrivacyChipDrawableListener { + void onFadeOutFinished(); + } + + public PrivacyChipDrawable(Context context) { + mChipPaint = new Paint(); + mChipPaint.setStyle(Paint.Style.FILL); + mChipPaint.setColor(context.getColor(R.color.privacy_circle)); + mChipPaint.setAlpha(mDotAlpha); + mChipPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + + mBgPaint = new Paint(); + mBgPaint.setStyle(Paint.Style.FILL); + mBgPaint.setColor(context.getColor(R.color.privacy_chip_dot_bg_tint)); + mBgPaint.setAlpha(mBgAlpha); + mBgPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + + mBgWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_dot_bg_width); + mBgHeight = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_dot_bg_height); + mBgRadius = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_dot_bg_radius); + + mMinWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_min_width); + mIconWidth = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_icon_size); + mIconPadding = context.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_margin_in_between); + mDotSize = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_dot_size); + + mWidth = mMinWidth; + mHeight = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_height); + mRadius = context.getResources().getDimensionPixelSize(R.dimen.privacy_chip_radius); + + mExpand = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_expand); + mExpand.setTarget(this); + + mCollapse = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_collapse); + mCollapse.setTarget(this); + + mFadeIn = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_fade_in); + mFadeIn.setTarget(this); + + mFadeOut = (AnimatorSet) AnimatorInflater.loadAnimator(context, + R.anim.tv_privacy_chip_fade_out); + mFadeOut.setTarget(this); + mFadeOut.addListener(new Animator.AnimatorListener() { + private boolean mCancelled; + + @Override + public void onAnimationStart(Animator animation) { + mCancelled = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCancelled && mListener != null) { + if (DEBUG) Log.d(TAG, "Fade-out complete"); + mListener.onFadeOutFinished(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationRepeat(Animator animation) { + // no-op + } + }); + } + + /** + * Pass null to remove listener. + */ + public void setListener(@Nullable PrivacyChipDrawableListener listener) { + this.mListener = listener; + } + + /** + * Call once the view that is showing the drawable is visible to start fading the chip in. + */ + public void startInitialFadeIn() { + if (DEBUG) Log.d(TAG, "initial fade-in"); + mFadeIn.start(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + Rect bounds = getBounds(); + + int centerVertical = (bounds.bottom - bounds.top) / 2; + // Dot background + RectF bgBounds = new RectF( + mIsRtl ? bounds.left : bounds.right - mBgWidth, + centerVertical - mBgHeight / 2f, + mIsRtl ? bounds.left + mBgWidth : bounds.right, + centerVertical + mBgHeight / 2f); + if (DEBUG) Log.v(TAG, "bg: " + bgBounds.toShortString()); + canvas.drawRoundRect(bgBounds, mBgRadius, mBgRadius, mBgPaint); + + // Icon background / dot + RectF greenBounds = new RectF( + mIsRtl ? bounds.left + mMarginEnd : bounds.right - mWidth - mMarginEnd, + centerVertical - mHeight / 2, + mIsRtl ? bounds.left + mWidth + mMarginEnd : bounds.right - mMarginEnd, + centerVertical + mHeight / 2); + if (DEBUG) Log.v(TAG, "green: " + greenBounds.toShortString()); + canvas.drawRoundRect(greenBounds, mRadius, mRadius, mChipPaint); + } + + private void animateToNewTargetWidth(float width) { + if (DEBUG) Log.d(TAG, "new target width: " + width); + if (width != mTargetWidth) { + mTargetWidth = width; + Animator newWidthAnimator = ObjectAnimator.ofFloat(this, "width", mTargetWidth); + newWidthAnimator.start(); + if (mWidthAnimator != null) { + mWidthAnimator.cancel(); + } + mWidthAnimator = newWidthAnimator; + } + } + + private void expand() { + if (DEBUG) Log.d(TAG, "expanding"); + if (mIsExpanded) { + return; + } + mIsExpanded = true; + + mExpand.start(); + mCollapse.cancel(); + } + + /** + * Starts the animation to a dot. + */ + public void collapse() { + if (DEBUG) Log.d(TAG, "collapsing"); + if (!mIsExpanded) { + return; + } + mIsExpanded = false; + + animateToNewTargetWidth(mDotSize); + mCollapse.start(); + mExpand.cancel(); + } + + /** + * Fades out the view if 0 icons are to be shown, expands the chip if it has been collapsed and + * makes the width of the chip adjust to the amount of icons to be shown. + * Should not be called when only the order of the icons was changed as the chip will expand + * again without there being any real update. + * + * @param iconCount Can be 0 to fade out the chip. + */ + public void updateIcons(int iconCount) { + if (DEBUG) Log.d(TAG, "updating icons: " + iconCount); + + // calculate chip size and use it for end value of animation that is specified in code, + // not xml + if (iconCount == 0) { + // fade out if there are no icons + mFadeOut.start(); + + mWidthAnimator.cancel(); + mFadeIn.cancel(); + mExpand.cancel(); + mCollapse.cancel(); + return; + } + + mFadeOut.cancel(); + expand(); + animateToNewTargetWidth(mMinWidth + (iconCount - 1) * (mIconWidth + mIconPadding)); + } + + @Override + public void setAlpha(int alpha) { + setDotAlpha(alpha); + setBgAlpha(alpha); + } + + @Override + public int getAlpha() { + return mDotAlpha; + } + + /** + * Set alpha value the green part of the chip. + */ + @Keep + public void setDotAlpha(int alpha) { + if (DEBUG) Log.v(TAG, "dot alpha updated to: " + alpha); + mDotAlpha = alpha; + mChipPaint.setAlpha(alpha); + } + + @Keep + public int getDotAlpha() { + return mDotAlpha; + } + + /** + * Set alpha value of the background of the chip. + */ + @Keep + public void setBgAlpha(int alpha) { + if (DEBUG) Log.v(TAG, "bg alpha updated to: " + alpha); + mBgAlpha = alpha; + mBgPaint.setAlpha(alpha); + } + + @Keep + public int getBgAlpha() { + return mBgAlpha; + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // no-op + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + /** + * The radius of the green part of the chip, not the background. + */ + @Keep + public void setRadius(float radius) { + mRadius = radius; + invalidateSelf(); + } + + /** + * @return The radius of the green part of the chip, not the background. + */ + @Keep + public float getRadius() { + return mRadius; + } + + /** + * Height of the green part of the chip, not including the background. + */ + @Keep + public void setHeight(float height) { + mHeight = height; + invalidateSelf(); + } + + /** + * @return Height of the green part of the chip, not including the background. + */ + @Keep + public float getHeight() { + return mHeight; + } + + /** + * Width of the green part of the chip, not including the background. + */ + @Keep + public void setWidth(float width) { + mWidth = width; + invalidateSelf(); + } + + /** + * @return Width of the green part of the chip, not including the background. + */ + @Keep + public float getWidth() { + return mWidth; + } + + /** + * Margin at the end of the green part of the chip, so that it will be placed in the middle of + * the rounded rectangle in the background. + */ + @Keep + public void setMarginEnd(float marginEnd) { + mMarginEnd = marginEnd; + invalidateSelf(); + } + + /** + * @return Margin at the end of the green part of the chip, so that it will be placed in the + * middle of the rounded rectangle in the background. + */ + @Keep + public float getMarginEnd() { + return mMarginEnd; + } + + /** + * Sets the layout direction. + */ + public void setRtl(boolean isRtl) { + mIsRtl = isRtl; + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java index 5ab7bd88e49b2..e4f5cde37f1de 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java +++ b/packages/SystemUI/src/com/android/systemui/privacy/television/TvOngoingPrivacyChip.java @@ -25,9 +25,10 @@ import android.annotation.IntDef; import android.annotation.UiThread; import android.content.Context; import android.content.res.Resources; -import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; @@ -38,15 +39,20 @@ import android.view.WindowManager; import android.widget.ImageView; import android.widget.LinearLayout; +import androidx.annotation.NonNull; + import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.privacy.PrivacyChipBuilder; import com.android.systemui.privacy.PrivacyItem; import com.android.systemui.privacy.PrivacyItemController; +import com.android.systemui.privacy.PrivacyType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -56,9 +62,10 @@ import javax.inject.Inject; * recording audio, accessing the camera or accessing the location. */ @SysUISingleton -public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback { +public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemController.Callback, + PrivacyChipDrawable.PrivacyChipDrawableListener { private static final String TAG = "TvOngoingPrivacyChip"; - static final boolean DEBUG = false; + private static final boolean DEBUG = false; // This title is used in CameraMicIndicatorsPermissionTest and // RecognitionServiceMicIndicatorTest. @@ -68,7 +75,8 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl @IntDef(prefix = {"STATE_"}, value = { STATE_NOT_SHOWN, STATE_APPEARING, - STATE_SHOWN, + STATE_EXPANDED, + STATE_COLLAPSED, STATE_DISAPPEARING }) public @interface State { @@ -76,46 +84,58 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl private static final int STATE_NOT_SHOWN = 0; private static final int STATE_APPEARING = 1; - private static final int STATE_SHOWN = 2; - private static final int STATE_DISAPPEARING = 3; + private static final int STATE_EXPANDED = 2; + private static final int STATE_COLLAPSED = 3; + private static final int STATE_DISAPPEARING = 4; - private static final int ANIMATION_DURATION_MS = 200; + private static final int EXPANDED_DURATION_MS = 4000; + public final int mAnimationDurationMs; private final Context mContext; private final PrivacyItemController mPrivacyItemController; - private View mIndicatorView; + private ViewGroup mIndicatorView; private boolean mViewAndWindowAdded; private ObjectAnimator mAnimator; private boolean mMicCameraIndicatorFlagEnabled; - private boolean mLocationIndicatorEnabled; - private List mPrivacyItems; + private boolean mAllIndicatorsEnabled; + + @NonNull + private List mPrivacyItems = Collections.emptyList(); private LinearLayout mIconsContainer; private final int mIconSize; private final int mIconMarginStart; + private PrivacyChipDrawable mChipDrawable; + + private final Handler mUiThreadHandler = new Handler(Looper.getMainLooper()); + private final Runnable mCollapseRunnable = this::collapseChip; + @State private int mState = STATE_NOT_SHOWN; @Inject public TvOngoingPrivacyChip(Context context, PrivacyItemController privacyItemController) { super(context); - Log.d(TAG, "Privacy chip running without id"); + if (DEBUG) Log.d(TAG, "Privacy chip running"); mContext = context; mPrivacyItemController = privacyItemController; Resources res = mContext.getResources(); - mIconMarginStart = Math.round(res.getDimension(R.dimen.privacy_chip_icon_margin)); + mIconMarginStart = Math.round( + res.getDimension(R.dimen.privacy_chip_icon_margin_in_between)); mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size); + mAnimationDurationMs = res.getInteger(R.integer.privacy_chip_animation_millis); + mMicCameraIndicatorFlagEnabled = privacyItemController.getMicCameraAvailable(); - mLocationIndicatorEnabled = privacyItemController.getLocationAvailable(); + mAllIndicatorsEnabled = privacyItemController.getAllIndicatorsAvailable(); if (DEBUG) { Log.d(TAG, "micCameraIndicators: " + mMicCameraIndicatorFlagEnabled); - Log.d(TAG, "locationIndicators: " + mLocationIndicatorEnabled); + Log.d(TAG, "allIndicators: " + mAllIndicatorsEnabled); } } @@ -125,69 +145,145 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl } @Override - public void onPrivacyItemsChanged(List privacyItems) { + public void onPrivacyItemsChanged(@NonNull List privacyItems) { if (DEBUG) Log.d(TAG, "PrivacyItemsChanged"); - mPrivacyItems = privacyItems; - updateUI(); + + List updatedPrivacyItems = new ArrayList<>(privacyItems); + // Never show the location indicator on tv. + if (updatedPrivacyItems.removeIf( + privacyItem -> privacyItem.getPrivacyType() == PrivacyType.TYPE_LOCATION)) { + if (DEBUG) Log.v(TAG, "Removed the location item"); + } + + if (isChipDisabled()) { + fadeOutIndicator(); + mPrivacyItems = updatedPrivacyItems; + return; + } + + // Do they have the same elements? (order doesn't matter) + if (updatedPrivacyItems.size() == mPrivacyItems.size() + && mPrivacyItems.containsAll(updatedPrivacyItems)) { + if (DEBUG) Log.d(TAG, "List wasn't updated"); + return; + } + + mPrivacyItems = updatedPrivacyItems; + updateChip(); + } + + private void updateChip() { + if (DEBUG) Log.d(TAG, mPrivacyItems.size() + " privacy items"); + + if (mPrivacyItems.isEmpty()) { + if (DEBUG) Log.d(TAG, "removing indicator (state: " + stateToString(mState) + ")"); + fadeOutIndicator(); + return; + } + + if (DEBUG) Log.d(TAG, "Current state: " + stateToString(mState)); + switch (mState) { + case STATE_NOT_SHOWN: + createAndShowIndicator(); + break; + case STATE_APPEARING: + case STATE_EXPANDED: + updateIcons(); + collapseLater(); + break; + case STATE_COLLAPSED: + case STATE_DISAPPEARING: + mState = STATE_EXPANDED; + updateIcons(); + animateIconAppearance(); + break; + } + } + + /** + * Collapse the chip EXPANDED_DURATION_MS from now. + */ + private void collapseLater() { + mUiThreadHandler.removeCallbacks(mCollapseRunnable); + if (DEBUG) Log.d(TAG, "chip will collapse in " + EXPANDED_DURATION_MS + "ms"); + mUiThreadHandler.postDelayed(mCollapseRunnable, EXPANDED_DURATION_MS); + } + + private void collapseChip() { + if (DEBUG) Log.d(TAG, "collapseChip"); + + if (mState != STATE_EXPANDED) { + return; + } + mState = STATE_COLLAPSED; + + if (mChipDrawable != null) { + mChipDrawable.collapse(); + } + animateIconDisappearance(); } @Override public void onFlagMicCameraChanged(boolean flag) { if (DEBUG) Log.d(TAG, "mic/camera indicators enabled: " + flag); mMicCameraIndicatorFlagEnabled = flag; + updateChipOnFlagChanged(); } @Override - public void onFlagLocationChanged(boolean flag) { - if (DEBUG) Log.d(TAG, "location indicators enabled: " + flag); - mLocationIndicatorEnabled = flag; + public void onFlagAllChanged(boolean flag) { + if (DEBUG) Log.d(TAG, "all indicators enabled: " + flag); + mAllIndicatorsEnabled = flag; + updateChipOnFlagChanged(); } - private void updateUI() { - if (DEBUG) Log.d(TAG, mPrivacyItems.size() + " privacy items"); + private boolean isChipDisabled() { + return !(mMicCameraIndicatorFlagEnabled || mAllIndicatorsEnabled); + } - if ((mMicCameraIndicatorFlagEnabled || mLocationIndicatorEnabled) - && !mPrivacyItems.isEmpty()) { - if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) { - showIndicator(); - } else { - if (DEBUG) Log.d(TAG, "only updating icons"); - PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems); - setIcons(builder.generateIcons(), mIconsContainer); - mIconsContainer.requestLayout(); - } + private void updateChipOnFlagChanged() { + if (isChipDisabled()) { + fadeOutIndicator(); } else { - hideIndicatorIfNeeded(); + updateChip(); } } @UiThread - private void hideIndicatorIfNeeded() { + private void fadeOutIndicator() { if (mState == STATE_NOT_SHOWN || mState == STATE_DISAPPEARING) return; + mUiThreadHandler.removeCallbacks(mCollapseRunnable); + if (mViewAndWindowAdded) { mState = STATE_DISAPPEARING; - animateDisappearance(); + animateIconDisappearance(); } else { // Appearing animation has not started yet, as we were still waiting for the View to be // laid out. mState = STATE_NOT_SHOWN; removeIndicatorView(); } + if (mChipDrawable != null) { + mChipDrawable.updateIcons(0); + } } @UiThread - private void showIndicator() { + private void createAndShowIndicator() { mState = STATE_APPEARING; + if (mIndicatorView != null || mViewAndWindowAdded) { + removeIndicatorView(); + } + // Inflate the indicator view - mIndicatorView = LayoutInflater.from(mContext).inflate( + mIndicatorView = (ViewGroup) LayoutInflater.from(mContext).inflate( R.layout.tv_ongoing_privacy_chip, null); - // 1. Set alpha to 0. + // 1. Set icon alpha to 0. // 2. Wait until the window is shown and the view is laid out. // 3. Start a "fade in" (alpha) animation. - mIndicatorView.setAlpha(0f); mIndicatorView .getViewTreeObserver() .addOnGlobalLayoutListener( @@ -196,20 +292,35 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl public void onGlobalLayout() { // State could have changed to NOT_SHOWN (if all the recorders are // already gone) - if (mState != STATE_APPEARING) return; + if (mState != STATE_APPEARING) { + return; + } mViewAndWindowAdded = true; // Remove the observer mIndicatorView.getViewTreeObserver().removeOnGlobalLayoutListener( this); - animateAppearance(); + animateIconAppearance(); + mChipDrawable.startInitialFadeIn(); } }); + final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() + == View.LAYOUT_DIRECTION_RTL; + if (DEBUG) Log.d(TAG, "is RTL: " + isRtl); + + mChipDrawable = new PrivacyChipDrawable(mContext); + mChipDrawable.setListener(this); + mChipDrawable.setRtl(isRtl); + ImageView chipBackground = mIndicatorView.findViewById(R.id.chip_drawable); + if (chipBackground != null) { + chipBackground.setImageDrawable(mChipDrawable); + } + mIconsContainer = mIndicatorView.findViewById(R.id.icons_container); - PrivacyChipBuilder builder = new PrivacyChipBuilder(mContext, mPrivacyItems); - setIcons(builder.generateIcons(), mIconsContainer); + mIconsContainer.setAlpha(0f); + updateIcons(); final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( WRAP_CONTENT, @@ -217,19 +328,19 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); - layoutParams.gravity = Gravity.TOP | Gravity.END; + layoutParams.gravity = Gravity.TOP | (isRtl ? Gravity.LEFT : Gravity.RIGHT); layoutParams.setTitle(LAYOUT_PARAMS_TITLE); layoutParams.packageName = mContext.getPackageName(); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); windowManager.addView(mIndicatorView, layoutParams); - } - private void setIcons(List icons, ViewGroup iconsContainer) { - iconsContainer.removeAllViews(); + private void updateIcons() { + List icons = new PrivacyChipBuilder(mContext, mPrivacyItems).generateIcons(); + mIconsContainer.removeAllViews(); for (int i = 0; i < icons.size(); i++) { Drawable icon = icons.get(i); - icon.mutate().setTint(Color.WHITE); + icon.mutate().setTint(mContext.getColor(R.color.privacy_icon_tint)); ImageView imageView = new ImageView(mContext); imageView.setImageDrawable(icon); imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); @@ -241,22 +352,25 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl imageView.setLayoutParams(layoutParams); } } + if (mChipDrawable != null) { + mChipDrawable.updateIcons(icons.size()); + } } - private void animateAppearance() { - animateAlphaTo(1f); + private void animateIconAppearance() { + animateIconAlphaTo(1f); } - private void animateDisappearance() { - animateAlphaTo(0f); + private void animateIconDisappearance() { + animateIconAlphaTo(0f); } - private void animateAlphaTo(final float endValue) { + private void animateIconAlphaTo(float endValue) { if (mAnimator == null) { if (DEBUG) Log.d(TAG, "set up animator"); mAnimator = new ObjectAnimator(); - mAnimator.setTarget(mIndicatorView); + mAnimator.setTarget(mIconsContainer); mAnimator.setProperty(View.ALPHA); mAnimator.addListener(new AnimatorListenerAdapter() { boolean mCancelled; @@ -280,7 +394,7 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl // and then onAnimationEnd(...). We, however, only want to proceed here if the // animation ended "naturally". if (!mCancelled) { - onAnimationFinished(); + onIconAnimationFinished(); } } }); @@ -289,19 +403,37 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl mAnimator.cancel(); } - final float currentValue = mIndicatorView.getAlpha(); + final float currentValue = mIconsContainer.getAlpha(); + if (currentValue == endValue) { + if (DEBUG) Log.d(TAG, "alpha not changing"); + return; + } if (DEBUG) Log.d(TAG, "animate alpha to " + endValue + " from " + currentValue); - mAnimator.setDuration((int) (Math.abs(currentValue - endValue) * ANIMATION_DURATION_MS)); + mAnimator.setDuration(mAnimationDurationMs); mAnimator.setFloatValues(endValue); mAnimator.start(); } - private void onAnimationFinished() { - if (DEBUG) Log.d(TAG, "onAnimationFinished"); + @Override + public void onFadeOutFinished() { + if (DEBUG) Log.d(TAG, "drawable fade-out finished"); + + if (mState == STATE_DISAPPEARING) { + removeIndicatorView(); + mState = STATE_NOT_SHOWN; + } + } + + private void onIconAnimationFinished() { + if (DEBUG) Log.d(TAG, "onAnimationFinished (icon fade)"); + + if (mState == STATE_APPEARING || mState == STATE_EXPANDED) { + collapseLater(); + } if (mState == STATE_APPEARING) { - mState = STATE_SHOWN; + mState = STATE_EXPANDED; } else if (mState == STATE_DISAPPEARING) { removeIndicatorView(); mState = STATE_NOT_SHOWN; @@ -312,14 +444,39 @@ public class TvOngoingPrivacyChip extends SystemUI implements PrivacyItemControl if (DEBUG) Log.d(TAG, "removeIndicatorView"); final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - if (windowManager != null) { + if (windowManager != null && mIndicatorView != null) { windowManager.removeView(mIndicatorView); } mIndicatorView = null; mAnimator = null; + if (mChipDrawable != null) { + mChipDrawable.setListener(null); + mChipDrawable = null; + } + mViewAndWindowAdded = false; } + /** + * Used in debug logs. + */ + private String stateToString(@State int state) { + switch (state) { + case STATE_NOT_SHOWN: + return "NOT_SHOWN"; + case STATE_APPEARING: + return "APPEARING"; + case STATE_EXPANDED: + return "EXPANDED"; + case STATE_COLLAPSED: + return "COLLAPSED"; + case STATE_DISAPPEARING: + return "DISAPPEARING"; + default: + return "INVALID"; + } + } + }