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