diff options
author | Yuncheol Heo <ycheo@google.com> | 2021-09-28 18:23:22 -0700 |
---|---|---|
committer | Yuncheol Heo <ycheo@google.com> | 2021-10-06 21:04:04 -0700 |
commit | 592145aaf8d780fe478d4751ddf81d959debd008 (patch) | |
tree | c772deb29d1564a8c64395b9870d5da8e41defd0 | |
parent | 4e5596685a60e63ede7e8fbabbd6e61abae73325 (diff) | |
download | Car-592145aaf8d780fe478d4751ddf81d959debd008.tar.gz |
Introduce CarActivityManager.
- It provides CAM.setPersistentActivity() to map Activity with TDA.
Bug: 200044674
CTS-Coverage-Bug: 201828357
Test: atest CarActivityManagerTest CarActivityServiceUnitTest
Change-Id: Iad146b398c4f70d9bfa253a89412305409b36d6d
Merged-In: Iad146b398c4f70d9bfa253a89412305409b36d6d
13 files changed, 537 insertions, 19 deletions
diff --git a/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl b/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl index 22148eb44b..cabbc07524 100644 --- a/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl +++ b/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl @@ -55,4 +55,10 @@ interface ICarServiceHelper { * Creates the given user, even when it's disallowed by DevicePolicyManager. */ UserInfo createUserEvenWhenDisallowed(String name, String userType, int flags); + + /** + * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of + * {@code featureId} in the display of {@code displayId}. + */ + int setPersistentActivity(in ComponentName activity, int displayId, int featureId); } diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java index 23ed24d8f7..6647a32675 100644 --- a/car-lib/src/android/car/Car.java +++ b/car-lib/src/android/car/Car.java @@ -32,6 +32,7 @@ import android.app.Service; import android.car.admin.CarDevicePolicyManager; import android.car.annotation.MandatoryFeature; import android.car.annotation.OptionalFeature; +import android.car.app.CarActivityManager; import android.car.cluster.CarInstrumentClusterManager; import android.car.cluster.ClusterActivityState; import android.car.cluster.ClusterHomeManager; @@ -373,6 +374,14 @@ public final class Car { @OptionalFeature public static final String CAR_TELEMETRY_SERVICE = "car_telemetry_service"; + /** + * Service name for {@link android.car.app.CarActivityManager} + * + * @hide + */ + @MandatoryFeature + public static final String CAR_ACTIVITY_SERVICE = "car_activity_service"; + /** Permission necessary to access car's mileage information. * @hide */ @@ -868,6 +877,14 @@ public final class Car { public static final String PERMISSION_COLLECT_CAR_WATCHDOG_METRICS = "android.car.permission.COLLECT_CAR_WATCHDOG_METRICS"; + /** + * Permission necessary to control launching applications in Car. + * + * @hide + */ + public static final String PERMISSION_CONTROL_CAR_APP_LAUNCH = + "android.car.permission.CONTROL_CAR_APP_LAUNCH"; + /** @hide */ @IntDef({CONNECTION_TYPE_EMBEDDED}) @Retention(RetentionPolicy.SOURCE) @@ -904,25 +921,6 @@ public final class Car { public static final String CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION = "android.media.session.BROWSE_SERVICE"; - /** - * If some specific Activity should be launched on the designated TDA all the time, include this - * integer extra in the first launching Intent and ActivityOption with the launch TDA. - * If the value is {@link #LAUNCH_PERSISTENT_ADD}, CarLaunchParamsModifier will memorize - * the Activity and the TDA pair, and assign the TDA in the following Intents for the Activity. - * If there is any assigned Activity on the TDA, it'll be replaced with the new Activity. - * If the value is {@Link #LAUNCH_PERSISTENT_DELETE}, it'll remove the stored info for the given - * Activity. - * - * @hide - */ - public static final String CAR_EXTRA_LAUNCH_PERSISTENT = - "android.car.intent.extra.launchparams.PERSISTENT"; - - /** @hide */ - public static final int LAUNCH_PERSISTENT_DELETE = 0; - /** @hide */ - public static final int LAUNCH_PERSISTENT_ADD = 1; - /** @hide */ public static final String CAR_SERVICE_INTERFACE_NAME = CommonConstants.CAR_SERVICE_INTERFACE; @@ -1919,6 +1917,9 @@ public final class Car { case CAR_TELEMETRY_SERVICE: manager = new CarTelemetryManager(this, binder); break; + case CAR_ACTIVITY_SERVICE: + manager = new CarActivityManager(this, binder); + break; default: // Experimental or non-existing String className = null; diff --git a/car-lib/src/android/car/app/CarActivityManager.java b/car-lib/src/android/car/app/CarActivityManager.java new file mode 100644 index 0000000000..dd521f9a85 --- /dev/null +++ b/car-lib/src/android/car/app/CarActivityManager.java @@ -0,0 +1,140 @@ +/* + * 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 android.car.app; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.app.Activity; +import android.car.Car; +import android.car.CarManagerBase; +import android.car.user.CarUserManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceSpecificException; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * API to manage {@link android.app.Activity} in Car. + * + * @hide + */ +public final class CarActivityManager extends CarManagerBase { + private static final String TAG = CarUserManager.class.getSimpleName(); + + /** Indicates that the operation was successful. */ + public static final int RESULT_SUCCESS = 0; + /** Indicates that the operation was failed with the unknown reason. */ + public static final int RESULT_FAILURE = -1; + /** + * Indicates that the operation was failed because the requester isn't the current user or + * the system user + */ + public static final int RESULT_INVALID_USER = -2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "RESULT_", value = { + RESULT_SUCCESS, + RESULT_FAILURE, + RESULT_INVALID_USER, + }) + @Target({ElementType.TYPE_USE}) + public @interface ResultTypeEnum {} + + /** + * Internal error code for throwing {@link ActivityNotFoundException} from service. + * @hide + */ + public static final int ERROR_CODE_ACTIVITY_NOT_FOUND = -101; + + private final ICarActivityService mService; + + /** + * @hide + */ + public CarActivityManager(@NonNull Car car, @NonNull IBinder service) { + this(car, ICarActivityService.Stub.asInterface(service)); + } + + /** + * @hide + */ + @VisibleForTesting + public CarActivityManager(@NonNull Car car, @NonNull ICarActivityService service) { + super(car); + + mService = service; + } + + /** + * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of + * {@code featureId} in the display of {@code displayId}. + * <p>Note: this will not affect the existing {@link Activity}. + * Note: You can map assign {@code Activity} to one {@code TaskDisplayArea} only. If + * you assign it to the multiple {@code TaskDisplayArea}s, then the last one wins. + * Note: The requester should be the current user or the system user, if not, the operation will + * be failed with {@code RESULT_INVALID_USER}. + * + * @param activity {@link Activity} to designate + * @param displayId {@code Display} where {@code TaskDisplayArea} is located in + * @param featureId {@code TaskDisplayArea} where {@link Activity} is launched in, if it is + * {@code DisplayAreaOrganizer.FEATURE_UNDEFINED}, then it'll remove the existing one. + * @return {@code ResultTypeEnum}. {@code RESULT_SUCCESS} if the operation is successful, + * otherwise, {@code RESULT_XXX} depending on the type of the error. + * @throws {@link IllegalArgumentException} if {@code displayId} or {@code featureId} is + * invalid. {@link ActivityNotFoundException} if {@code activity} is not found + * when it tries to remove. + */ + @RequiresPermission(Car.PERMISSION_CONTROL_CAR_APP_LAUNCH) + @ResultTypeEnum + public int setPersistentActivity( + @NonNull ComponentName activity, int displayId, int featureId) { + try { + return mService.setPersistentActivity(activity, displayId, featureId); + } catch (IllegalArgumentException | IllegalStateException | SecurityException e) { + throw e; + } catch (ServiceSpecificException e) { + return handleServiceSpecificFromCarService(e); + } catch (RemoteException | RuntimeException e) { + return handleExceptionFromCarService(e, RESULT_FAILURE); + } + } + + /** @hide */ + @Override + protected void onCarDisconnected() { + // nothing to do + } + + private int handleServiceSpecificFromCarService(ServiceSpecificException e) + throws ActivityNotFoundException { + if (e.errorCode == ERROR_CODE_ACTIVITY_NOT_FOUND) { + throw new ActivityNotFoundException(e.getMessage()); + } + // don't know what this is + throw new IllegalStateException(e); + } +} diff --git a/car-lib/src/android/car/app/ICarActivityService.aidl b/car-lib/src/android/car/app/ICarActivityService.aidl new file mode 100644 index 0000000000..9f3e76489e --- /dev/null +++ b/car-lib/src/android/car/app/ICarActivityService.aidl @@ -0,0 +1,29 @@ +/* + * 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 android.car.app; + +import android.content.ComponentName; + +/** @hide */ +interface ICarActivityService { + /** + * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of + * {@code featureId} in the display of {@code displayId}. + */ + int setPersistentActivity(in ComponentName activity, int displayId, int featureId) = 0; +} + diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml index f8a5e9c691..9cf986d30f 100644 --- a/service/AndroidManifest.xml +++ b/service/AndroidManifest.xml @@ -892,6 +892,14 @@ android:label="@string/car_permission_label_template_renderer" android:description="@string/car_permission_desc_template_renderer"/> + <!-- Allows an application to control launching applications in Car. + <p>Protection level: signature|privileged + --> + <permission android:name="android.car.permission.CONTROL_CAR_APP_LAUNCH" + android:description="@string/car_permission_desc_control_car_app_launch" + android:label="@string/car_permission_label_control_car_app_launch" + android:protectionLevel="signature|privileged" /> + <uses-permission android:name="android.permission.CALL_PHONE"/> <uses-permission android:name="android.permission.DEVICE_POWER"/> <uses-permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS"/> diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml index 416981be0f..70a6ecf102 100644 --- a/service/res/values/strings.xml +++ b/service/res/values/strings.xml @@ -548,6 +548,11 @@ <!-- Permission text: app can render templates provided by another app [CHAR LIMIT=NONE] --> <string name="car_permission_desc_template_renderer">Render templates.</string> + <!-- Permission text: app can control launching applications in Car [CHAR LIMIT=NONE] --> + <string name="car_permission_label_control_car_app_launch">control launching applications</string> + <!-- Permission text: app can control launching applications in Car [CHAR LIMIT=NONE] --> + <string name="car_permission_desc_control_car_app_launch">Control launching applications.</string> + <!-- The default name of device enrolled as trust device [CHAR LIMIT=NONE] --> <string name="trust_device_default_name">My Device</string> diff --git a/service/src/com/android/car/CarFeatureController.java b/service/src/com/android/car/CarFeatureController.java index f5df9e47d6..528353db82 100644 --- a/service/src/com/android/car/CarFeatureController.java +++ b/service/src/com/android/car/CarFeatureController.java @@ -63,6 +63,7 @@ public final class CarFeatureController implements CarServiceBase { Car.APP_FOCUS_SERVICE, Car.AUDIO_SERVICE, Car.BLUETOOTH_SERVICE, + Car.CAR_ACTIVITY_SERVICE, Car.CAR_BUGREPORT_SERVICE, Car.CAR_DEVICE_POLICY_SERVICE, Car.CAR_DRIVING_STATE_SERVICE, diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java index d5900833f7..4b7c75d34b 100644 --- a/service/src/com/android/car/ICarImpl.java +++ b/service/src/com/android/car/ICarImpl.java @@ -54,6 +54,7 @@ import android.util.TimingsTraceLog; import com.android.car.admin.CarDevicePolicyService; import com.android.car.admin.FactoryResetActivity; +import com.android.car.am.CarActivityService; import com.android.car.am.FixedActivityService; import com.android.car.audio.CarAudioService; import com.android.car.cluster.ClusterHomeService; @@ -136,6 +137,7 @@ public class ICarImpl extends ICar.Stub { private final ClusterHomeService mClusterHomeService; private final CarEvsService mCarEvsService; private final CarTelemetryService mCarTelemetryService; + private final CarActivityService mCarActivityService; private final CarServiceBase[] mAllServices; @@ -358,6 +360,8 @@ public class ICarImpl extends ICar.Stub { } else { mCarTelemetryService = null; } + mCarActivityService = constructWithTrace(t, CarActivityService.class, + () -> new CarActivityService(serviceContext)); // Be careful with order. Service depending on other service should be inited later. List<CarServiceBase> allServices = new ArrayList<>(); @@ -394,6 +398,7 @@ public class ICarImpl extends ICar.Stub { addServiceIfNonNull(allServices, mClusterHomeService); addServiceIfNonNull(allServices, mCarEvsService); addServiceIfNonNull(allServices, mCarTelemetryService); + allServices.add(mCarActivityService); // Always put mCarExperimentalFeatureServiceController in last. addServiceIfNonNull(allServices, mCarExperimentalFeatureServiceController); @@ -463,6 +468,7 @@ public class ICarImpl extends ICar.Stub { mSystemInterface.setCarServiceHelper(carServiceHelper); mCarOccupantZoneService.setCarServiceHelper(carServiceHelper); mCarUserService.setCarServiceHelper(carServiceHelper); + mCarActivityService.setICarServiceHelper(carServiceHelper); bundle = new Bundle(); bundle.putBinder(ICAR_SYSTEM_SERVER_CLIENT, mICarSystemServerClientImpl.asBinder()); @@ -631,6 +637,8 @@ public class ICarImpl extends ICar.Stub { return mCarEvsService; case Car.CAR_TELEMETRY_SERVICE: return mCarTelemetryService; + case Car.CAR_ACTIVITY_SERVICE: + return mCarActivityService; default: IBinder service = null; if (mCarExperimentalFeatureServiceController != null) { diff --git a/service/src/com/android/car/am/CarActivityService.java b/service/src/com/android/car/am/CarActivityService.java new file mode 100644 index 0000000000..61d309e38d --- /dev/null +++ b/service/src/com/android/car/am/CarActivityService.java @@ -0,0 +1,97 @@ +/* + * 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.car.am; + +import android.app.ActivityManager; +import android.car.Car; +import android.car.app.CarActivityManager; +import android.car.app.ICarActivityService; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.IndentingPrintWriter; + +import com.android.car.CarServiceBase; +import com.android.car.internal.ICarServiceHelper; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +/** + * Service responsible for Activities in Car. + */ +public final class CarActivityService extends ICarActivityService.Stub + implements CarServiceBase { + + private final Context mContext; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + ICarServiceHelper mICarServiceHelper; + + public CarActivityService(Context context) { + mContext = context; + } + + @Override + public void init() {} + + @Override + public void release() {} + + /** + * Sets {@code ICarServiceHelper}. + */ + public void setICarServiceHelper(ICarServiceHelper helper) { + synchronized (mLock) { + mICarServiceHelper = helper; + } + } + + @Override + public int setPersistentActivity(ComponentName activity, int displayId, int featureId) throws + RemoteException { + if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission( + Car.PERMISSION_CONTROL_CAR_APP_LAUNCH)) { + throw new SecurityException("Requires " + Car.PERMISSION_CONTROL_CAR_APP_LAUNCH); + } + int caller = getCaller(); + if (caller != UserHandle.USER_SYSTEM && caller != ActivityManager.getCurrentUser()) { + return CarActivityManager.RESULT_INVALID_USER; + } + + ICarServiceHelper helper; + synchronized (mLock) { + helper = mICarServiceHelper; + } + if (helper == null) { + throw new IllegalStateException("ICarServiceHelper isn't connected yet"); + } + return helper.setPersistentActivity(activity, displayId, featureId); + } + + @VisibleForTesting + int getCaller() { // Non static for mocking. + return UserHandle.getUserId(Binder.getCallingUid()); + } + + @Override + public void dump(IndentingPrintWriter writer) {} +} diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml index 5c5029b2b3..3b709b53a2 100644 --- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml +++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml @@ -46,6 +46,8 @@ <uses-permission android:name="android.car.permission.CAR_VENDOR_EXTENSION"/> <!-- use for CarServiceTest --> <uses-permission android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"/> + <!-- use for AndroidCarApiTest --> + <uses-permission android:name="android.car.permission.CONTROL_CAR_APP_LAUNCH"/> <uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE"/> <uses-permission android:name="android.car.permission.READ_CAR_STEERING"/> <uses-permission android:name="android.car.permission.STORAGE_MONITORING"/> diff --git a/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java new file mode 100644 index 0000000000..f1d90beb32 --- /dev/null +++ b/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java @@ -0,0 +1,91 @@ +/* + * 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 android.car.apitest; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.car.Car; +import android.car.app.CarActivityManager; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.test.suitebuilder.annotation.MediumTest; +import android.view.Display; + +import org.junit.Before; +import org.junit.Test; + +@MediumTest +public class CarActivityManagerTest extends CarApiTestBase { + private static final String TAG = CarActivityManagerTest.class.getSimpleName(); + + // Comes from android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER + private static final int FEATURE_DEFAULT_TASK_CONTAINER = 1; + + // Comes from android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED + private static final int FEATURE_UNDEFINED = -1; + + private CarActivityManager mCarActivityManager; + + private final ComponentName mTestActivity = new ComponentName("test.pkg", "test.activity"); + + @Before + public void setUp() throws Exception { + mCarActivityManager = (CarActivityManager) getCar().getCarManager(Car.CAR_ACTIVITY_SERVICE); + assertThat(mCarActivityManager).isNotNull(); + } + + @Test + public void testSetPersistentActivity() { + // Set + int retSet = mCarActivityManager.setPersistentActivity( + mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER); + assertThat(retSet).isEqualTo(CarActivityManager.RESULT_SUCCESS); + + // Remove + int retRemove = mCarActivityManager.setPersistentActivity( + mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_UNDEFINED); + assertThat(retRemove).isEqualTo(CarActivityManager.RESULT_SUCCESS); + } + + @Test + public void testSetPersistentActivity_throwsExceptionForInvalidDisplayId() { + int invalidDisplayId = 999999990; + + assertThrows(IllegalArgumentException.class, + () -> mCarActivityManager.setPersistentActivity( + mTestActivity, invalidDisplayId, FEATURE_DEFAULT_TASK_CONTAINER)); + } + + @Test + public void testSetPersistentActivity_throwsExceptionForInvalidFeatureId() { + int unknownFeatureId = 999999990; + + assertThrows(IllegalArgumentException.class, + () -> mCarActivityManager.setPersistentActivity( + mTestActivity, Display.DEFAULT_DISPLAY, unknownFeatureId)); + } + + @Test + public void testSetPersistentActivity_throwsExceptionForUnknownActivity() { + // Tries to remove the Activity without registering it. + assertThrows(ActivityNotFoundException.class, + () -> mCarActivityManager.setPersistentActivity( + mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_UNDEFINED)); + } +} diff --git a/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java b/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java index 7304e23655..5d387cc1cb 100644 --- a/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java +++ b/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java @@ -16,6 +16,7 @@ package com.android.car; import android.annotation.UserIdInt; +import android.car.app.CarActivityManager; import android.content.ComponentName; import android.content.pm.UserInfo; import android.os.RemoteException; @@ -73,4 +74,11 @@ abstract class AbstractICarServiceHelperStub extends ICarServiceHelper.Stub { + ", flags=" + flags + ")"); return null; } + + @Override + public int setPersistentActivity(ComponentName activity, int displayId, int featureId) { + Log.d(TAG, "setPersistentActivity(activity=" + activity.toShortString() + + ", displayId=" + displayId + ", featureId=" + featureId + ")"); + return CarActivityManager.RESULT_SUCCESS; + } } diff --git a/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java new file mode 100644 index 0000000000..4051686f0b --- /dev/null +++ b/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java @@ -0,0 +1,122 @@ +/* + * 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.car.am; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertThrows; + +import android.car.Car; +import android.car.app.CarActivityManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.car.internal.ICarServiceHelper; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CarActivityServiceUnitTest { + + // Comes from android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER + private static final int FEATURE_DEFAULT_TASK_CONTAINER = 1; + + private CarActivityService mCarActivityService; + + private final ComponentName mTestActivity = new ComponentName("test.pkg", "test.activity"); + + @Rule + public TestName mTestName = new TestName(); + @Mock + private Context mContext; + @Mock + private ICarServiceHelper mICarServiceHelper; + + @Before + public void setUp() { + mCarActivityService = spy(new CarActivityService(mContext)); + + int nonCurrentUserId = 9999990; + boolean isNonCurrentUserTest = mTestName.getMethodName().contains("NonCurrentUser"); + int callerId = isNonCurrentUserTest ? nonCurrentUserId : UserHandle.USER_SYSTEM; + when(mCarActivityService.getCaller()).thenReturn(callerId); + } + + @Test + public void setPersistentActivityThrowsException_ifICarServiceHelperIsNotSet() { + assertThrows(IllegalStateException.class, + () -> mCarActivityService.setPersistentActivity( + mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER)); + } + + @Test + public void setPersistentActivityThrowsException_withoutPermission() { + mCarActivityService.setICarServiceHelper(mICarServiceHelper); + when(mContext.checkCallingOrSelfPermission(eq(Car.PERMISSION_CONTROL_CAR_APP_LAUNCH))) + .thenReturn(PackageManager.PERMISSION_DENIED); + + assertThrows(SecurityException.class, + () -> mCarActivityService.setPersistentActivity( + mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER)); + } + + @Test + public void setPersistentActivityInvokesICarServiceHelper() throws RemoteException { + int displayId = 9; + + mCarActivityService.setICarServiceHelper(mICarServiceHelper); + + int ret = mCarActivityService.setPersistentActivity( + mTestActivity, displayId, FEATURE_DEFAULT_TASK_CONTAINER); + assertThat(ret).isEqualTo(CarActivityManager.RESULT_SUCCESS); + + ArgumentCaptor<ComponentName> activityCaptor = ArgumentCaptor.forClass(ComponentName.class); + ArgumentCaptor<Integer> displayIdCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<Integer> featureIdCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mICarServiceHelper).setPersistentActivity( + activityCaptor.capture(), displayIdCaptor.capture(), featureIdCaptor.capture()); + + assertThat(activityCaptor.getValue()).isEqualTo(mTestActivity); + assertThat(displayIdCaptor.getValue()).isEqualTo(displayId); + assertThat(featureIdCaptor.getValue()).isEqualTo(FEATURE_DEFAULT_TASK_CONTAINER); + } + + @Test + public void setPersistentActivityReturnsErrorForNonCurrentUser() throws RemoteException { + mCarActivityService.setICarServiceHelper(mICarServiceHelper); + + int ret = mCarActivityService.setPersistentActivity( + mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER); + assertThat(ret).isEqualTo(CarActivityManager.RESULT_INVALID_USER); + } +} |