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