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