/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.car.systembar; import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; import static android.hardware.SensorPrivacyManager.Sources.QS_TILE; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.SensorPrivacyManager; import android.os.UserHandle; import android.util.Log; import android.view.View; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.car.CarServiceProvider; import com.android.systemui.car.privacy.MicPrivacyChip; import com.android.systemui.car.privacy.MicQcPanel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.privacy.OngoingPrivacyChip; import com.android.systemui.privacy.PrivacyItem; import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.privacy.PrivacyType; import java.util.List; import java.util.Optional; import javax.inject.Inject; /** * Controls a Privacy Chip view in system icons. */ @SysUISingleton public class PrivacyChipViewController implements MicQcPanel.MicSensorInfoProvider { private static final String TAG = "PrivacyChipViewContrllr"; private static final boolean DEBUG = false; private final PrivacyItemController mPrivacyItemController; private final CarServiceProvider mCarServiceProvider; private final BroadcastDispatcher mBroadcastDispatcher; private final CarDeviceProvisionedController mCarDeviceProvisionedController; private final SensorPrivacyManager mSensorPrivacyManager; private Context mContext; private MicPrivacyChip mPrivacyChip; private Runnable mQsTileNotifyUpdateRunnable; private final SensorPrivacyManager.OnSensorPrivacyChangedListener mOnSensorPrivacyChangedListener = (sensor, sensorPrivacyEnabled) -> { if (mContext == null) { return; } // Since this is launched using a callback thread, its UI based elements need // to execute on main executor. mContext.getMainExecutor().execute(() -> { // We need to negate sensorPrivacyEnabled since when it is {@code true} it means // microphone has been toggled off. mPrivacyChip.setMicrophoneEnabled(/* isMicrophoneEnabled= */ !sensorPrivacyEnabled); mQsTileNotifyUpdateRunnable.run(); }); }; private boolean mAllIndicatorsEnabled; private boolean mMicCameraIndicatorsEnabled; private boolean mIsMicPrivacyChipVisible; private final PrivacyItemController.Callback mPicCallback = new PrivacyItemController.Callback() { @Override public void onPrivacyItemsChanged(@NonNull List privacyItems) { if (mPrivacyChip == null) { return; } // Call QS Tile notify update runnable here so that QS tile can update when app // usage is added/removed/updated mQsTileNotifyUpdateRunnable.run(); boolean shouldShowMicPrivacyChip = isMicPartOfPrivacyItems(privacyItems); if (mIsMicPrivacyChipVisible == shouldShowMicPrivacyChip) { return; } mIsMicPrivacyChipVisible = shouldShowMicPrivacyChip; setChipVisibility(shouldShowMicPrivacyChip); } @Override public void onFlagAllChanged(boolean enabled) { onAllIndicatorsToggled(enabled); } @Override public void onFlagMicCameraChanged(boolean enabled) { onMicCameraToggled(enabled); } private void onMicCameraToggled(boolean enabled) { if (mMicCameraIndicatorsEnabled != enabled) { mMicCameraIndicatorsEnabled = enabled; } } private void onAllIndicatorsToggled(boolean enabled) { if (mAllIndicatorsEnabled != enabled) { mAllIndicatorsEnabled = enabled; } } }; private int mCurrentUserId; private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mPrivacyChip == null) { return; } if (!Intent.ACTION_USER_INFO_CHANGED.equals(intent.getAction())) { return; } int currentUserId = mCarDeviceProvisionedController.getCurrentUser(); if (mCurrentUserId == currentUserId) { return; } setUser(currentUserId); } }; private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (mPrivacyChip == null) { return; } int currentUserId = mCarDeviceProvisionedController.getCurrentUser(); if (mCurrentUserId == currentUserId) { return; } setUser(currentUserId); } }; @Inject public PrivacyChipViewController(Context context, PrivacyItemController privacyItemController, CarServiceProvider carServiceProvider, BroadcastDispatcher broadcastDispatcher, SensorPrivacyManager sensorPrivacyManager, CarDeviceProvisionedController carDeviceProvisionedController) { mContext = context; mPrivacyItemController = privacyItemController; mCarServiceProvider = carServiceProvider; mBroadcastDispatcher = broadcastDispatcher; mSensorPrivacyManager = sensorPrivacyManager; mCarDeviceProvisionedController = carDeviceProvisionedController; mQsTileNotifyUpdateRunnable = () -> { }; mIsMicPrivacyChipVisible = false; mCurrentUserId = carDeviceProvisionedController.getCurrentUser(); } @Override public boolean isMicEnabled() { // We need to negate return of isSensorPrivacyEnabled since when it is {@code true} it // means microphone has been toggled off return !mSensorPrivacyManager.isSensorPrivacyEnabled(MICROPHONE, mCurrentUserId); } @Override public void toggleMic() { mSensorPrivacyManager.setSensorPrivacy(QS_TILE, MICROPHONE, isMicEnabled(), mCurrentUserId); } @Override public void setNotifyUpdateRunnable(Runnable runnable) { mQsTileNotifyUpdateRunnable = runnable; } private boolean isMicPartOfPrivacyItems(@NonNull List privacyItems) { Optional optionalMicPrivacyItem = privacyItems.stream() .filter(privacyItem -> privacyItem.getPrivacyType() .equals(PrivacyType.TYPE_MICROPHONE)) .findAny(); return optionalMicPrivacyItem.isPresent(); } /** * Finds the {@link OngoingPrivacyChip} and sets relevant callbacks. */ public void addPrivacyChipView(View view) { if (mPrivacyChip != null) { return; } mPrivacyChip = view.findViewById(R.id.privacy_chip); if (mPrivacyChip == null) return; mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); mPrivacyItemController.addCallback(mPicCallback); registerForUserChangeEvents(); setUser(mCarDeviceProvisionedController.getCurrentUser()); } /** * Cleans up the controller and removes callbacks. */ public void removeAll() { if (mPrivacyChip != null) { mPrivacyChip.setOnClickListener(null); } mPrivacyItemController.removeCallback(mPicCallback); mBroadcastDispatcher.unregisterReceiver(mUserUpdateReceiver); mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); mSensorPrivacyManager.removeSensorPrivacyListener(MICROPHONE, mOnSensorPrivacyChangedListener); mPrivacyChip = null; } private void setChipVisibility(boolean chipVisible) { if (mPrivacyChip == null) { return; } // Since this is launched using a callback thread, its UI based elements need // to execute on main executor. mContext.getMainExecutor().execute(() -> { if (chipVisible && getChipEnabled()) { mPrivacyChip.animateIn(); } else { mPrivacyChip.animateOut(); } }); } private boolean getChipEnabled() { return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; } private void registerForUserChangeEvents() { // Register for user switching IntentFilter userChangeFilter = new IntentFilter(Intent.ACTION_USER_FOREGROUND); mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, userChangeFilter, /* executor= */ null, UserHandle.ALL); // Also register for user info changing IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_INFO_CHANGED); mBroadcastDispatcher.registerReceiver(mUserUpdateReceiver, filter, /* executor= */ null, UserHandle.ALL); } @AnyThread private void setUser(int userId) { if (DEBUG) Log.d(TAG, "New user ID: " + userId); mCurrentUserId = userId; mSensorPrivacyManager.removeSensorPrivacyListener(MICROPHONE, mOnSensorPrivacyChangedListener); mSensorPrivacyManager.addSensorPrivacyListener(MICROPHONE, userId, mOnSensorPrivacyChangedListener); // Since this can be launched using a callback thread, its UI based elements need // to execute on main executor. mContext.getMainExecutor().execute(() -> { mPrivacyChip.setMicrophoneEnabled(isMicEnabled()); }); } }