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