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