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