diff options
| author | 2022-12-18 17:50:16 +0000 | |
|---|---|---|
| committer | 2022-12-18 17:50:16 +0000 | |
| commit | b9b844866be534a828f7157b1fa194320604e591 (patch) | |
| tree | f02d482235e14dabdb27838b0984c7b886f5878a | |
| parent | 258944c4cce0a0bfa4a60d9360d30b1cebf77321 (diff) | |
| parent | e00edd6df653f56253dd7d87aa85107c796b2e88 (diff) | |
Merge "Add Start Activity API for TileServices" into tm-qpr-dev
7 files changed, 199 insertions, 7 deletions
diff --git a/core/java/android/service/quicksettings/IQSService.aidl b/core/java/android/service/quicksettings/IQSService.aidl index d03ff93be5ce..7b690eaad96c 100644 --- a/core/java/android/service/quicksettings/IQSService.aidl +++ b/core/java/android/service/quicksettings/IQSService.aidl @@ -15,6 +15,7 @@ */ package android.service.quicksettings; +import android.app.PendingIntent; import android.content.ComponentName; import android.graphics.drawable.Icon; import android.service.quicksettings.Tile; @@ -29,10 +30,10 @@ interface IQSService { String contentDescription); void onShowDialog(in IBinder tile); void onStartActivity(in IBinder tile); + void startActivity(in IBinder tile, in PendingIntent pendingIntent); boolean isLocked(); boolean isSecure(); void startUnlockAndRun(in IBinder tile); - void onDialogHidden(in IBinder tile); void onStartSuccessful(in IBinder tile); } diff --git a/core/java/android/service/quicksettings/Tile.java b/core/java/android/service/quicksettings/Tile.java index 40c0ac00a5a9..d60b225e695a 100644 --- a/core/java/android/service/quicksettings/Tile.java +++ b/core/java/android/service/quicksettings/Tile.java @@ -16,6 +16,7 @@ package android.service.quicksettings; import android.annotation.Nullable; +import android.app.PendingIntent; import android.graphics.drawable.Icon; import android.os.IBinder; import android.os.Parcel; @@ -66,6 +67,7 @@ public final class Tile implements Parcelable { private CharSequence mSubtitle; private CharSequence mContentDescription; private CharSequence mStateDescription; + private PendingIntent mPendingIntent; // Default to inactive until clients of the new API can update. private int mState = STATE_INACTIVE; @@ -223,6 +225,34 @@ public final class Tile implements Parcelable { } } + /** + * Gets the Activity {@link PendingIntent} to be launched when the tile is clicked. + * @hide + */ + @Nullable + public PendingIntent getActivityLaunchForClick() { + return mPendingIntent; + } + + /** + * Sets an Activity {@link PendingIntent} to be launched when the tile is clicked. + * + * The last value set here will be launched when the user clicks in the tile, instead of + * forwarding the `onClick` message to the {@link TileService}. Set to {@code null} to handle + * the `onClick` in the `TileService` + * (This is the default behavior if this method is never called.) + * @param pendingIntent a PendingIntent for an activity to be launched onclick, or {@code null} + * to handle the clicks in the `TileService`. + * @hide + */ + public void setActivityLaunchForClick(@Nullable PendingIntent pendingIntent) { + if (pendingIntent != null && !pendingIntent.isActivity()) { + throw new IllegalArgumentException(); + } else { + mPendingIntent = pendingIntent; + } + } + @Override public void writeToParcel(Parcel dest, int flags) { if (mIcon != null) { @@ -231,6 +261,12 @@ public final class Tile implements Parcelable { } else { dest.writeByte((byte) 0); } + if (mPendingIntent != null) { + dest.writeByte((byte) 1); + mPendingIntent.writeToParcel(dest, flags); + } else { + dest.writeByte((byte) 0); + } dest.writeInt(mState); TextUtils.writeToParcel(mLabel, dest, flags); TextUtils.writeToParcel(mSubtitle, dest, flags); @@ -244,6 +280,11 @@ public final class Tile implements Parcelable { } else { mIcon = null; } + if (source.readByte() != 0) { + mPendingIntent = PendingIntent.CREATOR.createFromParcel(source); + } else { + mPendingIntent = null; + } mState = source.readInt(); mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java index 85502197ea7e..506b3b81eb9a 100644 --- a/core/java/android/service/quicksettings/TileService.java +++ b/core/java/android/service/quicksettings/TileService.java @@ -20,6 +20,7 @@ import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.Dialog; +import android.app.PendingIntent; import android.app.Service; import android.app.StatusBarManager; import android.content.ComponentName; @@ -336,6 +337,20 @@ public class TileService extends Service { } /** + * Starts an {@link android.app.Activity}. + * Will collapse Quick Settings after launching. + * + * @param pendingIntent A PendingIntent for an Activity to be launched immediately. + * @hide + */ + public void startActivityAndCollapse(PendingIntent pendingIntent) { + try { + mService.startActivity(mTileToken, pendingIntent); + } catch (RemoteException e) { + } + } + + /** * Gets the {@link Tile} for this service. * <p/> * This tile may be used to get or set the current state for this diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index c4386ab9a3df..cfda9fd6cb96 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -18,6 +18,7 @@ package com.android.systemui.qs.external; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -51,6 +52,7 @@ import androidx.annotation.WorkerThread; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; @@ -92,6 +94,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener private android.graphics.drawable.Icon mDefaultIcon; @Nullable private CharSequence mDefaultLabel; + @Nullable + private View mViewClicked; private final Context mUserContext; @@ -202,7 +206,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener * Compare two icons, only works for resources. */ private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1, - @Nullable android.graphics.drawable.Icon icon2) { + @Nullable android.graphics.drawable.Icon icon2) { if (icon1 == icon2) { return true; } @@ -229,7 +233,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener /** * Custom tile is considered available if there is a default icon (obtained from PM). - * + * <p> * It will return {@code true} before initialization, so tiles are not destroyed prematurely. */ @Override @@ -262,6 +266,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener /** * Update state of {@link this#mTile} from a remote {@link TileService}. + * * @param tile tile populated with state to apply */ public void updateTileState(Tile tile) { @@ -293,6 +298,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener if (tile.getStateDescription() != null || overwriteNulls) { mTile.setStateDescription(tile.getStateDescription()); } + mTile.setActivityLaunchForClick(tile.getActivityLaunchForClick()); mTile.setState(tile.getState()); } @@ -324,6 +330,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener mService.onStartListening(); } } else { + mViewClicked = null; mService.onStopListening(); if (mIsTokenGranted && !mIsShowingDialog) { try { @@ -388,6 +395,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener if (mTile.getState() == Tile.STATE_UNAVAILABLE) { return; } + mViewClicked = view; try { if (DEBUG) Log.d(TAG, "Adding token"); mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY, @@ -400,7 +408,12 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener mServiceManager.setBindRequested(true); mService.onStartListening(); } - mService.onClick(mToken); + + if (mTile.getActivityLaunchForClick() != null) { + startActivityAndCollapse(mTile.getActivityLaunchForClick()); + } else { + mService.onClick(mToken); + } } catch (RemoteException e) { // Called through wrapper, won't happen here. } @@ -483,6 +496,27 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener }); } + /** + * Starts an {@link android.app.Activity} + * @param pendingIntent A PendingIntent for an Activity to be launched immediately. + */ + public void startActivityAndCollapse(PendingIntent pendingIntent) { + if (!pendingIntent.isActivity()) { + Log.i(TAG, "Intent not for activity."); + } else if (!mIsTokenGranted) { + Log.i(TAG, "Launching activity before click"); + } else { + Log.i(TAG, "The activity is starting"); + ActivityLaunchAnimator.Controller controller = mViewClicked == null + ? null + : ActivityLaunchAnimator.Controller.fromView(mViewClicked, 0); + mUiHandler.post(() -> + mActivityStarter.startPendingIntentDismissingKeyguard( + pendingIntent, null, controller) + ); + } + } + public static String toSpec(ComponentName name) { return PREFIX + name.flattenToShortString() + ")"; } @@ -509,8 +543,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener /** * Create a {@link CustomTile} for a given spec and user. * - * @param builder including injected common dependencies. - * @param spec as provided by {@link CustomTile#toSpec} + * @param builder including injected common dependencies. + * @param spec as provided by {@link CustomTile#toSpec} * @param userContext context for the user that is creating this tile. * @return a new {@link CustomTile} */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index 5d03da3cc113..3d48fd109e39 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -15,6 +15,7 @@ */ package com.android.systemui.qs.external; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageInfo; @@ -32,6 +33,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -276,6 +278,19 @@ public class TileServices extends IQSService.Stub { } @Override + public void startActivity(IBinder token, PendingIntent pendingIntent) { + startActivity(getTileForToken(token), pendingIntent); + } + + @VisibleForTesting + protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) { + if (customTile != null) { + verifyCaller(customTile); + customTile.startActivityAndCollapse(pendingIntent); + } + } + + @Override public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) { CustomTile customTile = getTileForToken(token); if (customTile != null) { @@ -336,7 +351,7 @@ public class TileServices extends IQSService.Stub { } @Nullable - private CustomTile getTileForToken(IBinder token) { + public CustomTile getTileForToken(IBinder token) { synchronized (mServices) { return mTokenMap.get(token); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt index f3fcdbf329b9..2bd068a674ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/CustomTileTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.external +import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.pm.ApplicationInfo @@ -30,8 +31,10 @@ import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.IWindowManager +import android.view.View import com.android.internal.logging.MetricsLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.qs.QSTile @@ -39,8 +42,11 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.logging.QSLogger import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -236,6 +242,10 @@ class CustomTileTest : SysuiTestCase() { `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) .thenReturn(mock(Drawable::class.java)) + val pi = mock(PendingIntent::class.java) + `when`(pi.isActivity).thenReturn(true) + tile.qsTile.activityLaunchForClick = pi + tile.refreshState() testableLooper.processAllMessages() @@ -289,4 +299,52 @@ class CustomTileTest : SysuiTestCase() { assertFalse(tile.isAvailable) verify(tileHost).removeTile(tile.tileSpec) } + + @Test + fun testInvalidPendingIntentDoesNotStartActivity() { + val pi = mock(PendingIntent::class.java) + `when`(pi.isActivity).thenReturn(false) + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + + assertThrows(IllegalArgumentException::class.java) { + tile.qsTile.activityLaunchForClick = pi + } + + tile.handleClick(mock(View::class.java)) + testableLooper.processAllMessages() + + verify(activityStarter, never()) + .startPendingIntentDismissingKeyguard( + any(), any(), any(ActivityLaunchAnimator.Controller::class.java)) + } + + @Test + fun testValidPendingIntentWithNoClickDoesNotStartActivity() { + val pi = mock(PendingIntent::class.java) + `when`(pi.isActivity).thenReturn(true) + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.qsTile.activityLaunchForClick = pi + + testableLooper.processAllMessages() + + verify(activityStarter, never()) + .startPendingIntentDismissingKeyguard( + any(), any(), any(ActivityLaunchAnimator.Controller::class.java)) + } + + @Test + fun testValidPendingIntentStartsActivity() { + val pi = mock(PendingIntent::class.java) + `when`(pi.isActivity).thenReturn(true) + val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) + tile.qsTile.activityLaunchForClick = pi + + tile.handleClick(mock(View::class.java)) + + testableLooper.processAllMessages() + + verify(activityStarter) + .startPendingIntentDismissingKeyguard( + eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>()) + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java index 25c95ef58d85..172c87f0c50b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.os.Handler; @@ -58,6 +59,7 @@ import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.SecureSettings; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -245,6 +247,32 @@ public class TileServicesTest extends SysuiTestCase { verify(manager.getTileService()).onStartListening(); } + @Test + public void testValidCustomTileStartsActivity() { + CustomTile tile = mock(CustomTile.class); + PendingIntent pi = mock(PendingIntent.class); + ComponentName componentName = mock(ComponentName.class); + when(tile.getComponent()).thenReturn(componentName); + when(componentName.getPackageName()).thenReturn(this.getContext().getPackageName()); + + mTileService.startActivity(tile, pi); + + verify(tile).startActivityAndCollapse(pi); + } + + @Test + public void testInvalidCustomTileDoesNotStartActivity() { + CustomTile tile = mock(CustomTile.class); + PendingIntent pi = mock(PendingIntent.class); + ComponentName componentName = mock(ComponentName.class); + when(tile.getComponent()).thenReturn(componentName); + when(componentName.getPackageName()).thenReturn("invalid.package.name"); + + Assert.assertThrows(SecurityException.class, () -> mTileService.startActivity(tile, pi)); + + verify(tile, never()).startActivityAndCollapse(pi); + } + private class TestTileServices extends TileServices { TestTileServices(QSTileHost host, Provider<Handler> handlerProvider, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, |