• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.car.statusicon;
18 
19 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
20 import static android.widget.ListPopupWindow.WRAP_CONTENT;
21 import static android.widget.PopupWindow.INPUT_METHOD_NOT_NEEDED;
22 
23 import android.annotation.DimenRes;
24 import android.annotation.LayoutRes;
25 import android.app.PendingIntent;
26 import android.car.drivingstate.CarUxRestrictions;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.graphics.Outline;
32 import android.graphics.drawable.Drawable;
33 import android.view.Gravity;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.ViewOutlineProvider;
38 import android.view.ViewTreeObserver;
39 import android.view.WindowManager;
40 import android.widget.PopupWindow;
41 import android.widget.Toast;
42 
43 import androidx.annotation.NonNull;
44 import androidx.annotation.VisibleForTesting;
45 
46 import com.android.car.qc.QCItem;
47 import com.android.car.qc.view.QCView;
48 import com.android.car.ui.FocusParkingView;
49 import com.android.car.ui.utils.CarUxRestrictionsUtil;
50 import com.android.systemui.R;
51 import com.android.systemui.broadcast.BroadcastDispatcher;
52 import com.android.systemui.car.CarDeviceProvisionedController;
53 import com.android.systemui.car.qc.SystemUIQCViewController;
54 import com.android.systemui.car.systembar.element.CarSystemBarElementController;
55 import com.android.systemui.car.systembar.element.CarSystemBarElementInitializer;
56 import com.android.systemui.settings.UserTracker;
57 import com.android.systemui.statusbar.policy.ConfigurationController;
58 import com.android.systemui.util.ViewController;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 import javax.inject.Inject;
64 
65 /**
66  * A controller for a panel view associated with a status icon.
67  */
68 public class StatusIconPanelViewController extends ViewController<View> {
69     private final Context mContext;
70     private final UserTracker mUserTracker;
71     private final BroadcastDispatcher mBroadcastDispatcher;
72     private final ConfigurationController mConfigurationController;
73     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
74     private final CarSystemBarElementInitializer mCarSystemBarElementInitializer;
75     private final String mIdentifier;
76     @LayoutRes
77     private final int mPanelLayoutRes;
78     @DimenRes
79     private final int mPanelWidthRes;
80     private final int mXOffsetPixel;
81     private final int mYOffsetPixel;
82     private final int mPanelGravity;
83     private final boolean mIsDisabledWhileDriving;
84     private final boolean mIsDisabledWhileUnprovisioned;
85     private final boolean mShowAsDropDown;
86     private final ArrayList<SystemUIQCViewController> mQCViewControllers = new ArrayList<>();
87 
88     private PopupWindow mPanel;
89     private ViewGroup mPanelContent;
90     private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
91     private float mDimValue = -1.0f;
92     private View.OnClickListener mOnClickListener;
93 
94     private final ConfigurationController.ConfigurationListener mConfigurationListener =
95             new ConfigurationController.ConfigurationListener() {
96                 @Override
97                 public void onLayoutDirectionChanged(boolean isLayoutRtl) {
98                     recreatePanel();
99                 }
100             };
101 
102     private final View.OnLayoutChangeListener mPanelContentLayoutChangeListener =
103             new View.OnLayoutChangeListener() {
104                 @Override
105                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
106                         int oldLeft, int oldTop, int oldRight, int oldBottom) {
107                     if (mPanelContent != null) {
108                         mPanelContent.invalidateOutline();
109                     }
110                 }
111             };
112 
113     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
114             mUxRestrictionsChangedListener =
115             new CarUxRestrictionsUtil.OnUxRestrictionsChangedListener() {
116                 @Override
117                 public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
118                     if (mIsDisabledWhileDriving
119                             && carUxRestrictions.isRequiresDistractionOptimization()
120                             && isPanelShowing()) {
121                         mPanel.dismiss();
122                     }
123                 }
124             };
125 
126     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
127         @Override
128         public void onReceive(Context context, Intent intent) {
129             String action = intent.getAction();
130             boolean isIntentFromSelf =
131                     intent.getIdentifier() != null && intent.getIdentifier().equals(mIdentifier);
132 
133             if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) && !isIntentFromSelf
134                     && isPanelShowing()) {
135                 mPanel.dismiss();
136             }
137         }
138     };
139 
140     private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() {
141         @Override
142         public void onUserChanged(int newUser, Context userContext) {
143             mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
144             mBroadcastDispatcher.registerReceiver(mBroadcastReceiver,
145                     new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), /* executor= */ null,
146                     mUserTracker.getUserHandle());
147         }
148     };
149 
150     private final ViewTreeObserver.OnGlobalFocusChangeListener mFocusChangeListener =
151             (oldFocus, newFocus) -> {
152                 if (isPanelShowing() && oldFocus != null && newFocus instanceof FocusParkingView) {
153                     // When nudging out of the panel, RotaryService will focus on the
154                     // FocusParkingView to clear the focus highlight. When this occurs, dismiss the
155                     // panel.
156                     mPanel.dismiss();
157                 }
158             };
159 
160     private final QCView.QCActionListener mQCActionListener = (item, action) -> {
161         if (!isPanelShowing()) {
162             return;
163         }
164         if (action instanceof PendingIntent) {
165             if (((PendingIntent) action).isActivity()) {
166                 mPanel.dismiss();
167             }
168         } else if (action instanceof QCItem.ActionHandler) {
169             if (((QCItem.ActionHandler) action).isActivity()) {
170                 mPanel.dismiss();
171             }
172         }
173     };
174 
StatusIconPanelViewController(Context context, UserTracker userTracker, BroadcastDispatcher broadcastDispatcher, ConfigurationController configurationController, CarDeviceProvisionedController deviceProvisionedController, CarSystemBarElementInitializer elementInitializer, View anchorView, @LayoutRes int layoutRes, @DimenRes int widthRes, int xOffset, int yOffset, int gravity, boolean isDisabledWhileDriving, boolean isDisabledWhileUnprovisioned, boolean showAsDropDown)175     private StatusIconPanelViewController(Context context,
176             UserTracker userTracker,
177             BroadcastDispatcher broadcastDispatcher,
178             ConfigurationController configurationController,
179             CarDeviceProvisionedController deviceProvisionedController,
180             CarSystemBarElementInitializer elementInitializer,
181             View anchorView, @LayoutRes int layoutRes, @DimenRes int widthRes,
182             int xOffset, int yOffset, int gravity, boolean isDisabledWhileDriving,
183             boolean isDisabledWhileUnprovisioned, boolean showAsDropDown) {
184         super(anchorView);
185         mContext = context;
186         mUserTracker = userTracker;
187         mBroadcastDispatcher = broadcastDispatcher;
188         mConfigurationController = configurationController;
189         mCarDeviceProvisionedController = deviceProvisionedController;
190         mCarSystemBarElementInitializer = elementInitializer;
191         mIsDisabledWhileDriving = isDisabledWhileDriving;
192         mIsDisabledWhileUnprovisioned = isDisabledWhileUnprovisioned;
193         mPanelLayoutRes = layoutRes;
194         mPanelWidthRes = widthRes;
195         mXOffsetPixel = xOffset;
196         mYOffsetPixel = yOffset;
197         mPanelGravity = gravity;
198         mShowAsDropDown = showAsDropDown;
199         mIdentifier = Integer.toString(System.identityHashCode(this));
200     }
201 
202     @Override
onInit()203     protected void onInit() {
204         mOnClickListener = v -> {
205             if (mIsDisabledWhileUnprovisioned && !isDeviceSetupForUser()) {
206                 return;
207             }
208             if (mIsDisabledWhileDriving && mCarUxRestrictionsUtil.getCurrentRestrictions()
209                     .isRequiresDistractionOptimization()) {
210                 dismissAllSystemDialogs();
211                 Toast.makeText(mContext, R.string.car_ui_restricted_while_driving,
212                         Toast.LENGTH_LONG).show();
213                 return;
214             }
215 
216             if (mPanel == null && !createPanel()) {
217                 return;
218             }
219 
220             if (mPanel.isShowing()) {
221                 mPanel.dismiss();
222                 return;
223             }
224 
225             // Dismiss all currently open system dialogs before opening this panel.
226             dismissAllSystemDialogs();
227 
228             registerFocusListener(true);
229 
230             if (mShowAsDropDown) {
231                 // TODO(b/202563671): remove yOffsetPixel when the PopupWindow API is updated.
232                 mPanel.showAsDropDown(mView, mXOffsetPixel, mYOffsetPixel, mPanelGravity);
233             } else {
234                 int verticalGravity = mPanelGravity & Gravity.VERTICAL_GRAVITY_MASK;
235                 int animationStyle = verticalGravity == Gravity.BOTTOM
236                         ? com.android.internal.R.style.Animation_DropDownUp
237                         : com.android.internal.R.style.Animation_DropDownDown;
238                 mPanel.setAnimationStyle(animationStyle);
239                 mPanel.showAtLocation(mView, mPanelGravity, mXOffsetPixel, mYOffsetPixel);
240             }
241             mView.setSelected(true);
242             setAnimatedStatusIconHighlightedStatus(true);
243             dimBehind(mPanel);
244         };
245 
246         mView.setOnClickListener(mOnClickListener);
247     }
248 
249     @Override
onViewAttached()250     protected void onViewAttached() {
251         if (mPanel == null) {
252             createPanel();
253         }
254         mBroadcastDispatcher.registerReceiver(mBroadcastReceiver,
255                 new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), /* executor= */ null,
256                 mUserTracker.getUserHandle());
257         mUserTracker.addCallback(mUserTrackerCallback, mContext.getMainExecutor());
258         mConfigurationController.addCallback(mConfigurationListener);
259 
260         if (mIsDisabledWhileDriving) {
261             mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext);
262             mCarUxRestrictionsUtil.register(mUxRestrictionsChangedListener);
263         }
264     }
265 
266     @Override
onViewDetached()267     protected void onViewDetached() {
268         reset();
269         if (mCarUxRestrictionsUtil != null) {
270             mCarUxRestrictionsUtil.unregister(mUxRestrictionsChangedListener);
271         }
272         mConfigurationController.removeCallback(mConfigurationListener);
273         mUserTracker.removeCallback(mUserTrackerCallback);
274         mBroadcastDispatcher.unregisterReceiver(mBroadcastReceiver);
275     }
276 
277     @VisibleForTesting
getPanel()278     PopupWindow getPanel() {
279         return mPanel;
280     }
281 
282     @VisibleForTesting
getBroadcastReceiver()283     BroadcastReceiver getBroadcastReceiver() {
284         return mBroadcastReceiver;
285     }
286 
287     @VisibleForTesting
getIdentifier()288     String getIdentifier() {
289         return mIdentifier;
290     }
291 
292     @VisibleForTesting
getOnClickListener()293     View.OnClickListener getOnClickListener() {
294         return mOnClickListener;
295     }
296 
297     @VisibleForTesting
getConfigurationListener()298     ConfigurationController.ConfigurationListener getConfigurationListener() {
299         return mConfigurationListener;
300     }
301 
302     @VisibleForTesting
getUserTrackerCallback()303     UserTracker.Callback getUserTrackerCallback() {
304         return mUserTrackerCallback;
305     }
306 
307     @VisibleForTesting
getFocusChangeListener()308     ViewTreeObserver.OnGlobalFocusChangeListener getFocusChangeListener() {
309         return mFocusChangeListener;
310     }
311 
312     @VisibleForTesting
getQCActionListener()313     QCView.QCActionListener getQCActionListener() {
314         return mQCActionListener;
315     }
316 
317     /**
318      * Create the PopupWindow panel and assign to {@link mPanel}.
319      * @return true if the panel was created, false otherwise
320      */
createPanel()321     private boolean createPanel() {
322         if (mPanelWidthRes == 0 || mPanelLayoutRes == 0) {
323             return false;
324         }
325 
326         int panelWidth = mContext.getResources().getDimensionPixelSize(mPanelWidthRes);
327         Drawable panelBackgroundDrawable = mContext.getResources()
328                 .getDrawable(R.drawable.status_icon_panel_bg, mContext.getTheme());
329         mPanelContent = (ViewGroup) LayoutInflater.from(mContext).inflate(mPanelLayoutRes,
330                 /* root= */ null);
331         // clip content to the panel background (to handle rounded corners)
332         mPanelContent.setOutlineProvider(new DrawableViewOutlineProvider(panelBackgroundDrawable));
333         mPanelContent.setClipToOutline(true);
334         mPanelContent.addOnLayoutChangeListener(mPanelContentLayoutChangeListener);
335 
336         // initialize special views
337         initQCElementViews(mPanelContent);
338 
339         // initialize panel
340         mPanel = new PopupWindow(mPanelContent, panelWidth, WRAP_CONTENT);
341         mPanel.setBackgroundDrawable(panelBackgroundDrawable);
342         mPanel.setWindowLayoutType(TYPE_SYSTEM_DIALOG);
343         mPanel.setFocusable(true);
344         mPanel.setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
345         mPanel.setOutsideTouchable(false);
346         mPanel.setOnDismissListener(() -> {
347             setAnimatedStatusIconHighlightedStatus(false);
348             mView.setSelected(false);
349             registerFocusListener(false);
350         });
351 
352         return true;
353     }
354 
dimBehind(PopupWindow popupWindow)355     private void dimBehind(PopupWindow popupWindow) {
356         View container = popupWindow.getContentView().getRootView();
357         WindowManager wm = mContext.getSystemService(WindowManager.class);
358 
359         if (wm == null) return;
360 
361         if (mDimValue < 0) {
362             mDimValue = mContext.getResources().getFloat(R.dimen.car_status_icon_panel_dim);
363         }
364 
365         WindowManager.LayoutParams lp = (WindowManager.LayoutParams) container.getLayoutParams();
366         lp.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
367         lp.dimAmount = mDimValue;
368         wm.updateViewLayout(container, lp);
369     }
370 
dismissAllSystemDialogs()371     private void dismissAllSystemDialogs() {
372         Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
373         intent.setIdentifier(mIdentifier);
374         mContext.sendBroadcastAsUser(intent, mUserTracker.getUserHandle());
375     }
376 
registerFocusListener(boolean register)377     private void registerFocusListener(boolean register) {
378         if (mPanelContent == null) {
379             return;
380         }
381         if (register) {
382             mPanelContent.getViewTreeObserver().addOnGlobalFocusChangeListener(
383                     mFocusChangeListener);
384         } else {
385             mPanelContent.getViewTreeObserver().removeOnGlobalFocusChangeListener(
386                     mFocusChangeListener);
387         }
388     }
389 
reset()390     private void reset() {
391         if (mPanel == null) return;
392 
393         mPanel.dismiss();
394         mPanel = null;
395         if (mPanelContent != null) {
396             mPanelContent.removeOnLayoutChangeListener(mPanelContentLayoutChangeListener);
397         }
398         mPanelContent = null;
399         mQCViewControllers.forEach(SystemUIQCViewController::destroyQCViews);
400         mQCViewControllers.clear();
401     }
402 
recreatePanel()403     private void recreatePanel() {
404         reset();
405         createPanel();
406     }
407 
initQCElementViews(ViewGroup rootView)408     private void initQCElementViews(ViewGroup rootView) {
409         List<CarSystemBarElementController> controllers =
410                 mCarSystemBarElementInitializer.initializeCarSystemBarElements(rootView);
411         for (CarSystemBarElementController controller : controllers) {
412             if (controller instanceof SystemUIQCViewController) {
413                 SystemUIQCViewController qcController = (SystemUIQCViewController) controller;
414                 qcController.setActionListener(mQCActionListener);
415                 mQCViewControllers.add(qcController);
416             }
417         }
418     }
419 
findViewsOfType(ViewGroup rootView, Class<T> clazz)420     private <T extends View> List<T> findViewsOfType(ViewGroup rootView, Class<T> clazz) {
421         List<T> views = new ArrayList<>();
422         for (int i = 0; i < rootView.getChildCount(); i++) {
423             View v = rootView.getChildAt(i);
424             if (clazz.isInstance(v)) {
425                 views.add(clazz.cast(v));
426             } else if (v instanceof ViewGroup) {
427                 views.addAll(findViewsOfType((ViewGroup) v, clazz));
428             }
429         }
430         return views;
431     }
432 
setAnimatedStatusIconHighlightedStatus(boolean isHighlighted)433     private void setAnimatedStatusIconHighlightedStatus(boolean isHighlighted) {
434         if (mView instanceof AnimatedStatusIcon) {
435             ((AnimatedStatusIcon) mView).setIconHighlighted(isHighlighted);
436         }
437     }
438 
isPanelShowing()439     private boolean isPanelShowing() {
440         return mPanel != null && mPanel.isShowing();
441     }
442 
isDeviceSetupForUser()443     private boolean isDeviceSetupForUser() {
444         return mCarDeviceProvisionedController.isCurrentUserSetup()
445                 && !mCarDeviceProvisionedController.isCurrentUserSetupInProgress();
446     }
447 
448     private static class DrawableViewOutlineProvider extends ViewOutlineProvider {
449         private final Drawable mDrawable;
450 
DrawableViewOutlineProvider(Drawable drawable)451         private DrawableViewOutlineProvider(Drawable drawable) {
452             mDrawable = drawable;
453         }
454 
455         @Override
getOutline(View view, Outline outline)456         public void getOutline(View view, Outline outline) {
457             if (mDrawable != null) {
458                 mDrawable.getOutline(outline);
459             } else {
460                 outline.setRect(0, 0, view.getWidth(), view.getHeight());
461                 outline.setAlpha(0.0f);
462             }
463         }
464     }
465 
466     /** Daggerized builder for StatusIconPanelViewController */
467     public static class Builder {
468         private final Context mContext;
469         private final UserTracker mUserTracker;
470         private final BroadcastDispatcher mBroadcastDispatcher;
471         private final ConfigurationController mConfigurationController;
472         private final CarDeviceProvisionedController mCarDeviceProvisionedController;
473         private final CarSystemBarElementInitializer mCarSystemBarElementInitializer;
474 
475         private int mXOffset = 0;
476         private int mYOffset;
477         private int mGravity = Gravity.TOP | Gravity.START;
478         private boolean mIsDisabledWhileDriving = false;
479         private boolean mIsDisabledWhileUnprovisioned = false;
480         private boolean mShowAsDropDown = true;
481 
482         @Inject
Builder( Context context, UserTracker userTracker, BroadcastDispatcher broadcastDispatcher, ConfigurationController configurationController, CarDeviceProvisionedController deviceProvisionedController, CarSystemBarElementInitializer elementInitializer)483         public Builder(
484                 Context context,
485                 UserTracker userTracker,
486                 BroadcastDispatcher broadcastDispatcher,
487                 ConfigurationController configurationController,
488                 CarDeviceProvisionedController deviceProvisionedController,
489                 CarSystemBarElementInitializer elementInitializer) {
490             mContext = context;
491             mUserTracker = userTracker;
492             mBroadcastDispatcher = broadcastDispatcher;
493             mConfigurationController = configurationController;
494             mCarDeviceProvisionedController = deviceProvisionedController;
495             mCarSystemBarElementInitializer = elementInitializer;
496 
497             int panelMarginTop = mContext.getResources().getDimensionPixelSize(
498                     R.dimen.car_status_icon_panel_margin_top);
499             int topSystemBarHeight = mContext.getResources().getDimensionPixelSize(
500                     R.dimen.car_top_system_bar_height);
501             // TODO(b/202563671): remove mYOffset when the PopupWindow API is updated.
502             mYOffset = panelMarginTop - topSystemBarHeight;
503         }
504 
505         /** Set the panel offset in the x direction by a specified number of pixels. */
setXOffset(int offset)506         public Builder setXOffset(int offset) {
507             mXOffset = offset;
508             return this;
509         }
510 
511         /** Set the panel offset in the y direction by a specified number of pixels. */
setYOffset(int offset)512         public Builder setYOffset(int offset) {
513             mYOffset = offset;
514             return this;
515         }
516 
517         /** Set the panel's gravity - by default the gravity will be `Gravity.TOP | Gravity.START`*/
setGravity(int gravity)518         public Builder setGravity(int gravity) {
519             mGravity = gravity;
520             return this;
521         }
522 
523         /** Set whether the panel should be shown while driving or not - defaults to false. */
setDisabledWhileDriving(boolean disabled)524         public Builder setDisabledWhileDriving(boolean disabled) {
525             mIsDisabledWhileDriving = disabled;
526             return this;
527         }
528 
529         /**
530          * Sets whether the panel should be disabled when the device is unprovisioned - defaults
531          * to false
532          */
setDisabledWhileUnprovisioned(boolean disabled)533         public Builder setDisabledWhileUnprovisioned(boolean disabled) {
534             mIsDisabledWhileUnprovisioned = disabled;
535             return this;
536         }
537 
538         /**
539          * Set whether the panel should be shown as a dropdown (vs. at a specific location)
540          * - defaults to true.
541          */
setShowAsDropDown(boolean dropDown)542         public Builder setShowAsDropDown(boolean dropDown) {
543             mShowAsDropDown = dropDown;
544             return this;
545         }
546 
547         /**
548          * Builds the controller with the required parameters of anchor view, panel layout resource,
549          * and panel width resources.
550          */
build(View anchorView, @LayoutRes int layoutRes, @DimenRes int widthRes)551         public StatusIconPanelViewController build(View anchorView, @LayoutRes int layoutRes,
552                 @DimenRes int widthRes) {
553             return new StatusIconPanelViewController(mContext, mUserTracker, mBroadcastDispatcher,
554                     mConfigurationController, mCarDeviceProvisionedController,
555                     mCarSystemBarElementInitializer, anchorView, layoutRes, widthRes, mXOffset,
556                     mYOffset, mGravity, mIsDisabledWhileDriving, mIsDisabledWhileUnprovisioned,
557                     mShowAsDropDown);
558         }
559     }
560 }
561