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