• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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