diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index b172e92871ce1..535b54887e12e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -490,5 +490,4 @@ public class FrameworkServicesModule { static SafetyCenterManager provideSafetyCenterManager(Context context) { return context.getSystemService(SafetyCenterManager.class); } - } diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index 5d4b3da58cf06..628964af3380a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.policy.CastController; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.SecureSettings; @@ -66,6 +67,7 @@ public interface QSModule { ReduceBrightColorsController reduceBrightColorsController, DeviceControlsController deviceControlsController, WalletController walletController, + SafetyController safetyController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) { AutoTileManager manager = new AutoTileManager( context, @@ -81,6 +83,7 @@ public interface QSModule { reduceBrightColorsController, deviceControlsController, walletController, + safetyController, isReduceBrightColorsAvailable ); manager.init(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index ccb37aeb18a9b..61780cd53bb24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.policy.DataSaverController.Listener; import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; import com.android.systemui.statusbar.policy.HotspotController.Callback; +import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.UserAwareController; import com.android.systemui.util.settings.SecureSettings; @@ -83,6 +84,7 @@ public class AutoTileManager implements UserAwareController { private final DeviceControlsController mDeviceControlsController; private final WalletController mWalletController; private final ReduceBrightColorsController mReduceBrightColorsController; + private final SafetyController mSafetyController; private final boolean mIsReduceBrightColorsAvailable; private final ArrayList mAutoAddSettingList = new ArrayList<>(); @@ -98,6 +100,7 @@ public class AutoTileManager implements UserAwareController { ReduceBrightColorsController reduceBrightColorsController, DeviceControlsController deviceControlsController, WalletController walletController, + SafetyController safetyController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) { mContext = context; mHost = host; @@ -114,6 +117,7 @@ public class AutoTileManager implements UserAwareController { mIsReduceBrightColorsAvailable = isReduceBrightColorsAvailable; mDeviceControlsController = deviceControlsController; mWalletController = walletController; + mSafetyController = safetyController; String safetySpecRes; try { safetySpecRes = context.getResources().getString(R.string.safety_quick_settings_tile); @@ -163,8 +167,11 @@ public class AutoTileManager implements UserAwareController { if (!mAutoTracker.isAdded(WALLET)) { initWalletController(); } - if (mSafetySpec != null && !mAutoTracker.isAdded(mSafetySpec)) { - initSafetyTile(); + if (mSafetySpec != null) { + if (!mAutoTracker.isAdded(mSafetySpec)) { + initSafetyTile(); + } + mSafetyController.addCallback(mSafetyCallback); } int settingsN = mAutoAddSettingList.size(); @@ -187,6 +194,9 @@ public class AutoTileManager implements UserAwareController { } mCastController.removeCallback(mCastCallback); mDeviceControlsController.removeCallback(); + if (mSafetySpec != null) { + mSafetyController.removeCallback(mSafetyCallback); + } int settingsN = mAutoAddSettingList.size(); for (int i = 0; i < settingsN; i++) { mAutoAddSettingList.get(i).setListening(false); @@ -327,10 +337,9 @@ public class AutoTileManager implements UserAwareController { } private void initSafetyTile() { - if (mSafetySpec == null) { + if (mSafetySpec == null || mAutoTracker.isAdded(mSafetySpec)) { return; } - if (mAutoTracker.isAdded(mSafetySpec)) return; mHost.addTile(CustomTile.getComponentFromSpec(mSafetySpec), true); mAutoTracker.setTileAdded(mSafetySpec); } @@ -402,6 +411,23 @@ public class AutoTileManager implements UserAwareController { } }; + @VisibleForTesting + final SafetyController.Listener mSafetyCallback = new SafetyController.Listener() { + @Override + public void onSafetyCenterEnableChanged(boolean isSafetyCenterEnabled) { + if (mSafetySpec == null) { + return; + } + + if (isSafetyCenterEnabled && !mAutoTracker.isAdded(mSafetySpec)) { + initSafetyTile(); + } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) { + mHost.removeTile(CustomTile.getComponentFromSpec(mSafetySpec)); + mHost.unmarkTileAsAutoAdded(mSafetySpec); + } + } + }; + @VisibleForTesting protected SettingObserver getSecureSettingForKey(String key) { for (SettingObserver s : mAutoAddSettingList) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java new file mode 100644 index 0000000000000..f3d183ceb45f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SafetyController.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 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.statusbar.policy; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.safetycenter.SafetyCenterManager; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.qualifiers.Background; + +import java.util.ArrayList; +import java.util.Objects; + +import javax.inject.Inject; + +/** + * Controller which calls listeners when a PACKAGE_CHANGED broadcast is sent for the + * PermissionController. These broadcasts may be because the PermissionController enabled or + * disabled its TileService, and the tile should be added if the component was enabled, or removed + * if disabled. + */ +public class SafetyController implements + CallbackController { + private boolean mSafetyCenterEnabled; + private final Handler mBgHandler; + private final ArrayList mListeners = new ArrayList<>(); + private static final IntentFilter PKG_CHANGE_INTENT_FILTER; + private final Context mContext; + private final SafetyCenterManager mSafetyCenterManager; + private final PackageManager mPackageManager; + + static { + PKG_CHANGE_INTENT_FILTER = new IntentFilter(Intent.ACTION_PACKAGE_CHANGED); + PKG_CHANGE_INTENT_FILTER.addDataScheme("package"); + } + + @Inject + public SafetyController(Context context, PackageManager pm, SafetyCenterManager scm, + @Background Handler bgHandler) { + mContext = context; + mSafetyCenterManager = scm; + mPackageManager = pm; + mBgHandler = bgHandler; + mSafetyCenterEnabled = mSafetyCenterManager.isSafetyCenterEnabled(); + } + + public boolean isSafetyCenterEnabled() { + return mSafetyCenterEnabled; + } + + /** + * Adds a listener, registering the broadcast receiver if need be. Immediately calls the + * provided listener on the calling thread. + */ + @Override + public void addCallback(@NonNull Listener listener) { + synchronized (mListeners) { + mListeners.add(listener); + if (mListeners.size() == 1) { + mContext.registerReceiver(mPermControllerChangeReceiver, PKG_CHANGE_INTENT_FILTER); + mBgHandler.post(() -> { + mSafetyCenterEnabled = mSafetyCenterManager.isSafetyCenterEnabled(); + listener.onSafetyCenterEnableChanged(isSafetyCenterEnabled()); + }); + } else { + listener.onSafetyCenterEnableChanged(isSafetyCenterEnabled()); + } + } + } + + @Override + public void removeCallback(@NonNull Listener listener) { + synchronized (mListeners) { + mListeners.remove(listener); + if (mListeners.isEmpty()) { + mContext.unregisterReceiver(mPermControllerChangeReceiver); + } + } + } + + private void handleSafetyCenterEnableChange() { + synchronized (mListeners) { + for (int i = 0; i < mListeners.size(); i++) { + mListeners.get(i).onSafetyCenterEnableChanged(mSafetyCenterEnabled); + } + } + } + + /** + * Upon receiving a package changed broadcast for the PermissionController, checks if the + * safety center is enabled or disabled, and sends an update on the main thread if the state + * changed. + */ + @VisibleForTesting + final BroadcastReceiver mPermControllerChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String packageName = intent.getData() != null ? intent.getData().getSchemeSpecificPart() + : null; + if (!Objects.equals(packageName, + mPackageManager.getPermissionControllerPackageName())) { + return; + } + + boolean wasSafetyCenterEnabled = mSafetyCenterEnabled; + mSafetyCenterEnabled = mSafetyCenterManager.isSafetyCenterEnabled(); + if (wasSafetyCenterEnabled == mSafetyCenterEnabled) { + return; + } + + mBgHandler.post(() -> handleSafetyCenterEnableChange()); + } + }; + + /** + * Listener for safety center enabled changes + */ + public interface Listener { + /** + * Callback to be notified when the safety center is enabled or disabled + * @param isSafetyCenterEnabled If the safety center is enabled + */ + void onSafetyCenterEnableChanged(boolean isSafetyCenterEnabled); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java index 0e12c2adc09a9..03a6c19dc86ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java @@ -58,6 +58,7 @@ import com.android.systemui.statusbar.policy.CastController.CastDevice; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DeviceControlsController; import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; @@ -101,6 +102,7 @@ public class AutoTileManagerTest extends SysuiTestCase { @Mock private ReduceBrightColorsController mReduceBrightColorsController; @Mock private DeviceControlsController mDeviceControlsController; @Mock private WalletController mWalletController; + @Mock private SafetyController mSafetyController; @Mock(answer = Answers.RETURNS_SELF) private AutoAddTracker.Builder mAutoAddTrackerBuilder; @Mock private Context mUserContext; @@ -150,6 +152,7 @@ public class AutoTileManagerTest extends SysuiTestCase { ReduceBrightColorsController reduceBrightColorsController, DeviceControlsController deviceControlsController, WalletController walletController, + SafetyController safetyController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) { return new AutoTileManager(context, autoAddTrackerBuilder, mQsTileHost, Handler.createAsync(TestableLooper.get(this).getLooper()), @@ -162,6 +165,7 @@ public class AutoTileManagerTest extends SysuiTestCase { reduceBrightColorsController, deviceControlsController, walletController, + safetyController, isReduceBrightColorsAvailable); } @@ -169,7 +173,7 @@ public class AutoTileManagerTest extends SysuiTestCase { return createAutoTileManager(context, mAutoAddTrackerBuilder, mHotspotController, mDataSaverController, mManagedProfileController, mNightDisplayListener, mCastController, mReduceBrightColorsController, mDeviceControlsController, - mWalletController, mIsReduceBrightColorsAvailable); + mWalletController, mSafetyController, mIsReduceBrightColorsAvailable); } @Test @@ -185,10 +189,11 @@ public class AutoTileManagerTest extends SysuiTestCase { ReduceBrightColorsController rBC = mock(ReduceBrightColorsController.class); DeviceControlsController dCC = mock(DeviceControlsController.class); WalletController wC = mock(WalletController.class); + SafetyController sC = mock(SafetyController.class); AutoTileManager manager = createAutoTileManager(mock(Context.class), builder, hC, dSC, mPC, nDS, cC, rBC, - dCC, wC, true); + dCC, wC, sC, true); verify(tracker, never()).initialize(); verify(hC, never()).addCallback(any()); @@ -199,6 +204,7 @@ public class AutoTileManagerTest extends SysuiTestCase { verify(rBC, never()).addCallback(any()); verify(dCC, never()).setCallback(any()); verify(wC, never()).getWalletPosition(); + verify(sC, never()).addCallback(any()); assertNull(manager.getSecureSettingForKey(TEST_SETTING)); assertNull(manager.getSecureSettingForKey(TEST_SETTING_COMPONENT)); } @@ -253,6 +259,10 @@ public class AutoTileManagerTest extends SysuiTestCase { verify(mWalletController, times(2)).getWalletPosition(); + InOrder inOrderSafety = inOrder(mSafetyController); + inOrderSafety.verify(mSafetyController).removeCallback(any()); + inOrderSafety.verify(mSafetyController).addCallback(any()); + SettingObserver setting = mAutoTileManager.getSecureSettingForKey(TEST_SETTING); assertEquals(USER + 1, setting.getCurrentUser()); assertTrue(setting.isListening()); @@ -303,6 +313,10 @@ public class AutoTileManagerTest extends SysuiTestCase { verify(mWalletController, times(2)).getWalletPosition(); + InOrder inOrderSafety = inOrder(mSafetyController); + inOrderSafety.verify(mSafetyController).removeCallback(any()); + inOrderSafety.verify(mSafetyController).addCallback(any()); + SettingObserver setting = mAutoTileManager.getSecureSettingForKey(TEST_SETTING); assertEquals(USER + 1, setting.getCurrentUser()); assertFalse(setting.isListening()); @@ -457,6 +471,26 @@ public class AutoTileManagerTest extends SysuiTestCase { mAutoTileManager.changeUser(UserHandle.of(USER + 1)); verify(mQsTileHost, times(2)).addTile(safetyComponent, true); } + + @Test + public void testSafetyTileRemoved_onSafetyCenterDisable() { + ComponentName safetyComponent = CustomTile.getComponentFromSpec(TEST_CUSTOM_SAFETY_SPEC); + mAutoTileManager.init(); + when(mAutoAddTracker.isAdded(TEST_CUSTOM_SAFETY_SPEC)).thenReturn(true); + mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(false); + verify(mQsTileHost, times(1)).removeTile(safetyComponent); + } + + @Test + public void testSafetyTileAdded_onSafetyCenterEnable() { + ComponentName safetyComponent = CustomTile.getComponentFromSpec(TEST_CUSTOM_SAFETY_SPEC); + mAutoTileManager.init(); + verify(mQsTileHost, times(1)).addTile(safetyComponent, true); + mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(false); + mAutoTileManager.mSafetyCallback.onSafetyCenterEnableChanged(true); + verify(mQsTileHost, times(2)).addTile(safetyComponent, true); + } + @Test public void testEmptyArray_doesNotCrash() { mContext.getOrCreateTestableResources().addOverride( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt new file mode 100644 index 0000000000000..89989ce81dd67 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2022 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.statusbar.policy + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Handler +import android.safetycenter.SafetyCenterManager +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.ArgumentMatchers.same +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SafetyControllerTest : SysuiTestCase() { + + private val TEST_PC_PKG = "testPermissionControllerPackageName" + private val OTHER_PKG = "otherPackageName" + + @Mock + private lateinit var context: Context + @Mock + private lateinit var scm: SafetyCenterManager + @Mock + private lateinit var pm: PackageManager + @Mock + private lateinit var handler: Handler + @Mock + private lateinit var listener: SafetyController.Listener + + private val packageDataScheme = "package" + + private lateinit var controller: SafetyController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(pm.permissionControllerPackageName).thenReturn(TEST_PC_PKG) + `when`(scm.isSafetyCenterEnabled).thenReturn(false) + `when`(handler.post(any(Runnable::class.java))).thenAnswer { + (it.arguments[0] as Runnable).run() + true + } + + controller = SafetyController(context, pm, scm, handler) + } + + @Test + fun addingFirstListenerRegistersReceiver() { + `when`(scm.isSafetyCenterEnabled).thenReturn(true) + controller.addCallback(listener) + verify(listener, times(1)).onSafetyCenterEnableChanged(true) + val filter = ArgumentCaptor.forClass(IntentFilter::class.java) + verify(context, times(1)).registerReceiver( + same(controller.mPermControllerChangeReceiver), filter.capture()) + assertEquals(Intent.ACTION_PACKAGE_CHANGED, filter.value.getAction(0)) + assertEquals(packageDataScheme, filter.value.getDataScheme(0)) + } + + @Test + fun removingLastListenerDeregistersReceiver() { + controller.addCallback(listener) + controller.removeCallback(listener) + verify(context, times(1)).unregisterReceiver( + eq(controller.mPermControllerChangeReceiver)) + } + + @Test + fun listenersCalledWhenBroadcastReceivedWithPCPackageAndStateChange() { + `when`(scm.isSafetyCenterEnabled).thenReturn(false) + controller.addCallback(listener) + reset(listener) + `when`(scm.isSafetyCenterEnabled).thenReturn(true) + val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED) + testIntent.data = Uri.parse("package:$TEST_PC_PKG") + controller.mPermControllerChangeReceiver.onReceive(context, testIntent) + verify(listener, times(1)).onSafetyCenterEnableChanged(true) + } + + @Test + fun listenersNotCalledWhenBroadcastReceivedWithOtherPackage() { + `when`(scm.isSafetyCenterEnabled).thenReturn(true) + controller.addCallback(listener) + reset(listener) + val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED) + testIntent.data = Uri.parse("package:$OTHER_PKG") + controller.mPermControllerChangeReceiver.onReceive(context, testIntent) + verify(listener, never()).onSafetyCenterEnableChanged(true) + } + + @Test + fun listenersNotCalledWhenBroadcastReceivedWithNoStateChange() { + `when`(scm.isSafetyCenterEnabled).thenReturn(false) + controller.addCallback(listener) + reset(listener) + val testIntent = Intent(Intent.ACTION_PACKAGE_CHANGED) + testIntent.data = Uri.parse("package:$TEST_PC_PKG") + controller.mPermControllerChangeReceiver.onReceive(context, testIntent) + verify(listener, never()).onSafetyCenterEnableChanged(true) + } +} \ No newline at end of file