Make AutoTileManager lisen for component enable/disable

This will allow the Safety Tile to be shown/hidden.

Test: atest AutoTileManagerTest,SafetyControllerTest
Bug: 222127376
Change-Id: Iad666a9aafe74092871ca18fc8b1973a68e1312b
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index b172e92..535b548 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 @@
     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 5d4b3da..628964a 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.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 @@
             ReduceBrightColorsController reduceBrightColorsController,
             DeviceControlsController deviceControlsController,
             WalletController walletController,
+            SafetyController safetyController,
             @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) {
         AutoTileManager manager = new AutoTileManager(
                 context,
@@ -81,6 +83,7 @@
                 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 ccb37ae..61780cd 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.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 @@
     private final DeviceControlsController mDeviceControlsController;
     private final WalletController mWalletController;
     private final ReduceBrightColorsController mReduceBrightColorsController;
+    private final SafetyController mSafetyController;
     private final boolean mIsReduceBrightColorsAvailable;
     private final ArrayList<AutoAddSetting> mAutoAddSettingList = new ArrayList<>();
 
@@ -98,6 +100,7 @@
             ReduceBrightColorsController reduceBrightColorsController,
             DeviceControlsController deviceControlsController,
             WalletController walletController,
+            SafetyController safetyController,
             @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) {
         mContext = context;
         mHost = host;
@@ -114,6 +117,7 @@
         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 @@
         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 @@
         }
         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 @@
     }
 
     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);
     }
@@ -403,6 +412,23 @@
     };
 
     @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) {
             if (Objects.equals(key, s.getKey())) {
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 0000000..f3d183c
--- /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<SafetyController.Listener> {
+    private boolean mSafetyCenterEnabled;
+    private final Handler mBgHandler;
+    private final ArrayList<Listener> 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 0e12c2a..03a6c19 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.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 @@
     @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 @@
             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 @@
                 reduceBrightColorsController,
                 deviceControlsController,
                 walletController,
+                safetyController,
                 isReduceBrightColorsAvailable);
     }
 
@@ -169,7 +173,7 @@
         return createAutoTileManager(context, mAutoAddTrackerBuilder, mHotspotController,
                 mDataSaverController, mManagedProfileController, mNightDisplayListener,
                 mCastController, mReduceBrightColorsController, mDeviceControlsController,
-                mWalletController, mIsReduceBrightColorsAvailable);
+                mWalletController, mSafetyController, mIsReduceBrightColorsAvailable);
     }
 
     @Test
@@ -185,10 +189,11 @@
         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 @@
         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 @@
 
         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 @@
 
         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 @@
         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 0000000..89989ce
--- /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