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