blob: 0b717560f9c85f5e69cf5568545f6144933fbb68 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.car.statusicon;
import static android.content.Intent.ACTION_USER_FOREGROUND;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
import static android.widget.ListPopupWindow.WRAP_CONTENT;
import android.annotation.ColorInt;
import android.annotation.DimenRes;
import android.annotation.LayoutRes;
import android.car.drivingstate.CarUxRestrictions;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.car.ui.FocusParkingView;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import com.android.car.ui.utils.ViewUtils;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.car.CarServiceProvider;
import com.android.systemui.car.qc.SystemUIQCView;
import com.android.systemui.statusbar.policy.ConfigurationController;
import java.util.ArrayList;
/**
* A controller for a panel view associated with a status icon.
*/
public class StatusIconPanelController {
private static final int DEFAULT_POPUP_WINDOW_ANCHOR_GRAVITY = Gravity.TOP | Gravity.START;
private static final IntentFilter INTENT_FILTER_USER_CHANGED = new IntentFilter(
ACTION_USER_FOREGROUND);
private final Context mContext;
private final String mIdentifier;
private final String mIconTag;
private final @ColorInt int mIconHighlightedColor;
private final @ColorInt int mIconNotHighlightedColor;
private final int mYOffsetPixel;
private final boolean mIsDisabledWhileDriving;
private final ArrayList<SystemUIQCView> mQCViews = new ArrayList<>();
private PopupWindow mPanel;
private ViewGroup mPanelContent;
private OnQcViewsFoundListener mOnQcViewsFoundListener;
private View mAnchorView;
private ImageView mStatusIconView;
private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
private float mDimValue = -1.0f;
private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
reset();
}
};
private final ConfigurationController.ConfigurationListener mConfigurationListener =
new ConfigurationController.ConfigurationListener() {
@Override
public void onLayoutDirectionChanged(boolean isLayoutRtl) {
reset();
}
};
private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener
mUxRestrictionsChangedListener =
new CarUxRestrictionsUtil.OnUxRestrictionsChangedListener() {
@Override
public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
if (mIsDisabledWhileDriving
&& carUxRestrictions.isRequiresDistractionOptimization()
&& mPanel != null) {
mPanel.dismiss();
}
}
};
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
boolean isIntentFromSelf =
intent.getIdentifier() != null && intent.getIdentifier().equals(mIdentifier);
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action) && !isIntentFromSelf
&& mPanel != null && mPanel.isShowing()) {
mPanel.dismiss();
}
}
};
private final ViewTreeObserver.OnGlobalFocusChangeListener mFocusChangeListener =
(oldFocus, newFocus) -> {
if (mPanel != null && oldFocus != null && newFocus instanceof FocusParkingView) {
// When nudging out of the panel, RotaryService will focus on the
// FocusParkingView to clear the focus highlight. When this occurs, dismiss the
// panel.
mPanel.dismiss();
}
};
public StatusIconPanelController(
Context context,
CarServiceProvider carServiceProvider,
BroadcastDispatcher broadcastDispatcher,
ConfigurationController configurationController) {
this(context, carServiceProvider, broadcastDispatcher, configurationController,
/* isDisabledWhileDriving= */ false);
}
public StatusIconPanelController(
Context context,
CarServiceProvider carServiceProvider,
BroadcastDispatcher broadcastDispatcher,
ConfigurationController configurationController,
boolean isDisabledWhileDriving) {
mContext = context;
mIdentifier = Integer.toString(System.identityHashCode(this));
mIconTag = mContext.getResources().getString(R.string.qc_icon_tag);
mIconHighlightedColor = mContext.getColor(R.color.status_icon_highlighted_color);
mIconNotHighlightedColor = mContext.getColor(R.color.status_icon_not_highlighted_color);
int panelMarginTop = mContext.getResources().getDimensionPixelSize(
R.dimen.car_status_icon_panel_margin_top);
int topSystemBarHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.car_top_system_bar_height);
// Cancel out the superfluous inset automatically applied to the panel.
mYOffsetPixel = panelMarginTop - topSystemBarHeight;
broadcastDispatcher.registerReceiver(mBroadcastReceiver,
new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), /* executor= */ null,
UserHandle.ALL);
configurationController.addCallback(mConfigurationListener);
context.registerReceiverForAllUsers(mUserChangeReceiver, INTENT_FILTER_USER_CHANGED,
/* broadcastPermission= */ null, /* scheduler= */ null);
mIsDisabledWhileDriving = isDisabledWhileDriving;
if (mIsDisabledWhileDriving) {
mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(mContext);
mCarUxRestrictionsUtil.register(mUxRestrictionsChangedListener);
}
}
/**
* @return default Y offset in pixels that cancels out the superfluous inset automatically
* applied to the panel
*/
public int getDefaultYOffset() {
return mYOffsetPixel;
}
/**
* @return list of {@link SystemUIQCView} in this controller
*/
public ArrayList<SystemUIQCView> getQCViews() {
return mQCViews;
}
public void setOnQcViewsFoundListener(OnQcViewsFoundListener onQcViewsFoundListener) {
mOnQcViewsFoundListener = onQcViewsFoundListener;
}
/**
* A listener that can be used to attach controllers quick control panels using
* {@link SystemUIQCView#getLocalQCProvider()}
*/
public interface OnQcViewsFoundListener {
/**
* This method is call up when {@link SystemUIQCView}s are found
*/
void qcViewsFound(ArrayList<SystemUIQCView> qcViews);
}
/**
* Attaches a panel to a root view that toggles the panel visibility when clicked.
*
* Variant of {@link #attachPanel(View, int, int, int, int, int)} with
* xOffset={@code 0}, yOffset={@link #mYOffsetPixel} &
* gravity={@link #DEFAULT_POPUP_WINDOW_ANCHOR_GRAVITY}.
*/
public void attachPanel(View view, @LayoutRes int layoutRes, @DimenRes int widthRes) {
attachPanel(view, layoutRes, widthRes, DEFAULT_POPUP_WINDOW_ANCHOR_GRAVITY);
}
/**
* Attaches a panel to a root view that toggles the panel visibility when clicked.
*
* Variant of {@link #attachPanel(View, int, int, int, int, int)} with
* xOffset={@code 0} & yOffset={@link #mYOffsetPixel}.
*/
public void attachPanel(View view, @LayoutRes int layoutRes, @DimenRes int widthRes,
int gravity) {
attachPanel(view, layoutRes, widthRes, /* xOffset= */ 0, mYOffsetPixel,
gravity);
}
/**
* Attaches a panel to a root view that toggles the panel visibility when clicked.
*
* Variant of {@link #attachPanel(View, int, int, int, int, int)} with
* gravity={@link #DEFAULT_POPUP_WINDOW_ANCHOR_GRAVITY}.
*/
public void attachPanel(View view, @LayoutRes int layoutRes, @DimenRes int widthRes,
int xOffset, int yOffset) {
attachPanel(view, layoutRes, widthRes, xOffset, yOffset,
DEFAULT_POPUP_WINDOW_ANCHOR_GRAVITY);
}
/**
* Attaches a panel to a root view that toggles the panel visibility when clicked.
*/
public void attachPanel(View view, @LayoutRes int layoutRes, @DimenRes int widthRes,
int xOffset, int yOffset, int gravity) {
if (mAnchorView == null) {
mAnchorView = view;
}
mAnchorView.setOnClickListener(v -> {
if (mIsDisabledWhileDriving && mCarUxRestrictionsUtil.getCurrentRestrictions()
.isRequiresDistractionOptimization()) {
dismissAllSystemDialogs();
Toast.makeText(mContext, R.string.car_ui_restricted_while_driving,
Toast.LENGTH_LONG).show();
return;
}
if (mPanel == null) {
mPanel = createPanel(layoutRes, widthRes);
}
if (mPanel.isShowing()) {
mPanel.dismiss();
return;
}
// Dismiss all currently open system dialogs before opening this panel.
dismissAllSystemDialogs();
mQCViews.forEach(qcView -> qcView.listen(true));
// Clear the focus highlight in this window since a dialog window is about to show.
// TODO(b/201700195): remove this workaround once the window focus issue is fixed.
if (view.isFocused()) {
ViewUtils.hideFocus(view.getRootView());
}
registerFocusListener(true);
// TODO(b/202563671): remove yOffsetPixel when the PopupWindow API is updated.
mPanel.showAsDropDown(mAnchorView, xOffset, yOffset, gravity);
mAnchorView.setSelected(true);
highlightStatusIcon(true);
setAnimatedStatusIconHighlightedStatus(true);
dimBehind(mPanel);
});
}
@VisibleForTesting
protected PopupWindow getPanel() {
return mPanel;
}
@VisibleForTesting
protected BroadcastReceiver getBroadcastReceiver() {
return mBroadcastReceiver;
}
@VisibleForTesting
protected String getIdentifier() {
return mIdentifier;
}
@VisibleForTesting
@ColorInt
protected int getIconHighlightedColor() {
return mIconHighlightedColor;
}
@VisibleForTesting
@ColorInt
protected int getIconNotHighlightedColor() {
return mIconNotHighlightedColor;
}
private PopupWindow createPanel(@LayoutRes int layoutRes, @DimenRes int widthRes) {
int panelWidth = mContext.getResources().getDimensionPixelSize(widthRes);
mPanelContent = (ViewGroup) LayoutInflater.from(mContext).inflate(layoutRes, /* root= */
null);
mPanelContent.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
findQcViews(mPanelContent);
if (mOnQcViewsFoundListener != null) {
mOnQcViewsFoundListener.qcViewsFound(mQCViews);
}
PopupWindow panel = new PopupWindow(mPanelContent, panelWidth, WRAP_CONTENT);
panel.setBackgroundDrawable(
mContext.getResources().getDrawable(R.drawable.status_icon_panel_bg,
mContext.getTheme()));
panel.setWindowLayoutType(TYPE_SYSTEM_DIALOG);
panel.setFocusable(true);
panel.setOutsideTouchable(false);
panel.setOnDismissListener(() -> {
setAnimatedStatusIconHighlightedStatus(false);
mAnchorView.setSelected(false);
highlightStatusIcon(false);
registerFocusListener(false);
mQCViews.forEach(qcView -> qcView.listen(false));
});
addFocusParkingView();
return panel;
}
private void dimBehind(PopupWindow popupWindow) {
View container = popupWindow.getContentView().getRootView();
WindowManager wm = mContext.getSystemService(WindowManager.class);
if (wm == null) return;
if (mDimValue < 0) {
mDimValue = mContext.getResources().getFloat(R.dimen.car_status_icon_panel_dim);
}
WindowManager.LayoutParams lp = (WindowManager.LayoutParams) container.getLayoutParams();
lp.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
lp.dimAmount = mDimValue;
wm.updateViewLayout(container, lp);
}
private void dismissAllSystemDialogs() {
Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
intent.setIdentifier(mIdentifier);
mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
}
/**
* Add a FocusParkingView to the panel content to prevent rotary controller rotation wrapping
* around in the panel - this only should be called once per panel.
*/
private void addFocusParkingView() {
if (mPanelContent != null) {
FocusParkingView fpv = new FocusParkingView(mContext);
mPanelContent.addView(fpv);
}
}
private void registerFocusListener(boolean register) {
if (mPanelContent == null) {
return;
}
if (register) {
mPanelContent.getViewTreeObserver().addOnGlobalFocusChangeListener(
mFocusChangeListener);
} else {
mPanelContent.getViewTreeObserver().removeOnGlobalFocusChangeListener(
mFocusChangeListener);
}
}
private void reset() {
if (mPanel == null) return;
mPanel.dismiss();
mPanel = null;
mPanelContent = null;
mOnQcViewsFoundListener = null;
mQCViews.forEach(v -> v.destroy());
mQCViews.clear();
}
private void findQcViews(ViewGroup rootView) {
for (int i = 0; i < rootView.getChildCount(); i++) {
View v = rootView.getChildAt(i);
if (v instanceof SystemUIQCView) {
mQCViews.add((SystemUIQCView) v);
} else if (v instanceof ViewGroup) {
this.findQcViews((ViewGroup) v);
}
}
}
private void setAnimatedStatusIconHighlightedStatus(boolean isHighlighted) {
if (mAnchorView instanceof AnimatedStatusIcon) {
((AnimatedStatusIcon) mAnchorView).setIconHighlighted(isHighlighted);
}
}
private void highlightStatusIcon(boolean isHighlighted) {
if (mStatusIconView == null) {
mStatusIconView = mAnchorView.findViewWithTag(mIconTag);
}
if (mStatusIconView != null) {
mStatusIconView.setColorFilter(
isHighlighted ? mIconHighlightedColor : mIconNotHighlightedColor);
}
}
}