aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuncheol Heo <ycheo@google.com>2021-09-28 18:23:22 -0700
committerYuncheol Heo <ycheo@google.com>2021-10-06 21:04:04 -0700
commit592145aaf8d780fe478d4751ddf81d959debd008 (patch)
treec772deb29d1564a8c64395b9870d5da8e41defd0
parent4e5596685a60e63ede7e8fbabbd6e61abae73325 (diff)
downloadCar-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
-rw-r--r--car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl6
-rw-r--r--car-lib/src/android/car/Car.java39
-rw-r--r--car-lib/src/android/car/app/CarActivityManager.java140
-rw-r--r--car-lib/src/android/car/app/ICarActivityService.aidl29
-rw-r--r--service/AndroidManifest.xml8
-rw-r--r--service/res/values/strings.xml5
-rw-r--r--service/src/com/android/car/CarFeatureController.java1
-rw-r--r--service/src/com/android/car/ICarImpl.java8
-rw-r--r--service/src/com/android/car/am/CarActivityService.java97
-rw-r--r--tests/EmbeddedKitchenSinkApp/AndroidManifest.xml2
-rw-r--r--tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java91
-rw-r--r--tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java8
-rw-r--r--tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java122
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);
+ }
+}