add CarUserNoticeService
- The Service launches UserNoticeUI Service for following conditions:
cold boot, user switching, wake up from sleep.
- UI is set from config_userNoticeUiService CarService resource.
- The feature is disabled if CarService's resource string is empty.
- UI launching can be disabled by setting
CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER to 0.
- UI Service should implement IUserNoticeUI binder.
- Added dummy UI to Kitchensink for manual testing. It is intentionally
handling binder messages without classes generated from aidl so that it can
be implemented without adding public API.
- The dummy UI uses AlertDialog with TYPE_APPLICATION_OVERLAY window type to
show it above normal Activities.
- Target package specified will be auto-granted TYPE_SYSTEM_ALERT permission
through appops. Permission granting only works for TYPE_APPLICATION_OVERLAY.
UI using any higher priority window should resolve necessary permission by
itself.
Bug: 140875332
Bug: 140032243
Test: Test with added dummy UI and confirm that it shows up for mentioned
events.
adb reboot
user switching
suspend to ram and wakeup:
call once: adb shell setprop android.car.garagemodeduration 1
adb shell dumpsys car_service suspend
adb shell dumpsys car_service resume
Merged-In: I544d2c7fe2e821ee9794e49d00d35c5977912632
Change-Id: I544d2c7fe2e821ee9794e49d00d35c5977912632
diff --git a/car-lib/src/android/car/settings/CarSettings.java b/car-lib/src/android/car/settings/CarSettings.java
index c307cab..ab7c906 100644
--- a/car-lib/src/android/car/settings/CarSettings.java
+++ b/car-lib/src/android/car/settings/CarSettings.java
@@ -144,5 +144,14 @@
*/
public static final String KEY_BLUETOOTH_PROFILES_INHIBITED =
"android.car.BLUETOOTH_PROFILES_INHIBITED";
+
+ /**
+ * Key to enable / disable initial notice screen that will be shown for all user-starting
+ * moments including cold boot, wake up from suspend, and user switching.
+ * The value is boolean (1 or 0).
+ * @hide
+ */
+ public static final String KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER =
+ "android.car.ENABLE_INITIAL_NOTICE_SCREEN_TO_USER";
}
}
diff --git a/car-lib/src/android/car/user/IUserNotice.aidl b/car-lib/src/android/car/user/IUserNotice.aidl
new file mode 100644
index 0000000..1debc84
--- /dev/null
+++ b/car-lib/src/android/car/user/IUserNotice.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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.user;
+
+/**
+ * Binder for UserNotice UI to notify status change to CarUserNoticeService/CarService.
+ * This binder is implemented inside CarService.
+ * @hide
+*/
+interface IUserNotice {
+ /**
+ * Notify CarUserNoticeService/CarSercice that UI dialog is dismissed.
+ * CarUserNoticeService will unbind the UI servie to finish it.
+ */
+ void onDialogDismissed();
+}
\ No newline at end of file
diff --git a/car-lib/src/android/car/user/IUserNoticeUI.aidl b/car-lib/src/android/car/user/IUserNoticeUI.aidl
new file mode 100644
index 0000000..44717d0
--- /dev/null
+++ b/car-lib/src/android/car/user/IUserNoticeUI.aidl
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 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.user;
+
+import android.car.user.IUserNotice;
+
+/**
+ * Binder for CarUserNoticeService/CarService to pass IUserNotice binder to UserNotice UI.
+ * UserNotice UI implements this binder.
+ * @hide
+*/
+oneway interface IUserNoticeUI {
+ /**
+ * CarUserNoticeService will use this call to pass IUserNotice binder which can be used
+ * to notify dismissal of UI dialog.
+ */
+ void setCallbackBinder(in IUserNotice binder);
+}
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index 7d85445..ebea889 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -492,6 +492,7 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" />
<uses-permission android:name="android.permission.MANAGE_USERS" />
<uses-permission android:name="android.permission.LOCATION_HARDWARE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 370fe5d..3eb1007 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -229,4 +229,11 @@
There is no default bugreporting app.-->
<string name="config_car_bugreport_application" translatable="false"></string>
+ <!-- Specifies notice UI that will be launched when user starts a car or do user
+ switching. It is recommended to use dialog with at least TYPE_APPLICATION_OVERLAY window
+ type to show the UI regardless of activity launches. Target package will be auto-granted
+ necessary permission for TYPE_APPLICATION_OVERLAY window type. The UI package should
+ resolve permission by itself to use any higher priority window type.
+ Setting this string to empty will disable the feature. -->
+ <string name="config_userNoticeUiService" translatable="false">com.google.android.car.kitchensink/.UserNoiticeDemoUiService</string>
</resources>
diff --git a/service/src/com/android/car/CarLog.java b/service/src/com/android/car/CarLog.java
index ede5565..a77a549 100644
--- a/service/src/com/android/car/CarLog.java
+++ b/service/src/com/android/car/CarLog.java
@@ -26,9 +26,9 @@
public static final String TAG_CAMERA = "CAR.CAMERA";
public static final String TAG_CAN_BUS = "CAR.CAN_BUS";
public static final String TAG_CLUSTER = "CAR.CLUSTER";
+ public static final String TAG_DIAGNOSTIC = "CAR.DIAGNOSTIC";
public static final String TAG_HAL = "CAR.HAL";
public static final String TAG_HVAC = "CAR.HVAC";
- public static final String TAG_VENDOR_EXT = "CAR.VENDOR_EXT";
public static final String TAG_INFO = "CAR.INFO";
public static final String TAG_INPUT = "CAR.INPUT";
public static final String TAG_MEDIA = "CAR.MEDIA";
@@ -40,10 +40,11 @@
public static final String TAG_PROPERTY = "CAR.PROPERTY";
public static final String TAG_SENSOR = "CAR.SENSOR";
public static final String TAG_SERVICE = "CAR.SERVICE";
+ public static final String TAG_STORAGE = "CAR.STORAGE";
public static final String TAG_SYS = "CAR.SYS";
public static final String TAG_TEST = "CAR.TEST";
- public static final String TAG_DIAGNOSTIC = "CAR.DIAGNOSTIC";
- public static final String TAG_STORAGE = "CAR.STORAGE";
+ public static final String TAG_USER = "CAR.USER";
+ public static final String TAG_VENDOR_EXT = "CAR.VENDOR_EXT";
public static String concatTag(String tagPrefix, Class clazz) {
String tag = tagPrefix + "." + clazz.getSimpleName();
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 8527208..fe7c4d6 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -45,6 +45,7 @@
import com.android.car.pm.CarPackageManagerService;
import com.android.car.systeminterface.SystemInterface;
import com.android.car.trust.CarTrustedDeviceService;
+import com.android.car.user.CarUserNoticeService;
import com.android.car.user.CarUserService;
import com.android.car.vms.VmsBrokerService;
import com.android.car.vms.VmsClientManager;
@@ -92,6 +93,7 @@
private final CarMediaService mCarMediaService;
private final CarUserManagerHelper mUserManagerHelper;
private final CarUserService mCarUserService;
+ private final CarUserNoticeService mCarUserNoticeService;
private final VmsClientManager mVmsClientManager;
private final VmsBrokerService mVmsBrokerService;
private final VmsSubscriberService mVmsSubscriberService;
@@ -129,6 +131,7 @@
mSystemActivityMonitoringService = new SystemActivityMonitoringService(serviceContext);
mCarPowerManagementService = new CarPowerManagementService(mContext, mHal.getPowerHal(),
systemInterface, mUserManagerHelper);
+ mCarUserNoticeService = new CarUserNoticeService(serviceContext);
mCarPropertyService = new CarPropertyService(serviceContext, mHal.getPropertyHal());
mCarDrivingStateService = new CarDrivingStateService(serviceContext, mCarPropertyService);
mCarUXRestrictionsService = new CarUxRestrictionsManagerService(serviceContext,
@@ -186,6 +189,7 @@
allServices.add(mCarPackageManagerService);
allServices.add(mCarInputService);
allServices.add(mGarageModeService);
+ allServices.add(mCarUserNoticeService);
allServices.add(mAppFocusService);
allServices.add(mCarAudioService);
allServices.add(mCarNightService);
diff --git a/service/src/com/android/car/user/CarUserNoticeService.java b/service/src/com/android/car/user/CarUserNoticeService.java
new file mode 100644
index 0000000..22640e2
--- /dev/null
+++ b/service/src/com/android/car/user/CarUserNoticeService.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2019 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.user;
+
+import static android.car.hardware.power.CarPowerManager.CarPowerStateListener;
+
+import static com.android.car.CarLog.TAG_USER;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.car.CarNotConnectedException;
+import android.car.hardware.power.CarPowerManager;
+import android.car.settings.CarSettings;
+import android.car.user.IUserNotice;
+import android.car.user.IUserNoticeUI;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
+import com.android.car.CarLocalServices;
+import com.android.car.CarServiceBase;
+import com.android.car.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+
+/**
+ * Service to show initial notice UI to user. It only launches it when setting is enabled and
+ * it is up to notice UI (=Service) to dismiss itself upon user's request.
+ *
+ * <p>Conditions to show notice UI are:
+ * <ol>
+ * <li>Cold boot
+ * <li><User switching
+ * <li>Car power state change to ON (happens in wakeup from suspend to RAM)
+ * </ol>
+ */
+public final class CarUserNoticeService implements CarServiceBase {
+
+ // Keyguard unlocking can be only polled as we cannot dismiss keyboard.
+ // Polling will stop when keyguard is unlocked.
+ private static final long KEYGUARD_POLLING_INTERVAL_MS = 100;
+
+ private final Context mContext;
+
+ // null means feature disabled.
+ @Nullable
+ private final Intent mServiceIntent;
+
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ private final Object mLock = new Object();
+
+ // This one records if there is a service bound. This will be cleared as soon as service is
+ // unbound (=UI dismissed)
+ @GuardedBy("mLock")
+ private boolean mServiceBound = false;
+
+ // This one represents if UI is shown for the current session. This should be kept until
+ // next event to show UI comes up.
+ @GuardedBy("mLock")
+ private boolean mUiShown = false;
+
+ @GuardedBy("mLock")
+ @UserIdInt
+ private int mUserId = UserHandle.USER_NULL;
+
+ @GuardedBy("mLock")
+ private CarPowerManager mCarPowerManager;
+
+ @GuardedBy("mLock")
+ private IUserNoticeUI mUiService;
+
+ private final CarUserService.UserCallback mUserCallback = new CarUserService.UserCallback() {
+ @Override
+ public void onUserLockChanged(@UserIdInt int userId, boolean unlocked) {
+ // Nothing to do
+ }
+
+ @Override
+ public void onSwitchUser(@UserIdInt int userId) {
+ mMainHandler.post(() -> {
+ stopUi(/* clearUiShown= */ true);
+ synchronized (mLock) {
+ // This should be the only place to change user
+ mUserId = userId;
+ }
+ startNoticeUiIfNecessary();
+ });
+ }
+ };
+
+ private final CarPowerStateListener mPowerStateListener = new CarPowerStateListener() {
+ @Override
+ public void onStateChanged(int state) {
+ if (state == CarPowerManager.CarPowerStateListener.SHUTDOWN_PREPARE) {
+ mMainHandler.post(() -> stopUi(/* clearUiShown= */ true));
+ } else if (state == CarPowerManager.CarPowerStateListener.ON) {
+ // Only ON can be relied on as car can restart while in garage mode.
+ mMainHandler.post(() -> startNoticeUiIfNecessary());
+ }
+ }
+ };
+
+ private final BroadcastReceiver mDisplayBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Runs in main thread, so do not use Handler.
+ if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+ if (isDisplayOn()) {
+ Log.i(TAG_USER, "SCREEN_OFF while display is already on");
+ return;
+ }
+ Log.i(TAG_USER, "Display off, stopping UI");
+ stopUi(/* clearUiShown= */ true);
+ } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
+ if (!isDisplayOn()) {
+ Log.i(TAG_USER, "SCREEN_ON while display is already off");
+ return;
+ }
+ Log.i(TAG_USER, "Display on, starting UI");
+ startNoticeUiIfNecessary();
+ }
+ }
+ };
+
+ private final IUserNotice.Stub mIUserNotice = new IUserNotice.Stub() {
+ @Override
+ public void onDialogDismissed() {
+ mMainHandler.post(() -> stopUi(/* clearUiShown= */ false));
+ }
+ };
+
+ private final ServiceConnection mUiServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mLock) {
+ if (!mServiceBound) {
+ // already unbound but passed due to timing. This should be just ignored.
+ return;
+ }
+ }
+ IUserNoticeUI binder = IUserNoticeUI.Stub.asInterface(service);
+ try {
+ binder.setCallbackBinder(mIUserNotice);
+ } catch (RemoteException e) {
+ Log.w(TAG_USER, "UserNoticeUI Service died", e);
+ // Wait for reconnect
+ binder = null;
+ }
+ synchronized (mLock) {
+ mUiService = binder;
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ // UI crashed. Stop it so that it does not come again.
+ stopUi(/* clearUiShown= */ true);
+ }
+ };
+
+ // added for debugging purpose
+ @GuardedBy("mLock")
+ private int mKeyguardPollingCounter;
+
+ private final Runnable mKeyguardPollingRunnable = () -> {
+ synchronized (mLock) {
+ mKeyguardPollingCounter++;
+ }
+ startNoticeUiIfNecessary();
+ };
+
+ public CarUserNoticeService(Context context) {
+ Resources res = context.getResources();
+ String componentName = res.getString(R.string.config_userNoticeUiService);
+ if (componentName.isEmpty()) {
+ // feature disabled
+ mContext = null;
+ mServiceIntent = null;
+ return;
+ }
+ mContext = context;
+ mServiceIntent = new Intent();
+ mServiceIntent.setComponent(ComponentName.unflattenFromString(componentName));
+ }
+
+ private boolean checkKeyguardLockedWithPolling() {
+ mMainHandler.removeCallbacks(mKeyguardPollingRunnable);
+ IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
+ boolean locked = true;
+ if (wm != null) {
+ try {
+ locked = wm.isKeyguardLocked();
+ } catch (RemoteException e) {
+ Log.w(TAG_USER, "system server crashed", e);
+ }
+ }
+ if (locked) {
+ mMainHandler.postDelayed(mKeyguardPollingRunnable, KEYGUARD_POLLING_INTERVAL_MS);
+ }
+ return locked;
+ }
+
+ private boolean isNoticeScreenEnabledInSetting(@UserIdInt int userId) {
+ return Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER,
+ 1 /*enable by default*/, userId) == 1;
+ }
+
+ private boolean isDisplayOn() {
+ PowerManager pm = mContext.getSystemService(PowerManager.class);
+ if (pm == null) {
+ return false;
+ }
+ return pm.isInteractive();
+ }
+
+ private boolean grantSystemAlertWindowPermission(@UserIdInt int userId) {
+ AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class);
+ if (appOpsManager == null) {
+ Log.w(TAG_USER, "AppOpsManager not ready yet");
+ return false;
+ }
+ String packageName = mServiceIntent.getComponent().getPackageName();
+ int packageUid;
+ try {
+ packageUid = mContext.getPackageManager().getPackageUidAsUser(packageName, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.wtf(TAG_USER, "Target package for config_userNoticeUiService not found:"
+ + packageName + " userId:" + userId);
+ return false;
+ }
+ appOpsManager.setMode(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, packageUid, packageName,
+ AppOpsManager.MODE_ALLOWED);
+ Log.i(TAG_USER, "Granted SYSTEM_ALERT_WINDOW permission to package:" + packageName
+ + " package uid:" + packageUid);
+ return true;
+ }
+
+ private void startNoticeUiIfNecessary() {
+ int userId;
+ synchronized (mLock) {
+ if (mUiShown || mServiceBound) {
+ return;
+ }
+ userId = mUserId;
+ }
+ if (userId == UserHandle.USER_NULL) {
+ return;
+ }
+ // headless user 0 is ignored.
+ if (userId == UserHandle.USER_SYSTEM) {
+ return;
+ }
+ if (!isNoticeScreenEnabledInSetting(userId)) {
+ return;
+ }
+ if (userId != ActivityManager.getCurrentUser()) {
+ // user has switched. will be handled by user switch callback
+ return;
+ }
+ // Dialog can be not shown if display is off.
+ // DISPLAY_ON broadcast will handle this later.
+ if (!isDisplayOn()) {
+ return;
+ }
+ // Do not show it until keyguard is dismissed.
+ if (checkKeyguardLockedWithPolling()) {
+ return;
+ }
+ if (!grantSystemAlertWindowPermission(userId)) {
+ return;
+ }
+ boolean bound = mContext.bindServiceAsUser(mServiceIntent, mUiServiceConnection,
+ Context.BIND_AUTO_CREATE, UserHandle.of(userId));
+ if (bound) {
+ Log.i(TAG_USER, "Bound UserNoticeUI Service Service:" + mServiceIntent);
+ synchronized (mLock) {
+ mServiceBound = true;
+ mUiShown = true;
+ }
+ } else {
+ Log.w(TAG_USER, "Cannot bind to UserNoticeUI Service Service" + mServiceIntent);
+ }
+ }
+
+ private void stopUi(boolean clearUiShown) {
+ mMainHandler.removeCallbacks(mKeyguardPollingRunnable);
+ boolean serviceBound;
+ synchronized (mLock) {
+ mUiService = null;
+ serviceBound = mServiceBound;
+ mServiceBound = false;
+ if (clearUiShown) {
+ mUiShown = false;
+ }
+ }
+ if (serviceBound) {
+ Log.i(TAG_USER, "Unbound UserNoticeUI Service");
+ mContext.unbindService(mUiServiceConnection);
+ }
+ }
+
+ @Override
+ public void init() {
+ if (mServiceIntent == null) {
+ // feature disabled
+ return;
+ }
+
+ CarPowerManager carPowerManager;
+ synchronized (mLock) {
+ mCarPowerManager = CarLocalServices.createCarPowerManager(mContext);
+ carPowerManager = mCarPowerManager;
+ }
+ try {
+ carPowerManager.setListener(mPowerStateListener);
+ } catch (CarNotConnectedException e) {
+ // should not happen
+ throw new RuntimeException("CarNotConnectedException from CarPowerManager", e);
+ }
+ CarUserService userService = CarLocalServices.getService(CarUserService.class);
+ userService.addUserCallback(mUserCallback);
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ intentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mDisplayBroadcastReceiver, intentFilter);
+ }
+
+ @Override
+ public void release() {
+ if (mServiceIntent == null) {
+ // feature disabled
+ return;
+ }
+ mContext.unregisterReceiver(mDisplayBroadcastReceiver);
+ CarUserService userService = CarLocalServices.getService(CarUserService.class);
+ userService.removeUserCallback(mUserCallback);
+ CarPowerManager carPowerManager;
+ synchronized (mLock) {
+ carPowerManager = mCarPowerManager;
+ mUserId = UserHandle.USER_NULL;
+ }
+ carPowerManager.clearListener();
+ stopUi(/* clearUiShown= */ true);
+ }
+
+ @Override
+ public void dump(PrintWriter writer) {
+ synchronized (mLock) {
+ if (mServiceIntent == null) {
+ writer.println("*CarUserNoticeService* disabled");
+ return;
+ }
+ if (mUserId == UserHandle.USER_NULL) {
+ writer.println("*CarUserNoticeService* User not started yet.");
+ return;
+ }
+ writer.println("*CarUserNoticeService* mServiceIntent:" + mServiceIntent
+ + ", mUserId:" + mUserId
+ + ", mUiShown:" + mUiShown
+ + ", mServiceBound:" + mServiceBound
+ + ", mKeyguardPollingCounter:" + mKeyguardPollingCounter
+ + " Setting enabled:" + isNoticeScreenEnabledInSetting(mUserId));
+ }
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index a8b8356..8997e32 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -108,6 +108,8 @@
android:exported="false" android:directBootAware="true">
</service>
+ <service android:name=".UserNoiticeDemoUiService" android:directBootAware="true" />
+
<!-- Content provider for images -->
<provider android:name=".cluster.ClusterContentProvider"
android:authorities="com.google.android.car.kitchensink.cluster.clustercontentprovider"
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 27844f3..6575ce1 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -327,4 +327,9 @@
<string name="disconnect" translatable="false">disconnect</string>
<string name="createcar" translatable="false">createCar</string>
<string name="createcar_with_status_change" translatable="false">createCarWithStatusChange</string>
+
+ <!-- UserNoiticeDemoUiService -->
+ <string name="usernotice" translatable="false">This screen is for showing initial user notice and is not for product. Plz change config_userNoticeUiService in CarService before shipping.</string>
+ <string name="dismiss_now" translatable="false">Dismiss for now</string>
+ <string name="dismiss_forever" translatable="false">Do not show again</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/UserNoiticeDemoUiService.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/UserNoiticeDemoUiService.java
new file mode 100644
index 0000000..dfe18dc
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/UserNoiticeDemoUiService.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2019 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.google.android.car.kitchensink;
+
+import android.app.AlertDialog;
+import android.app.Service;
+import android.car.settings.CarSettings;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Example service of implementing UserNoticeUI.
+ * <p>IUserNotice and IUserNoticeUI are intentionally accessed / implemented without using the
+ * generated code from aidl so that this can be done without accessing hidden API.
+ */
+public class UserNoiticeDemoUiService extends Service {
+
+ private static final String TAG = UserNoiticeDemoUiService.class.getSimpleName();
+
+ private static final String IUSER_NOTICE_BINDER_DESCRIPTOR = "android.car.user.IUserNotice";
+ private static final int IUSER_NOTICE_TR_ON_DIALOG_DISMISSED =
+ android.os.IBinder.FIRST_CALL_TRANSACTION;
+
+ private static final String IUSER_NOTICE_UI_BINDER_DESCRIPTOR =
+ "android.car.user.IUserNoticeUI";
+ private static final int IUSER_NOTICE_UI_BINDER_TR_SET_CALLBACK =
+ android.os.IBinder.FIRST_CALL_TRANSACTION;
+
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ private final Object mLock = new Object();
+
+ // Do not use IUserNoticeUI class intentionally to show how it can be
+ // implemented without accessing the hidden API.
+ private IBinder mIUserNoticeUiBinder = new Binder() {
+ @Override
+ protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ switch (code) {
+ case IUSER_NOTICE_UI_BINDER_TR_SET_CALLBACK:
+ data.enforceInterface(IUSER_NOTICE_UI_BINDER_DESCRIPTOR);
+ IBinder binder = data.readStrongBinder();
+ onSetCallbackBinder(binder);
+ return true;
+ default:
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ };
+
+ @GuardedBy("mLock")
+ private IBinder mIUserNoticeService;
+
+ @GuardedBy("mLock")
+ private AlertDialog mDialog;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mIUserNoticeUiBinder;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ stopDialog(true);
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ stopDialog(true);
+ }
+
+ private void onSetCallbackBinder(IBinder binder) {
+ if (binder == null) {
+ Log.wtf(TAG, "No binder set in onSetCallbackBinder call", new RuntimeException());
+ return;
+ }
+ mMainHandler.post(() -> {
+ synchronized (mLock) {
+ mIUserNoticeService = binder;
+ }
+ startDialog();
+ });
+ }
+
+ private void startDialog() {
+ synchronized (mLock) {
+ if (mDialog != null) {
+ Log.wtf(TAG, "Dialog already created", new RuntimeException());
+ return;
+ }
+ mDialog = createDialog();
+ // Necessary permission is auto-granted by car service before starting this.
+ mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+ mDialog.setCancelable(false);
+ }
+ mDialog.show();
+ }
+
+ private void stopDialog(boolean dismiss) {
+ IBinder userNotice;
+ AlertDialog dialog;
+ synchronized (mLock) {
+ userNotice = mIUserNoticeService;
+ dialog = mDialog;
+ mDialog = null;
+ mIUserNoticeService = null;
+ }
+ if (userNotice != null) {
+ sendOnDialogDismissedToCarService(userNotice);
+ }
+ if (dialog != null && dismiss) {
+ dialog.dismiss();
+ }
+ stopSelf();
+ }
+
+ private void sendOnDialogDismissedToCarService(IBinder userNotice) {
+ Parcel data = Parcel.obtain();
+ data.writeInterfaceToken(IUSER_NOTICE_BINDER_DESCRIPTOR);
+ try {
+ userNotice.transact(IUSER_NOTICE_TR_ON_DIALOG_DISMISSED, data, null, 0);
+ } catch (RemoteException e) {
+ Log.w(TAG, "CarService crashed, finish now");
+ stopSelf();
+ }
+ }
+
+ private AlertDialog createDialog() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ AlertDialog dialog = builder.setMessage(R.string.usernotice)
+ .setPositiveButton(R.string.dismiss_now, (DialogInterface d, int w) -> {
+ stopDialog(true);
+ })
+ .setNegativeButton(R.string.dismiss_forever, (DialogInterface d, int w) -> {
+ Settings.Secure.putInt(getContentResolver(),
+ CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER,
+ /* enable= */ 0);
+ stopDialog(true);
+ })
+ .setOnDismissListener((DialogInterface d) -> {
+ stopDialog(false);
+ })
+ .create();
+ return dialog;
+ }
+}