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 /**
52  * A car launcher view model to manage the lifecycle of {@link RemoteCarTaskView}.
53  */
54 public final class CarLauncherViewModel extends ViewModel implements DefaultLifecycleObserver {
55     private static final String TAG = CarLauncher.TAG;
56     private static final boolean DEBUG = CarLauncher.DEBUG;
57     private static final boolean sAutoRestartOnCrash = Build.IS_USER;
58 
59     private final CarActivityManager mCarActivityManager;
60     private final Car mCar;
61     private final CarTaskViewControllerHostLifecycle mHostLifecycle;
62     @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the window context.
63     private final Context mWindowContext;
64     private final Intent mMapsIntent;
65     private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
66 
CarLauncherViewModel(@iContext Context context, @NonNull Intent mapsIntent)67     public CarLauncherViewModel(@UiContext Context context, @NonNull Intent mapsIntent) {
68         mWindowContext = context.createWindowContext(TYPE_APPLICATION_STARTING, /* options */ null);
69         mMapsIntent = mapsIntent;
70         mCar = Car.createCar(mWindowContext);
71         mCarActivityManager = mCar.getCarManager(CarActivityManager.class);
72         mHostLifecycle = new CarTaskViewControllerHostLifecycle();
73         mRemoteCarTaskView = new MutableLiveData<>(null);
74         ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback =
75                 new ControlledRemoteCarTaskViewCallbackImpl(mRemoteCarTaskView);
76 
77         CarTaskViewControllerCallback carTaskViewControllerCallback =
78                 new CarTaskViewControllerCallbackImpl(controlledRemoteCarTaskViewCallback);
79 
80         mCarActivityManager.getCarTaskViewController(mWindowContext, mHostLifecycle,
81                 mWindowContext.getMainExecutor(), carTaskViewControllerCallback);
82     }
83 
getRemoteCarTaskView()84     LiveData<RemoteCarTaskView> getRemoteCarTaskView() {
85         return mRemoteCarTaskView;
86     }
87 
88     /**
89      * Returns remote car task view task Id.
90      */
getRemoteCarTaskViewTaskId()91     public int getRemoteCarTaskViewTaskId() {
92         if (mRemoteCarTaskView != null && mRemoteCarTaskView.getValue() != null
93                 && mRemoteCarTaskView.getValue().getTaskInfo() != null) {
94             return mRemoteCarTaskView.getValue().getTaskInfo().taskId;
95         }
96         return INVALID_TASK_ID;
97     }
98 
99     /**
100      * Shows remote car task view when activity is resumed.
101      */
102     @Override
onResume(@onNull LifecycleOwner owner)103     public void onResume(@NonNull LifecycleOwner owner) {
104         DefaultLifecycleObserver.super.onResume(owner);
105         // Do not trigger 'hostAppeared()'}' in onResume.
106         // If the host Activity was hidden by an Activity, the Activity is moved to the other
107         // display, what the system expects would be the new moved Activity becomes the top one.
108         // But, at the time, the host Activity became visible and 'onResume()' is triggered.
109         // If 'hostAppeared()' is called in onResume, which moves the embeddedTask to the top and
110         // breaks the contract (the newly moved Activity becomes top).
111         // The contract is maintained by android.server.wm.multidisplay.MultiDisplayClientTests.
112         // BTW, if we don't invoke 'hostAppeared()', which makes the embedded task invisible if
113         // the host Activity gets the new Intent, so we'd call 'hostAppeared()' in onNewIntent.
114     }
115 
116     @Override
onStop(@onNull LifecycleOwner owner)117     public void onStop(@NonNull LifecycleOwner owner) {
118         DefaultLifecycleObserver.super.onStop(owner);
119         mHostLifecycle.hostDisappeared();
120     }
121 
122     @Override
onCleared()123     protected void onCleared() {
124         if (mRemoteCarTaskView != null) {
125             mRemoteCarTaskView.setValue(null);
126         }
127         if (mCar != null) {
128             mCar.disconnect();
129         }
130         mHostLifecycle.hostDestroyed();
131         super.onCleared();
132     }
133 
getNewIntentListener()134     public Consumer<Intent> getNewIntentListener() {
135         return mNewIntentConsumer;
136     }
137 
138     private final Consumer<Intent> mNewIntentConsumer = new Consumer<Intent>() {
139         @Override
140         public void accept(Intent intent) {
141             mHostLifecycle.hostAppeared();
142         }
143     };
144 
145     private static final class ControlledRemoteCarTaskViewCallbackImpl implements
146             ControlledRemoteCarTaskViewCallback {
147         private final MutableLiveData<RemoteCarTaskView> mRemoteCarTaskView;
148 
ControlledRemoteCarTaskViewCallbackImpl( MutableLiveData<RemoteCarTaskView> remoteCarTaskView)149         private ControlledRemoteCarTaskViewCallbackImpl(
150                 MutableLiveData<RemoteCarTaskView> remoteCarTaskView) {
151             mRemoteCarTaskView = remoteCarTaskView;
152         }
153 
154         @Override
onTaskViewCreated(@onNull ControlledRemoteCarTaskView taskView)155         public void onTaskViewCreated(@NonNull ControlledRemoteCarTaskView taskView) {
156             mRemoteCarTaskView.setValue(taskView);
157         }
158 
159         @Override
onTaskViewInitialized()160         public void onTaskViewInitialized() {
161             if (DEBUG) {
162                 Log.d(TAG, "MapsTaskView: onTaskViewInitialized");
163             }
164         }
165 
166         @Override
onTaskAppeared(@onNull ActivityManager.RunningTaskInfo taskInfo)167         public void onTaskAppeared(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
168             if (DEBUG) {
169                 Log.d(TAG, "MapsTaskView: onTaskAppeared: taskId=" + taskInfo.taskId);
170             }
171             if (!sAutoRestartOnCrash) {
172                 mRemoteCarTaskView.getValue().setBackgroundColor(Color.TRANSPARENT);
173             }
174         }
175 
176         @Override
onTaskVanished(@onNull ActivityManager.RunningTaskInfo taskInfo)177         public void onTaskVanished(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
178             if (DEBUG) {
179                 Log.d(TAG, "MapsTaskView: onTaskVanished: taskId=" + taskInfo.taskId);
180             }
181             if (!sAutoRestartOnCrash) {
182                 // RemoteCarTaskView color is set to red to indicate
183                 // that nothing is wrong with the task view but maps
184                 // in the task view has crashed. More details in
185                 // b/247156851.
186                 mRemoteCarTaskView.getValue().setBackgroundColor(Color.RED);
187             }
188         }
189     }
190 
191     private final class CarTaskViewControllerCallbackImpl implements CarTaskViewControllerCallback {
192         private final ControlledRemoteCarTaskViewCallback mControlledRemoteCarTaskViewCallback;
193 
CarTaskViewControllerCallbackImpl( ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback)194         private CarTaskViewControllerCallbackImpl(
195                 ControlledRemoteCarTaskViewCallback controlledRemoteCarTaskViewCallback) {
196             mControlledRemoteCarTaskViewCallback = controlledRemoteCarTaskViewCallback;
197         }
198 
199         @Override
onConnected(@onNull CarTaskViewController carTaskViewController)200         public void onConnected(@NonNull CarTaskViewController carTaskViewController) {
201             carTaskViewController.createControlledRemoteCarTaskView(
202                     new ControlledRemoteCarTaskViewConfig.Builder()
203                             .setActivityIntent(mMapsIntent)
204                             .setShouldAutoRestartOnTaskRemoval(sAutoRestartOnCrash)
205                             .build(),
206                     mWindowContext.getMainExecutor(),
207                     mControlledRemoteCarTaskViewCallback);
208         }
209 
210         @Override
onDisconnected(@onNull CarTaskViewController carTaskViewController)211         public void onDisconnected(@NonNull CarTaskViewController carTaskViewController) {
212             if (DEBUG) {
213                 Log.d(TAG, "onDisconnected");
214             }
215             mRemoteCarTaskView.setValue(null);
216         }
217     }
218 
219     static final class CarLauncherViewModelFactory implements ViewModelProvider.Factory {
220         private final Context mContext;
221         private final Intent mMapsIntent;
222 
CarLauncherViewModelFactory(@iContext Context context, @NonNull Intent mapsIntent)223         CarLauncherViewModelFactory(@UiContext Context context, @NonNull Intent mapsIntent) {
224             mMapsIntent = requireNonNull(mapsIntent);
225             mContext = requireNonNull(context);
226         }
227 
228         @NonNull
229         @Override
create(Class<T> modelClass)230         public <T extends ViewModel> T create(Class<T> modelClass) {
231             return modelClass.cast(new CarLauncherViewModel(mContext, mMapsIntent));
232         }
233     }
234 }
235