1 /* 2 * Copyright (C) 2022 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 package com.android.systemui.car.privacy; 17 18 import static com.android.car.qc.QCItem.QC_TYPE_ACTION_SWITCH; 19 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.Icon; 29 import android.os.UserHandle; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import androidx.annotation.DrawableRes; 34 import androidx.annotation.NonNull; 35 import androidx.core.text.BidiFormatter; 36 37 import com.android.car.qc.QCActionItem; 38 import com.android.car.qc.QCItem; 39 import com.android.car.qc.QCList; 40 import com.android.car.qc.QCRow; 41 import com.android.car.qc.provider.BaseLocalQCProvider; 42 import com.android.systemui.R; 43 import com.android.systemui.privacy.PrivacyDialog; 44 45 import java.util.List; 46 import java.util.Optional; 47 import java.util.stream.Collectors; 48 49 /** 50 * A {@link BaseLocalQCProvider} that builds the sensor (such as microphone or camera) privacy 51 * panel. 52 */ 53 public abstract class SensorQcPanel extends BaseLocalQCProvider 54 implements SensorInfoUpdateListener { 55 private static final String TAG = "SensorQcPanel"; 56 57 private final String mPhoneCallTitle; 58 59 protected Icon mSensorOnIcon; 60 protected String mSensorOnTitleText; 61 protected Icon mSensorOffIcon; 62 protected String mSensorOffTitleText; 63 protected String mSensorSubtitleText; 64 65 private SensorPrivacyElementsProvider mSensorPrivacyElementsProvider; 66 private SensorInfoProvider mSensorInfoProvider; 67 SensorQcPanel(Context context, SensorInfoProvider infoProvider, SensorPrivacyElementsProvider elementsProvider)68 public SensorQcPanel(Context context, SensorInfoProvider infoProvider, 69 SensorPrivacyElementsProvider elementsProvider) { 70 super(context); 71 mSensorInfoProvider = infoProvider; 72 mSensorPrivacyElementsProvider = elementsProvider; 73 mPhoneCallTitle = context.getString(R.string.ongoing_privacy_dialog_phonecall); 74 mSensorOnTitleText = context.getString(R.string.privacy_chip_use_sensor, getSensorName()); 75 mSensorOffTitleText = context.getString(R.string.privacy_chip_off_content, 76 getSensorNameWithFirstLetterCapitalized()); 77 mSensorSubtitleText = context.getString(R.string.privacy_chip_use_sensor_subtext); 78 79 mSensorOnIcon = Icon.createWithResource(context, getSensorOnIconResourceId()); 80 mSensorOffIcon = Icon.createWithResource(context, getSensorOffIconResourceId()); 81 } 82 83 @Override getQCItem()84 public QCItem getQCItem() { 85 if (mSensorInfoProvider == null || mSensorPrivacyElementsProvider == null) { 86 return null; 87 } 88 89 QCList.Builder listBuilder = new QCList.Builder(); 90 listBuilder.addRow(createSensorToggleRow(mSensorInfoProvider.isSensorEnabled())); 91 92 List<PrivacyDialog.PrivacyElement> elements = 93 mSensorPrivacyElementsProvider.getPrivacyElements(); 94 95 List<PrivacyDialog.PrivacyElement> activeElements = elements.stream() 96 .filter(PrivacyDialog.PrivacyElement::getActive) 97 .collect(Collectors.toList()); 98 addPrivacyElementsToQcList(listBuilder, activeElements); 99 100 List<PrivacyDialog.PrivacyElement> inactiveElements = elements.stream() 101 .filter(privacyElement -> !privacyElement.getActive()) 102 .collect(Collectors.toList()); 103 addPrivacyElementsToQcList(listBuilder, inactiveElements); 104 105 return listBuilder.build(); 106 } 107 getApplicationInfo(PrivacyDialog.PrivacyElement element)108 private Optional<ApplicationInfo> getApplicationInfo(PrivacyDialog.PrivacyElement element) { 109 return getApplicationInfo(element.getPackageName(), element.getUserId()); 110 } 111 getApplicationInfo(String packageName, int userId)112 private Optional<ApplicationInfo> getApplicationInfo(String packageName, int userId) { 113 ApplicationInfo applicationInfo; 114 try { 115 applicationInfo = mContext.getPackageManager() 116 .getApplicationInfoAsUser(packageName, /* flags= */ 0, userId); 117 return Optional.of(applicationInfo); 118 } catch (PackageManager.NameNotFoundException e) { 119 Log.w(TAG, "Application info not found for: " + packageName); 120 return Optional.empty(); 121 } 122 } 123 createSensorToggleRow(boolean isMicEnabled)124 private QCRow createSensorToggleRow(boolean isMicEnabled) { 125 QCActionItem actionItem = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH) 126 .setChecked(isMicEnabled) 127 .build(); 128 actionItem.setActionHandler(new SensorToggleActionHandler(mSensorInfoProvider)); 129 130 return new QCRow.Builder() 131 .setIcon(isMicEnabled ? mSensorOnIcon : mSensorOffIcon) 132 .setIconTintable(false) 133 .setTitle(isMicEnabled ? mSensorOnTitleText : mSensorOffTitleText) 134 .setSubtitle(mSensorSubtitleText) 135 .addEndItem(actionItem) 136 .build(); 137 } 138 addPrivacyElementsToQcList(QCList.Builder listBuilder, List<PrivacyDialog.PrivacyElement> elements)139 private void addPrivacyElementsToQcList(QCList.Builder listBuilder, 140 List<PrivacyDialog.PrivacyElement> elements) { 141 for (int i = 0; i < elements.size(); i++) { 142 PrivacyDialog.PrivacyElement element = elements.get(i); 143 Optional<ApplicationInfo> applicationInfo = getApplicationInfo(element); 144 if (!applicationInfo.isPresent()) continue; 145 146 String appName = element.getPhoneCall() 147 ? mPhoneCallTitle 148 : getAppLabel(applicationInfo.get(), mContext); 149 150 String title; 151 if (element.getActive()) { 152 title = mContext.getString(R.string.privacy_chip_app_using_sensor_suffix, 153 appName, getSensorShortName()); 154 } else { 155 if (i == elements.size() - 1) { 156 title = mContext 157 .getString(R.string.privacy_chip_app_recently_used_sensor_suffix, 158 appName, getSensorShortName()); 159 } else { 160 title = mContext 161 .getString(R.string.privacy_chip_apps_recently_used_sensor_suffix, 162 appName, elements.size() - 1 - i, getSensorShortName()); 163 } 164 } 165 166 listBuilder.addRow(new QCRow.Builder() 167 .setIcon(loadAppIcon(applicationInfo.get())) 168 .setIconTintable(false) 169 .setTitle(title) 170 .build()); 171 172 if (!element.getActive()) return; 173 } 174 } 175 getSensorShortName()176 protected String getSensorShortName() { 177 return null; 178 } 179 getSensorName()180 protected String getSensorName() { 181 return null; 182 } 183 getSensorNameWithFirstLetterCapitalized()184 protected String getSensorNameWithFirstLetterCapitalized() { 185 return null; 186 } 187 getSensorOnIconResourceId()188 protected abstract @DrawableRes int getSensorOnIconResourceId(); 189 getSensorOffIconResourceId()190 protected abstract @DrawableRes int getSensorOffIconResourceId(); 191 getAppLabel(@onNull ApplicationInfo applicationInfo, @NonNull Context context)192 private String getAppLabel(@NonNull ApplicationInfo applicationInfo, @NonNull Context context) { 193 return BidiFormatter.getInstance() 194 .unicodeWrap(applicationInfo.loadSafeLabel(context.getPackageManager(), 195 /* ellipsizeDip= */ 0, 196 /* flags= */ TextUtils.SAFE_STRING_FLAG_TRIM 197 | TextUtils.SAFE_STRING_FLAG_FIRST_LINE) 198 .toString()); 199 } 200 loadAppIcon(@onNull ApplicationInfo appInfo)201 private Icon loadAppIcon(@NonNull ApplicationInfo appInfo) { 202 UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid); 203 Context userContext = mContext.createContextAsUser(user, /* flags= */ 0); 204 PackageManager pm = userContext.getPackageManager(); 205 Drawable appIcon = pm.getApplicationIcon(appInfo); 206 return Icon.createWithBitmap(createBitmapFromDrawable(pm.getUserBadgedIcon(appIcon, user))); 207 } 208 createBitmapFromDrawable(Drawable drawable)209 private static Bitmap createBitmapFromDrawable(Drawable drawable) { 210 Bitmap bitmap; 211 if (drawable instanceof BitmapDrawable) { 212 bitmap = ((BitmapDrawable) drawable).getBitmap(); 213 } else { 214 int width = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : 1; 215 int height = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : 1; 216 bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 217 Canvas canvas = new Canvas(bitmap); 218 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 219 drawable.draw(canvas); 220 } 221 return bitmap; 222 } 223 224 @Override onSensorPrivacyChanged()225 public void onSensorPrivacyChanged() { 226 notifyChange(); 227 } 228 229 @Override onPrivacyItemsChanged()230 public void onPrivacyItemsChanged() { 231 notifyChange(); 232 } 233 234 @Override onSubscribed()235 protected void onSubscribed() { 236 mSensorInfoProvider.setSensorInfoUpdateListener(this); 237 } 238 239 @Override onUnsubscribed()240 protected void onUnsubscribed() { 241 mSensorInfoProvider.setSensorInfoUpdateListener(null); 242 } 243 244 /** 245 * A helper object that retrieves sensor 246 * {@link com.android.systemui.privacy.PrivacyDialog.PrivacyElement} list for 247 * {@link SensorQcPanel} 248 */ 249 public interface SensorPrivacyElementsProvider { 250 /** 251 * @return A list of sensors 252 * {@link com.android.systemui.privacy.PrivacyDialog.PrivacyElement} 253 */ getPrivacyElements()254 List<PrivacyDialog.PrivacyElement> getPrivacyElements(); 255 } 256 257 /** 258 * A helper object that allows the {@link SensorQcPanel} to communicate with 259 * {@link android.hardware.SensorPrivacyManager} 260 */ 261 public interface SensorInfoProvider { 262 /** 263 * @return {@code true} if sensor privacy is not enabled (e.g., microphone/camera is on) 264 */ isSensorEnabled()265 boolean isSensorEnabled(); 266 267 /** 268 * Toggles sensor privacy 269 */ toggleSensor()270 void toggleSensor(); 271 272 /** 273 * Informs {@link SensorQcPanel} to update its state. 274 */ setNotifyUpdateRunnable(Runnable runnable)275 void setNotifyUpdateRunnable(Runnable runnable); 276 277 /** 278 * Set the listener to monitor the update. 279 */ setSensorInfoUpdateListener(SensorInfoUpdateListener listener)280 void setSensorInfoUpdateListener(SensorInfoUpdateListener listener); 281 } 282 283 private static class SensorToggleActionHandler implements QCItem.ActionHandler { 284 private final SensorInfoProvider mSensorInfoProvider; 285 SensorToggleActionHandler(SensorInfoProvider sensorInfoProvider)286 SensorToggleActionHandler(SensorInfoProvider sensorInfoProvider) { 287 this.mSensorInfoProvider = sensorInfoProvider; 288 } 289 290 @Override onAction(@onNull QCItem item, @NonNull Context context, @NonNull Intent intent)291 public void onAction(@NonNull QCItem item, @NonNull Context context, 292 @NonNull Intent intent) { 293 mSensorInfoProvider.toggleSensor(); 294 } 295 } 296 } 297