Home | History | Annotate | Download | only in car
      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 
     17 package com.android.systemui.statusbar.car;
     18 
     19 import android.app.ActivityManager;
     20 import android.app.ActivityOptions;
     21 import android.content.BroadcastReceiver;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.IntentFilter;
     25 import android.graphics.PixelFormat;
     26 import android.graphics.drawable.Drawable;
     27 import android.os.Bundle;
     28 import android.os.RemoteException;
     29 import android.os.UserHandle;
     30 import android.service.notification.StatusBarNotification;
     31 import android.util.Log;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.ViewGroup.LayoutParams;
     35 import android.view.ViewStub;
     36 import android.view.WindowManager;
     37 import android.widget.LinearLayout;
     38 
     39 import com.android.systemui.BatteryMeterView;
     40 import com.android.systemui.Dependency;
     41 import com.android.systemui.R;
     42 import com.android.systemui.SwipeHelper;
     43 import com.android.systemui.fragments.FragmentHostManager;
     44 import com.android.systemui.recents.Recents;
     45 import com.android.systemui.recents.misc.SystemServicesProxy;
     46 import com.android.systemui.recents.misc.SystemServicesProxy.TaskStackListener;
     47 import com.android.systemui.statusbar.NotificationData;
     48 import com.android.systemui.statusbar.StatusBarState;
     49 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
     50 import com.android.systemui.statusbar.phone.NavigationBarView;
     51 import com.android.systemui.statusbar.phone.StatusBar;
     52 import com.android.systemui.statusbar.policy.BatteryController;
     53 import com.android.systemui.statusbar.policy.UserSwitcherController;
     54 import com.android.keyguard.KeyguardUpdateMonitor;
     55 import com.android.systemui.classifier.FalsingLog;
     56 import com.android.systemui.classifier.FalsingManager;
     57 import com.android.systemui.Prefs;
     58 
     59 import java.io.FileDescriptor;
     60 import java.io.PrintWriter;
     61 import java.util.Map;
     62 /**
     63  * A status bar (and navigation bar) tailored for the automotive use case.
     64  */
     65 public 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