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