1 /*
2  * Copyright (C) 2017 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 package android.car.cluster;
17 
18 import static android.car.cluster.ClusterRenderingService.LOCAL_BINDING_ACTION;
19 import static android.content.Intent.ACTION_SCREEN_OFF;
20 import static android.content.Intent.ACTION_USER_PRESENT;
21 import static android.content.Intent.ACTION_USER_SWITCHED;
22 import static android.content.Intent.ACTION_USER_UNLOCKED;
23 import static android.content.PermissionChecker.PERMISSION_GRANTED;
24 
25 import android.annotation.NonNull;
26 import android.app.ActivityManager;
27 import android.app.ActivityOptions;
28 import android.car.Car;
29 import android.car.cluster.navigation.NavigationState.NavigationStateProto;
30 import android.car.cluster.sensors.Sensors;
31 import android.content.ActivityNotFoundException;
32 import android.content.BroadcastReceiver;
33 import android.content.ComponentName;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.content.ServiceConnection;
38 import android.content.pm.ActivityInfo;
39 import android.content.pm.PackageManager;
40 import android.content.pm.ResolveInfo;
41 import android.graphics.Rect;
42 import android.os.Bundle;
43 import android.os.Handler;
44 import android.os.IBinder;
45 import android.os.UserHandle;
46 import android.util.Log;
47 import android.util.SparseArray;
48 import android.view.Display;
49 import android.view.InputDevice;
50 import android.view.KeyEvent;
51 import android.view.View;
52 import android.view.inputmethod.InputMethodManager;
53 import android.widget.Button;
54 import android.widget.TextView;
55 
56 import androidx.fragment.app.Fragment;
57 import androidx.fragment.app.FragmentActivity;
58 import androidx.fragment.app.FragmentManager;
59 import androidx.fragment.app.FragmentPagerAdapter;
60 import androidx.lifecycle.LiveData;
61 import androidx.lifecycle.ViewModelProvider;
62 import androidx.lifecycle.ViewModelProviders;
63 import androidx.viewpager.widget.ViewPager;
64 
65 import com.android.car.telephony.common.InMemoryPhoneBook;
66 
67 import java.lang.ref.WeakReference;
68 import java.lang.reflect.InvocationTargetException;
69 import java.net.URISyntaxException;
70 import java.util.HashMap;
71 import java.util.Map;
72 
73 /**
74  * Main activity displayed on the instrument cluster. This activity contains fragments for each of
75  * the cluster "facets" (e.g.: navigation, communication, media and car state). Users can navigate
76  * to each facet by using the steering wheel buttons.
77  * <p>
78  * This activity runs on "system user" (see {@link UserHandle#USER_SYSTEM}) but it is visible on
79  * all users (the same activity remains active even during user switch).
80  * <p>
81  * This activity also launches a default navigation app inside a virtual display (which is located
82  * inside {@link NavigationFragment}). This navigation app is launched when:
83  * <ul>
84  * <li>Virtual display for navigation apps is ready.
85  * <li>After every user switch.
86  * </ul>
87  * This is necessary because the navigation app runs under a normal user, and different users will
88  * see different instances of the same application, with their own personalized data.
89  */
90 public class MainClusterActivity extends FragmentActivity implements
91         ClusterRenderingService.ServiceClient {
92     private static final String TAG = "Cluster.MainActivity";
93 
94     private static final int NAV_FACET_ID = 0;
95     private static final int COMMS_FACET_ID = 1;
96     private static final int MEDIA_FACET_ID = 2;
97     private static final int INFO_FACET_ID = 3;
98 
99     private static final NavigationStateProto NULL_NAV_STATE =
100             NavigationStateProto.getDefaultInstance();
101     private static final int NO_DISPLAY = -1;
102 
103     private ViewPager mPager;
104     private NavStateController mNavStateController;
105     private ClusterViewModel mClusterViewModel;
106 
107     private Map<View, Facet<?>> mButtonToFacet = new HashMap<>();
108     private SparseArray<Facet<?>> mOrderToFacet = new SparseArray<>();
109 
110     private Map<Sensors.Gear, View> mGearsToIcon = new HashMap<>();
111     private InputMethodManager mInputMethodManager;
112     private ClusterRenderingService mService;
113     private VirtualDisplay mPendingVirtualDisplay = null;
114 
115     private static final int NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS = 1000;
116     private static final int NAVIGATION_ACTIVITY_RELAUNCH_DELAY_MS = 5000;
117 
118     private final UserReceiver mUserReceiver = new UserReceiver();
119     private ActivityMonitor mActivityMonitor = new ActivityMonitor();
120     private final Handler mHandler = new Handler();
121     private final Runnable mRetryLaunchNavigationActivity = this::tryLaunchNavigationActivity;
122     private VirtualDisplay mNavigationDisplay = new VirtualDisplay(NO_DISPLAY, null);
123 
124     private int mPreviousFacet = COMMS_FACET_ID;
125 
126     /**
127      * Description of a virtual display
128      */
129     public static class VirtualDisplay {
130         /** Identifier of the display */
131         public final int mDisplayId;
132         /** Rectangular area inside this display that can be viewed without obstructions */
133         public final Rect mUnobscuredBounds;
134 
VirtualDisplay(int displayId, Rect unobscuredBounds)135         public VirtualDisplay(int displayId, Rect unobscuredBounds) {
136             mDisplayId = displayId;
137             mUnobscuredBounds = unobscuredBounds;
138         }
139     }
140 
141     private final View.OnFocusChangeListener mFacetButtonFocusListener =
142             new View.OnFocusChangeListener() {
143                 @Override
144                 public void onFocusChange(View v, boolean hasFocus) {
145                     if (hasFocus) {
146                         mPager.setCurrentItem(mButtonToFacet.get(v).mOrder);
147                     }
148                 }
149             };
150 
151     private ServiceConnection mClusterRenderingServiceConnection = new ServiceConnection() {
152         @Override
153         public void onServiceConnected(ComponentName name, IBinder service) {
154             Log.i(TAG, "onServiceConnected, name: " + name + ", service: " + service);
155             mService = ((ClusterRenderingService.LocalBinder) service).getService();
156             mService.registerClient(MainClusterActivity.this);
157             mNavStateController.setImageResolver(mService.getImageResolver());
158             if (mPendingVirtualDisplay != null) {
159                 // If haven't reported the virtual display yet, do so on service connect.
160                 reportNavDisplay(mPendingVirtualDisplay);
161                 mPendingVirtualDisplay = null;
162             }
163         }
164 
165         @Override
166         public void onServiceDisconnected(ComponentName name) {
167             Log.i(TAG, "onServiceDisconnected, name: " + name);
168             mService = null;
169             mNavStateController.setImageResolver(null);
170             onNavigationStateChange(NULL_NAV_STATE);
171         }
172     };
173 
174     private ActivityMonitor.ActivityListener mNavigationActivityMonitor = (displayId, activity) -> {
175         if (displayId != mNavigationDisplay.mDisplayId) {
176             return;
177         }
178         mClusterViewModel.setCurrentNavigationActivity(activity);
179     };
180 
181     /**
182      * On user switch the navigation application must be re-launched on the new user. Otherwise
183      * the navigation fragment will keep showing the application on the previous user.
184      * {@link MainClusterActivity} is shared between all users (it is not restarted on user switch)
185      */
186     private class UserReceiver extends BroadcastReceiver {
register(Context context)187         void register(Context context) {
188             IntentFilter intentFilter = new IntentFilter(ACTION_USER_UNLOCKED);
189             context.registerReceiverForAllUsers(this, intentFilter, null, null);
190         }
unregister(Context context)191         void unregister(Context context) {
192             context.unregisterReceiver(this);
193         }
194         @Override
onReceive(Context context, Intent intent)195         public void onReceive(Context context, Intent intent) {
196             if (Log.isLoggable(TAG, Log.DEBUG)) {
197                 Log.d(TAG, "Broadcast received: " + intent);
198             }
199             tryLaunchNavigationActivity();
200         }
201     }
202 
203     private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver(){
204         @Override
205         public void onReceive(final Context context, final Intent intent) {
206             if (!intent.getAction().equals(Intent.ACTION_SCREEN_OFF)){
207                 return;
208             }
209             if (Log.isLoggable(TAG, Log.DEBUG)) {
210                 Log.d(TAG, "ACTION_SCREEN_OFF");
211             }
212             mNavStateController.hideNavigationStateInfo();
213         }
214     };
215 
216     private final BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver(){
217         @Override
218         public void onReceive(final Context context, final Intent intent) {
219             if (!intent.getAction().equals(Intent.ACTION_USER_PRESENT)) {
220                 return;
221             }
222             if (Log.isLoggable(TAG, Log.DEBUG)) {
223                 Log.d(TAG, "ACTION_USER_PRESENT");
224             }
225             mNavStateController.showNavigationStateInfo();
226         }
227     };
228 
229     @Override
onCreate(Bundle savedInstanceState)230     protected void onCreate(Bundle savedInstanceState) {
231         super.onCreate(savedInstanceState);
232         Log.d(TAG, "onCreate");
233         setContentView(R.layout.activity_main);
234 
235         mInputMethodManager = getSystemService(InputMethodManager.class);
236 
237         Intent intent = new Intent(this, ClusterRenderingService.class);
238         intent.setAction(LOCAL_BINDING_ACTION);
239         bindServiceAsUser(intent, mClusterRenderingServiceConnection, 0, UserHandle.SYSTEM);
240 
241         registerFacet(new Facet<>(findViewById(R.id.btn_nav),
242                 NAV_FACET_ID, NavigationFragment.class));
243         registerFacet(new Facet<>(findViewById(R.id.btn_phone),
244                 COMMS_FACET_ID, PhoneFragment.class));
245         registerFacet(new Facet<>(findViewById(R.id.btn_music),
246                 MEDIA_FACET_ID, MusicFragment.class));
247         registerFacet(new Facet<>(findViewById(R.id.btn_car_info),
248                 INFO_FACET_ID, CarInfoFragment.class));
249         registerGear(findViewById(R.id.gear_parked), Sensors.Gear.PARK);
250         registerGear(findViewById(R.id.gear_reverse), Sensors.Gear.REVERSE);
251         registerGear(findViewById(R.id.gear_neutral), Sensors.Gear.NEUTRAL);
252         registerGear(findViewById(R.id.gear_drive), Sensors.Gear.DRIVE);
253 
254         mPager = findViewById(R.id.pager);
255         mPager.setAdapter(new ClusterPageAdapter(getSupportFragmentManager()));
256         mOrderToFacet.get(NAV_FACET_ID).mButton.requestFocus();
257         mNavStateController = new NavStateController(findViewById(R.id.navigation_state));
258 
259         IntentFilter screenOffFilter = new IntentFilter();
260         screenOffFilter.addAction(ACTION_SCREEN_OFF);
261         registerReceiver(mScreenOffReceiver, screenOffFilter);
262 
263         IntentFilter userPresentFilter = new IntentFilter();
264         userPresentFilter.addAction(ACTION_USER_PRESENT);
265         registerReceiver(mUserPresentReceiver, userPresentFilter);
266 
267         mClusterViewModel = new ViewModelProvider(this).get(ClusterViewModel.class);
268         mClusterViewModel.getNavigationFocus().observe(this, focus -> {
269             if (!focus) {
270                 mNavStateController.update(null);
271             }
272         });
273         mClusterViewModel.getNavigationActivityState().observe(this, state -> {
274             if (state == ClusterViewModel.NavigationActivityState.LOADING) {
275                 if (!mHandler.hasCallbacks(mRetryLaunchNavigationActivity)) {
276                     mHandler.postDelayed(mRetryLaunchNavigationActivity,
277                             NAVIGATION_ACTIVITY_RELAUNCH_DELAY_MS);
278                 }
279             } else {
280                 mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
281             }
282         });
283 
284         mClusterViewModel.getSensor(Sensors.SENSOR_GEAR).observe(this, this::updateSelectedGear);
285 
286         registerSensor(findViewById(R.id.info_fuel), mClusterViewModel.getFuelLevel());
287         registerSensor(findViewById(R.id.info_speed), mClusterViewModel.getSpeed());
288         registerSensor(findViewById(R.id.info_range), mClusterViewModel.getRange());
289         registerSensor(findViewById(R.id.info_rpm), mClusterViewModel.getRPM());
290 
291         mActivityMonitor.start();
292 
293         mUserReceiver.register(this);
294 
295         InMemoryPhoneBook.init(this);
296 
297         PhoneFragmentViewModel phoneViewModel = new ViewModelProvider(this).get(
298                 PhoneFragmentViewModel.class);
299 
300         phoneViewModel.setPhoneStateCallback(new PhoneFragmentViewModel.PhoneStateCallback() {
301             @Override
302             public void onCall() {
303                 if (mPager.getCurrentItem() != COMMS_FACET_ID) {
304                     mPreviousFacet = mPager.getCurrentItem();
305                 }
306                 mOrderToFacet.get(COMMS_FACET_ID).mButton.requestFocus();
307             }
308 
309             @Override
310             public void onDisconnect() {
311                 if (mPreviousFacet != COMMS_FACET_ID) {
312                     mOrderToFacet.get(mPreviousFacet).mButton.requestFocus();
313                 }
314             }
315         });
316     }
317 
registerSensor(TextView textView, LiveData<V> source)318     private <V> void registerSensor(TextView textView, LiveData<V> source) {
319         String emptyValue = getString(R.string.info_value_empty);
320         source.observe(this, value -> {
321             // Need to check that the text is actually different, or else
322             // it will generate a bunch of CONTENT_CHANGE_TYPE_TEXT accessability
323             // actions. This will cause cts tests to fail when they waitForIdle(),
324             // and the system never idles because it's constantly updating these
325             // TextViews
326             if (value != null && !value.toString().contentEquals(textView.getText())) {
327                 textView.setText(value.toString());
328             }
329             if (value == null && !emptyValue.contentEquals(textView.getText())) {
330                 textView.setText(emptyValue);
331             }
332         });
333     }
334 
335     @Override
onDestroy()336     protected void onDestroy() {
337         super.onDestroy();
338         Log.d(TAG, "onDestroy");
339         mUserReceiver.unregister(this);
340         mActivityMonitor.stop();
341         if (mService != null) {
342             mService.unregisterClient(this);
343             mService = null;
344         }
345         unbindService(mClusterRenderingServiceConnection);
346         unregisterReceiver(mScreenOffReceiver);
347         unregisterReceiver(mUserPresentReceiver);
348     }
349 
350     @Override
onKeyEvent(KeyEvent event)351     public void onKeyEvent(KeyEvent event) {
352         Log.i(TAG, "onKeyEvent, event: " + event);
353 
354         // This is a hack. We use SOURCE_CLASS_POINTER here because this type of input is associated
355         // with the display. otherwise this event will be ignored in ViewRootImpl because injecting
356         // KeyEvent w/o activity being focused is useless.
357         event.setSource(event.getSource() | InputDevice.SOURCE_CLASS_POINTER);
358         mInputMethodManager.dispatchKeyEventFromInputMethod(getCurrentFocus(), event);
359     }
360 
361     @Override
onNavigationStateChange(NavigationStateProto state)362     public void onNavigationStateChange(NavigationStateProto state) {
363         Log.d(TAG, "onNavigationStateChange: " + state);
364         if (mNavStateController != null) {
365             mNavStateController.update(state);
366         }
367     }
368 
updateNavDisplay(VirtualDisplay virtualDisplay)369     public void updateNavDisplay(VirtualDisplay virtualDisplay) {
370         // Starting the default navigation activity. This activity will be shown when navigation
371         // focus is not taken.
372         startNavigationActivity(virtualDisplay);
373         // Notify the service (so it updates display properties on car service)
374         if (mService == null) {
375             // Service is not bound yet. Hold the information and notify when the service is bound.
376             mPendingVirtualDisplay = virtualDisplay;
377             return;
378         } else {
379             reportNavDisplay(virtualDisplay);
380         }
381     }
382 
reportNavDisplay(VirtualDisplay virtualDisplay)383     private void reportNavDisplay(VirtualDisplay virtualDisplay) {
384         mService.setActivityLaunchOptions(virtualDisplay.mDisplayId, ClusterActivityState
385                 .create(virtualDisplay.mDisplayId != Display.INVALID_DISPLAY,
386                         virtualDisplay.mUnobscuredBounds));
387     }
388 
389     public class ClusterPageAdapter extends FragmentPagerAdapter {
ClusterPageAdapter(FragmentManager fm)390         public ClusterPageAdapter(FragmentManager fm) {
391             super(fm);
392         }
393 
394         @Override
getCount()395         public int getCount() {
396             return mButtonToFacet.size();
397         }
398 
399         @Override
getItem(int position)400         public Fragment getItem(int position) {
401             return mOrderToFacet.get(position).getOrCreateFragment();
402         }
403     }
404 
registerFacet(Facet<T> facet)405     private <T> void registerFacet(Facet<T> facet) {
406         mOrderToFacet.append(facet.mOrder, facet);
407         mButtonToFacet.put(facet.mButton, facet);
408 
409         facet.mButton.setOnFocusChangeListener(mFacetButtonFocusListener);
410     }
411 
412     private static class Facet<T> {
413         Button mButton;
414         Class<T> mClazz;
415         int mOrder;
416 
Facet(Button button, int order, Class<T> clazz)417         Facet(Button button, int order, Class<T> clazz) {
418             this.mButton = button;
419             this.mOrder = order;
420             this.mClazz = clazz;
421         }
422 
423         private Fragment mFragment;
424 
getOrCreateFragment()425         Fragment getOrCreateFragment() {
426             if (mFragment == null) {
427                 try {
428                     mFragment = (Fragment) mClazz.getConstructors()[0].newInstance();
429                 } catch (InstantiationException | IllegalAccessException
430                         | InvocationTargetException e) {
431                     throw new RuntimeException(e);
432                 }
433             }
434             return mFragment;
435         }
436     }
437 
startNavigationActivity(VirtualDisplay virtualDisplay)438     private void startNavigationActivity(VirtualDisplay virtualDisplay) {
439         mActivityMonitor.removeListener(mNavigationDisplay.mDisplayId, mNavigationActivityMonitor);
440         mActivityMonitor.addListener(virtualDisplay.mDisplayId, mNavigationActivityMonitor);
441         mNavigationDisplay = virtualDisplay;
442         tryLaunchNavigationActivity();
443     }
444 
445     /**
446      * Tries to start a default navigation activity in the cluster. During system initialization
447      * launching user activities might fail due the system not being ready or {@link PackageManager}
448      * not being able to resolve the implicit intent. It is also possible that the system doesn't
449      * have a default navigation activity selected yet.
450      */
tryLaunchNavigationActivity()451     private void tryLaunchNavigationActivity() {
452         if (mNavigationDisplay.mDisplayId == NO_DISPLAY) {
453             if (Log.isLoggable(TAG, Log.DEBUG)) {
454                 Log.d(TAG, String.format("Launch activity ignored (no display yet)"));
455             }
456             // Not ready to launch yet.
457             return;
458         }
459         mHandler.removeCallbacks(mRetryLaunchNavigationActivity);
460 
461         ActivityInfo activityInfo = getNavigationActivity(this);
462         ComponentName navigationActivity = new ComponentName(activityInfo.packageName,
463                 activityInfo.name);
464         int userId = (activityInfo.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0
465                 ? UserHandle.USER_SYSTEM : ActivityManager.getCurrentUser();
466         mClusterViewModel.setFreeNavigationActivity(navigationActivity);
467 
468         try {
469             ClusterActivityState activityState = ClusterActivityState
470                     .create(true, mNavigationDisplay.mUnobscuredBounds);
471             Intent intent = new Intent(Intent.ACTION_MAIN)
472                     .setComponent(navigationActivity)
473                     .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
474                     .putExtra(Car.CAR_EXTRA_CLUSTER_ACTIVITY_STATE,
475                             activityState.toBundle());
476 
477             Log.d(TAG, "Launching: " + intent + " on display" + mNavigationDisplay.mDisplayId
478                     + " as user" + userId);
479             ActivityOptions activityOptions = ActivityOptions.makeBasic()
480                     .setLaunchDisplayId(mNavigationDisplay.mDisplayId);
481 
482             mService.startFixedActivityModeForDisplayAndUser(intent, activityOptions, userId);
483         } catch (ActivityNotFoundException ex) {
484             // Some activities might not be available right on startup. We will retry.
485             mHandler.postDelayed(mRetryLaunchNavigationActivity,
486                     NAVIGATION_ACTIVITY_RETRY_INTERVAL_MS);
487         } catch (Exception ex) {
488             Log.e(TAG, "Unable to start navigation activity: " + navigationActivity, ex);
489         }
490     }
491 
492     /**
493      * Returns a default navigation activity to show in the cluster.
494      * In the current implementation we obtain this activity from an intent defined in a resources
495      * file (which OEMs can overlay).
496      * When it fails to find, parse or resolve the activity, it'll throw ActivityNotFoundException.
497      */
getNavigationActivity(Context context)498     static @NonNull ActivityInfo getNavigationActivity(Context context) {
499         PackageManager pm = context.getPackageManager();
500         String intentString = context.getString(R.string.freeNavigationIntent);
501 
502         if (intentString == null) {
503             throw new ActivityNotFoundException("No free navigation activity defined");
504         }
505         Log.i(TAG, "Free navigation intent: " + intentString);
506 
507         try {
508             Intent intent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
509             ResolveInfo navigationApp = pm.resolveActivity(intent,
510                     PackageManager.MATCH_DEFAULT_ONLY);
511             if (navigationApp == null) {
512                 throw new ActivityNotFoundException("Can't resolve freeNavigationIntent");
513             }
514             return navigationApp.activityInfo;
515         } catch (URISyntaxException ex) {
516             throw new ActivityNotFoundException("Unable to parse freeNavigationIntent");
517         }
518     }
519 
registerGear(View view, Sensors.Gear gear)520     private void registerGear(View view, Sensors.Gear gear) {
521         mGearsToIcon.put(gear, view);
522     }
523 
updateSelectedGear(Sensors.Gear gear)524     private void updateSelectedGear(Sensors.Gear gear) {
525         for (Map.Entry<Sensors.Gear, View> entry : mGearsToIcon.entrySet()) {
526             entry.getValue().setSelected(entry.getKey() == gear);
527         }
528     }
529 }
530