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
This commit is contained in:
Jacqueline Bronger
2021-06-03 11:50:53 +00:00
parent b3db287943
commit 78adf767d8
14 changed files with 850 additions and 82 deletions

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together"
android:interpolator="@interpolator/tv_privacy_chip_collapse_interpolator"
android:duration="@integer/privacy_chip_animation_millis">
<objectAnimator
android:propertyName="height"
android:valueTo="@dimen/privacy_chip_dot_size"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="marginEnd"
android:valueTo="@dimen/privacy_chip_dot_margin_horizontal"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="radius"
android:valueTo="@dimen/privacy_chip_dot_radius"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="dotAlpha"
android:valueTo="255"
android:valueType="intType"/>
<objectAnimator
android:propertyName="bgAlpha"
android:valueTo="255"
android:valueType="intType"/>
</set>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together"
android:interpolator="@interpolator/tv_privacy_chip_expand_interpolator"
android:duration="@integer/privacy_chip_animation_millis">
<objectAnimator
android:propertyName="height"
android:valueTo="@dimen/privacy_chip_height"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="marginEnd"
android:valueTo="0"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="radius"
android:valueTo="@dimen/privacy_chip_radius"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="dotAlpha"
android:valueTo="255"
android:valueType="intType"/>
<objectAnimator
android:propertyName="bgAlpha"
android:valueTo="0"
android:valueType="intType"/>
</set>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together"
android:interpolator="@interpolator/tv_privacy_chip_collapse_interpolator"
android:duration="@integer/privacy_chip_animation_millis">
<objectAnimator
android:propertyName="dotAlpha"
android:valueTo="255"
android:valueType="intType"/>
</set>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together"
android:interpolator="@interpolator/tv_privacy_chip_collapse_interpolator"
android:duration="@integer/privacy_chip_animation_millis">
<objectAnimator
android:propertyName="dotAlpha"
android:valueTo="0"
android:valueType="intType"/>
<objectAnimator
android:propertyName="bgAlpha"
android:valueTo="0"
android:valueType="intType"/>
</set>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~ 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.
@@ -15,11 +15,8 @@
~ limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="20dp"/>
<solid android:color="@color/tv_audio_recording_indicator_icon_background"/>
<stroke android:width="1dp" android:color="@color/tv_audio_recording_indicator_stroke"/>
</shape>
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:controlX1="0.4"
android:controlY1="1.00"
android:controlX2="0.12"
android:controlY2="1.00"/>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:controlX1="0.12"
android:controlY1="1.00"
android:controlX2="0.4"
android:controlY2="1.00"/>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2019 The Android Open Source Project
~ 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.
@@ -20,16 +20,25 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:layout_gravity="center">
android:gravity="center"
android:animateLayoutChanges="false"
android:padding="@dimen/privacy_chip_margin">
<ImageView
android:id="@+id/chip_drawable"
android:layout_width="51dp"
android:layout_height="@dimen/privacy_chip_height"
android:minWidth="@dimen/privacy_chip_dot_bg_width"
android:minHeight="@dimen/privacy_chip_dot_bg_height"
android:layout_gravity="top|end" />
<LinearLayout
android:id="@+id/icons_container"
android:background="@drawable/tv_rect_shadow_rounded"
android:padding="@dimen/privacy_chip_icon_padding"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"/>
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical|end"
android:animateLayoutChanges="true"
android:paddingHorizontal="@dimen/privacy_chip_padding_horizontal" />
</FrameLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<fade android:fadingMode="fade_in" />
<changeBounds/>
</transitionSet>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
<changeBounds/>
<fade android:fadingMode="fade_out" />
</transitionSet>

View File

@@ -34,4 +34,7 @@
<color name="bottom_sheet_button_text_color_focused">#DB202124</color>
<color name="bottom_sheet_button_text_color_unfocused">#B5E8EAED</color>
<color name="privacy_circle">#5BB974</color> <!-- g400 -->
<color name="privacy_icon_tint">#30302A</color>
<color name="privacy_chip_dot_bg_tint">#66000000</color>
</resources>

View File

@@ -18,10 +18,6 @@
<!-- Opacity at which the background for the shutdown UI will be drawn. -->
<item name="shutdown_scrim_behind_alpha" format="float" type="dimen">1.0</item>
<dimen name="privacy_chip_icon_margin">3dp</dimen>
<dimen name="privacy_chip_icon_padding">8dp</dimen>
<dimen name="privacy_chip_icon_size">13dp</dimen>
<dimen name="bottom_sheet_padding_horizontal">32dp</dimen>
<dimen name="bottom_sheet_padding_vertical">24dp</dimen>
@@ -44,4 +40,20 @@
<dimen name="bottom_sheet_margin">24dp</dimen>
<dimen name="bottom_sheet_background_blur_radius">120dp</dimen>
<dimen name="privacy_chip_margin">12dp</dimen>
<dimen name="privacy_chip_icon_margin_in_between">9dp</dimen>
<dimen name="privacy_chip_padding_horizontal">9dp</dimen>
<dimen name="privacy_chip_icon_size">12dp</dimen>
<dimen name="privacy_chip_height">24dp</dimen>
<dimen name="privacy_chip_min_width">30dp</dimen>
<dimen name="privacy_chip_radius">12dp</dimen>
<dimen name="privacy_chip_dot_size">8dp</dimen>
<dimen name="privacy_chip_dot_radius">4dp</dimen>
<dimen name="privacy_chip_dot_margin_horizontal">8dp</dimen>
<dimen name="privacy_chip_dot_bg_width">24dp</dimen>
<dimen name="privacy_chip_dot_bg_height">18dp</dimen>
<dimen name="privacy_chip_dot_bg_radius">9dp</dimen>
</resources>

View File

@@ -20,4 +20,6 @@
Value 81 corresponds to BOTTOM|CENTER_HORIZONTAL.
Value 21 corresponds to RIGHT|CENTER_VERTICAL. -->
<integer name="volume_dialog_gravity">21</integer>
<integer name="privacy_chip_animation_millis">300</integer>
</resources>

View File

@@ -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;
}
}

View File

@@ -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<PrivacyItem> mPrivacyItems;
private boolean mAllIndicatorsEnabled;
@NonNull
private List<PrivacyItem> 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<PrivacyItem> privacyItems) {
public void onPrivacyItemsChanged(@NonNull List<PrivacyItem> privacyItems) {
if (DEBUG) Log.d(TAG, "PrivacyItemsChanged");
mPrivacyItems = privacyItems;
updateUI();
List<PrivacyItem> 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<Drawable> icons, ViewGroup iconsContainer) {
iconsContainer.removeAllViews();
private void updateIcons() {
List<Drawable> 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";
}
}
}