1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.tv.privacy;
18 
19 import android.annotation.ColorRes;
20 import android.annotation.IntDef;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Canvas;
24 import android.graphics.drawable.Drawable;
25 import android.util.Log;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.FrameLayout;
30 import android.widget.ImageView;
31 import android.widget.LinearLayout;
32 
33 import androidx.annotation.NonNull;
34 
35 import com.android.systemui.privacy.PrivacyType;
36 import com.android.systemui.tv.res.R;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Set;
43 
44 /**
45  * View that shows indicator icons for privacy items.
46  */
47 public class PrivacyItemsChip extends FrameLayout {
48     private static final String TAG = "PrivacyItemsChip";
49     private static final boolean DEBUG = false;
50 
51     /**
52      * Configuration for a PrivacyItemsChip's appearance.
53      */
54     public static class ChipConfig {
55         public final List<PrivacyType> privacyTypes;
56         @ColorRes
57         public final int colorRes;
58         public final boolean collapseToDot;
59 
60         /**
61          * @param privacyTypes Privacy types to show icons for, in order.
62          * @param colorRes Color resource for the chip's foreground color.
63          * @param collapseToDot Whether to collapse the chip in to a dot,
64          *                      or just collapse it into a smaller chip with icons still visible.
65          */
ChipConfig(@onNull List<PrivacyType> privacyTypes, int colorRes, boolean collapseToDot)66         public ChipConfig(@NonNull List<PrivacyType> privacyTypes, int colorRes,
67                 boolean collapseToDot) {
68             this.privacyTypes = privacyTypes;
69             this.colorRes = colorRes;
70             this.collapseToDot = collapseToDot;
71         }
72     }
73 
74     @Retention(RetentionPolicy.SOURCE)
75     @IntDef(prefix = {"STATE_"}, value = {
76             STATE_NOT_SHOWN,
77             STATE_EXPANDED,
78             STATE_COLLAPSED,
79     })
80     public @interface State {
81     }
82 
83     private static final int STATE_NOT_SHOWN = 0;
84     private static final int STATE_EXPANDED = 1;
85     private static final int STATE_COLLAPSED = 2;
86 
87     private final ChipConfig mConfig;
88     private final int mIconSize;
89     private final int mCollapsedIconSize;
90     private final int mIconMarginHorizontal;
91     private final PrivacyChipDrawable mChipBackgroundDrawable;
92     private final List<ImageView> mIcons = new ArrayList<>();
93 
94     @State
95     private int mState = STATE_NOT_SHOWN;
96 
PrivacyItemsChip(@onNull Context context, @NonNull ChipConfig config)97     public PrivacyItemsChip(@NonNull Context context, @NonNull ChipConfig config) {
98         super(context);
99         mConfig = config;
100         setVisibility(View.GONE);
101 
102         Resources res = context.getResources();
103         mIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_icon_size);
104         mCollapsedIconSize = res.getDimensionPixelSize(R.dimen.privacy_chip_collapsed_icon_size);
105         mIconMarginHorizontal =
106                 res.getDimensionPixelSize(R.dimen.privacy_chip_icon_margin_in_between);
107 
108         LayoutInflater.from(context).inflate(R.layout.ongoing_privacy_chip, this);
109         LinearLayout iconsContainer = findViewById(R.id.icons_container);
110 
111         mChipBackgroundDrawable = new PrivacyChipDrawable(
112                 context, config.colorRes, config.collapseToDot);
113         mChipBackgroundDrawable.setCallback(new Drawable.Callback() {
114             @Override
115             public void invalidateDrawable(@NonNull Drawable who) {
116                 invalidate();
117             }
118 
119             @Override
120             public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
121             }
122 
123             @Override
124             public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
125             }
126         });
127 
128         setBackground(mChipBackgroundDrawable);
129 
130         for (PrivacyType type : config.privacyTypes) {
131             ImageView typeIconView = new ImageView(context);
132             Drawable icon = type.getIcon(context);
133             icon.mutate().setTint(context.getColor(R.color.privacy_icon_tint));
134 
135             typeIconView.setImageDrawable(icon);
136             typeIconView.setScaleType(ImageView.ScaleType.FIT_CENTER);
137             mIcons.add(typeIconView);
138             iconsContainer.addView(typeIconView, mIconSize, mIconSize);
139             LinearLayout.LayoutParams lp =
140                     (LinearLayout.LayoutParams) typeIconView.getLayoutParams();
141             lp.leftMargin = mIconMarginHorizontal;
142             lp.rightMargin = mIconMarginHorizontal;
143             typeIconView.setVisibility(View.GONE);
144         }
145     }
146 
147     /**
148      * Sets the active privacy types, and expands the chip if there are active items and the chip is
149      * currently collapsed, or hides the chip if there are no active items.
150      *
151      * @param types The set of active privacy types. Only types configured in {@link ChipConfig}
152      *              are shown.
153      */
expandForTypes(Set<PrivacyType> types)154     public void expandForTypes(Set<PrivacyType> types) {
155         if (DEBUG) Log.d(TAG, "expandForTypes, state=" + stateToString(mState));
156 
157         boolean hasActiveTypes = false;
158 
159         for (int i = 0; i < mConfig.privacyTypes.size(); i++) {
160             PrivacyType type = mConfig.privacyTypes.get(i);
161             ImageView icon = mIcons.get(i);
162             boolean isTypeActive = types.contains(type);
163             hasActiveTypes = hasActiveTypes || isTypeActive;
164 
165             icon.setVisibility(isTypeActive ? View.VISIBLE : View.GONE);
166 
167             // Set icon size to expanded size
168             ViewGroup.LayoutParams lp = icon.getLayoutParams();
169             lp.width = mIconSize;
170             lp.height = mIconSize;
171             icon.requestLayout();
172         }
173 
174         if (hasActiveTypes) {
175             if (DEBUG) Log.d(TAG, "Chip has active types, expanding");
176             if (mState == STATE_NOT_SHOWN) {
177                 mChipBackgroundDrawable.expand(/* animate= */ false);
178             } else if (mState == STATE_COLLAPSED) {
179                 mChipBackgroundDrawable.expand(/* animate= */ true);
180             }
181             setVisibility(View.VISIBLE);
182             setState(STATE_EXPANDED);
183         } else {
184             if (DEBUG) Log.d(TAG, "Chip has no active types, hiding");
185             setVisibility(View.GONE);
186             setState(STATE_NOT_SHOWN);
187         }
188     }
189 
190     /**
191      * Collapses this chip if currently expanded.
192      */
collapse()193     public void collapse() {
194         if (DEBUG) Log.d(TAG, "collapse");
195 
196         if (mState != STATE_EXPANDED) {
197             return;
198         }
199         setState(STATE_COLLAPSED);
200 
201         for (ImageView icon : mIcons) {
202             if (mConfig.collapseToDot) {
203                 icon.setVisibility(View.GONE);
204             } else {
205                 ViewGroup.LayoutParams lp = icon.getLayoutParams();
206                 lp.width = mCollapsedIconSize;
207                 lp.height = mCollapsedIconSize;
208                 icon.requestLayout();
209             }
210         }
211 
212         mChipBackgroundDrawable.collapse();
213     }
214 
isExpanded()215     public boolean isExpanded() {
216         return mState == STATE_EXPANDED;
217     }
218 
setState(@tate int state)219     private void setState(@State int state) {
220         if (mState != state) {
221             if (DEBUG) Log.d(TAG, "State changed: " + stateToString(state));
222             mState = state;
223         }
224     }
225 
226     @Override
dispatchDraw(Canvas canvas)227     protected void dispatchDraw(Canvas canvas) {
228         mChipBackgroundDrawable.clipToForeground(canvas);
229         super.dispatchDraw(canvas);
230     }
231 
232     /**
233      * Used in debug logs.
234      */
stateToString(@tate int state)235     private static String stateToString(@State int state) {
236         switch (state) {
237             case STATE_NOT_SHOWN:
238                 return "NOT_SHOWN";
239             case STATE_EXPANDED:
240                 return "EXPANDED";
241             case STATE_COLLAPSED:
242                 return "COLLAPSED";
243             default:
244                 return "INVALID";
245         }
246     }
247 }
248