CompatChanges for TileService#startActivityAndCollapse

For versions U+, the old startActivityAndCollapse(Intent) should not be
allowed to be called, and the binding flag
Context#BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS should not be bound.

Bug: 241766793
Test: atest TileLifecycleManagerTest
Change-Id: I95a823902b15b2a8c8309bf3cf1ff4ef8d4a26b5
diff --git a/core/api/current.txt b/core/api/current.txt
index 5dd1b39..42a979b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -40383,7 +40383,7 @@
     method public void onTileRemoved();
     method public static final void requestListeningState(android.content.Context, android.content.ComponentName);
     method public final void showDialog(android.app.Dialog);
-    method public final void startActivityAndCollapse(android.content.Intent);
+    method @Deprecated public final void startActivityAndCollapse(android.content.Intent);
     method public final void startActivityAndCollapse(@NonNull android.app.PendingIntent);
     method public final void unlockAndRun(Runnable);
     field public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE";
diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java
index 7b6ff97..d957029 100644
--- a/core/java/android/service/quicksettings/TileService.java
+++ b/core/java/android/service/quicksettings/TileService.java
@@ -24,6 +24,9 @@
 import android.app.PendingIntent;
 import android.app.Service;
 import android.app.StatusBarManager;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -166,6 +169,17 @@
      */
     public static final String EXTRA_STATE = "state";
 
+    /**
+     * The method {@link TileService#startActivityAndCollapse(Intent)} will verify that only
+     * apps targeting {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or higher will
+     * not be allowed to use it.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public static final long START_ACTIVITY_NEEDS_PENDING_INTENT = 241766793L;
+
     private final H mHandler = new H(Looper.getMainLooper());
 
     private boolean mListening = false;
@@ -251,7 +265,6 @@
      * This will collapse the Quick Settings panel and show the dialog.
      *
      * @param dialog Dialog to show.
-     *
      * @see #isLocked()
      */
     public final void showDialog(Dialog dialog) {
@@ -330,8 +343,19 @@
 
     /**
      * Start an activity while collapsing the panel.
+     *
+     * @deprecated for versions {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and up,
+     * use {@link TileService#startActivityAndCollapse(PendingIntent)} instead.
+     * @throws UnsupportedOperationException if called in versions
+     * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and up
      */
+    @Deprecated
     public final void startActivityAndCollapse(Intent intent) {
+        if (CompatChanges.isChangeEnabled(START_ACTIVITY_NEEDS_PENDING_INTENT)) {
+            throw new UnsupportedOperationException(
+                    "startActivityAndCollapse: Starting activity from TileService using an Intent"
+                            + " is not allowed.");
+        }
         startActivity(intent);
         try {
             mService.onStartActivity(mTileToken);
@@ -410,7 +434,7 @@
             }
 
             @Override
-            public void onUnlockComplete() throws RemoteException{
+            public void onUnlockComplete() throws RemoteException {
                 mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE);
             }
         };
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index d393680..385e720 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -15,6 +15,9 @@
  */
 package com.android.systemui.qs.external;
 
+import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT;
+
+import android.app.compat.CompatChanges;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -41,15 +44,15 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
 
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
+
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import dagger.assisted.Assisted;
-import dagger.assisted.AssistedFactory;
-import dagger.assisted.AssistedInject;
-
 /**
  * Manages the lifecycle of a TileService.
  * <p>
@@ -124,7 +127,9 @@
     /** Injectable factory for TileLifecycleManager. */
     @AssistedFactory
     public interface Factory {
-        /** */
+        /**
+         *
+         */
         TileLifecycleManager create(Intent intent, UserHandle userHandle);
     }
 
@@ -161,7 +166,7 @@
      * Determines whether the associated TileService is a Boolean Tile.
      *
      * @return true if {@link TileService#META_DATA_TOGGLEABLE_TILE} is set to {@code true} for this
-     *         tile
+     * tile
      * @see TileService#META_DATA_TOGGLEABLE_TILE
      */
     public boolean isToggleableTile() {
@@ -207,12 +212,7 @@
             if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
             mBindTryCount++;
             try {
-                mIsBound = mContext.bindServiceAsUser(mIntent, this,
-                        Context.BIND_AUTO_CREATE
-                                | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
-                                | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
-                                | Context.BIND_WAIVE_PRIORITY,
-                        mUser);
+                mIsBound = bindServices();
                 if (!mIsBound) {
                     mContext.unbindService(this);
                 }
@@ -237,6 +237,24 @@
         }
     }
 
+    private boolean bindServices() {
+        String packageName = mIntent.getComponent().getPackageName();
+        if (CompatChanges.isChangeEnabled(START_ACTIVITY_NEEDS_PENDING_INTENT, packageName,
+                mUser)) {
+            return mContext.bindServiceAsUser(mIntent, this,
+                    Context.BIND_AUTO_CREATE
+                            | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
+                            | Context.BIND_WAIVE_PRIORITY,
+                    mUser);
+        }
+        return mContext.bindServiceAsUser(mIntent, this,
+                Context.BIND_AUTO_CREATE
+                        | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
+                        | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
+                        | Context.BIND_WAIVE_PRIORITY,
+                mUser);
+    }
+
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
         if (DEBUG) Log.d(TAG, "onServiceConnected " + name);
@@ -418,8 +436,11 @@
             mPackageManagerAdapter.getPackageInfoAsUser(packageName, 0, mUser.getIdentifier());
             return true;
         } catch (PackageManager.NameNotFoundException e) {
-            if (DEBUG) Log.d(TAG, "Package not available: " + packageName, e);
-            else Log.d(TAG, "Package not available: " + packageName);
+            if (DEBUG) {
+                Log.d(TAG, "Package not available: " + packageName, e);
+            } else {
+                Log.d(TAG, "Package not available: " + packageName);
+            }
         }
         return false;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index 04b50d8..2e6b0cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -15,11 +15,17 @@
  */
 package com.android.systemui.qs.external;
 
+import static android.service.quicksettings.TileService.START_ACTIVITY_NEEDS_PENDING_INTENT;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.anyString;
@@ -29,6 +35,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.compat.CompatChanges;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -59,6 +66,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.MockitoSession;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -77,11 +85,16 @@
     private Handler mHandler;
     private TileLifecycleManager mStateManager;
     private TestContextWrapper mWrappedContext;
+    private MockitoSession mMockitoSession;
 
     @Before
     public void setUp() throws Exception {
         setPackageEnabled(true);
         mTileServiceComponentName = new ComponentName(mContext, "FakeTileService.class");
+        mMockitoSession = mockitoSession()
+                .initMocks(this)
+                .mockStatic(CompatChanges.class)
+                .startMocking();
 
         // Stub.asInterface will just return itself.
         when(mMockTileService.queryLocalInterface(anyString())).thenReturn(mMockTileService);
@@ -106,7 +119,13 @@
 
     @After
     public void tearDown() throws Exception {
-        mThread.quit();
+        if (mMockitoSession != null) {
+            mMockitoSession.finishMocking();
+        }
+        if (mThread != null) {
+            mThread.quit();
+        }
+
         mStateManager.handleDestroy();
     }
 
@@ -290,6 +309,50 @@
         verify(falseContext).unbindService(captor.getValue());
     }
 
+    @Test
+    public void testVersionUDoesNotBindsAllowBackgroundActivity() {
+        mockChangeEnabled(START_ACTIVITY_NEEDS_PENDING_INTENT, true);
+        Context falseContext = mock(Context.class);
+        TileLifecycleManager manager = new TileLifecycleManager(mHandler, falseContext,
+                mock(IQSService.class),
+                mMockPackageManagerAdapter,
+                mMockBroadcastDispatcher,
+                mTileServiceIntent,
+                mUser);
+
+        manager.setBindService(true);
+        int flags = Context.BIND_AUTO_CREATE
+                | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
+                | Context.BIND_WAIVE_PRIORITY;
+
+        verify(falseContext).bindServiceAsUser(any(), any(), eq(flags), any());
+    }
+
+    @Test
+    public void testVersionLessThanUBindsAllowBackgroundActivity() {
+        mockChangeEnabled(START_ACTIVITY_NEEDS_PENDING_INTENT, false);
+        Context falseContext = mock(Context.class);
+        TileLifecycleManager manager = new TileLifecycleManager(mHandler, falseContext,
+                mock(IQSService.class),
+                mMockPackageManagerAdapter,
+                mMockBroadcastDispatcher,
+                mTileServiceIntent,
+                mUser);
+
+        manager.setBindService(true);
+        int flags = Context.BIND_AUTO_CREATE
+                | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
+                | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
+                | Context.BIND_WAIVE_PRIORITY;
+
+        verify(falseContext).bindServiceAsUser(any(), any(), eq(flags), any());
+    }
+
+    private void mockChangeEnabled(long changeId, boolean enabled) {
+        doReturn(enabled).when(() -> CompatChanges.isChangeEnabled(eq(changeId), anyString(),
+                any(UserHandle.class)));
+    }
+
     private static class TestContextWrapper extends ContextWrapper {
         private IntentFilter mLastIntentFilter;
         private int mLastFlag;