1 /* 2 * Copyright (C) 2021 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.Icon; 25 import android.os.UserHandle; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 import androidx.core.text.BidiFormatter; 31 32 import com.android.car.qc.QCActionItem; 33 import com.android.car.qc.QCItem; 34 import com.android.car.qc.QCList; 35 import com.android.car.qc.QCRow; 36 import com.android.car.qc.provider.BaseLocalQCProvider; 37 import com.android.launcher3.icons.BitmapInfo; 38 import com.android.launcher3.icons.IconFactory; 39 import com.android.systemui.R; 40 import com.android.systemui.privacy.PrivacyDialog; 41 42 import java.util.List; 43 import java.util.Optional; 44 import java.util.stream.Collectors; 45 46 /** 47 * A {@link BaseLocalQCProvider} that builds the microphone privacy panel. 48 */ 49 public class MicQcPanel extends BaseLocalQCProvider { 50 private static final String TAG = "MicPrivacyChipDialog"; 51 52 private final Context mContext; 53 private final String mPhoneCallTitle; 54 private final Icon mMicOnIcon; 55 private final String mMicOnTitleText; 56 private final Icon mMicOffIcon; 57 private final String mMicOffTitleText; 58 private final String mMicSubtitleText; 59 60 private MicPrivacyElementsProvider mMicPrivacyElementsProvider; 61 private MicSensorInfoProvider mMicSensorInfoProvider; 62 MicQcPanel(Context context)63 public MicQcPanel(Context context) { 64 super(context); 65 mContext = context; 66 mPhoneCallTitle = context.getString(R.string.ongoing_privacy_dialog_phonecall); 67 mMicOnIcon = Icon.createWithResource(context, R.drawable.ic_mic_light); 68 mMicOnTitleText = context.getString(R.string.mic_privacy_chip_use_microphone); 69 mMicOffIcon = Icon.createWithResource(context, R.drawable.ic_mic_off_light); 70 mMicOffTitleText = context.getString(R.string.mic_privacy_chip_off_content); 71 mMicSubtitleText = context.getString(R.string.mic_privacy_chip_use_microphone_subtext); 72 } 73 74 /** 75 * Sets controllers for this {@link BaseLocalQCProvider}. 76 */ setControllers(Object... objects)77 public void setControllers(Object... objects) { 78 if (objects == null) { 79 return; 80 } 81 82 for (int i = 0; i < objects.length; i++) { 83 Object object = objects[i]; 84 85 if (object instanceof MicSensorInfoProvider) { 86 mMicSensorInfoProvider = (MicSensorInfoProvider) object; 87 mMicSensorInfoProvider.setNotifyUpdateRunnable(() -> notifyChange()); 88 } 89 90 if (object instanceof MicPrivacyElementsProvider) { 91 mMicPrivacyElementsProvider = (MicPrivacyElementsProvider) object; 92 } 93 } 94 95 if (mMicSensorInfoProvider != null && mMicPrivacyElementsProvider != null) { 96 notifyChange(); 97 } 98 } 99 100 @Override getQCItem()101 public QCItem getQCItem() { 102 if (mMicSensorInfoProvider == null || mMicPrivacyElementsProvider == null) { 103 return null; 104 } 105 106 QCList.Builder listBuilder = new QCList.Builder(); 107 listBuilder.addRow(createMicToggleRow(mMicSensorInfoProvider.isMicEnabled())); 108 109 List<PrivacyDialog.PrivacyElement> elements = 110 mMicPrivacyElementsProvider.getPrivacyElements(); 111 112 List<PrivacyDialog.PrivacyElement> activeElements = elements.stream() 113 .filter(PrivacyDialog.PrivacyElement::getActive) 114 .collect(Collectors.toList()); 115 addPrivacyElementsToQcList(listBuilder, activeElements); 116 117 List<PrivacyDialog.PrivacyElement> inactiveElements = elements.stream() 118 .filter(privacyElement -> !privacyElement.getActive()) 119 .collect(Collectors.toList()); 120 addPrivacyElementsToQcList(listBuilder, inactiveElements); 121 122 return listBuilder.build(); 123 } 124 getApplicationInfo(PrivacyDialog.PrivacyElement element)125 private Optional<ApplicationInfo> getApplicationInfo(PrivacyDialog.PrivacyElement element) { 126 return getApplicationInfo(element.getPackageName(), element.getUserId()); 127 } 128 getApplicationInfo(String packageName, int userId)129 private Optional<ApplicationInfo> getApplicationInfo(String packageName, int userId) { 130 ApplicationInfo applicationInfo; 131 try { 132 applicationInfo = mContext.getPackageManager() 133 .getApplicationInfoAsUser(packageName, /* flags= */ 0, userId); 134 return Optional.of(applicationInfo); 135 } catch (PackageManager.NameNotFoundException e) { 136 Log.w(TAG, "Application info not found for: " + packageName); 137 return Optional.empty(); 138 } 139 } 140 createMicToggleRow(boolean isMicEnabled)141 private QCRow createMicToggleRow(boolean isMicEnabled) { 142 QCActionItem actionItem = new QCActionItem.Builder(QC_TYPE_ACTION_SWITCH) 143 .setChecked(isMicEnabled) 144 .build(); 145 actionItem.setActionHandler(new MicToggleActionHandler(mMicSensorInfoProvider)); 146 147 return new QCRow.Builder() 148 .setIcon(isMicEnabled ? mMicOnIcon : mMicOffIcon) 149 .setIconTintable(false) 150 .setTitle(isMicEnabled ? mMicOnTitleText : mMicOffTitleText) 151 .setSubtitle(mMicSubtitleText) 152 .addEndItem(actionItem) 153 .build(); 154 } 155 addPrivacyElementsToQcList(QCList.Builder listBuilder, List<PrivacyDialog.PrivacyElement> elements)156 private void addPrivacyElementsToQcList(QCList.Builder listBuilder, 157 List<PrivacyDialog.PrivacyElement> elements) { 158 for (int i = 0; i < elements.size(); i++) { 159 PrivacyDialog.PrivacyElement element = elements.get(i); 160 Optional<ApplicationInfo> applicationInfo = getApplicationInfo(element); 161 if (!applicationInfo.isPresent()) continue; 162 163 String appName = element.getPhoneCall() 164 ? mPhoneCallTitle 165 : getAppLabel(applicationInfo.get(), mContext); 166 167 String title; 168 if (element.getActive()) { 169 title = mContext.getString(R.string.mic_privacy_chip_app_using_mic_suffix, appName); 170 } else { 171 if (i == elements.size() - 1) { 172 title = mContext 173 .getString(R.string.mic_privacy_chip_app_recently_used_mic_suffix, 174 appName); 175 } else { 176 title = mContext 177 .getString(R.string.mic_privacy_chip_apps_recently_used_mic_suffix, 178 appName, elements.size() - 1 - i); 179 } 180 } 181 182 listBuilder.addRow(new QCRow.Builder() 183 .setIcon(getBadgedIcon(mContext, applicationInfo.get())) 184 .setIconTintable(false) 185 .setTitle(title) 186 .build()); 187 188 if (!element.getActive()) return; 189 } 190 } 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 201 @NonNull getBadgedIcon(@onNull Context context, @NonNull ApplicationInfo appInfo)202 private Icon getBadgedIcon(@NonNull Context context, 203 @NonNull ApplicationInfo appInfo) { 204 UserHandle user = UserHandle.getUserHandleForUid(appInfo.uid); 205 try (IconFactory iconFactory = IconFactory.obtain(context)) { 206 BitmapInfo bitmapInfo = 207 iconFactory.createBadgedIconBitmap( 208 appInfo.loadUnbadgedIcon(context.getPackageManager()), user, 209 /* shrinkNonAdaptiveIcons= */ false); 210 return Icon.createWithBitmap(bitmapInfo.icon); 211 } 212 } 213 214 /** 215 * A helper object that retrieves microphone 216 * {@link com.android.systemui.privacy.PrivacyDialog.PrivacyElement} list for {@link MicQcPanel} 217 */ 218 public interface MicPrivacyElementsProvider { 219 /** 220 * @return A list of microphone 221 * {@link com.android.systemui.privacy.PrivacyDialog.PrivacyElement} 222 */ getPrivacyElements()223 List<PrivacyDialog.PrivacyElement> getPrivacyElements(); 224 } 225 226 /** 227 * A helper object that allows the {@link MicQcPanel} to communicate with 228 * {@link android.hardware.SensorPrivacyManager} 229 */ 230 public interface MicSensorInfoProvider { 231 /** 232 * @return {@code true} if microphone sensor privacy is not enabled (microphone is on) 233 */ isMicEnabled()234 boolean isMicEnabled(); 235 236 /** 237 * Toggles microphone sensor privacy 238 */ toggleMic()239 void toggleMic(); 240 241 /** 242 * Informs {@link MicQcPanel} to update its state. 243 */ setNotifyUpdateRunnable(Runnable runnable)244 void setNotifyUpdateRunnable(Runnable runnable); 245 } 246 247 private static class MicToggleActionHandler implements QCItem.ActionHandler { 248 private final MicSensorInfoProvider mMicSensorInfoProvider; 249 MicToggleActionHandler(MicSensorInfoProvider micSensorInfoProvider)250 MicToggleActionHandler(MicSensorInfoProvider micSensorInfoProvider) { 251 this.mMicSensorInfoProvider = micSensorInfoProvider; 252 } 253 254 @Override onAction(@onNull QCItem item, @NonNull Context context, @NonNull Intent intent)255 public void onAction(@NonNull QCItem item, @NonNull Context context, 256 @NonNull Intent intent) { 257 mMicSensorInfoProvider.toggleMic(); 258 } 259 } 260 } 261