Limit relaunch retry if it keeps failing
- Limit it to 5 times with 500ms delay (about 2 secs for total duration)
- Being alive for more than 2 mins reset the crash counter.
- Retry for user switching, power ON (including wakeup from sleep),
and package update.
- Added always crashing Activity and other Activities in kietchensink for testing.
- Add shell command to test fixed mode start and stopping in easier way:
$ adb shell dumpsys car_service start-fixed-activity-mode 1 com.google.android.car.kitchensink com.google.android.car.kitchensink.AlwaysCrashingActivity
$ adb shell dumpsys car_service stop-fixed-activity-mode 1
- Added test script to test all recovery cases: fixed_activity_mode_test.sh
* give up after retry
* retry after package update
* retry after suspend - resume
* successfully launch for NoCrashActivity
* Re-launch the Activity if other Activity launched
* Re-launch after package update
Bug: 141721242
Test: $ ./packages/services/Car/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
Change-Id: I86cdc80705d936c47323be985d6daa1d4ac4d9f4
Merged-In: I86cdc80705d936c47323be985d6daa1d4ac4d9f4
(cherry picked from commit dd163af6f4034d93dccc279a7f611813c81d922e)
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 3c6129e..498ae82 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -18,12 +18,15 @@
import android.annotation.MainThread;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.UiModeManager;
import android.car.Car;
import android.car.ICar;
import android.car.cluster.renderer.IInstrumentClusterNavigation;
import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.automotive.vehicle.V2_0.IVehicle;
@@ -545,6 +548,8 @@
private static final String COMMAND_SUSPEND = "suspend";
private static final String COMMAND_ENABLE_TRUSTED_DEVICE = "enable-trusted-device";
private static final String COMMAND_REMOVE_TRUSTED_DEVICES = "remove-trusted-devices";
+ private static final String COMMAND_START_FIXED_ACTIVITY_MODE = "start-fixed-activity-mode";
+ private static final String COMMAND_STOP_FIXED_ACTIVITY_MODE = "stop-fixed-activity-mode";
private static final String PARAM_DAY_MODE = "day";
private static final String PARAM_NIGHT_MODE = "night";
@@ -589,6 +594,11 @@
+ " wireless projection");
pw.println("\t--metrics");
pw.println("\t When used with dumpsys, only metrics will be in the dumpsys output.");
+ pw.println("\tstart-fixed-activity displayId packageName activityName");
+ pw.println("\t Start an Activity the specified display as fixed mode");
+ pw.println("\tstop-fixed-mode displayId");
+ pw.println("\t Stop fixed Activity mode for the given display. "
+ + "The Activity will not be restarted upon crash.");
}
public void exec(String[] args, PrintWriter writer) {
@@ -715,12 +725,62 @@
.removeAllTrustedDevices(
mUserManagerHelper.getCurrentForegroundUserId());
break;
+ case COMMAND_START_FIXED_ACTIVITY_MODE:
+ handleStartFixedActivity(args, writer);
+ break;
+ case COMMAND_STOP_FIXED_ACTIVITY_MODE:
+ handleStopFixedMode(args, writer);
+ break;
default:
writer.println("Unknown command: \"" + arg + "\"");
dumpHelp(writer);
}
}
+ private void handleStartFixedActivity(String[] args, PrintWriter writer) {
+ if (args.length != 4) {
+ writer.println("Incorrect number of arguments");
+ dumpHelp(writer);
+ return;
+ }
+ int displayId;
+ try {
+ displayId = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ writer.println("Wrong display id:" + args[1]);
+ return;
+ }
+ String packageName = args[2];
+ String activityName = args[3];
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(packageName, activityName));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ ActivityOptions options = ActivityOptions.makeBasic();
+ options.setLaunchDisplayId(displayId);
+ if (!mFixedActivityService.startFixedActivityModeForDisplayAndUser(intent, options,
+ displayId, ActivityManager.getCurrentUser())) {
+ writer.println("Failed to start");
+ return;
+ }
+ writer.println("Succeeded");
+ }
+
+ private void handleStopFixedMode(String[] args, PrintWriter writer) {
+ if (args.length != 2) {
+ writer.println("Incorrect number of arguments");
+ dumpHelp(writer);
+ return;
+ }
+ int displayId;
+ try {
+ displayId = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ writer.println("Wrong display id:" + args[1]);
+ return;
+ }
+ mFixedActivityService.stopFixedActivityMode(displayId);
+ }
+
private void forceDayNightMode(String arg, PrintWriter writer) {
int mode;
switch (arg) {
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
index b8f8608..20885b8 100644
--- a/service/src/com/android/car/am/FixedActivityService.java
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -15,6 +15,9 @@
*/
package com.android.car.am;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.os.Process.INVALID_UID;
+
import static com.android.car.CarLog.TAG_AM;
import android.annotation.NonNull;
@@ -26,6 +29,7 @@
import android.app.IActivityManager;
import android.app.IProcessObserver;
import android.app.TaskStackListener;
+import android.car.hardware.power.CarPowerManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -34,7 +38,10 @@
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.HandlerThread;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -60,6 +67,11 @@
private static final boolean DBG = false;
+ private static final long RECHECK_INTERVAL_MS = 500;
+ private static final int MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY = 5;
+ // If process keep running without crashing, will reset consecutive crash counts.
+ private static final long CRASH_FORGET_INTERVAL_MS = 2 * 60 * 1000; // 2 mins
+
private static class RunningActivityInfo {
@NonNull
public final Intent intent;
@@ -70,9 +82,20 @@
@UserIdInt
public final int userId;
- // Only used in a method for local book-keeping. So do not need a lock.
- // This does not represent the current visibility.
+ @GuardedBy("mLock")
public boolean isVisible;
+ @GuardedBy("mLock")
+ public long lastLaunchTimeMs = 0;
+ @GuardedBy("mLock")
+ public int consecutiveRetries = 0;
+ @GuardedBy("mLock")
+ public int taskId = INVALID_TASK_ID;
+ @GuardedBy("mLock")
+ public int previousTaskId = INVALID_TASK_ID;
+ @GuardedBy("mLock")
+ public boolean inBackground;
+ @GuardedBy("mLock")
+ public boolean failureLogged;
RunningActivityInfo(@NonNull Intent intent, @NonNull ActivityOptions activityOptions,
@UserIdInt int userId) {
@@ -81,10 +104,17 @@
this.userId = userId;
}
+ private void resetCrashCounterLocked() {
+ consecutiveRetries = 0;
+ failureLogged = false;
+ }
+
@Override
public String toString() {
return "RunningActivityInfo{intent:" + intent + ",activityOptions:" + activityOptions
- + ",userId:" + userId + "}";
+ + ",userId:" + userId + ",isVisible:" + isVisible
+ + ",lastLaunchTimeMs:" + lastLaunchTimeMs
+ + ",consecutiveRetries:" + consecutiveRetries + ",taskId:" + taskId + "}";
}
}
@@ -111,11 +141,41 @@
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
+ String action = intent.getAction();
if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
|| Intent.ACTION_PACKAGE_REPLACED.equals(
action)) {
- launchIfNecessary();
+ Uri packageData = intent.getData();
+ if (packageData == null) {
+ Log.w(TAG_AM, "null packageData");
+ return;
+ }
+ String packageName = packageData.getSchemeSpecificPart();
+ if (packageName == null) {
+ Log.w(TAG_AM, "null packageName");
+ return;
+ }
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
+ int userId = UserHandle.getUserId(uid);
+ boolean tryLaunch = false;
+ synchronized (mLock) {
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ RunningActivityInfo info = mRunningActivities.valueAt(i);
+ ComponentName component = info.intent.getComponent();
+ // should do this for all activities as the same package can cover multiple
+ // displays.
+ if (packageName.equals(component.getPackageName())
+ && info.userId == userId) {
+ Log.i(TAG_AM, "Package updated:" + packageName
+ + ",user:" + userId);
+ info.resetCrashCounterLocked();
+ tryLaunch = true;
+ }
+ }
+ }
+ if (tryLaunch) {
+ launchIfNecessary();
+ }
}
}
};
@@ -126,6 +186,26 @@
public void onTaskStackChanged() {
launchIfNecessary();
}
+
+ @Override
+ public void onTaskCreated(int taskId, ComponentName componentName) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ launchIfNecessary();
+ }
+
+ @Override
+ public void onTaskRemovalStarted(int taskId) {
+ launchIfNecessary();
+ }
};
private final IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@@ -145,6 +225,13 @@
}
};
+ private final HandlerThread mHandlerThread = new HandlerThread(
+ FixedActivityService.class.getSimpleName());
+
+ private final Runnable mActivityCheckRunnable = () -> {
+ launchIfNecessary();
+ };
+
private final Object mLock = new Object();
// key: displayId
@@ -155,13 +242,29 @@
@GuardedBy("mLock")
private boolean mEventMonitoringActive;
+ @GuardedBy("mLock")
+ private CarPowerManager mCarPowerManager;
+
+ private final CarPowerManager.CarPowerStateListener mCarPowerStateListener = (state) -> {
+ if (state != CarPowerManager.CarPowerStateListener.ON) {
+ return;
+ }
+ synchronized (mLock) {
+ for (int i = 0; i < mRunningActivities.size(); i++) {
+ RunningActivityInfo info = mRunningActivities.valueAt(i);
+ info.resetCrashCounterLocked();
+ }
+ }
+ launchIfNecessary();
+ };
+
public FixedActivityService(Context context) {
mContext = context;
mAm = ActivityManager.getService();
mUm = context.getSystemService(UserManager.class);
+ mHandlerThread.start();
}
-
@Override
public void init() {
// nothing to do
@@ -181,18 +284,26 @@
}
}
+ private void postRecheck(long delayMs) {
+ mHandlerThread.getThreadHandler().postDelayed(mActivityCheckRunnable, delayMs);
+ }
+
private void startMonitoringEvents() {
+ CarPowerManager carPowerManager;
synchronized (mLock) {
if (mEventMonitoringActive) {
return;
}
mEventMonitoringActive = true;
+ carPowerManager = CarLocalServices.createCarPowerManager(mContext);
+ mCarPowerManager = carPowerManager;
}
CarUserService userService = CarLocalServices.getService(CarUserService.class);
userService.addUserCallback(mUserCallback);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addDataScheme("package");
mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
/* broadcastPermission= */ null, /* scheduler= */ null);
try {
@@ -201,15 +312,28 @@
} catch (RemoteException e) {
Log.e(TAG_AM, "remote exception from AM", e);
}
+ try {
+ carPowerManager.setListener(mCarPowerStateListener);
+ } catch (Exception e) {
+ // should not happen
+ Log.e(TAG_AM, "Got exception from CarPowerManager", e);
+ }
}
private void stopMonitoringEvents() {
+ CarPowerManager carPowerManager;
synchronized (mLock) {
if (!mEventMonitoringActive) {
return;
}
mEventMonitoringActive = false;
+ carPowerManager = mCarPowerManager;
+ mCarPowerManager = null;
}
+ if (carPowerManager != null) {
+ carPowerManager.clearListener();
+ }
+ mHandlerThread.getThreadHandler().removeCallbacks(mActivityCheckRunnable);
CarUserService userService = CarLocalServices.getService(CarUserService.class);
userService.removeUserCallback(mUserCallback);
try {
@@ -243,6 +367,7 @@
Log.e(TAG_AM, "cannot get StackInfo from AM");
return false;
}
+ long now = SystemClock.elapsedRealtime();
synchronized (mLock) {
if (mRunningActivities.size() == 0) {
// it must have been stopped.
@@ -264,16 +389,30 @@
&& activityInfo.userId == topUserId && stackInfo.visible) {
// top one is matching.
activityInfo.isVisible = true;
+ activityInfo.taskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
continue;
}
- if (DBG) {
- Log.i(TAG_AM, "Unmatched top activity:" + stackInfo.topActivity
- + " user:" + topUserId + " display:" + stackInfo.displayId);
+ activityInfo.previousTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
+ Log.i(TAG_AM, "Unmatched top activity will be removed:"
+ + stackInfo.topActivity + " top task id:" + activityInfo.previousTaskId
+ + " user:" + topUserId + " display:" + stackInfo.displayId);
+ activityInfo.inBackground = false;
+ for (int i = 0; i < stackInfo.taskIds.length - 1; i++) {
+ if (activityInfo.taskId == stackInfo.taskIds[i]) {
+ activityInfo.inBackground = true;
+ }
+ }
+ if (!activityInfo.inBackground) {
+ activityInfo.taskId = INVALID_TASK_ID;
}
}
for (int i = 0; i < mRunningActivities.size(); i++) {
RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
+ long timeSinceLastLaunchMs = now - activityInfo.lastLaunchTimeMs;
if (activityInfo.isVisible) {
+ if (timeSinceLastLaunchMs >= CRASH_FORGET_INTERVAL_MS) {
+ activityInfo.consecutiveRetries = 0;
+ }
continue;
}
if (!isComponentAvailable(activityInfo.intent.getComponent(),
@@ -281,14 +420,40 @@
activityInfo.userId)) {
continue;
}
+ // For 1st call (consecutiveRetries == 0), do not wait as there can be no posting
+ // for recheck.
+ if (activityInfo.consecutiveRetries > 0 && (timeSinceLastLaunchMs
+ < RECHECK_INTERVAL_MS)) {
+ // wait until next check interval comes.
+ continue;
+ }
+ if (activityInfo.consecutiveRetries >= MAX_NUMBER_OF_CONSECUTIVE_CRASH_RETRY) {
+ // re-tried too many times, give up for now.
+ if (!activityInfo.failureLogged) {
+ activityInfo.failureLogged = true;
+ Log.w(TAG_AM, "Too many relaunch failure of fixed activity:"
+ + activityInfo);
+ }
+ continue;
+ }
+
Log.i(TAG_AM, "Launching Activity for fixed mode. Intent:" + activityInfo.intent
+ ",userId:" + UserHandle.of(activityInfo.userId) + ",displayId:"
+ mRunningActivities.keyAt(i));
+ // Increase retry count if task is not in background. In case like other app is
+ // launched and the target activity is still in background, do not consider it
+ // as retry.
+ if (!activityInfo.inBackground) {
+ activityInfo.consecutiveRetries++;
+ }
try {
+ postRecheck(RECHECK_INTERVAL_MS);
+ postRecheck(CRASH_FORGET_INTERVAL_MS);
mContext.startActivityAsUser(activityInfo.intent,
activityInfo.activityOptions.toBundle(),
UserHandle.of(activityInfo.userId));
activityInfo.isVisible = true;
+ activityInfo.lastLaunchTimeMs = SystemClock.elapsedRealtime();
} catch (Exception e) { // Catch all for any app related issues.
Log.w(TAG_AM, "Cannot start activity:" + activityInfo.intent, e);
}
@@ -368,6 +533,10 @@
if (!isDisplayAllowedForFixedMode(displayId)) {
return false;
}
+ if (options == null) {
+ Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, null options");
+ return false;
+ }
if (!isUserAllowedToLaunchActivity(userId)) {
Log.e(TAG_AM, "startFixedActivityModeForDisplayAndUser, requested user:" + userId
+ " cannot launch activity, Intent:" + intent);
@@ -390,7 +559,16 @@
startMonitoringEvents = true;
}
RunningActivityInfo activityInfo = mRunningActivities.get(displayId);
- if (activityInfo == null) {
+ boolean replaceEntry = true;
+ if (activityInfo != null && activityInfo.intent.equals(intent)
+ && options.equals(activityInfo.activityOptions)
+ && userId == activityInfo.userId) {
+ replaceEntry = false;
+ if (activityInfo.isVisible) { // already shown.
+ return true;
+ }
+ }
+ if (replaceEntry) {
activityInfo = new RunningActivityInfo(intent, options, userId);
mRunningActivities.put(displayId, activityInfo);
}
diff --git a/service/src/com/android/car/cluster/InstrumentClusterService.java b/service/src/com/android/car/cluster/InstrumentClusterService.java
index df6c476..cc0a6b7 100644
--- a/service/src/com/android/car/cluster/InstrumentClusterService.java
+++ b/service/src/com/android/car/cluster/InstrumentClusterService.java
@@ -30,6 +30,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -133,6 +134,7 @@
@Override
public boolean startFixedActivityModeForDisplayAndUser(Intent intent,
Bundle activityOptionsBundle, int userId) {
+ Binder.clearCallingIdentity();
ActivityOptions options = new ActivityOptions(activityOptionsBundle);
FixedActivityService service = CarLocalServices.getService(
FixedActivityService.class);
@@ -142,6 +144,7 @@
@Override
public void stopFixedActivityMode(int displayId) {
+ Binder.clearCallingIdentity();
FixedActivityService service = CarLocalServices.getService(
FixedActivityService.class);
service.stopFixedActivityMode(displayId);
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 8997e32..5612807 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -116,5 +116,26 @@
android:grantUriPermissions="true"
android:exported="true" />
+ <activity android:name=".AlwaysCrashingActivity"
+ android:label="@string/always_crashing_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".NoCrashActivity"
+ android:label="@string/no_crash_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".EmptyActivity"
+ android:label="@string/empty_activity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
</application>
</manifest>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
new file mode 100644
index 0000000..5312fee
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/empty_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/empty_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/empty_activity"
+ android:layout_weight="1" />
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 6575ce1..0d56369 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -332,4 +332,9 @@
<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>
+
+ <!-- [AlwaysCrashing|NoCrash|Empty]Activity -->
+ <string name="always_crashing_activity" translatable="false">Always Crash Activity</string>
+ <string name="no_crash_activity" translatable="false">No Crash Activity</string>
+ <string name="empty_activity" translatable="false">Empty Activity</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
new file mode 100644
index 0000000..05529b2
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/AlwaysCrashingActivity.java
@@ -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 com.google.android.car.kitchensink;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+/**
+ * Activity for testing purpose. This one always crashes inside onCreate.
+ */
+public class AlwaysCrashingActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ throw new RuntimeException("Intended crash for testing");
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
new file mode 100644
index 0000000..aad25cb
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/EmptyActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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.Activity;
+import android.os.Bundle;
+
+public class EmptyActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.empty_activity);
+ }
+
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
new file mode 100644
index 0000000..f10e1db
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/NoCrashActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class NoCrashActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.empty_activity);
+ TextView text = findViewById(R.id.empty_text);
+ text.setText(R.string.no_crash_activity);
+ }
+}
diff --git a/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
new file mode 100755
index 0000000..8beb1f4
--- /dev/null
+++ b/tests/fixed_activity_mode_test/fixed_activity_mode_test.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+if [ -z "$ANDROID_PRODUCT_OUT" ]; then
+ echo "ANDROID_PRODUCT_OUT not set"
+ exit
+fi
+DISP_ID=1
+if [[ $# -eq 1 ]]; then
+ echo "$1"
+ DISP_ID=$1
+fi
+echo "Use display:$DISP_ID"
+
+adb root
+# Check always crashing one
+echo "Start AlwaysCrashingActivity in fixed mode"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.AlwaysCrashingActivity
+sleep 1
+read -p "AlwaysCrashingAvtivity should not be tried any more. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# suspend-resume
+echo "Check retry for suspend - resume"
+adb shell setprop android.car.garagemodeduration 1
+adb shell dumpsys car_service suspend
+adb shell dumpsys car_service resume
+read -p "AlwaysCrashingAvtivity should have been retried. Press Enter"
+# starting other Activity
+echo "Switch to no crash Activity"
+adb shell dumpsys car_service start-fixed-activity-mode $DISP_ID com.google.android.car.kitchensink com.google.android.car.kitchensink.NoCrashActivity
+read -p "NoCrashAvtivity should have been shown. Press Enter"
+# stating other non-fixed Activity
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "NoCrashAvtivity should be shown after showing EmptyActivity. Press Enter"
+# package update
+echo "Will try package update:"
+adb install -r -g $ANDROID_PRODUCT_OUT/system/priv-app/EmbeddedKitchenSinkApp/EmbeddedKitchenSinkApp.apk
+read -p "NoCrashActivity should be shown. Press Enter"
+# stop the mode
+echo "Stop fixed activity mode"
+adb shell dumpsys car_service stop-fixed-activity-mode $DISP_ID
+adb shell am start-activity --display $DISP_ID -n com.google.android.car.kitchensink/.EmptyActivity
+read -p "EmptyActivity should be shown. Press Enter"