1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.carlauncher; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; 21 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.NonNull; 25 import android.annotation.SuppressLint; 26 import android.annotation.UiContext; 27 import android.app.ActivityManager; 28 import android.car.Car; 29 import android.car.app.CarActivityManager; 30 import android.car.app.CarTaskViewController; 31 import android.car.app.CarTaskViewControllerCallback; 32 import android.car.app.CarTaskViewControllerHostLifecycle; 33 import android.car.app.ControlledRemoteCarTaskView; 34 import android.car.app.ControlledRemoteCarTaskViewCallback; 35 import android.car.app.ControlledRemoteCarTaskViewConfig; 36 import android.car.app.RemoteCarTaskView; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.graphics.Color; 40 import android.os.Build; 41 import android.util.Log; 42 43 import androidx.core.util.Consumer; 44 import androidx.lifecycle.DefaultLifecycleObserver; 45 import androidx.lifecycle.LifecycleOwner; 46 import androidx.lifecycle.LiveData; 47 import androidx.lifecycle.MutableLiveData; 48 import androidx.lifecycle.ViewModel; 49 import androidx.lifecycle.ViewModelProvider; 50 51 import com.google.common.annotations.VisibleForTesting; 52 53 /** 54 * A car launcher view model to manage the lifecycle of {@link RemoteCarTaskView}. 55 */ 56 public final class CarLauncherViewModel extends ViewModel implements DefaultLifecycleObserver { 57 private static final String TAG = CarLauncher.TAG; 58 private static final boolean DEBUG = CarLauncher.DEBUG; 59 private static final boolean sAutoRestartOnCrash = Build.IS_USER; 60 61 private final CarActivityManager mCarActivityManager; 62 private final Car mCar; 63 @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the window context. 64 private final Context mWindowContext; 65 66 // Do not make this final because the maps intent can be changed based on the state of TOS. 67 private Intent mMapsIntent; 68 private CarTaskViewControllerHostLifecycle mHostLifecycle; 69 private MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView; 70 CarLauncherViewModel(@iContext Context context, Intent mapsIntent)71 public CarLauncherViewModel(@UiContext Context context, Intent mapsIntent) { 72 mWindowContext = context.createWindowContext(TYPE_APPLICATION_STARTING, /* options */ null); 73 mCar = Car.createCar(mWindowContext); 74 mCarActivityManager = mCar.getCarManager(CarActivityManager.class); 75 initializeRemoteCarTaskView(mapsIntent); 76 } 77 78 /** 79 * Initialize the remote car task view with the maps intent. 80 */ initializeRemoteCarTaskView(@onNull Intent mapsIntent)81 public void initializeRemoteCarTaskView(@NonNull Intent mapsIntent) { 82 if (DEBUG) { 83 Log.d(TAG, "Maps intent in the task view = " + mapsIntent.getComponent()); 84 } 85 mMapsIntent = mapsIntent; 86 if (mRemoteCarTaskView != null && mRemoteCarTaskView.getValue() != null) { 87 // Release the remote car task view instance if it exists since otherwise there could 88 // be a memory leak 89 mRemoteCarTaskView.getValue().release(); 90 } 91 mRemoteCarTaskView = new MutableLiveData<>(/* value= */ null); 92 mHostLifecycle = new CarTaskViewControllerHostLifecycle(); 93 ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback = 94 new ControlledRemoteCarTaskViewCallbackImpl(mRemoteCarTaskView); 95 96 CarTaskViewControllerCallback carTaskViewControllerCallback = 97 new CarTaskViewControllerCallbackImpl(controlledRemoteCarTaskViewCallback); 98 99 mCarActivityManager.getCarTaskViewController(mWindowContext, mHostLifecycle, 100 mWindowContext.getMainExecutor(), carTaskViewControllerCallback); 101 } 102 getRemoteCarTaskView()103 LiveData<RemoteCarTaskView> getRemoteCarTaskView() { 104 return mRemoteCarTaskView; 105 } 106 107 @VisibleForTesting getMapsIntent()108 Intent getMapsIntent() { 109 return mMapsIntent; 110 } 111 112 /** 113 * Returns remote car task view task Id. 114 */ getRemoteCarTaskViewTaskId()115 public int getRemoteCarTaskViewTaskId() { 116 if (mRemoteCarTaskView != null && mRemoteCarTaskView.getValue() != null 117 && mRemoteCarTaskView.getValue().getTaskInfo() != null) { 118 return mRemoteCarTaskView.getValue().getTaskInfo().taskId; 119 } 120 return INVALID_TASK_ID; 121 } 122 123 /** 124 * Shows remote car task view when activity is resumed. 125 */ 126 @Override onResume(@onNull LifecycleOwner owner)127 public void onResume(@NonNull LifecycleOwner owner) { 128 DefaultLifecycleObserver.super.onResume(owner); 129 // Do not trigger 'hostAppeared()' in onResume. 130 // If the host Activity was hidden by an Activity, the Activity is moved to the other 131 // display, what the system expects would be the new moved Activity becomes the top one. 132 // But, at the time, the host Activity became visible and 'onResume()' is triggered. 133 // If 'hostAppeared()' is called in onResume, which moves the embeddedTask to the top and 134 // breaks the contract (the newly moved Activity becomes top). 135 // The contract is maintained by android.server.wm.multidisplay.MultiDisplayClientTests. 136 // BTW, if we don't invoke 'hostAppeared()', which makes the embedded task invisible if 137 // the host Activity gets the new Intent, so we'd call 'hostAppeared()' in onNewIntent. 138 } 139 140 @Override onStop(@onNull LifecycleOwner owner)141 public void onStop(@NonNull LifecycleOwner owner) { 142 DefaultLifecycleObserver.super.onStop(owner); 143 mHostLifecycle.hostDisappeared(); 144 } 145 146 @Override onCleared()147 protected void onCleared() { 148 if (mRemoteCarTaskView != null) { 149 mRemoteCarTaskView.setValue(null); 150 } 151 if (mCar != null) { 152 mCar.disconnect(); 153 } 154 mHostLifecycle.hostDestroyed(); 155 super.onCleared(); 156 } 157 getNewIntentListener()158 public Consumer<Intent> getNewIntentListener() { 159 return mNewIntentConsumer; 160 } 161 162 private final Consumer<Intent> mNewIntentConsumer = new Consumer<Intent>() { 163 @Override 164 public void accept(Intent intent) { 165 mHostLifecycle.hostAppeared(); 166 } 167 }; 168 169 private static final class ControlledRemoteCarTaskViewCallbackImpl implements 170 ControlledRemoteCarTaskViewCallback { 171 private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView; 172 ControlledRemoteCarTaskViewCallbackImpl( MutableLiveData<RemoteCarTaskView> remoteCarTaskView)173 private ControlledRemoteCarTaskViewCallbackImpl( 174 MutableLiveData<RemoteCarTaskView> remoteCarTaskView) { 175 mRemoteCarTaskView = remoteCarTaskView; 176 } 177 178 @Override onTaskViewCreated(@onNull ControlledRemoteCarTaskView taskView)179 public void onTaskViewCreated(@NonNull ControlledRemoteCarTaskView taskView) { 180 if (DEBUG) { 181 Log.d(TAG, "MapsTaskView: onTaskViewCreated"); 182 } 183 mRemoteCarTaskView.setValue(taskView); 184 } 185 186 @Override onTaskViewInitialized()187 public void onTaskViewInitialized() { 188 if (DEBUG) { 189 Log.d(TAG, "MapsTaskView: onTaskViewInitialized"); 190 } 191 } 192 193 @Override onTaskAppeared(@onNull ActivityManager.RunningTaskInfo taskInfo)194 public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 195 if (DEBUG) { 196 Log.d(TAG, "MapsTaskView: onTaskAppeared: taskId=" + taskInfo.taskId); 197 } 198 if (!sAutoRestartOnCrash) { 199 mRemoteCarTaskView.getValue().setBackgroundColor(Color.TRANSPARENT); 200 } 201 } 202 203 @Override onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)204 public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) { 205 if (DEBUG) { 206 Log.d(TAG, "MapsTaskView: onTaskVanished: taskId=" + taskInfo.taskId); 207 } 208 if (!sAutoRestartOnCrash) { 209 // RemoteCarTaskView color is set to red to indicate 210 // that nothing is wrong with the task view but maps 211 // in the task view has crashed. More details in 212 // b/247156851. 213 mRemoteCarTaskView.getValue().setBackgroundColor(Color.RED); 214 } 215 } 216 } 217 218 private final class CarTaskViewControllerCallbackImpl implements CarTaskViewControllerCallback { 219 private final ControlledRemoteCarTaskViewCallback mControlledRemoteCarTaskViewCallback; 220 CarTaskViewControllerCallbackImpl( ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback)221 private CarTaskViewControllerCallbackImpl( 222 ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback) { 223 mControlledRemoteCarTaskViewCallback = controlledRemoteCarTaskViewCallback; 224 } 225 226 @Override onConnected(@onNull CarTaskViewController carTaskViewController)227 public void onConnected(@NonNull CarTaskViewController carTaskViewController) { 228 carTaskViewController.createControlledRemoteCarTaskView( 229 new ControlledRemoteCarTaskViewConfig.Builder() 230 .setActivityIntent(mMapsIntent) 231 .setShouldAutoRestartOnTaskRemoval(sAutoRestartOnCrash) 232 .build(), 233 mWindowContext.getMainExecutor(), 234 mControlledRemoteCarTaskViewCallback); 235 } 236 237 @Override onDisconnected(@onNull CarTaskViewController carTaskViewController)238 public void onDisconnected(@NonNull CarTaskViewController carTaskViewController) { 239 mRemoteCarTaskView.setValue(null); 240 } 241 } 242 243 static final class CarLauncherViewModelFactory implements ViewModelProvider.Factory { 244 private final Context mContext; 245 private final Intent mMapsIntent; 246 CarLauncherViewModelFactory(@iContext Context context, @NonNull Intent mapsIntent)247 CarLauncherViewModelFactory(@UiContext Context context, @NonNull Intent mapsIntent) { 248 mMapsIntent = requireNonNull(mapsIntent); 249 mContext = requireNonNull(context); 250 } 251 252 @NonNull 253 @Override create(Class<T> modelClass)254 public <T extends ViewModel> T create(Class<T> modelClass) { 255 return modelClass.cast(new CarLauncherViewModel(mContext, mMapsIntent)); 256 } 257 } 258 } 259