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