From db0a9431dc41ea2cd5b7a245c3a85f3169c2f178 Mon Sep 17 00:00:00 2001 From: Robert Horvath Date: Thu, 10 Mar 2022 18:06:34 +0100 Subject: [PATCH] Add support for HW privacy toggles in sensor unblock dialog - Adapt strings for when HW toggle is blocking sensor access - Use hw-toggle specific resource names that can be overlayed by OEMs to provide device-specific guidance (strings, icons) - Call #onSensorBlockedChanged when going from blocked by SW toggle to blocked by HW toggle (still blocked, but for a different reason) to update UI - Change action button to open corresponding sensor privacy Settings page if blocked by HW toggle. Open mic settings if both mic & camera are blocked, since assuming a 3-way-switch (unblocked -> mic blocked -> mic & camera blocked) unblocking mic will also unblock the camera. Bug: 220717475 Test: manually, on device with physical mic toggle Change-Id: I64e37f29e73fb48d1d53a35a4ff72d5a77c6a6bc --- packages/SystemUI/res/drawable/ic_mic_off.xml | 10 + .../SystemUI/res/drawable/ic_videocam_off.xml | 13 ++ .../SystemUI/res/values-television/config.xml | 4 + .../SystemUI/res/values-television/dimens.xml | 3 + packages/SystemUI/res/values/drawables.xml | 26 +++ packages/SystemUI/res/values/strings.xml | 40 +++- .../television/TvUnblockSensorActivity.java | 207 +++++++++++++++--- .../IndividualSensorPrivacyController.java | 6 + ...IndividualSensorPrivacyControllerImpl.java | 46 +++- 9 files changed, 310 insertions(+), 45 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_mic_off.xml create mode 100644 packages/SystemUI/res/drawable/ic_videocam_off.xml create mode 100644 packages/SystemUI/res/values/drawables.xml diff --git a/packages/SystemUI/res/drawable/ic_mic_off.xml b/packages/SystemUI/res/drawable/ic_mic_off.xml new file mode 100644 index 0000000000000..fe23754f74677 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_mic_off.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_videocam_off.xml b/packages/SystemUI/res/drawable/ic_videocam_off.xml new file mode 100644 index 0000000000000..b609f36d912d6 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_videocam_off.xml @@ -0,0 +1,13 @@ + + + + diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml index a9e6d22461b61..94f6c3952e06d 100644 --- a/packages/SystemUI/res/values-television/config.xml +++ b/packages/SystemUI/res/values-television/config.xml @@ -42,4 +42,8 @@ true + + + true diff --git a/packages/SystemUI/res/values-television/dimens.xml b/packages/SystemUI/res/values-television/dimens.xml index d4bfc4d11b89b..7845ac49c06dd 100644 --- a/packages/SystemUI/res/values-television/dimens.xml +++ b/packages/SystemUI/res/values-television/dimens.xml @@ -57,4 +57,7 @@ 18dp 9dp + @dimen/bottom_sheet_icon_size + @dimen/bottom_sheet_icon_size + diff --git a/packages/SystemUI/res/values/drawables.xml b/packages/SystemUI/res/values/drawables.xml new file mode 100644 index 0000000000000..a1d6baa4808ec --- /dev/null +++ b/packages/SystemUI/res/values/drawables.xml @@ -0,0 +1,26 @@ + + + + + @drawable/ic_videocam_off + + @drawable/ic_mic_off + + @drawable/ic_videocam_off + + @drawable/ic_mic_off + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index e00c7bd398be5..d32490f31d694 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -720,16 +720,52 @@ Unblock device microphone? Unblock device camera? - + Unblock device camera and microphone? This unblocks access for all apps and services allowed to use your microphone. This unblocks access for all apps and services allowed to use your camera. - + This unblocks access for all apps and services allowed to use your camera or microphone. + + Microphone is blocked + + Camera is blocked + + The mic & camera are blocked + + + + To unblock, move the privacy switch on your device to the microphone on position to allow + microphone access. Refer to the device manual to locate the privacy switch on your device. + + + + To unblock, move the privacy switch on your device to the camera on position to allow camera + access. Refer to the device manual to locate the privacy switch on your device. + + + + To unblock them, move the privacy switch on your device to the unblocked position to allow + access. Refer to the device manual to locate the privacy switch on your device. + + + + + Microphone available + + + + Camera available + + + + Microphone and camera available + + Other device diff --git a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java index cc5cf4b63f990..d543eb2d647ac 100644 --- a/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java +++ b/packages/SystemUI/src/com/android/systemui/sensorprivacy/television/TvUnblockSensorActivity.java @@ -20,6 +20,11 @@ import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; import static android.hardware.SensorPrivacyManager.Sources.OTHER; +import android.annotation.DimenRes; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; import android.hardware.SensorPrivacyManager; import android.os.Bundle; import android.util.Log; @@ -28,6 +33,7 @@ import android.view.WindowManager; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import android.widget.Toast; import com.android.systemui.R; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; @@ -36,18 +42,29 @@ import com.android.systemui.tv.TvBottomSheetActivity; import javax.inject.Inject; /** - * Bottom sheet that is shown when the camera/mic sensors are blocked by the global toggle and - * allows the user to re-enable them. + * Bottom sheet that is shown when the camera/mic sensors are blocked by the global software toggle + * or physical privacy switch. */ public class TvUnblockSensorActivity extends TvBottomSheetActivity { private static final String TAG = TvUnblockSensorActivity.class.getSimpleName(); + private static final String ACTION_MANAGE_CAMERA_PRIVACY = + "android.settings.MANAGE_CAMERA_PRIVACY"; + private static final String ACTION_MANAGE_MICROPHONE_PRIVACY = + "android.settings.MANAGE_MICROPHONE_PRIVACY"; private static final int ALL_SENSORS = Integer.MAX_VALUE; + private int mSensor = -1; private final IndividualSensorPrivacyController mSensorPrivacyController; private IndividualSensorPrivacyController.Callback mSensorPrivacyCallback; + private TextView mTitle; + private TextView mContent; + private ImageView mIcon; + private ImageView mSecondIcon; + private Button mPositiveButton; + private Button mCancelButton; @Inject public TvUnblockSensorActivity( @@ -76,52 +93,145 @@ public class TvUnblockSensorActivity extends TvBottomSheetActivity { } mSensorPrivacyCallback = (sensor, blocked) -> { - if (mSensor == ALL_SENSORS) { - if (!mSensorPrivacyController.isSensorBlocked(CAMERA) - && !mSensorPrivacyController.isSensorBlocked(MICROPHONE)) { - finish(); - } + if (mSensor == ALL_SENSORS && !mSensorPrivacyController.isSensorBlocked(CAMERA) + && !mSensorPrivacyController.isSensorBlocked(MICROPHONE)) { + showToastAndFinish(); } else if (this.mSensor == sensor && !blocked) { - finish(); + showToastAndFinish(); + } else { + updateUI(); } }; initUI(); } - private void initUI() { - TextView title = findViewById(R.id.bottom_sheet_title); - TextView content = findViewById(R.id.bottom_sheet_body); - ImageView icon = findViewById(R.id.bottom_sheet_icon); - // mic icon if both icons are shown - ImageView secondIcon = findViewById(R.id.bottom_sheet_second_icon); - Button unblockButton = findViewById(R.id.bottom_sheet_positive_button); - Button cancelButton = findViewById(R.id.bottom_sheet_negative_button); - - switch (mSensor) { + private void showToastAndFinish() { + final int toastMsgResId; + switch(mSensor) { case MICROPHONE: - title.setText(R.string.sensor_privacy_start_use_mic_dialog_title); - content.setText(R.string.sensor_privacy_start_use_mic_dialog_content); - icon.setImageResource(com.android.internal.R.drawable.perm_group_microphone); - secondIcon.setVisibility(View.GONE); + toastMsgResId = R.string.sensor_privacy_mic_unblocked_toast_content; break; case CAMERA: - title.setText(R.string.sensor_privacy_start_use_camera_dialog_title); - content.setText(R.string.sensor_privacy_start_use_camera_dialog_content); - icon.setImageResource(com.android.internal.R.drawable.perm_group_camera); - secondIcon.setVisibility(View.GONE); + toastMsgResId = R.string.sensor_privacy_camera_unblocked_toast_content; break; case ALL_SENSORS: default: - title.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_title); - content.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_content); - icon.setImageResource(com.android.internal.R.drawable.perm_group_camera); - secondIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone); + toastMsgResId = R.string.sensor_privacy_mic_camera_unblocked_toast_content; break; } - unblockButton.setText( + Toast.makeText(this, toastMsgResId, Toast.LENGTH_SHORT).show(); + finish(); + } + + private boolean isBlockedByHardwareToggle() { + if (mSensor == ALL_SENSORS) { + return mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA) + || mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE); + } else { + return mSensorPrivacyController.isSensorBlockedByHardwareToggle(mSensor); + } + } + + private void initUI() { + mTitle = findViewById(R.id.bottom_sheet_title); + mContent = findViewById(R.id.bottom_sheet_body); + mIcon = findViewById(R.id.bottom_sheet_icon); + // mic icon if both icons are shown + mSecondIcon = findViewById(R.id.bottom_sheet_second_icon); + mPositiveButton = findViewById(R.id.bottom_sheet_positive_button); + mCancelButton = findViewById(R.id.bottom_sheet_negative_button); + + mCancelButton.setText(android.R.string.cancel); + mCancelButton.setOnClickListener(v -> finish()); + + updateUI(); + } + + private void updateUI() { + if (isBlockedByHardwareToggle()) { + updateUiForHardwareToggle(); + } else { + updateUiForSoftwareToggle(); + } + } + + private void updateUiForHardwareToggle() { + final Resources resources = getResources(); + + boolean micBlocked = (mSensor == MICROPHONE || mSensor == ALL_SENSORS) + && mSensorPrivacyController.isSensorBlockedByHardwareToggle(MICROPHONE); + boolean cameraBlocked = (mSensor == CAMERA || mSensor == ALL_SENSORS) + && mSensorPrivacyController.isSensorBlockedByHardwareToggle(CAMERA); + + setIconTint(resources.getBoolean(R.bool.config_unblockHwSensorIconEnableTint)); + setIconSize(R.dimen.unblock_hw_sensor_icon_width, R.dimen.unblock_hw_sensor_icon_height); + + if (micBlocked && cameraBlocked) { + mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_title); + mContent.setText( + R.string.sensor_privacy_start_use_mic_camera_blocked_dialog_content); + mIcon.setImageResource(R.drawable.unblock_hw_sensor_all); + + Drawable secondIconDrawable = resources.getDrawable( + R.drawable.unblock_hw_sensor_all_second, getTheme()); + if (secondIconDrawable == null) { + mSecondIcon.setVisibility(View.GONE); + } else { + mSecondIcon.setImageDrawable(secondIconDrawable); + } + } else if (cameraBlocked) { + mTitle.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_title); + mContent.setText(R.string.sensor_privacy_start_use_camera_blocked_dialog_content); + mIcon.setImageResource(R.drawable.unblock_hw_sensor_camera); + mSecondIcon.setVisibility(View.GONE); + } else if (micBlocked) { + mTitle.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_title); + mContent.setText(R.string.sensor_privacy_start_use_mic_blocked_dialog_content); + mIcon.setImageResource(R.drawable.unblock_hw_sensor_microphone); + mSecondIcon.setVisibility(View.GONE); + } + + // Start animation if drawable is animated + Drawable iconDrawable = mIcon.getDrawable(); + if (iconDrawable instanceof Animatable) { + ((Animatable) iconDrawable).start(); + } + + mPositiveButton.setVisibility(View.GONE); + mCancelButton.setText(android.R.string.ok); + } + + private void updateUiForSoftwareToggle() { + setIconTint(true); + setIconSize(R.dimen.bottom_sheet_icon_size, R.dimen.bottom_sheet_icon_size); + + switch (mSensor) { + case MICROPHONE: + mTitle.setText(R.string.sensor_privacy_start_use_mic_dialog_title); + mContent.setText(R.string.sensor_privacy_start_use_mic_dialog_content); + mIcon.setImageResource(com.android.internal.R.drawable.perm_group_microphone); + mSecondIcon.setVisibility(View.GONE); + break; + case CAMERA: + mTitle.setText(R.string.sensor_privacy_start_use_camera_dialog_title); + mContent.setText(R.string.sensor_privacy_start_use_camera_dialog_content); + mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera); + mSecondIcon.setVisibility(View.GONE); + break; + case ALL_SENSORS: + default: + mTitle.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_title); + mContent.setText(R.string.sensor_privacy_start_use_mic_camera_dialog_content); + mIcon.setImageResource(com.android.internal.R.drawable.perm_group_camera); + mSecondIcon.setImageResource( + com.android.internal.R.drawable.perm_group_microphone); + break; + } + + mPositiveButton.setText( com.android.internal.R.string.sensor_privacy_start_use_dialog_turn_on_button); - unblockButton.setOnClickListener(v -> { + mPositiveButton.setOnClickListener(v -> { if (mSensor == ALL_SENSORS) { mSensorPrivacyController.setSensorBlocked(OTHER, CAMERA, false); mSensorPrivacyController.setSensorBlocked(OTHER, MICROPHONE, false); @@ -129,14 +239,43 @@ public class TvUnblockSensorActivity extends TvBottomSheetActivity { mSensorPrivacyController.setSensorBlocked(OTHER, mSensor, false); } }); + } - cancelButton.setText(android.R.string.cancel); - cancelButton.setOnClickListener(v -> finish()); + private void setIconTint(boolean enableTint) { + final Resources resources = getResources(); + + if (enableTint) { + final ColorStateList iconTint = resources.getColorStateList( + R.color.bottom_sheet_icon_color, getTheme()); + mIcon.setImageTintList(iconTint); + mSecondIcon.setImageTintList(iconTint); + } else { + mIcon.setImageTintList(null); + mSecondIcon.setImageTintList(null); + } + + mIcon.invalidate(); + mSecondIcon.invalidate(); + } + + private void setIconSize(@DimenRes int widthRes, @DimenRes int heightRes) { + final Resources resources = getResources(); + final int iconWidth = resources.getDimensionPixelSize(widthRes); + final int iconHeight = resources.getDimensionPixelSize(heightRes); + + mIcon.getLayoutParams().width = iconWidth; + mIcon.getLayoutParams().height = iconHeight; + mIcon.invalidate(); + + mSecondIcon.getLayoutParams().width = iconWidth; + mSecondIcon.getLayoutParams().height = iconHeight; + mSecondIcon.invalidate(); } @Override public void onResume() { super.onResume(); + updateUI(); mSensorPrivacyController.addCallback(mSensorPrivacyCallback); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java index e01b95e81da87..1e73d593c8de1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyController.java @@ -27,6 +27,12 @@ public interface IndividualSensorPrivacyController extends boolean isSensorBlocked(@Sensor int sensor); + /** + * Returns {@code true} if the given sensor is blocked by a hardware toggle, {@code false} + * if the sensor is not blocked or blocked by a software toggle. + */ + boolean isSensorBlockedByHardwareToggle(@Sensor int sensor); + void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked); void suppressSensorPrivacyReminders(int sensor, boolean suppress); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java index 1d71301c74545..e4c444da5da9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java @@ -22,6 +22,7 @@ import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManager.Sensors.Sensor; import android.hardware.SensorPrivacyManager.Sources.Source; +import android.hardware.SensorPrivacyManager.ToggleType; import android.util.ArraySet; import android.util.SparseBooleanArray; @@ -34,7 +35,8 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr private static final int[] SENSORS = new int[] {CAMERA, MICROPHONE}; private final @NonNull SensorPrivacyManager mSensorPrivacyManager; - private final SparseBooleanArray mState = new SparseBooleanArray(); + private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray(); + private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray(); private final Set mCallbacks = new ArraySet<>(); public IndividualSensorPrivacyControllerImpl( @@ -44,11 +46,27 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr @Override public void init() { - for (int sensor : SENSORS) { - mSensorPrivacyManager.addSensorPrivacyListener(sensor, - (s, enabled) -> onSensorPrivacyChanged(sensor, enabled)); + mSensorPrivacyManager.addSensorPrivacyListener( + new SensorPrivacyManager.OnSensorPrivacyChangedListener() { + @Override + public void onSensorPrivacyChanged(SensorPrivacyChangedParams params) { + IndividualSensorPrivacyControllerImpl.this.onSensorPrivacyChanged( + params.getSensor(), params.getToggleType(), params.isEnabled()); + } - mState.put(sensor, mSensorPrivacyManager.isSensorPrivacyEnabled(sensor)); + @Override + public void onSensorPrivacyChanged(int sensor, boolean enabled) { + // handled in onSensorPrivacyChanged(SensorPrivacyChangedParams) + } + }); + + for (int sensor : SENSORS) { + boolean softwarePrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, sensor); + boolean hardwarePrivacyEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled( + SensorPrivacyManager.TOGGLE_TYPE_HARDWARE, sensor); + mSoftwareToggleState.put(sensor, softwarePrivacyEnabled); + mHardwareToggleState.put(sensor, hardwarePrivacyEnabled); } } @@ -59,7 +77,12 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr @Override public boolean isSensorBlocked(@Sensor int sensor) { - return mState.get(sensor, false); + return mSoftwareToggleState.get(sensor, false) || mHardwareToggleState.get(sensor, false); + } + + @Override + public boolean isSensorBlockedByHardwareToggle(@Sensor int sensor) { + return mHardwareToggleState.get(sensor, false); } @Override @@ -82,11 +105,16 @@ public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPr mCallbacks.remove(listener); } - private void onSensorPrivacyChanged(@Sensor int sensor, boolean blocked) { - mState.put(sensor, blocked); + private void onSensorPrivacyChanged(@Sensor int sensor, @ToggleType int toggleType, + boolean enabled) { + if (toggleType == SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE) { + mSoftwareToggleState.put(sensor, enabled); + } else if (toggleType == SensorPrivacyManager.TOGGLE_TYPE_HARDWARE) { + mHardwareToggleState.put(sensor, enabled); + } for (Callback callback : mCallbacks) { - callback.onSensorBlockedChanged(sensor, blocked); + callback.onSensorBlockedChanged(sensor, isSensorBlocked(sensor)); } } }