From 1b6920d2eaaa903d9957db491eb63d2565d854db Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Wed, 18 Aug 2021 10:58:38 -0400 Subject: [PATCH] Add API for requesting add TileService This CL adds the API to StatusBarManager, as well as the constants. It also wires things into SystemUI so the API actually works as intended. TODO: * Rate limit requests (b/197534249) Fixes: 197207978 Test: manual using test app Test: atest SystemUITests Change-Id: I2a9d18535fde1d77e9d6792bd45002ce04b49dab --- core/api/current.txt | 11 ++ core/api/system-current.txt | 1 - core/java/android/app/StatusBarManager.java | 155 ++++++++++++++++++ core/java/android/content/Context.java | 4 +- .../statusbar/IAddTileResultCallback.aidl | 22 +++ .../internal/statusbar/IStatusBar.aidl | 4 + .../internal/statusbar/IStatusBarService.aidl | 4 + .../external/TileServiceRequestController.kt | 31 +++- .../systemui/statusbar/CommandQueue.java | 43 +++++ .../TileServiceRequestControllerTest.kt | 42 ++++- .../statusbar/StatusBarManagerService.java | 102 ++++++++++++ 11 files changed, 408 insertions(+), 11 deletions(-) create mode 100644 core/java/com/android/internal/statusbar/IAddTileResultCallback.aidl diff --git a/core/api/current.txt b/core/api/current.txt index f67c338822aa7..789a1c3f3e72c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -6693,6 +6693,16 @@ package android.app { } public class StatusBarManager { + method public int requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); + field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT = 3; // 0x3 + field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE = 1; // 0x1 + field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER = 4; // 0x4 + field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS = 2; // 0x2 + field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON = 5; // 0x5 + field public static final int TILE_ADD_REQUEST_ANSWER_SUCCESS = 0; // 0x0 + field public static final int TILE_ADD_REQUEST_RESULT_TILE_ADDED = 2; // 0x2 + field public static final int TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED = 1; // 0x1 + field public static final int TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED = 0; // 0x0 } public final class SyncNotedAppOp implements android.os.Parcelable { @@ -10695,6 +10705,7 @@ package android.content { field public static final String SEARCH_SERVICE = "search"; field public static final String SENSOR_SERVICE = "sensor"; field public static final String SHORTCUT_SERVICE = "shortcut"; + field public static final String STATUS_BAR_SERVICE = "statusbar"; field public static final String STORAGE_SERVICE = "storage"; field public static final String STORAGE_STATS_SERVICE = "storagestats"; field public static final String SYSTEM_HEALTH_SERVICE = "systemhealth"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 37bbebf2ae0c4..85b0415349afd 100755 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -2350,7 +2350,6 @@ package android.content { field public static final String SECURE_ELEMENT_SERVICE = "secure_element"; field public static final String SMARTSPACE_SERVICE = "smartspace"; field public static final String STATS_MANAGER = "stats"; - field public static final String STATUS_BAR_SERVICE = "statusbar"; field public static final String SYSTEM_CONFIG_SERVICE = "system_config"; field public static final String SYSTEM_UPDATE_SERVICE = "system_update"; field public static final String TETHERING_SERVICE = "tethering"; diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 77bcef3ae009f..d9a339025afd1 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -24,7 +24,9 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; import android.content.Context; +import android.graphics.drawable.Icon; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -35,11 +37,15 @@ import android.util.Pair; import android.util.Slog; import android.view.View; +import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Allows an app to control the status bar. @@ -206,6 +212,71 @@ public class StatusBarManager { /** @hide */ public static final int CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER = 2; + /** + * Response indicating that the tile was not added. + */ + public static final int TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED = 0; + /** + * Response indicating that the tile was already added and the user was not prompted. + */ + public static final int TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED = 1; + /** + * Response indicating that the tile was added. + */ + public static final int TILE_ADD_REQUEST_RESULT_TILE_ADDED = 2; + /** @hide */ + public static final int TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED = 3; + + /** @hide */ + @IntDef(prefix = {"TILE_ADD_REQUEST_RESULT_"}, value = { + TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, + TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED, + TILE_ADD_REQUEST_RESULT_TILE_ADDED, + TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestResult {} + + /** + * Indicates that the request was sent successfully. Does not indicate that the user + * has accepted the request. + */ + public static final int TILE_ADD_REQUEST_ANSWER_SUCCESS = 0; + /** + * Indicates that this package does not match that of the + * {@link android.service.quicksettings.TileService}. + */ + public static final int TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE = 1; + /** + * Indicates that there's a request in progress for this package. + */ + public static final int TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS = 2; + /** + * Indicates that the component does not match an enabled + * {@link android.service.quicksettings.TileService} for the current user. + */ + public static final int TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT = 3; + /** + * Indicates that the user is not the current user. + */ + public static final int TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER = 4; + /** + * The request could not be processed due to an unkonwn reason. + */ + public static final int TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON = 5; + + /** @hide */ + @IntDef(prefix = {"TILE_ADD_REQUEST_ANSWER_"}, value = { + TILE_ADD_REQUEST_ANSWER_SUCCESS, + TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE, + TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS, + TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT, + TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER, + TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RequestAnswer {} + @UnsupportedAppUsage private Context mContext; private IStatusBarService mService; @@ -529,6 +600,69 @@ public class StatusBarManager { } } + /** + * Request to the user to add a {@link android.service.quicksettings.TileService} + * to the set of current QS tiles. + *

+ * Calling this will prompt the user to decide whether they want to add the shown + * {@link android.service.quicksettings.TileService} to their current tiles. The user can + * deny the request and the system can stop processing requests for a given + * {@link ComponentName} after a number of requests. + *

+ * The request will show to the user information about the tile: + *

+ *

+ * The user for which this will be added is determined from the {@link Context} used to retrieve + * this service, and must match the current user. + * + * @param tileServiceComponentName {@link ComponentName} of the + * {@link android.service.quicksettings.TileService} for the request. + * @param tileLabel label of the tile to show to the user. + * @param icon icon to use in the tile shown to the user. + * @param resultExecutor an executor to run the callback on + * @param resultCallback callback to indicate the {@link RequestResult}. + * @return whether the request was successfully sent. + * + * @see android.service.quicksettings.TileService + */ + @RequestAnswer + public int requestAddTileService( + @NonNull ComponentName tileServiceComponentName, + @NonNull CharSequence tileLabel, + @NonNull Icon icon, + @NonNull Executor resultExecutor, + @NonNull Consumer resultCallback + ) { + Objects.requireNonNull(tileServiceComponentName); + Objects.requireNonNull(tileLabel); + Objects.requireNonNull(icon); + Objects.requireNonNull(resultExecutor); + Objects.requireNonNull(resultCallback); + if (!tileServiceComponentName.getPackageName().equals(mContext.getPackageName())) { + return TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE; + } + int userId = mContext.getUserId(); + RequestResultCallback callbackProxy = new RequestResultCallback(resultExecutor, + resultCallback); + IStatusBarService svc = getService(); + try { + return svc.requestAddTile( + tileServiceComponentName, + tileLabel, + icon, + userId, + callbackProxy + ); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + return TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON; + } + /** @hide */ public static String windowStateToString(int state) { if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING"; @@ -773,4 +907,25 @@ public class StatusBarManager { return new Pair(disable1, disable2); } } + + /** + * @hide + */ + static final class RequestResultCallback extends IAddTileResultCallback.Stub { + + @NonNull + private final Executor mExecutor; + @NonNull + private final Consumer mCallback; + + RequestResultCallback(@NonNull Executor executor, @NonNull Consumer callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onTileRequest(int userResponse) { + mExecutor.execute(() -> mCallback.accept(userResponse)); + } + } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index f5a0c431f4901..29b08af73034d 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4224,14 +4224,12 @@ public abstract class Context { /** * Use with {@link #getSystemService(String)} to retrieve a {@link - * android.app.StatusBarManager} for interacting with the status bar. + * android.app.StatusBarManager} for interacting with the status bar and quick settings. * * @see #getSystemService(String) * @see android.app.StatusBarManager * - * @hide */ - @SystemApi @SuppressLint("ServiceName") public static final String STATUS_BAR_SERVICE = "statusbar"; diff --git a/core/java/com/android/internal/statusbar/IAddTileResultCallback.aidl b/core/java/com/android/internal/statusbar/IAddTileResultCallback.aidl new file mode 100644 index 0000000000000..7053eef2858ae --- /dev/null +++ b/core/java/com/android/internal/statusbar/IAddTileResultCallback.aidl @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2021, 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.internal.statusbar; + +/** {@hide} */ +oneway interface IAddTileResultCallback { + void onTileRequest(int userResponse); +} \ No newline at end of file diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 84a7f2f5ddf90..6fb5c876c97b5 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -18,6 +18,7 @@ package com.android.internal.statusbar; import android.app.ITransientNotificationCallback; import android.content.ComponentName; +import android.graphics.drawable.Icon; import android.graphics.Rect; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; @@ -27,6 +28,7 @@ import android.os.ParcelFileDescriptor; import android.service.notification.StatusBarNotification; import android.view.InsetsVisibilities; +import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.view.AppearanceRegion; @@ -286,4 +288,6 @@ oneway interface IStatusBar * Triggers a GC in the system and status bar. */ void runGcForTest(); + + void requestAddTile(in ComponentName componentName, in CharSequence appName, in CharSequence label, in Icon icon, in IAddTileResultCallback callback); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index e7d6d6cf89366..4f38ff55ba3af 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -18,6 +18,7 @@ package com.android.internal.statusbar; import android.app.Notification; import android.content.ComponentName; +import android.graphics.drawable.Icon; import android.graphics.Rect; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; @@ -27,6 +28,7 @@ import android.os.Bundle; import android.os.UserHandle; import android.service.notification.StatusBarNotification; +import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.RegisterStatusBarResult; import com.android.internal.statusbar.StatusBarIcon; @@ -156,4 +158,6 @@ interface IStatusBarService * display. */ void suppressAmbientDisplay(boolean suppress); + + int requestAddTile(in ComponentName componentName, in CharSequence label, in Icon icon, int userId, in IAddTileResultCallback callback); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt index e6da234fe9c18..fe1a619f29af2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt @@ -17,17 +17,20 @@ package com.android.systemui.qs.external import android.app.Dialog +import android.app.StatusBarManager import android.content.ComponentName import android.content.DialogInterface import android.graphics.drawable.Icon import android.util.Log import androidx.annotation.VisibleForTesting +import com.android.internal.statusbar.IAddTileResultCallback import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.QSTileHost import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.R +import com.android.systemui.statusbar.CommandQueue import java.io.PrintWriter import java.util.function.Consumer import javax.inject.Inject @@ -39,24 +42,41 @@ private const val TAG = "TileServiceRequestController" */ class TileServiceRequestController constructor( private val qsTileHost: QSTileHost, + private val commandQueue: CommandQueue, private val commandRegistry: CommandRegistry, private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsTileHost.context) } ) { companion object { - // Temporary return values while there's no API - internal const val ADD_TILE = 0 - internal const val DONT_ADD_TILE = 1 - internal const val TILE_ALREADY_ADDED = 2 + internal const val ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED + internal const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED + internal const val TILE_ALREADY_ADDED = + StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED internal const val DISMISSED = 3 } + private val commandQueueCallback = object : CommandQueue.Callbacks { + override fun requestAddTile( + componentName: ComponentName, + appName: CharSequence, + label: CharSequence, + icon: Icon, + callback: IAddTileResultCallback + ) { + requestTileAdd(componentName, appName, label, icon) { + callback.onTileRequest(it) + } + } + } + fun init() { commandRegistry.registerCommand("tile-service-add") { TileServiceRequestCommand() } + commandQueue.addCallback(commandQueueCallback) } fun destroy() { commandRegistry.unregisterCommand("tile-service-add") + commandQueue.removeCallback(commandQueueCallback) } private fun addTile(componentName: ComponentName) { @@ -133,10 +153,11 @@ class TileServiceRequestController constructor( @SysUISingleton class Builder @Inject constructor( + private val commandQueue: CommandQueue, private val commandRegistry: CommandRegistry ) { fun create(qsTileHost: QSTileHost): TileServiceRequestController { - return TileServiceRequestController(qsTileHost, commandRegistry) + return TileServiceRequestController(qsTileHost, commandQueue, commandRegistry) } } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 90158c32c0991..2eaa2ae5cbcfc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -34,6 +34,7 @@ import android.app.StatusBarManager.WindowType; import android.app.StatusBarManager.WindowVisibleState; import android.content.ComponentName; import android.content.Context; +import android.graphics.drawable.Icon; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import android.hardware.biometrics.IBiometricSysuiReceiver; @@ -58,6 +59,7 @@ import android.view.WindowInsetsController.Behavior; import androidx.annotation.NonNull; import com.android.internal.os.SomeArgs; +import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.StatusBarIcon; import com.android.internal.util.GcUtils; @@ -148,6 +150,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< private static final int MSG_EMERGENCY_ACTION_LAUNCH_GESTURE = 58 << MSG_SHIFT; private static final int MSG_SET_NAVIGATION_BAR_LUMA_SAMPLING_ENABLED = 59 << MSG_SHIFT; private static final int MSG_SET_UDFPS_HBM_LISTENER = 60 << MSG_SHIFT; + private static final int MSG_TILE_SERVICE_REQUEST_ADD = 61 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -402,6 +405,16 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< * @see IStatusBar#setNavigationBarLumaSamplingEnabled(int, boolean) */ default void setNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {} + + /** + * @see IStatusBar#requestAddTile + */ + default void requestAddTile( + @NonNull ComponentName componentName, + @NonNull CharSequence appName, + @NonNull CharSequence label, + @NonNull Icon icon, + @NonNull IAddTileResultCallback callback) {} } public CommandQueue(Context context) { @@ -1106,6 +1119,23 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< GcUtils.runGcAndFinalizersSync(); } + @Override + public void requestAddTile( + @NonNull ComponentName componentName, + @NonNull CharSequence appName, + @NonNull CharSequence label, + @NonNull Icon icon, + @NonNull IAddTileResultCallback callback + ) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = componentName; + args.arg2 = appName; + args.arg3 = label; + args.arg4 = icon; + args.arg5 = callback; + mHandler.obtainMessage(MSG_TILE_SERVICE_REQUEST_ADD, args).sendToTarget(); + } + private final class H extends Handler { private H(Looper l) { super(l); @@ -1478,6 +1508,19 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< msg.arg2 != 0); } break; + case MSG_TILE_SERVICE_REQUEST_ADD: + args = (SomeArgs) msg.obj; + ComponentName componentName = (ComponentName) args.arg1; + CharSequence appName = (CharSequence) args.arg2; + CharSequence label = (CharSequence) args.arg3; + Icon icon = (Icon) args.arg4; + IAddTileResultCallback callback = (IAddTileResultCallback) args.arg5; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).requestAddTile( + componentName, appName, label, icon, callback); + } + args.recycle(); + break; } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt index ce8e58ca9e619..70e971cdbf324 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt @@ -21,8 +21,10 @@ import android.content.DialogInterface import android.graphics.drawable.Icon import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.internal.statusbar.IAddTileResultCallback import com.android.systemui.SysuiTestCase import com.android.systemui.qs.QSTileHost +import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture @@ -36,6 +38,7 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.anyString +import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -58,6 +61,8 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Mock private lateinit var commandRegistry: CommandRegistry @Mock + private lateinit var commandQueue: CommandQueue + @Mock private lateinit var icon: Icon private lateinit var controller: TileServiceRequestController @@ -69,7 +74,7 @@ class TileServiceRequestControllerTest : SysuiTestCase() { // Tile not present by default `when`(qsTileHost.indexOf(anyString())).thenReturn(-1) - controller = TileServiceRequestController(qsTileHost, commandRegistry) { + controller = TileServiceRequestController(qsTileHost, commandQueue, commandRegistry) { tileRequestDialog } @@ -152,11 +157,44 @@ class TileServiceRequestControllerTest : SysuiTestCase() { verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean()) } - private class Callback : Consumer { + @Test + fun commandQueueCallback_registered() { + verify(commandQueue).addCallback(any()) + } + + @Test + fun commandQueueCallback_dataPassedToDialog() { + val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) + verify(commandQueue, atLeastOnce()).addCallback(capture(captor)) + + captor.value.requestAddTile(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback()) + + verify(tileRequestDialog).setTileData( + TileRequestDialog.TileData(TEST_APP_NAME, TEST_LABEL, icon) + ) + } + + @Test + fun commandQueueCallback_callbackCalled() { + `when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2) + val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) + verify(commandQueue, atLeastOnce()).addCallback(capture(captor)) + val c = Callback() + + captor.value.requestAddTile(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, c) + + assertThat(c.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED) + } + + private class Callback : IAddTileResultCallback.Stub(), Consumer { var lastAccepted: Int? = null private set override fun accept(t: Int) { lastAccepted = t } + + override fun onTileRequest(r: Int) { + accept(r) + } } } \ No newline at end of file diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index ff7e903e80d83..60784046ba831 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -21,7 +21,9 @@ import static android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE; import static android.view.Display.DEFAULT_DISPLAY; import android.Manifest; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManagerInternal; import android.app.ActivityThread; import android.app.ITransientNotificationCallback; import android.app.Notification; @@ -31,7 +33,11 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Icon; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode; import android.hardware.biometrics.IBiometricSysuiReceiver; @@ -53,6 +59,7 @@ import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.UserHandle; import android.service.notification.NotificationStats; +import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -68,6 +75,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.os.TransferPipe; +import com.android.internal.statusbar.IAddTileResultCallback; import com.android.internal.statusbar.IStatusBar; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; @@ -123,7 +131,9 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D private final Object mLock = new Object(); private final DeathRecipient mDeathRecipient = new DeathRecipient(); + private final ActivityManagerInternal mActivityManagerInternal; private final ActivityTaskManagerInternal mActivityTaskManager; + private final PackageManagerInternal mPackageManagerInternal; private int mCurrentUserId; private boolean mTracingEnabled; @@ -227,6 +237,8 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayManager.registerDisplayListener(this, mHandler); mActivityTaskManager = LocalServices.getService(ActivityTaskManagerInternal.class); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); } @Override @@ -1639,6 +1651,96 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + private void checkCallingUidPackage(String packageName, int callingUid, int userId) { + int packageUid = mPackageManagerInternal.getPackageUid(packageName, 0, userId); + if (UserHandle.getAppId(callingUid) != UserHandle.getAppId(packageUid)) { + throw new SecurityException("Package " + packageName + + " does not belong to the calling uid " + callingUid); + } + } + + private ResolveInfo isComponentValidTileService(ComponentName componentName, int userId) { + Intent intent = new Intent(TileService.ACTION_QS_TILE); + intent.setComponent(componentName); + ResolveInfo r = mPackageManagerInternal.resolveService(intent, + intent.resolveTypeIfNeeded(mContext.getContentResolver()), 0, userId, + Process.myUid()); + int enabled = mPackageManagerInternal.getComponentEnabledSetting( + componentName, Process.myUid(), userId); + if (r != null + && r.serviceInfo != null + && resolveEnabledComponent(r.serviceInfo.enabled, enabled) + && Manifest.permission.BIND_QUICK_SETTINGS_TILE.equals(r.serviceInfo.permission)) { + return r; + } else { + return null; + } + } + + private boolean resolveEnabledComponent(boolean defaultValue, int pmResult) { + if (pmResult == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + return true; + } + if (pmResult == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { + return defaultValue; + } + return false; + } + + @Override + public int requestAddTile( + @NonNull ComponentName componentName, + @NonNull CharSequence label, + @NonNull Icon icon, + int userId, + @NonNull IAddTileResultCallback callback + ) { + int callingUid = Binder.getCallingUid(); + String packageName = componentName.getPackageName(); + + // Check calling user can act on behalf of current user + mActivityManagerInternal.handleIncomingUser(Binder.getCallingPid(), callingUid, userId, + false, ActivityManagerInternal.ALLOW_NON_FULL, "requestAddTile", packageName); + + // Check calling uid matches package + checkCallingUidPackage(packageName, callingUid, userId); + + int currentUser = mActivityManagerInternal.getCurrentUserId(); + + // Check current user + if (userId != currentUser) { + return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER; + } + + // We've checked that the package, component name and uid all match. + ResolveInfo r = isComponentValidTileService(componentName, userId); + if (r == null) { + return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT; + } + + IAddTileResultCallback proxyCallback = new IAddTileResultCallback.Stub() { + @Override + public void onTileRequest(int i) throws RemoteException { + if (i == StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED) { + i = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED; + } + callback.onTileRequest(i); + } + }; + + CharSequence appName = r.serviceInfo.applicationInfo + .loadLabel(mContext.getPackageManager()); + if (mBar != null) { + try { + mBar.requestAddTile(componentName, appName, label, icon, proxyCallback); + return StatusBarManager.TILE_ADD_REQUEST_ANSWER_SUCCESS; + } catch (RemoteException e) { + Slog.e(TAG, "requestAddTile", e); + } + } + return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON; + } + public String[] getStatusBarIcons() { return mContext.getResources().getStringArray(R.array.config_statusBarIcons); }