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 mSensorPrivacyManager.addSensorPrivacyListener(mToggle.sensor, 93 mPrivacyChangedListener); 94 updateSensorPrivacyState(); 95 } 96 97 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)98 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 99 Context themedContext = getPreferenceManager().getContext(); 100 PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(themedContext); 101 102 screen.setTitle(mToggle.screenTitle); 103 104 addPhysicalPrivacyEnabledInfo(screen, themedContext); 105 addSensorToggleWithInfo(screen, themedContext); 106 addHardwareToggle(screen, themedContext); 107 addRecentAppsGroup(screen, themedContext); 108 addPermissionControllerPreference(screen, themedContext); 109 updateSensorPrivacyState(); 110 111 setPreferenceScreen(screen); 112 } 113 addHardwareToggle(PreferenceScreen screen, Context themedContext)114 protected void addHardwareToggle(PreferenceScreen screen, Context themedContext) { 115 // no-op 116 } 117 updateHardwareToggle()118 protected void updateHardwareToggle() { 119 // no-nop 120 } 121 addPhysicalPrivacyEnabledInfo(PreferenceScreen screen, Context themedContext)122 private void addPhysicalPrivacyEnabledInfo(PreferenceScreen screen, Context themedContext) { 123 mPhysicalPrivacyEnabledInfo = new Preference(themedContext); 124 mPhysicalPrivacyEnabledInfo.setLayoutResource( 125 R.layout.sensor_physical_privacy_enabled_info); 126 mPhysicalPrivacyEnabledInfo.setSelectable(true); 127 mPhysicalPrivacyEnabledInfo.setTitle(mToggle.physicalPrivacyEnabledInfoTitle); 128 mPhysicalPrivacyEnabledInfo.setSummary(mToggle.physicalPrivacyEnabledInfoText); 129 mPhysicalPrivacyEnabledInfo.setIcon(mToggle.physicalPrivacyEnabledIcon); 130 131 // Use InfoFragment when using 2-panel settings 132 if (FlavorUtils.getFlavor(getContext()) == FLAVOR_CLASSIC) { 133 mPhysicalPrivacyEnabledInfo.setFragment(PhysicalPrivacyUnblockFragment.class.getName()); 134 mPhysicalPrivacyEnabledInfo.getExtras().putObject( 135 PhysicalPrivacyUnblockFragment.TOGGLE_EXTRA, mToggle); 136 } else { 137 mPhysicalPrivacyEnabledInfo.setFragment( 138 PhysicalPrivacyUnblockInfoFragment.class.getName()); 139 mPhysicalPrivacyEnabledInfo.getExtras().putObject( 140 PhysicalPrivacyUnblockInfoFragment.TOGGLE_EXTRA, mToggle); 141 } 142 143 screen.addPreference(mPhysicalPrivacyEnabledInfo); 144 } 145 146 /** 147 * Adds the sensor toggle with an InfoFragment (in two-panel mode) or an info text below (in 148 * one-panel mode). 149 */ addSensorToggleWithInfo(PreferenceScreen screen, Context themedContext)150 private void addSensorToggleWithInfo(PreferenceScreen screen, Context themedContext) { 151 mSensorToggle = new SwitchWithSoundPreference(themedContext); 152 screen.addPreference(mSensorToggle); 153 mSensorToggle.setKey(SENSOR_TOGGLE_KEY); 154 mSensorToggle.setTitle(mToggle.toggleTitle); 155 mSensorToggle.setSummary(R.string.sensor_toggle_description); 156 mSensorToggle.setFragment(SensorToggleInfoFragment.class.getName()); 157 mSensorToggle.getExtras().putObject(SensorToggleInfoFragment.TOGGLE_EXTRA, mToggle); 158 159 if (!FlavorUtils.isTwoPanel(themedContext)) { 160 // Show the toggle info text beneath instead. 161 Preference toggleInfo = new Preference(themedContext); 162 toggleInfo.setLayoutResource(R.layout.sensor_toggle_info); 163 toggleInfo.setSummary(mToggle.toggleInfoText); 164 toggleInfo.setSelectable(false); 165 screen.addPreference(toggleInfo); 166 } 167 } 168 updateSensorPrivacyState()169 private void updateSensorPrivacyState() { 170 boolean softwarePrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( 171 TOGGLE_TYPE_SOFTWARE, mToggle.sensor); 172 boolean physicalPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( 173 TOGGLE_TYPE_HARDWARE, mToggle.sensor); 174 175 if (DEBUG) { 176 Log.v(TAG, 177 "softwarePrivacyEnabled=" + softwarePrivacyEnabled + ", physicalPrivacyEnabled=" 178 + physicalPrivacyEnabled); 179 } 180 // If privacy is enabled, the sensor access is turned off 181 mSensorToggle.setChecked(!softwarePrivacyEnabled && !physicalPrivacyEnabled); 182 mSensorToggle.setEnabled(!physicalPrivacyEnabled); 183 mPhysicalPrivacyEnabledInfo.setVisible(physicalPrivacyEnabled); 184 185 if (physicalPrivacyEnabled) { 186 selectPreference(mPhysicalPrivacyEnabledInfo); 187 } 188 updateHardwareToggle(); 189 } 190 selectPreference(Preference preference)191 private void selectPreference(Preference preference) { 192 scrollToPreference(preference); 193 if (getListView() instanceof VerticalGridView) { 194 VerticalGridView listView = (VerticalGridView) getListView(); 195 PreferenceGroupAdapter adapter = (PreferenceGroupAdapter) (listView.getAdapter()); 196 197 ViewTreeObserver.OnPreDrawListener listener = new ViewTreeObserver.OnPreDrawListener() { 198 @Override 199 public boolean onPreDraw() { 200 listView.post(() -> { 201 int position = adapter.getPreferenceAdapterPosition(preference); 202 listView.setSelectedPositionSmooth(position); 203 }); 204 listView.getViewTreeObserver().removeOnPreDrawListener(this); 205 return true; 206 } 207 }; 208 listView.getViewTreeObserver().addOnPreDrawListener(listener); 209 } 210 } 211 212 /** 213 * Adds section that shows an expandable list of apps that have recently accessed the sensor. 214 */ addRecentAppsGroup(PreferenceScreen screen, Context themedContext)215 private void addRecentAppsGroup(PreferenceScreen screen, Context themedContext) { 216 // Create the Recently Accessed By section. 217 PreferenceCategory recentRequests = new PreferenceCategory(themedContext); 218 recentRequests.setTitle(R.string.recently_accessed_by_category); 219 screen.addPreference(recentRequests); 220 221 // Get recent accesses. 222 List<RecentlyAccessedByUtils.App> recentApps = RecentlyAccessedByUtils.getAppList( 223 themedContext, mToggle.appOps); 224 if (DEBUG) Log.v(TAG, "recently accessed by " + recentApps.size() + " apps"); 225 226 // Create a preference for each access. 227 mAllRecentAppPrefs = new ArrayList<>(recentApps.size()); 228 for (RecentlyAccessedByUtils.App app : recentApps) { 229 if (DEBUG) Log.v(TAG, "last access: " + app.mLastAccess); 230 Preference pref = new Preference(themedContext); 231 pref.setTitle(app.mLabel); 232 pref.setIcon(app.mIcon); 233 pref.setFragment(AppManagementFragment.class.getName()); 234 AppManagementFragment.prepareArgs(pref.getExtras(), app.mPackageName); 235 mAllRecentAppPrefs.add(pref); 236 } 237 238 for (int i = 0; i < MAX_RECENT_APPS_COLLAPSED; i++) { 239 if (mAllRecentAppPrefs.size() > i) { 240 recentRequests.addPreference(mAllRecentAppPrefs.get(i)); 241 } 242 } 243 if (mAllRecentAppPrefs.size() > MAX_RECENT_APPS_COLLAPSED) { 244 Preference showAllRecent = new Preference(themedContext); 245 showAllRecent.setTitle(R.string.recently_accessed_show_all); 246 showAllRecent.setOnPreferenceClickListener(preference -> { 247 preference.setVisible(false); 248 for (int i = MAX_RECENT_APPS_COLLAPSED; i < mAllRecentAppPrefs.size(); i++) { 249 recentRequests.addPreference(mAllRecentAppPrefs.get(i)); 250 } 251 return false; 252 }); 253 recentRequests.addPreference(showAllRecent); 254 } 255 256 if (mAllRecentAppPrefs.size() == 0) { 257 Preference banner = new Preference(themedContext); 258 banner.setSummary(R.string.no_recent_sensor_accesses); 259 banner.setSelectable(false); 260 recentRequests.addPreference(banner); 261 } 262 } 263 264 /** 265 * Adds a preference that opens the overview of the PermissionGroup pertaining to the sensor. 266 */ addPermissionControllerPreference(PreferenceScreen screen, Context themedContext)267 private void addPermissionControllerPreference(PreferenceScreen screen, Context themedContext) { 268 Preference openPermissionController = new Preference(themedContext); 269 openPermissionController.setTitle(mToggle.appPermissionsTitle); 270 Intent showSensorPermissions = new Intent(Intent.ACTION_MANAGE_PERMISSION_APPS); 271 showSensorPermissions.putExtra(Intent.EXTRA_PERMISSION_NAME, 272 mToggle.permissionsGroupName); 273 openPermissionController.setIntent(showSensorPermissions); 274 screen.addPreference(openPermissionController); 275 } 276 277 @Override onDestroyView()278 public void onDestroyView() { 279 mSensorPrivacyManager.removeSensorPrivacyListener(mToggle.sensor, mPrivacyChangedListener); 280 super.onDestroyView(); 281 } 282 283 @Override onPreferenceTreeClick(Preference preference)284 public boolean onPreferenceTreeClick(Preference preference) { 285 if (SENSOR_TOGGLE_KEY.equals(preference.getKey())) { 286 boolean physicalPrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( 287 TOGGLE_TYPE_HARDWARE, mToggle.sensor); 288 if (!physicalPrivacyEnabled) { 289 mSensorPrivacyManager.setSensorPrivacy(SETTINGS, mToggle.sensor, 290 !mSensorToggle.isChecked()); 291 } 292 updateSensorPrivacyState(); 293 return true; 294 } 295 return super.onPreferenceTreeClick(preference); 296 } 297 } 298