Merge "Add Start Activity API for TileServices" into tm-qpr-dev

This commit is contained in:
Rasheed Lewis
2022-12-18 17:50:16 +00:00
committed by Android (Google) Code Review
7 changed files with 199 additions and 7 deletions

View File

@@ -15,6 +15,7 @@
*/ */
package android.service.quicksettings; package android.service.quicksettings;
import android.app.PendingIntent;
import android.content.ComponentName; import android.content.ComponentName;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.service.quicksettings.Tile; import android.service.quicksettings.Tile;
@@ -29,10 +30,10 @@ interface IQSService {
String contentDescription); String contentDescription);
void onShowDialog(in IBinder tile); void onShowDialog(in IBinder tile);
void onStartActivity(in IBinder tile); void onStartActivity(in IBinder tile);
void startActivity(in IBinder tile, in PendingIntent pendingIntent);
boolean isLocked(); boolean isLocked();
boolean isSecure(); boolean isSecure();
void startUnlockAndRun(in IBinder tile); void startUnlockAndRun(in IBinder tile);
void onDialogHidden(in IBinder tile); void onDialogHidden(in IBinder tile);
void onStartSuccessful(in IBinder tile); void onStartSuccessful(in IBinder tile);
} }

View File

@@ -16,6 +16,7 @@
package android.service.quicksettings; package android.service.quicksettings;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.PendingIntent;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.IBinder; import android.os.IBinder;
import android.os.Parcel; import android.os.Parcel;
@@ -66,6 +67,7 @@ public final class Tile implements Parcelable {
private CharSequence mSubtitle; private CharSequence mSubtitle;
private CharSequence mContentDescription; private CharSequence mContentDescription;
private CharSequence mStateDescription; private CharSequence mStateDescription;
private PendingIntent mPendingIntent;
// Default to inactive until clients of the new API can update. // Default to inactive until clients of the new API can update.
private int mState = STATE_INACTIVE; 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 @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
if (mIcon != null) { if (mIcon != null) {
@@ -231,6 +261,12 @@ public final class Tile implements Parcelable {
} else { } else {
dest.writeByte((byte) 0); dest.writeByte((byte) 0);
} }
if (mPendingIntent != null) {
dest.writeByte((byte) 1);
mPendingIntent.writeToParcel(dest, flags);
} else {
dest.writeByte((byte) 0);
}
dest.writeInt(mState); dest.writeInt(mState);
TextUtils.writeToParcel(mLabel, dest, flags); TextUtils.writeToParcel(mLabel, dest, flags);
TextUtils.writeToParcel(mSubtitle, dest, flags); TextUtils.writeToParcel(mSubtitle, dest, flags);
@@ -244,6 +280,11 @@ public final class Tile implements Parcelable {
} else { } else {
mIcon = null; mIcon = null;
} }
if (source.readByte() != 0) {
mPendingIntent = PendingIntent.CREATOR.createFromParcel(source);
} else {
mPendingIntent = null;
}
mState = source.readInt(); mState = source.readInt();
mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);

View File

@@ -20,6 +20,7 @@ import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi; import android.annotation.SystemApi;
import android.annotation.TestApi; import android.annotation.TestApi;
import android.app.Dialog; import android.app.Dialog;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.app.StatusBarManager; import android.app.StatusBarManager;
import android.content.ComponentName; import android.content.ComponentName;
@@ -335,6 +336,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. * Gets the {@link Tile} for this service.
* <p/> * <p/>

View File

@@ -18,6 +18,7 @@ package com.android.systemui.qs.external;
import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
import android.app.PendingIntent;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -51,6 +52,7 @@ import androidx.annotation.WorkerThread;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 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.Background;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
@@ -92,6 +94,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
private android.graphics.drawable.Icon mDefaultIcon; private android.graphics.drawable.Icon mDefaultIcon;
@Nullable @Nullable
private CharSequence mDefaultLabel; private CharSequence mDefaultLabel;
@Nullable
private View mViewClicked;
private final Context mUserContext; private final Context mUserContext;
@@ -202,7 +206,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
* Compare two icons, only works for resources. * Compare two icons, only works for resources.
*/ */
private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1, private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1,
@Nullable android.graphics.drawable.Icon icon2) { @Nullable android.graphics.drawable.Icon icon2) {
if (icon1 == icon2) { if (icon1 == icon2) {
return true; 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). * 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. * It will return {@code true} before initialization, so tiles are not destroyed prematurely.
*/ */
@Override @Override
@@ -262,6 +266,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
/** /**
* Update state of {@link this#mTile} from a remote {@link TileService}. * Update state of {@link this#mTile} from a remote {@link TileService}.
*
* @param tile tile populated with state to apply * @param tile tile populated with state to apply
*/ */
public void updateTileState(Tile tile) { public void updateTileState(Tile tile) {
@@ -293,6 +298,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
if (tile.getStateDescription() != null || overwriteNulls) { if (tile.getStateDescription() != null || overwriteNulls) {
mTile.setStateDescription(tile.getStateDescription()); mTile.setStateDescription(tile.getStateDescription());
} }
mTile.setActivityLaunchForClick(tile.getActivityLaunchForClick());
mTile.setState(tile.getState()); mTile.setState(tile.getState());
} }
@@ -324,6 +330,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
mService.onStartListening(); mService.onStartListening();
} }
} else { } else {
mViewClicked = null;
mService.onStopListening(); mService.onStopListening();
if (mIsTokenGranted && !mIsShowingDialog) { if (mIsTokenGranted && !mIsShowingDialog) {
try { try {
@@ -388,6 +395,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
if (mTile.getState() == Tile.STATE_UNAVAILABLE) { if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
return; return;
} }
mViewClicked = view;
try { try {
if (DEBUG) Log.d(TAG, "Adding token"); if (DEBUG) Log.d(TAG, "Adding token");
mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY, mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY,
@@ -400,7 +408,12 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
mServiceManager.setBindRequested(true); mServiceManager.setBindRequested(true);
mService.onStartListening(); mService.onStartListening();
} }
mService.onClick(mToken);
if (mTile.getActivityLaunchForClick() != null) {
startActivityAndCollapse(mTile.getActivityLaunchForClick());
} else {
mService.onClick(mToken);
}
} catch (RemoteException e) { } catch (RemoteException e) {
// Called through wrapper, won't happen here. // 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) { public static String toSpec(ComponentName name) {
return PREFIX + name.flattenToShortString() + ")"; 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. * Create a {@link CustomTile} for a given spec and user.
* *
* @param builder including injected common dependencies. * @param builder including injected common dependencies.
* @param spec as provided by {@link CustomTile#toSpec} * @param spec as provided by {@link CustomTile#toSpec}
* @param userContext context for the user that is creating this tile. * @param userContext context for the user that is creating this tile.
* @return a new {@link CustomTile} * @return a new {@link CustomTile}
*/ */

View File

@@ -15,6 +15,7 @@
*/ */
package com.android.systemui.qs.external; package com.android.systemui.qs.external;
import android.app.PendingIntent;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
@@ -32,6 +33,7 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.statusbar.StatusBarIcon;
import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -275,6 +277,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 @Override
public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) { public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
CustomTile customTile = getTileForToken(token); CustomTile customTile = getTileForToken(token);
@@ -336,7 +351,7 @@ public class TileServices extends IQSService.Stub {
} }
@Nullable @Nullable
private CustomTile getTileForToken(IBinder token) { public CustomTile getTileForToken(IBinder token) {
synchronized (mServices) { synchronized (mServices) {
return mTokenMap.get(token); return mTokenMap.get(token);
} }

View File

@@ -16,6 +16,7 @@
package com.android.systemui.qs.external package com.android.systemui.qs.external
import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
@@ -30,8 +31,10 @@ import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.view.IWindowManager import android.view.IWindowManager
import android.view.View
import com.android.internal.logging.MetricsLogger import com.android.internal.logging.MetricsLogger
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.qs.QSTile 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.QSHost
import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.util.mockito.any 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.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@@ -236,6 +242,10 @@ class CustomTileTest : SysuiTestCase() {
`when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java)))
.thenReturn(mock(Drawable::class.java)) .thenReturn(mock(Drawable::class.java))
val pi = mock(PendingIntent::class.java)
`when`(pi.isActivity).thenReturn(true)
tile.qsTile.activityLaunchForClick = pi
tile.refreshState() tile.refreshState()
testableLooper.processAllMessages() testableLooper.processAllMessages()
@@ -289,4 +299,52 @@ class CustomTileTest : SysuiTestCase() {
assertFalse(tile.isAvailable) assertFalse(tile.isAvailable)
verify(tileHost).removeTile(tile.tileSpec) 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>())
}
} }

View File

@@ -26,6 +26,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.app.PendingIntent;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.os.Handler; import android.os.Handler;
@@ -58,6 +59,7 @@ import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.util.settings.SecureSettings;
import org.junit.After; import org.junit.After;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -245,6 +247,32 @@ public class TileServicesTest extends SysuiTestCase {
verify(manager.getTileService()).onStartListening(); 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 { private class TestTileServices extends TileServices {
TestTileServices(QSTileHost host, Provider<Handler> handlerProvider, TestTileServices(QSTileHost host, Provider<Handler> handlerProvider,
BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker,