1/* 2 * Copyright (C) 2015 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 17package com.android.systemui.statusbar.car; 18 19import android.app.ActivityManager; 20import android.app.ActivityOptions; 21import android.content.BroadcastReceiver; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.graphics.PixelFormat; 26import android.graphics.drawable.Drawable; 27import android.os.Bundle; 28import android.os.RemoteException; 29import android.os.UserHandle; 30import android.service.notification.StatusBarNotification; 31import android.util.Log; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.ViewGroup.LayoutParams; 35import android.view.ViewStub; 36import android.view.WindowManager; 37import android.widget.LinearLayout; 38 39import com.android.systemui.BatteryMeterView; 40import com.android.systemui.Dependency; 41import com.android.systemui.R; 42import com.android.systemui.SwipeHelper; 43import com.android.systemui.fragments.FragmentHostManager; 44import com.android.systemui.recents.Recents; 45import com.android.systemui.recents.misc.SystemServicesProxy; 46import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener; 47import com.android.systemui.statusbar.NotificationData; 48import com.android.systemui.statusbar.StatusBarState; 49import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; 50import com.android.systemui.statusbar.phone.NavigationBarView; 51import com.android.systemui.statusbar.phone.StatusBar; 52import com.android.systemui.statusbar.policy.BatteryController; 53import com.android.systemui.statusbar.policy.UserSwitcherController; 54import com.android.keyguard.KeyguardUpdateMonitor; 55import com.android.systemui.classifier.FalsingLog; 56import com.android.systemui.classifier.FalsingManager; 57import com.android.systemui.Prefs; 58 59import java.io.FileDescriptor; 60import java.io.PrintWriter; 61import java.util.Map; 62/** 63 * A status bar (and navigation bar) tailored for the automotive use case. 64 */ 65public class CarStatusBar extends StatusBar implements 66 CarBatteryController.BatteryViewHandler { 67 private static final String TAG = "CarStatusBar"; 68 69 private TaskStackListenerImpl mTaskStackListener; 70 71 private CarNavigationBarController mController; 72 private FullscreenUserSwitcher mFullscreenUserSwitcher; 73 74 private CarBatteryController mCarBatteryController; 75 private BatteryMeterView mBatteryMeterView; 76 private Drawable mNotificationPanelBackground; 77 78 private ConnectedDeviceSignalController mConnectedDeviceSignalController; 79 private ViewGroup mNavigationBarWindow; 80 private CarNavigationBarView mNavigationBarView; 81 82 private final Object mQueueLock = new Object(); 83 @Override 84 public void start() { 85 super.start(); 86 mTaskStackListener = new TaskStackListenerImpl(); 87 SystemServicesProxy.getInstance(mContext).registerTaskStackListener(mTaskStackListener); 88 registerPackageChangeReceivers(); 89 90 mStackScroller.setScrollingEnabled(true); 91 92 createBatteryController(); 93 mCarBatteryController.startListening(); 94 } 95 96 @Override 97 public void destroy() { 98 mCarBatteryController.stopListening(); 99 mConnectedDeviceSignalController.stopListening(); 100 101 if (mNavigationBarWindow != null) { 102 mWindowManager.removeViewImmediate(mNavigationBarWindow); 103 mNavigationBarView = null; 104 } 105 106 super.destroy(); 107 } 108 109 @Override 110 protected void makeStatusBarView() { 111 super.makeStatusBarView(); 112 113 mNotificationPanelBackground = getDefaultWallpaper(); 114 mScrimController.setScrimBehindDrawable(mNotificationPanelBackground); 115 116 FragmentHostManager manager = FragmentHostManager.get(mStatusBarWindow); 117 manager.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> { 118 mBatteryMeterView = fragment.getView().findViewById(R.id.battery); 119 120 // By default, the BatteryMeterView should not be visible. It will be toggled 121 // when a device has connected by bluetooth. 122 mBatteryMeterView.setVisibility(View.GONE); 123 124 ViewStub stub = fragment.getView().findViewById(R.id.connected_device_signals_stub); 125 View signalsView = stub.inflate(); 126 127 // When a ViewStub if inflated, it does not respect the margins on the 128 // inflated view. 129 // As a result, manually add the ending margin. 130 ((LinearLayout.LayoutParams) signalsView.getLayoutParams()).setMarginEnd( 131 mContext.getResources().getDimensionPixelOffset( 132 R.dimen.status_bar_connected_device_signal_margin_end)); 133 134 if (mConnectedDeviceSignalController != null) { 135 mConnectedDeviceSignalController.stopListening(); 136 } 137 mConnectedDeviceSignalController = new ConnectedDeviceSignalController(mContext, 138 signalsView); 139 mConnectedDeviceSignalController.startListening(); 140 141 if (Log.isLoggable(TAG, Log.DEBUG)) { 142 Log.d(TAG, "makeStatusBarView(). mBatteryMeterView: " + mBatteryMeterView); 143 } 144 }); 145 } 146 147 private BatteryController createBatteryController() { 148 mCarBatteryController = new CarBatteryController(mContext); 149 mCarBatteryController.addBatteryViewHandler(this); 150 return mCarBatteryController; 151 } 152 153 @Override 154 protected void createNavigationBar() { 155 if (mNavigationBarView != null) { 156 return; 157 } 158 159 // SystemUI requires that the navigation bar view have a parent. Since the regular 160 // StatusBar inflates navigation_bar_window as this parent view, use the same view for the 161 // CarNavigationBarView. 162 mNavigationBarWindow = (ViewGroup) View.inflate(mContext, 163 R.layout.navigation_bar_window, null); 164 if (mNavigationBarWindow == null) { 165 Log.e(TAG, "CarStatusBar failed inflate for R.layout.navigation_bar_window"); 166 } 167 168 169 View.inflate(mContext, R.layout.car_navigation_bar, mNavigationBarWindow); 170 mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0); 171 if (mNavigationBarView == null) { 172 Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar"); 173 } 174 175 176 mController = new CarNavigationBarController(mContext, mNavigationBarView, 177 this /* ActivityStarter*/); 178 mNavigationBarView.getBarTransitions().setAlwaysOpaque(true); 179 WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 180 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 181 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR, 182 WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING 183 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 184 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 185 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 186 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, 187 PixelFormat.TRANSLUCENT); 188 lp.setTitle("CarNavigationBar"); 189 lp.windowAnimations = 0; 190 191 mWindowManager.addView(mNavigationBarWindow, lp); 192 } 193 194 @Override 195 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 196 //When executing dump() funciton simultaneously, we need to serialize them 197 //to get mStackScroller's position correctly. 198 synchronized (mQueueLock) { 199 pw.println(" mStackScroller: " + viewInfo(mStackScroller)); 200 pw.println(" mStackScroller: " + viewInfo(mStackScroller) 201 + " scroll " + mStackScroller.getScrollX() 202 + "," + mStackScroller.getScrollY()); 203 } 204 205 pw.print(" mTaskStackListener="); pw.println(mTaskStackListener); 206 pw.print(" mController="); 207 pw.println(mController); 208 pw.print(" mFullscreenUserSwitcher="); pw.println(mFullscreenUserSwitcher); 209 pw.print(" mCarBatteryController="); 210 pw.println(mCarBatteryController); 211 pw.print(" mBatteryMeterView="); 212 pw.println(mBatteryMeterView); 213 pw.print(" mConnectedDeviceSignalController="); 214 pw.println(mConnectedDeviceSignalController); 215 pw.print(" mNavigationBarView="); 216 pw.println(mNavigationBarView); 217 218 if (KeyguardUpdateMonitor.getInstance(mContext) != null) { 219 KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args); 220 } 221 222 FalsingManager.getInstance(mContext).dump(pw); 223 FalsingLog.dump(pw); 224 225 pw.println("SharedPreferences:"); 226 for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) { 227 pw.print(" "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue()); 228 } 229 } 230 231 @Override 232 public NavigationBarView getNavigationBarView() { 233 return mNavigationBarView; 234 } 235 236 @Override 237 public View getNavigationBarWindow() { 238 return mNavigationBarWindow; 239 } 240 241 @Override 242 protected View.OnTouchListener getStatusBarWindowTouchListener() { 243 // Usually, a touch on the background window will dismiss the notification shade. However, 244 // for the car use-case, the shade should remain unless the user switches to a different 245 // facet (e.g. phone). 246 return null; 247 } 248 249 /** 250 * Returns the {@link com.android.systemui.SwipeHelper.LongPressListener} that will be 251 * triggered when a notification card is long-pressed. 252 */ 253 @Override 254 protected SwipeHelper.LongPressListener getNotificationLongClicker() { 255 // For the automative use case, we do not want to the user to be able to interact with 256 // a notification other than a regular click. As a result, just return null for the 257 // long click listener. 258 return null; 259 } 260 261 @Override 262 public void showBatteryView() { 263 if (Log.isLoggable(TAG, Log.DEBUG)) { 264 Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView); 265 } 266 267 if (mBatteryMeterView != null) { 268 mBatteryMeterView.setVisibility(View.VISIBLE); 269 } 270 } 271 272 @Override 273 public void hideBatteryView() { 274 if (Log.isLoggable(TAG, Log.DEBUG)) { 275 Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView); 276 } 277 278 if (mBatteryMeterView != null) { 279 mBatteryMeterView.setVisibility(View.GONE); 280 } 281 } 282 283 private BroadcastReceiver mPackageChangeReceiver = new BroadcastReceiver() { 284 @Override 285 public void onReceive(Context context, Intent intent) { 286 if (intent.getData() == null || mController == null) { 287 return; 288 } 289 String packageName = intent.getData().getSchemeSpecificPart(); 290 mController.onPackageChange(packageName); 291 } 292 }; 293 294 private void registerPackageChangeReceivers() { 295 IntentFilter filter = new IntentFilter(); 296 filter.addAction(Intent.ACTION_PACKAGE_ADDED); 297 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 298 filter.addDataScheme("package"); 299 mContext.registerReceiver(mPackageChangeReceiver, filter); 300 } 301 302 public boolean hasDockedTask() { 303 return Recents.getSystemServices().hasDockedTask(); 304 } 305 306 /** 307 * An implementation of TaskStackListener, that listens for changes in the system task 308 * stack and notifies the navigation bar. 309 */ 310 private class TaskStackListenerImpl extends TaskStackListener { 311 @Override 312 public void onTaskStackChanged() { 313 SystemServicesProxy ssp = Recents.getSystemServices(); 314 ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getRunningTask(); 315 if (runningTaskInfo != null && runningTaskInfo.baseActivity != null) { 316 mController.taskChanged(runningTaskInfo.baseActivity.getPackageName(), 317 runningTaskInfo.stackId); 318 } 319 } 320 } 321 322 @Override 323 protected void createUserSwitcher() { 324 UserSwitcherController userSwitcherController = 325 Dependency.get(UserSwitcherController.class); 326 if (userSwitcherController.useFullscreenUserSwitcher()) { 327 mFullscreenUserSwitcher = new FullscreenUserSwitcher(this, 328 userSwitcherController, 329 mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub)); 330 } else { 331 super.createUserSwitcher(); 332 } 333 } 334 335 @Override 336 public void userSwitched(int newUserId) { 337 super.userSwitched(newUserId); 338 if (mFullscreenUserSwitcher != null) { 339 mFullscreenUserSwitcher.onUserSwitched(newUserId); 340 } 341 } 342 343 @Override 344 public void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) { 345 super.updateKeyguardState(goingToFullShade, fromShadeLocked); 346 if (mFullscreenUserSwitcher != null) { 347 if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) { 348 mFullscreenUserSwitcher.show(); 349 } else { 350 mFullscreenUserSwitcher.hide(); 351 } 352 } 353 } 354 355 @Override 356 public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) { 357 // Do nothing, we don't want to display media art in the lock screen for a car. 358 } 359 360 private int startActivityWithOptions(Intent intent, Bundle options) { 361 int result = ActivityManager.START_CANCELED; 362 try { 363 result = ActivityManager.getService().startActivityAsUser(null /* caller */, 364 mContext.getBasePackageName(), 365 intent, 366 intent.resolveTypeIfNeeded(mContext.getContentResolver()), 367 null /* resultTo*/, 368 null /* resultWho*/, 369 0 /* requestCode*/, 370 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP, 371 null /* profilerInfo*/, 372 options, 373 UserHandle.CURRENT.getIdentifier()); 374 } catch (RemoteException e) { 375 Log.w(TAG, "Unable to start activity", e); 376 } 377 378 return result; 379 } 380 381 public int startActivityOnStack(Intent intent, int stackId) { 382 ActivityOptions options = ActivityOptions.makeBasic(); 383 options.setLaunchStackId(stackId); 384 return startActivityWithOptions(intent, options.toBundle()); 385 } 386 387 @Override 388 protected boolean shouldPeek(NotificationData.Entry entry, StatusBarNotification sbn) { 389 // Because space is usually constrained in the auto use-case, there should not be a 390 // pinned notification when the shade has been expanded. Ensure this by not pinning any 391 // notification if the shade is already opened. 392 if (mPanelExpanded) { 393 return false; 394 } 395 396 return super.shouldPeek(entry, sbn); 397 } 398 399 @Override 400 public void animateExpandNotificationsPanel() { 401 // Because space is usually constrained in the auto use-case, there should not be a 402 // pinned notification when the shade has been expanded. Ensure this by removing all heads- 403 // up notifications. 404 mHeadsUpManager.removeAllHeadsUpEntries(); 405 super.animateExpandNotificationsPanel(); 406 } 407 408 /** 409 * Ensures that relevant child views are appropriately recreated when the device's density 410 * changes. 411 */ 412 @Override 413 public void onDensityOrFontScaleChanged() { 414 super.onDensityOrFontScaleChanged(); 415 mController.onDensityOrFontScaleChanged(); 416 417 // Need to update the background on density changed in case the change was due to night 418 // mode. 419 mNotificationPanelBackground = getDefaultWallpaper(); 420 mScrimController.setScrimBehindDrawable(mNotificationPanelBackground); 421 } 422 423 /** 424 * Returns the {@link Drawable} that represents the wallpaper that the user has currently set. 425 */ 426 private Drawable getDefaultWallpaper() { 427 return mContext.getDrawable(com.android.internal.R.drawable.default_wallpaper); 428 } 429} 430