Preserve RemoteCarTaskViews on activity recreation
- Use a ViewModel to preserve task views on configuration change in
CarLauncher.
- Use Window context instead of activity context while creating the
controlled remote car task views.
- Use ViewModel.Factory for dependency injection.
Bug: 296957172
Test: m
Test: atest CarLauncherTests
Change-Id: Ide8bba0bd59c1033676145e263471fdeda855d70
diff --git a/app/src/com/android/car/carlauncher/CarLauncher.java b/app/src/com/android/car/carlauncher/CarLauncher.java
index 05a90db..c686584 100644
--- a/app/src/com/android/car/carlauncher/CarLauncher.java
+++ b/app/src/com/android/car/carlauncher/CarLauncher.java
@@ -20,23 +20,17 @@
import static android.car.settings.CarSettings.Secure.KEY_USER_TOS_ACCEPTED;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static com.android.car.carlauncher.CarLauncherViewModel.CarLauncherViewModelFactory;
+
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.TaskStackListener;
import android.car.Car;
-import android.car.app.CarActivityManager;
-import android.car.app.CarTaskViewController;
-import android.car.app.CarTaskViewControllerCallback;
-import android.car.app.ControlledRemoteCarTaskView;
-import android.car.app.ControlledRemoteCarTaskViewCallback;
-import android.car.app.ControlledRemoteCarTaskViewConfig;
import android.car.user.CarUserManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.database.ContentObserver;
-import android.graphics.Color;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.UserManager;
@@ -46,7 +40,6 @@
import android.view.ViewGroup;
import android.view.WindowManager;
-import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
@@ -76,15 +69,13 @@
*/
public class CarLauncher extends FragmentActivity {
public static final String TAG = "CarLauncher";
- private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private static boolean sAutoRestartOnCrash = false;
+ public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private ActivityManager mActivityManager;
private TaskViewManager mTaskViewManager;
private Car mCar;
private CarTaskView mTaskView;
- private ControlledRemoteCarTaskView mRemoteCarTaskView;
private int mCarLauncherTaskId = INVALID_TASK_ID;
private Set<HomeCardModule> mHomeCardModules;
@@ -93,6 +84,7 @@
private boolean mUseSmallCanvasOptimizedMap;
private boolean mUseRemoteCarTaskView;
private ViewGroup mMapsCard;
+ private CarLauncherViewModel mCarLauncherViewModel;
@VisibleForTesting
ContentObserver mTosContentObserver;
@@ -212,93 +204,17 @@
}
private void setupRemoteCarTaskView(ViewGroup parent) {
- long createCarStartTime = System.nanoTime();
- mCar = Car.createCar(this);
- if (DEBUG) {
- long createCarEndTime = System.nanoTime();
- Log.d(TAG, "Initialization of the CarApi took "
- + (createCarEndTime - createCarStartTime)
- + " nanoseconds");
- }
- CarActivityManager carAM = mCar.getCarManager(CarActivityManager.class);
+ mCarLauncherViewModel = new ViewModelProvider(this,
+ new CarLauncherViewModelFactory(this, getMapsIntent()))
+ .get(CarLauncherViewModel.class);
- if (Build.IS_USER) {
- sAutoRestartOnCrash = true;
- }
+ getLifecycle().addObserver(mCarLauncherViewModel);
- carAM.getCarTaskViewController(
- this,
- getMainExecutor(),
- new CarTaskViewControllerCallback() {
- @Override
- public void onConnected(
- CarTaskViewController carTaskViewController) {
- carTaskViewController.createControlledRemoteCarTaskView(
- new ControlledRemoteCarTaskViewConfig.Builder()
- .setActivityIntent(getMapsIntent())
- .setShouldAutoRestartOnTaskRemoval(sAutoRestartOnCrash)
- .build(),
- getMainExecutor(),
- new ControlledRemoteCarTaskViewCallback() {
- @Override
- public void onTaskViewCreated(
- ControlledRemoteCarTaskView taskView) {
- mRemoteCarTaskView = taskView;
- parent.addView(taskView);
- }
-
- @Override
- public void onTaskViewInitialized() {
- maybeLogReady();
- }
-
- @Override
- public void onTaskViewReleased() {
- mRemoteCarTaskView = null;
- parent.removeAllViews();
- }
-
- @Override
- public void onTaskAppeared(
- @NonNull ActivityManager.RunningTaskInfo taskInfo) {
- if (DEBUG) {
- Log.d(TAG, "MapsTaskView: onTaskAppeared: taskId="
- + taskInfo.taskId + " at "
- + System.currentTimeMillis() + " milliseconds");
- }
- if (!sAutoRestartOnCrash) {
- mRemoteCarTaskView.setBackgroundColor(
- Color.TRANSPARENT);
- }
- }
-
- @Override
- public void onTaskVanished(
- @NonNull ActivityManager.RunningTaskInfo taskInfo) {
- if (DEBUG) {
- Log.d(TAG, "MapsTaskView: onTaskVanished: taskId="
- + taskInfo.taskId + " at "
- + System.currentTimeMillis() + " milliseconds");
- }
- if (!sAutoRestartOnCrash) {
- // RemoteCarTaskView color is set to red to indicate
- // that nothing is wrong with the task view but maps
- // in the task view has crashed. More details in
- // b/247156851.
- mRemoteCarTaskView.setBackgroundColor(Color.RED);
- }
- }
- });
- }
-
- @Override
- public void onDisconnected(
- CarTaskViewController carTaskViewController) {
- Log.d(TAG, "onDisconnected");
- mRemoteCarTaskView = null;
- parent.removeAllViews();
- }
- });
+ mCarLauncherViewModel.getRemoteCarTaskView().observe(this, taskView -> {
+ if (taskView != null && taskView.getParent() == null) {
+ parent.addView(taskView);
+ }
+ });
}
private void setUpTaskView(ViewGroup parent) {
@@ -361,25 +277,22 @@
release();
}
- private boolean isTaskViewInitialized() {
- return (mTaskView != null && mTaskView.isInitialized())
- || (mRemoteCarTaskView != null && mRemoteCarTaskView.isInitialized());
- }
-
private int getTaskViewTaskId() {
if (mTaskView != null) {
return mTaskView.getTaskId();
}
- if (mRemoteCarTaskView != null) {
- return mRemoteCarTaskView.getTaskInfo() == null
- ? INVALID_TASK_ID : mRemoteCarTaskView.getTaskInfo().taskId;
+ if (mCarLauncherViewModel != null) {
+ return mCarLauncherViewModel.getRemoteCarTaskViewTaskId();
}
return INVALID_TASK_ID;
}
private void release() {
mTaskView = null;
- mRemoteCarTaskView = null;
+ // When using a ViewModel for the RemoteCarTaskViews, the task view can still be attached
+ // to the mMapsCard due to which the CarLauncher activity does not get garbage collected
+ // during activity recreation.
+ mMapsCard = null;
if (mCar != null) {
mCar.disconnect();
mCar = null;
@@ -424,7 +337,7 @@
/** Logs that the Activity is ready. Used for startup time diagnostics. */
private void maybeLogReady() {
boolean isResumed = isResumed();
- boolean taskViewInitialized = isTaskViewInitialized();
+ boolean taskViewInitialized = mTaskView != null && mTaskView.isInitialized();
if (DEBUG) {
Log.d(TAG, "maybeLogReady(" + getUserId() + "): mapsReady="
+ taskViewInitialized + ", started=" + isResumed + ", alreadyLogged: "
diff --git a/app/src/com/android/car/carlauncher/CarLauncherViewModel.java b/app/src/com/android/car/carlauncher/CarLauncherViewModel.java
new file mode 100644
index 0000000..3bafbd5
--- /dev/null
+++ b/app/src/com/android/car/carlauncher/CarLauncherViewModel.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 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.carlauncher;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.UiContext;
+import android.app.ActivityManager;
+import android.car.Car;
+import android.car.app.CarActivityManager;
+import android.car.app.CarTaskViewController;
+import android.car.app.CarTaskViewControllerCallback;
+import android.car.app.CarTaskViewControllerHostLifecycle;
+import android.car.app.ControlledRemoteCarTaskView;
+import android.car.app.ControlledRemoteCarTaskViewCallback;
+import android.car.app.ControlledRemoteCarTaskViewConfig;
+import android.car.app.RemoteCarTaskView;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+/**
+ * A car launcher view model to manage the lifecycle of {@link RemoteCarTaskView}.
+ */
+public final class CarLauncherViewModel extends ViewModel implements DefaultLifecycleObserver {
+ private static final String TAG = CarLauncher.TAG;
+ private static final boolean DEBUG = CarLauncher.DEBUG;
+ private static final boolean sAutoRestartOnCrash = Build.IS_USER;
+
+ private final CarActivityManager mCarActivityManager;
+ private final Car mCar;
+ private final CarTaskViewControllerHostLifecycle mHostLifecycle;
+ @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the window context.
+ private final Context mWindowContext;
+ private final Intent mMapsIntent;
+ private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
+
+ public CarLauncherViewModel(@UiContext Context context, @NonNull Intent mapsIntent) {
+ mWindowContext = context.createWindowContext(TYPE_APPLICATION_STARTING, /* options */ null);
+ mMapsIntent = mapsIntent;
+ mCar = Car.createCar(mWindowContext);
+ mCarActivityManager = mCar.getCarManager(CarActivityManager.class);
+ mHostLifecycle = new CarTaskViewControllerHostLifecycle();
+ mRemoteCarTaskView = new MutableLiveData<>(null);
+ ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback =
+ new ControlledRemoteCarTaskViewCallbackImpl(mRemoteCarTaskView);
+
+ CarTaskViewControllerCallback carTaskViewControllerCallback =
+ new CarTaskViewControllerCallbackImpl(controlledRemoteCarTaskViewCallback);
+
+ mCarActivityManager.getCarTaskViewController(mWindowContext, mHostLifecycle,
+ mWindowContext.getMainExecutor(), carTaskViewControllerCallback);
+ }
+
+ LiveData<RemoteCarTaskView> getRemoteCarTaskView() {
+ return mRemoteCarTaskView;
+ }
+
+ /**
+ * Returns remote car task view task Id.
+ */
+ public int getRemoteCarTaskViewTaskId() {
+ if (mRemoteCarTaskView != null && mRemoteCarTaskView.getValue() != null
+ && mRemoteCarTaskView.getValue().getTaskInfo() != null) {
+ return mRemoteCarTaskView.getValue().getTaskInfo().taskId;
+ }
+ return INVALID_TASK_ID;
+ }
+
+ /**
+ * Shows remote car task view when activity is resumed.
+ */
+ @Override
+ public void onResume(@NonNull LifecycleOwner owner) {
+ DefaultLifecycleObserver.super.onResume(owner);
+ mHostLifecycle.hostAppeared();
+ }
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ DefaultLifecycleObserver.super.onStop(owner);
+ mHostLifecycle.hostDisappeared();
+ }
+
+ @Override
+ protected void onCleared() {
+ if (mRemoteCarTaskView != null) {
+ mRemoteCarTaskView.setValue(null);
+ }
+ if (mCar != null) {
+ mCar.disconnect();
+ }
+ mHostLifecycle.hostDestroyed();
+ super.onCleared();
+ }
+
+ private static final class ControlledRemoteCarTaskViewCallbackImpl implements
+ ControlledRemoteCarTaskViewCallback {
+ private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
+
+ private ControlledRemoteCarTaskViewCallbackImpl(
+ MutableLiveData<RemoteCarTaskView> remoteCarTaskView) {
+ mRemoteCarTaskView = remoteCarTaskView;
+ }
+
+ @Override
+ public void onTaskViewCreated(@NonNull ControlledRemoteCarTaskView taskView) {
+ mRemoteCarTaskView.setValue(taskView);
+ }
+
+ @Override
+ public void onTaskViewInitialized() {
+ if (DEBUG) {
+ Log.d(TAG, "MapsTaskView: onTaskViewInitialized");
+ }
+ }
+
+ @Override
+ public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "MapsTaskView: onTaskAppeared: taskId=" + taskInfo.taskId);
+ }
+ if (!sAutoRestartOnCrash) {
+ mRemoteCarTaskView.getValue().setBackgroundColor(Color.TRANSPARENT);
+ }
+ }
+
+ @Override
+ public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "MapsTaskView: onTaskVanished: taskId=" + taskInfo.taskId);
+ }
+ if (!sAutoRestartOnCrash) {
+ // RemoteCarTaskView color is set to red to indicate
+ // that nothing is wrong with the task view but maps
+ // in the task view has crashed. More details in
+ // b/247156851.
+ mRemoteCarTaskView.getValue().setBackgroundColor(Color.RED);
+ }
+ }
+ }
+
+ private final class CarTaskViewControllerCallbackImpl implements CarTaskViewControllerCallback {
+ private final ControlledRemoteCarTaskViewCallback mControlledRemoteCarTaskViewCallback;
+
+ private CarTaskViewControllerCallbackImpl(
+ ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback) {
+ mControlledRemoteCarTaskViewCallback = controlledRemoteCarTaskViewCallback;
+ }
+
+ @Override
+ public void onConnected(@NonNull CarTaskViewController carTaskViewController) {
+ carTaskViewController.createControlledRemoteCarTaskView(
+ new ControlledRemoteCarTaskViewConfig.Builder()
+ .setActivityIntent(mMapsIntent)
+ .setShouldAutoRestartOnTaskRemoval(sAutoRestartOnCrash)
+ .build(),
+ mWindowContext.getMainExecutor(),
+ mControlledRemoteCarTaskViewCallback);
+ }
+
+ @Override
+ public void onDisconnected(@NonNull CarTaskViewController carTaskViewController) {
+ if (DEBUG) {
+ Log.d(TAG, "onDisconnected");
+ }
+ mRemoteCarTaskView.setValue(null);
+ }
+ }
+
+ static final class CarLauncherViewModelFactory implements ViewModelProvider.Factory {
+ private final Context mContext;
+ private final Intent mMapsIntent;
+
+ CarLauncherViewModelFactory(@UiContext Context context, @NonNull Intent mapsIntent) {
+ mMapsIntent = requireNonNull(mapsIntent);
+ mContext = requireNonNull(context);
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(Class<T> modelClass) {
+ return modelClass.cast(new CarLauncherViewModel(mContext, mMapsIntent));
+ }
+ }
+}