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