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 
17 package com.android.tv.settings.privacy;
18 
19 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
20 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_HARDWARE;
21 import static android.hardware.SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE;
22 
23 import static com.android.tv.settings.overlay.FlavorUtils.FLAVOR_CLASSIC;
24 
25 import android.content.Context;
26 import android.content.Intent;
27 import android.hardware.SensorPrivacyManager;
28 import android.os.Bundle;
29 import android.util.Log;
30 import android.view.View;
31 import android.view.ViewTreeObserver;
32 
33 import androidx.annotation.Keep;
34 import androidx.leanback.widget.VerticalGridView;
35 import androidx.preference.Preference;
36 import androidx.preference.PreferenceCategory;
37 import androidx.preference.PreferenceGroupAdapter;
38 import androidx.preference.PreferenceManager;
39 import androidx.preference.PreferenceScreen;
40 
41 import com.android.tv.settings.R;
42 import com.android.tv.settings.SettingsPreferenceFragment;
43 import com.android.tv.settings.device.apps.AppManagementFragment;
44 import com.android.tv.settings.overlay.FlavorUtils;
45 import com.android.tv.settings.widget.SwitchWithSoundPreference;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * The microphone/camera settings screen in TV settings.
52  * Allows the user to turn of the respective sensor.
53  */
54 @Keep
55 public class SensorFragment extends SettingsPreferenceFragment {
56 
57     private static final String TAG = "SensorFragment";
58     private static final boolean DEBUG = true;
59 
60     public static final String TOGGLE_EXTRA = "toggle";
61     /** How many recent apps should be shown when the list is collapsed. */
62     private static final int MAX_RECENT_APPS_COLLAPSED = 2;
63     private List<Preference> mAllRecentAppPrefs;
64 
65     protected static final String SENSOR_TOGGLE_KEY = "sensor_toggle";
66     private PrivacyToggle mToggle;
67     protected SwitchWithSoundPreference mSensorToggle;
68     private Preference mPhysicalPrivacyEnabledInfo;
69 
70     private SensorPrivacyManager mSensorPrivacyManager;
71     private final SensorPrivacyManager.OnSensorPrivacyChangedListener mPrivacyChangedListener =
72             (sensor, enabled) -> updateSensorPrivacyState();
73 
74     @Override
onCreate(Bundle savedInstanceState)75     public void onCreate(Bundle savedInstanceState) {
76         mSensorPrivacyManager = (SensorPrivacyManager)
77                 getContext().getSystemService(Context.SENSOR_PRIVACY_SERVICE);
78 
79         mToggle = (PrivacyToggle) getArguments().get(TOGGLE_EXTRA);
80         if (mToggle == null) {
81             throw new IllegalArgumentException("PrivacyToggle extra missing");
82         }
83 
84         super.onCreate(savedInstanceState);
85         getPreferenceManager().setPreferenceComparisonCallback(
86                 new PreferenceManager.SimplePreferenceComparisonCallback());
87     }
88 
89     @Override
onViewCreated(View view, Bundle savedInstanceState)90     public void onViewCreated(View view, Bundle savedInstanceState) {
91         super.onViewCreated(view, savedInstanceState);
92         updateSensorPrivacyState();
93     }
94 
95     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)96     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
97         Context themedContext = getPreferenceManager().getContext();
98         PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(themedContext);
99 
100         screen.setTitle(mToggle.screenTitle);
101 
102         addPhysicalPrivacyEnabledInfo(screen, themedContext);
103         addSensorToggleWithInfo(screen, themedContext);
104         addHardwareToggle(screen, themedContext);
105         addRecentAppsGroup(screen, themedContext);
106         addPermissionControllerPreference(screen, themedContext);
107         updateSensorPrivacyState();
108 
109         setPreferenceScreen(screen);
110     }
111 
112     @Override
onStart()113     public void onStart() {
114         super.onStart();
115         mSensorPrivacyManager.addSensorPrivacyListener(mToggle.sensor,
116                 mPrivacyChangedListener);
117     }
118 
119     @Override
onStop()120     public void onStop() {
121         mSensorPrivacyManager.removeSensorPrivacyListener(mToggle.sensor, mPrivacyChangedListener);
122         super.onStop();
123     }
124 
addHardwareToggle(PreferenceScreen screen, Context themedContext)125     protected void addHardwareToggle(PreferenceScreen screen, Context themedContext) {
126         // no-op
127     }
128 
updateHardwareToggle()129     protected void updateHardwareToggle() {
130         // no-nop
131     }
132 
addPhysicalPrivacyEnabledInfo(PreferenceScreen screen, Context themedContext)133     private void addPhysicalPrivacyEnabledInfo(PreferenceScreen screen, Context themedContext) {
134         mPhysicalPrivacyEnabledInfo = new Preference(themedContext);
135         mPhysicalPrivacyEnabledInfo.setLayoutResource(
136                 R.layout.sensor_physical_privacy_enabled_info);
137         mPhysicalPrivacyEnabledInfo.setSelectable(true);
138         mPhysicalPrivacyEnabledInfo.setTitle(mToggle.physicalPrivacyEnabledInfoTitle);
139         mPhysicalPrivacyEnabledInfo.setSummary(mToggle.physicalPrivacyEnabledInfoText);
140         mPhysicalPrivacyEnabledInfo.setIcon(mToggle.physicalPrivacyEnabledIcon);
141 
142         // Use InfoFragment when using 2-panel settings
143         if (FlavorUtils.getFlavor(getContext()) == FLAVOR_CLASSIC) {
144             mPhysicalPrivacyEnabledInfo.setFragment(PhysicalPrivacyUnblockFragment.class.getName());
145             mPhysicalPrivacyEnabledInfo.getExtras().putObject(
146                     PhysicalPrivacyUnblockFragment.TOGGLE_EXTRA, mToggle);
147         } else {
148             mPhysicalPrivacyEnabledInfo.setFragment(
149                     PhysicalPrivacyUnblockInfoFragment.class.getName());
150             mPhysicalPrivacyEnabledInfo.getExtras().putObject(
151                     PhysicalPrivacyUnblockInfoFragment.TOGGLE_EXTRA, mToggle);
152         }
153 
154         screen.addPreference(mPhysicalPrivacyEnabledInfo);
155     }
156 
157     /**
158      * Adds the sensor toggle with an InfoFragment (in two-panel mode) or an info text below (in
159      * one-panel mode).
160      */
addSensorToggleWithInfo(PreferenceScreen screen, Context themedContext)161     private void addSensorToggleWithInfo(PreferenceScreen screen, Context themedContext) {
162         mSensorToggle = new SwitchWithSoundPreference(themedContext);
163         screen.addPreference(mSensorToggle);
164         mSensorToggle.setKey(SENSOR_TOGGLE_KEY);
165         mSensorToggle.setTitle(mToggle.toggleTitle);
166         mSensorToggle.setSummary(R.string.sensor_toggle_description);
167         mSensorToggle.setFragment(SensorToggleInfoFragment.class.getName());
168         mSensorToggle.getExtras().putObject(SensorToggleInfoFragment.TOGGLE_EXTRA, mToggle);
169 
170         if (!FlavorUtils.isTwoPanel(themedContext)) {
171             // Show the toggle info text beneath instead.
172             Preference toggleInfo = new Preference(themedContext);
173             toggleInfo.setLayoutResource(R.layout.sensor_toggle_info);
174             toggleInfo.setSummary(mToggle.toggleInfoText);
175             toggleInfo.setSelectable(false);
176             screen.addPreference(toggleInfo);
177         }
178     }
179 
updateSensorPrivacyState()180     private void updateSensorPrivacyState() {
181         boolean softwarePrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
182                 TOGGLE_TYPE_SOFTWARE, mToggle.sensor);
183         boolean physicalPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
184                 TOGGLE_TYPE_HARDWARE, mToggle.sensor);
185 
186         if (DEBUG) {
187             Log.v(TAG,
188                     "softwarePrivacyEnabled=" + softwarePrivacyEnabled + ", physicalPrivacyEnabled="
189                             + physicalPrivacyEnabled);
190         }
191         // If privacy is enabled, the sensor access is turned off
192         mSensorToggle.setChecked(!softwarePrivacyEnabled && !physicalPrivacyEnabled);
193         mSensorToggle.setEnabled(!physicalPrivacyEnabled);
194         mPhysicalPrivacyEnabledInfo.setVisible(physicalPrivacyEnabled);
195 
196         if (physicalPrivacyEnabled) {
197             selectPreference(mPhysicalPrivacyEnabledInfo);
198         }
199         updateHardwareToggle();
200     }
201 
selectPreference(Preference preference)202     private void selectPreference(Preference preference) {
203         scrollToPreference(preference);
204         if (getListView() instanceof VerticalGridView) {
205             VerticalGridView listView = (VerticalGridView) getListView();
206             PreferenceGroupAdapter adapter = (PreferenceGroupAdapter) (listView.getAdapter());
207 
208             ViewTreeObserver.OnPreDrawListener listener = new ViewTreeObserver.OnPreDrawListener() {
209                 @Override
210                 public boolean onPreDraw() {
211                     listView.post(() -> {
212                         int position = adapter.getPreferenceAdapterPosition(preference);
213                         listView.setSelectedPositionSmooth(position);
214                     });
215                     listView.getViewTreeObserver().removeOnPreDrawListener(this);
216                     return true;
217                 }
218             };
219             listView.getViewTreeObserver().addOnPreDrawListener(listener);
220         }
221     }
222 
223     /**
224      * Adds section that shows an expandable list of apps that have recently accessed the sensor.
225      */
addRecentAppsGroup(PreferenceScreen screen, Context themedContext)226     private void addRecentAppsGroup(PreferenceScreen screen, Context themedContext) {
227         // Create the Recently Accessed By section.
228         PreferenceCategory recentRequests = new PreferenceCategory(themedContext);
229         recentRequests.setTitle(R.string.recently_accessed_by_category);
230         screen.addPreference(recentRequests);
231 
232         // Get recent accesses.
233         List<RecentlyAccessedByUtils.App> recentApps = RecentlyAccessedByUtils.getAppList(
234                 themedContext, mToggle.appOps);
235         if (DEBUG) Log.v(TAG, "recently accessed by " + recentApps.size() + " apps");
236 
237         // Create a preference for each access.
238         mAllRecentAppPrefs = new ArrayList<>(recentApps.size());
239         for (RecentlyAccessedByUtils.App app : recentApps) {
240             if (DEBUG) Log.v(TAG, "last access: " + app.mLastAccess);
241             Preference pref = new Preference(themedContext);
242             pref.setTitle(app.mLabel);
243             pref.setIcon(app.mIcon);
244             pref.setFragment(AppManagementFragment.class.getName());
245             AppManagementFragment.prepareArgs(pref.getExtras(), app.mPackageName);
246             mAllRecentAppPrefs.add(pref);
247         }
248 
249         for (int i = 0; i < MAX_RECENT_APPS_COLLAPSED; i++) {
250             if (mAllRecentAppPrefs.size() > i) {
251                 recentRequests.addPreference(mAllRecentAppPrefs.get(i));
252             }
253         }
254         if (mAllRecentAppPrefs.size() > MAX_RECENT_APPS_COLLAPSED) {
255             Preference showAllRecent = new Preference(themedContext);
256             showAllRecent.setTitle(R.string.recently_accessed_show_all);
257             showAllRecent.setOnPreferenceClickListener(preference -> {
258                 preference.setVisible(false);
259                 for (int i = MAX_RECENT_APPS_COLLAPSED; i < mAllRecentAppPrefs.size(); i++) {
260                     recentRequests.addPreference(mAllRecentAppPrefs.get(i));
261                 }
262                 return false;
263             });
264             recentRequests.addPreference(showAllRecent);
265         }
266 
267         if (mAllRecentAppPrefs.size() == 0) {
268             Preference banner = new Preference(themedContext);
269             banner.setSummary(R.string.no_recent_sensor_accesses);
270             banner.setSelectable(false);
271             recentRequests.addPreference(banner);
272         }
273     }
274 
275     /**
276      * Adds a preference that opens the overview of the PermissionGroup pertaining to the sensor.
277      */
addPermissionControllerPreference(PreferenceScreen screen, Context themedContext)278     private void addPermissionControllerPreference(PreferenceScreen screen, Context themedContext) {
279         Preference openPermissionController = new Preference(themedContext);
280         openPermissionController.setTitle(mToggle.appPermissionsTitle);
281         Intent showSensorPermissions = new Intent(Intent.ACTION_MANAGE_PERMISSION_APPS);
282         showSensorPermissions.putExtra(Intent.EXTRA_PERMISSION_NAME,
283                 mToggle.permissionsGroupName);
284         openPermissionController.setIntent(showSensorPermissions);
285         screen.addPreference(openPermissionController);
286     }
287 
288     @Override
onPreferenceTreeClick(Preference preference)289     public boolean onPreferenceTreeClick(Preference preference) {
290         if (SENSOR_TOGGLE_KEY.equals(preference.getKey())) {
291             boolean physicalPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
292                     TOGGLE_TYPE_HARDWARE, mToggle.sensor);
293             if (!physicalPrivacyEnabled) {
294                 mSensorPrivacyManager.setSensorPrivacy(SETTINGS, mToggle.sensor,
295                         !mSensorToggle.isChecked());
296             }
297             updateSensorPrivacyState();
298             return true;
299         }
300         return super.onPreferenceTreeClick(preference);
301     }
302 }
303