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.graphics.PixelFormat;
21import android.graphics.drawable.Drawable;
22import android.os.SystemProperties;
23import android.util.Log;
24import android.view.Gravity;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.ViewGroup.LayoutParams;
28import android.view.WindowManager;
29
30import com.android.keyguard.KeyguardUpdateMonitor;
31import com.android.systemui.BatteryMeterView;
32import com.android.systemui.Dependency;
33import com.android.systemui.Prefs;
34import com.android.systemui.R;
35import com.android.systemui.classifier.FalsingLog;
36import com.android.systemui.classifier.FalsingManager;
37import com.android.systemui.fragments.FragmentHostManager;
38import com.android.systemui.recents.Recents;
39import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
40import com.android.systemui.shared.system.ActivityManagerWrapper;
41import com.android.systemui.statusbar.StatusBarState;
42import com.android.systemui.statusbar.car.hvac.HvacController;
43import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
44import com.android.systemui.statusbar.phone.StatusBar;
45import com.android.systemui.statusbar.policy.BatteryController;
46import com.android.systemui.statusbar.policy.DeviceProvisionedController;
47import com.android.systemui.statusbar.policy.UserSwitcherController;
48
49import java.io.FileDescriptor;
50import java.io.PrintWriter;
51import java.util.Map;
52/**
53 * A status bar (and navigation bar) tailored for the automotive use case.
54 */
55public class CarStatusBar extends StatusBar implements
56        CarBatteryController.BatteryViewHandler {
57    private static final String TAG = "CarStatusBar";
58    public static final boolean ENABLE_HVAC_CONNECTION
59            = !SystemProperties.getBoolean("android.car.hvac.demo", true);
60
61    private TaskStackListenerImpl mTaskStackListener;
62
63    private FullscreenUserSwitcher mFullscreenUserSwitcher;
64
65    private CarBatteryController mCarBatteryController;
66    private BatteryMeterView mBatteryMeterView;
67    private Drawable mNotificationPanelBackground;
68
69    private ConnectedDeviceSignalController mConnectedDeviceSignalController;
70    private ViewGroup mNavigationBarWindow;
71    private ViewGroup mLeftNavigationBarWindow;
72    private ViewGroup mRightNavigationBarWindow;
73    private CarNavigationBarView mNavigationBarView;
74    private CarNavigationBarView mLeftNavigationBarView;
75    private CarNavigationBarView mRightNavigationBarView;
76
77    private final Object mQueueLock = new Object();
78    private boolean mShowLeft;
79    private boolean mShowRight;
80    private boolean mShowBottom;
81    private CarFacetButtonController mCarFacetButtonController;
82    private ActivityManagerWrapper mActivityManagerWrapper;
83    private DeviceProvisionedController mDeviceProvisionedController;
84    private boolean mDeviceIsProvisioned = true;
85
86    @Override
87    public void start() {
88        super.start();
89        mTaskStackListener = new TaskStackListenerImpl();
90        mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
91        mActivityManagerWrapper.registerTaskStackListener(mTaskStackListener);
92
93        mStackScroller.setScrollingEnabled(true);
94
95        createBatteryController();
96        mCarBatteryController.startListening();
97
98        if (ENABLE_HVAC_CONNECTION) {
99            Log.d(TAG, "Connecting to HVAC service");
100            Dependency.get(HvacController.class).connectToCarService();
101        }
102        mCarFacetButtonController = Dependency.get(CarFacetButtonController.class);
103        mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
104        mDeviceIsProvisioned = mDeviceProvisionedController.isDeviceProvisioned();
105        if (!mDeviceIsProvisioned) {
106            mDeviceProvisionedController.addCallback(
107                    new DeviceProvisionedController.DeviceProvisionedListener() {
108                        @Override
109                        public void onDeviceProvisionedChanged() {
110                            mDeviceIsProvisioned =
111                                    mDeviceProvisionedController.isDeviceProvisioned();
112                            restartNavBars();
113                        }
114                    });
115        }
116    }
117
118    /**
119     * Remove all content from navbars and rebuild them. Used to allow for different nav bars
120     * before and after the device is provisioned
121     */
122    private void restartNavBars() {
123        mCarFacetButtonController.removeAll();
124        if (ENABLE_HVAC_CONNECTION) {
125            Dependency.get(HvacController.class).removeAllComponents();
126        }
127        if (mNavigationBarWindow != null) {
128            mNavigationBarWindow.removeAllViews();
129            mNavigationBarView = null;
130        }
131
132        if (mLeftNavigationBarWindow != null) {
133            mLeftNavigationBarWindow.removeAllViews();
134            mLeftNavigationBarView = null;
135        }
136
137        if (mRightNavigationBarWindow != null) {
138            mRightNavigationBarWindow.removeAllViews();
139            mRightNavigationBarView = null;
140        }
141        buildNavBarContent();
142    }
143
144    /**
145     * Allows for showing or hiding just the navigation bars. This is indented to be used when
146     * the full screen user selector is shown.
147     */
148     void setNavBarVisibility(@View.Visibility int visibility) {
149        if (mNavigationBarWindow != null) {
150            mNavigationBarWindow.setVisibility(visibility);
151        }
152        if (mLeftNavigationBarWindow != null) {
153            mLeftNavigationBarWindow.setVisibility(visibility);
154        }
155        if (mRightNavigationBarWindow != null) {
156            mRightNavigationBarWindow.setVisibility(visibility);
157        }
158    }
159
160
161    @Override
162    public boolean hideKeyguard() {
163        boolean result = super.hideKeyguard();
164        if (mNavigationBarView != null) {
165            mNavigationBarView.hideKeyguardButtons();
166        }
167        if (mLeftNavigationBarView != null) {
168            mLeftNavigationBarView.hideKeyguardButtons();
169        }
170        if (mRightNavigationBarView != null) {
171            mRightNavigationBarView.hideKeyguardButtons();
172        }
173        return result;
174    }
175
176
177    @Override
178    public void showKeyguard() {
179        super.showKeyguard();
180        if (mNavigationBarView != null) {
181            mNavigationBarView.showKeyguardButtons();
182        }
183        if (mLeftNavigationBarView != null) {
184            mLeftNavigationBarView.showKeyguardButtons();
185        }
186        if (mRightNavigationBarView != null) {
187            mRightNavigationBarView.showKeyguardButtons();
188        }
189    }
190
191    @Override
192    public void destroy() {
193        mCarBatteryController.stopListening();
194        mConnectedDeviceSignalController.stopListening();
195        mActivityManagerWrapper.unregisterTaskStackListener(mTaskStackListener);
196
197        if (mNavigationBarWindow != null) {
198            mWindowManager.removeViewImmediate(mNavigationBarWindow);
199            mNavigationBarView = null;
200        }
201
202        if (mLeftNavigationBarWindow != null) {
203            mWindowManager.removeViewImmediate(mLeftNavigationBarWindow);
204            mLeftNavigationBarView = null;
205        }
206
207        if (mRightNavigationBarWindow != null) {
208            mWindowManager.removeViewImmediate(mRightNavigationBarWindow);
209            mRightNavigationBarView = null;
210        }
211        super.destroy();
212    }
213
214
215    @Override
216    protected void makeStatusBarView() {
217        super.makeStatusBarView();
218
219        mNotificationPanelBackground = getDefaultWallpaper();
220        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
221
222        FragmentHostManager manager = FragmentHostManager.get(mStatusBarWindow);
223        manager.addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
224            mBatteryMeterView = fragment.getView().findViewById(R.id.battery);
225
226            // By default, the BatteryMeterView should not be visible. It will be toggled
227            // when a device has connected by bluetooth.
228            mBatteryMeterView.setVisibility(View.GONE);
229        });
230    }
231
232    private BatteryController createBatteryController() {
233        mCarBatteryController = new CarBatteryController(mContext);
234        mCarBatteryController.addBatteryViewHandler(this);
235        return mCarBatteryController;
236    }
237
238    @Override
239    protected void createNavigationBar() {
240        mShowBottom = mContext.getResources().getBoolean(R.bool.config_enableBottomNavigationBar);
241        mShowLeft = mContext.getResources().getBoolean(R.bool.config_enableLeftNavigationBar);
242        mShowRight = mContext.getResources().getBoolean(R.bool.config_enableRightNavigationBar);
243
244        buildNavBarWindows();
245        buildNavBarContent();
246        attachNavBarWindows();
247    }
248
249    private void buildNavBarContent() {
250        if (mShowBottom) {
251            buildBottomBar((mDeviceIsProvisioned) ? R.layout.car_navigation_bar :
252                    R.layout.car_navigation_bar_unprovisioned);
253        }
254
255        if (mShowLeft) {
256            buildLeft((mDeviceIsProvisioned) ? R.layout.car_left_navigation_bar :
257                    R.layout.car_left_navigation_bar_unprovisioned);
258        }
259
260        if (mShowRight) {
261            buildRight((mDeviceIsProvisioned) ? R.layout.car_right_navigation_bar :
262                    R.layout.car_right_navigation_bar_unprovisioned);
263        }
264    }
265
266    private void buildNavBarWindows() {
267        if (mShowBottom) {
268
269             mNavigationBarWindow = (ViewGroup) View.inflate(mContext,
270                    R.layout.navigation_bar_window, null);
271        }
272        if (mShowLeft) {
273            mLeftNavigationBarWindow = (ViewGroup) View.inflate(mContext,
274                R.layout.navigation_bar_window, null);
275        }
276        if (mShowRight) {
277            mRightNavigationBarWindow = (ViewGroup) View.inflate(mContext,
278                    R.layout.navigation_bar_window, null);
279        }
280
281    }
282
283    private void attachNavBarWindows() {
284
285        if (mShowBottom) {
286            WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
287                    LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
288                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
289                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
290                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
291                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
292                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
293                    PixelFormat.TRANSLUCENT);
294            lp.setTitle("CarNavigationBar");
295            lp.windowAnimations = 0;
296            mWindowManager.addView(mNavigationBarWindow, lp);
297        }
298        if (mShowLeft) {
299            int width = mContext.getResources().getDimensionPixelSize(
300                    R.dimen.car_left_navigation_bar_width);
301            WindowManager.LayoutParams leftlp = new WindowManager.LayoutParams(
302                    width, LayoutParams.MATCH_PARENT,
303                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
304                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
305                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
306                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
307                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
308                    PixelFormat.TRANSLUCENT);
309            leftlp.setTitle("LeftCarNavigationBar");
310            leftlp.windowAnimations = 0;
311            leftlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
312            leftlp.gravity = Gravity.LEFT;
313            mWindowManager.addView(mLeftNavigationBarWindow, leftlp);
314        }
315        if (mShowRight) {
316            int width = mContext.getResources().getDimensionPixelSize(
317                    R.dimen.car_right_navigation_bar_width);
318            WindowManager.LayoutParams rightlp = new WindowManager.LayoutParams(
319                    width, LayoutParams.MATCH_PARENT,
320                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
321                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
322                            | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
323                            | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
324                            | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
325                    PixelFormat.TRANSLUCENT);
326            rightlp.setTitle("RightCarNavigationBar");
327            rightlp.windowAnimations = 0;
328            rightlp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR;
329            rightlp.gravity = Gravity.RIGHT;
330            mWindowManager.addView(mRightNavigationBarWindow, rightlp);
331        }
332
333    }
334
335    private void buildBottomBar(int layout) {
336        // SystemUI requires that the navigation bar view have a parent. Since the regular
337        // StatusBar inflates navigation_bar_window as this parent view, use the same view for the
338        // CarNavigationBarView.
339        View.inflate(mContext, layout, mNavigationBarWindow);
340        mNavigationBarView = (CarNavigationBarView) mNavigationBarWindow.getChildAt(0);
341        if (mNavigationBarView == null) {
342            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
343            throw new RuntimeException("Unable to build botom nav bar due to missing layout");
344        }
345        mNavigationBarView.setStatusBar(this);
346    }
347
348    private void buildLeft(int layout) {
349        View.inflate(mContext, layout, mLeftNavigationBarWindow);
350        mLeftNavigationBarView = (CarNavigationBarView) mLeftNavigationBarWindow.getChildAt(0);
351        if (mLeftNavigationBarView == null) {
352            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
353            throw new RuntimeException("Unable to build left nav bar due to missing layout");
354        }
355        mLeftNavigationBarView.setStatusBar(this);
356    }
357
358
359    private void buildRight(int layout) {
360        View.inflate(mContext, layout, mRightNavigationBarWindow);
361        mRightNavigationBarView = (CarNavigationBarView) mRightNavigationBarWindow.getChildAt(0);
362        if (mRightNavigationBarView == null) {
363            Log.e(TAG, "CarStatusBar failed inflate for R.layout.car_navigation_bar");
364            throw new RuntimeException("Unable to build right nav bar due to missing layout");
365        }
366    }
367
368    @Override
369    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
370        //When executing dump() funciton simultaneously, we need to serialize them
371        //to get mStackScroller's position correctly.
372        synchronized (mQueueLock) {
373            pw.println("  mStackScroller: " + viewInfo(mStackScroller));
374            pw.println("  mStackScroller: " + viewInfo(mStackScroller)
375                    + " scroll " + mStackScroller.getScrollX()
376                    + "," + mStackScroller.getScrollY());
377        }
378
379        pw.print("  mTaskStackListener="); pw.println(mTaskStackListener);
380        pw.print("  mCarFacetButtonController=");
381        pw.println(mCarFacetButtonController);
382        pw.print("  mFullscreenUserSwitcher="); pw.println(mFullscreenUserSwitcher);
383        pw.print("  mCarBatteryController=");
384        pw.println(mCarBatteryController);
385        pw.print("  mBatteryMeterView=");
386        pw.println(mBatteryMeterView);
387        pw.print("  mConnectedDeviceSignalController=");
388        pw.println(mConnectedDeviceSignalController);
389        pw.print("  mNavigationBarView=");
390        pw.println(mNavigationBarView);
391
392        if (KeyguardUpdateMonitor.getInstance(mContext) != null) {
393            KeyguardUpdateMonitor.getInstance(mContext).dump(fd, pw, args);
394        }
395
396        FalsingManager.getInstance(mContext).dump(pw);
397        FalsingLog.dump(pw);
398
399        pw.println("SharedPreferences:");
400        for (Map.Entry<String, ?> entry : Prefs.getAll(mContext).entrySet()) {
401            pw.print("  "); pw.print(entry.getKey()); pw.print("="); pw.println(entry.getValue());
402        }
403    }
404
405
406    @Override
407    public View getNavigationBarWindow() {
408        return mNavigationBarWindow;
409    }
410
411    @Override
412    protected View.OnTouchListener getStatusBarWindowTouchListener() {
413        // Usually, a touch on the background window will dismiss the notification shade. However,
414        // for the car use-case, the shade should remain unless the user switches to a different
415        // facet (e.g. phone).
416        return null;
417    }
418
419    @Override
420    public void showBatteryView() {
421        if (Log.isLoggable(TAG, Log.DEBUG)) {
422            Log.d(TAG, "showBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
423        }
424
425        if (mBatteryMeterView != null) {
426            mBatteryMeterView.setVisibility(View.VISIBLE);
427        }
428    }
429
430    @Override
431    public void hideBatteryView() {
432        if (Log.isLoggable(TAG, Log.DEBUG)) {
433            Log.d(TAG, "hideBatteryView(). mBatteryMeterView: " + mBatteryMeterView);
434        }
435
436        if (mBatteryMeterView != null) {
437            mBatteryMeterView.setVisibility(View.GONE);
438        }
439    }
440
441
442    public boolean hasDockedTask() {
443        return Recents.getSystemServices().hasDockedTask();
444    }
445
446    /**
447     * An implementation of SysUiTaskStackChangeListener, that listens for changes in the system task
448     * stack and notifies the navigation bar.
449     */
450    private class TaskStackListenerImpl extends SysUiTaskStackChangeListener {
451        @Override
452        public void onTaskStackChanged() {
453            try {
454                mCarFacetButtonController.taskChanged(
455                        ActivityManager.getService().getAllStackInfos());
456            } catch (Exception e) {
457                Log.e(TAG, "Getting StackInfo from activity manager failed", e);
458            }
459        }
460    }
461
462    @Override
463    protected void createUserSwitcher() {
464        UserSwitcherController userSwitcherController =
465                Dependency.get(UserSwitcherController.class);
466        if (userSwitcherController.useFullscreenUserSwitcher()) {
467            mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
468                    mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub), mContext);
469        } else {
470            super.createUserSwitcher();
471        }
472    }
473
474    @Override
475    public void onUserSwitched(int newUserId) {
476        super.onUserSwitched(newUserId);
477        if (mFullscreenUserSwitcher != null) {
478            mFullscreenUserSwitcher.onUserSwitched(newUserId);
479        }
480    }
481
482    @Override
483    public void updateKeyguardState(boolean goingToFullShade, boolean fromShadeLocked) {
484        super.updateKeyguardState(goingToFullShade, fromShadeLocked);
485        if (mFullscreenUserSwitcher != null) {
486            if (mState == StatusBarState.FULLSCREEN_USER_SWITCHER) {
487                mFullscreenUserSwitcher.show();
488            } else {
489                mFullscreenUserSwitcher.hide();
490            }
491        }
492    }
493
494    @Override
495    public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) {
496        // Do nothing, we don't want to display media art in the lock screen for a car.
497    }
498
499
500    @Override
501    public void animateExpandNotificationsPanel() {
502        // Because space is usually constrained in the auto use-case, there should not be a
503        // pinned notification when the shade has been expanded. Ensure this by removing all heads-
504        // up notifications.
505        mHeadsUpManager.releaseAllImmediately();
506        super.animateExpandNotificationsPanel();
507    }
508
509    /**
510     * Ensures that relevant child views are appropriately recreated when the device's density
511     * changes.
512     */
513    @Override
514    public void onDensityOrFontScaleChanged() {
515        super.onDensityOrFontScaleChanged();
516        // Need to update the background on density changed in case the change was due to night
517        // mode.
518        mNotificationPanelBackground = getDefaultWallpaper();
519        mScrimController.setScrimBehindDrawable(mNotificationPanelBackground);
520    }
521
522    /**
523     * Returns the {@link Drawable} that represents the wallpaper that the user has currently set.
524     */
525    private Drawable getDefaultWallpaper() {
526        return mContext.getDrawable(com.android.internal.R.drawable.default_wallpaper);
527    }
528}
529