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