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