/*
|
* Copyright (C) 2014 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.qs;
|
|
import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
|
import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEXT;
|
|
import android.annotation.Nullable;
|
import android.content.ComponentName;
|
import android.content.Context;
|
import android.content.res.Configuration;
|
import android.content.res.Resources;
|
import android.metrics.LogMaker;
|
import android.os.Bundle;
|
import android.os.Handler;
|
import android.os.Message;
|
import android.service.quicksettings.Tile;
|
import android.util.AttributeSet;
|
import android.view.LayoutInflater;
|
import android.view.View;
|
import android.widget.LinearLayout;
|
|
import com.android.internal.logging.MetricsLogger;
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
import com.android.settingslib.Utils;
|
import com.android.systemui.Dependency;
|
import com.android.systemui.DumpController;
|
import com.android.systemui.Dumpable;
|
import com.android.systemui.R;
|
import com.android.systemui.plugins.qs.DetailAdapter;
|
import com.android.systemui.plugins.qs.QSTile;
|
import com.android.systemui.plugins.qs.QSTileView;
|
import com.android.systemui.qs.QSHost.Callback;
|
import com.android.systemui.qs.customize.QSCustomizer;
|
import com.android.systemui.qs.external.CustomTile;
|
import com.android.systemui.settings.BrightnessController;
|
import com.android.systemui.settings.ToggleSliderView;
|
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
|
import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
|
import com.android.systemui.tuner.TunerService;
|
import com.android.systemui.tuner.TunerService.Tunable;
|
|
import java.io.FileDescriptor;
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
|
import javax.inject.Inject;
|
import javax.inject.Named;
|
|
/** View that represents the quick settings tile panel (when expanded/pulled down). **/
|
public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener,
|
Dumpable {
|
|
public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
|
public static final String QS_SHOW_HEADER = "qs_show_header";
|
|
private static final String TAG = "QSPanel";
|
|
protected final Context mContext;
|
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
|
protected final View mBrightnessView;
|
private final H mHandler = new H();
|
private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
|
private final QSTileRevealController mQsTileRevealController;
|
|
protected boolean mExpanded;
|
protected boolean mListening;
|
|
private QSDetail.Callback mCallback;
|
private BrightnessController mBrightnessController;
|
private DumpController mDumpController;
|
protected QSTileHost mHost;
|
|
protected QSSecurityFooter mFooter;
|
private PageIndicator mFooterPageIndicator;
|
private boolean mGridContentVisible = true;
|
|
protected QSTileLayout mTileLayout;
|
|
private QSCustomizer mCustomizePanel;
|
private Record mDetailRecord;
|
|
private BrightnessMirrorController mBrightnessMirrorController;
|
private View mDivider;
|
|
public QSPanel(Context context) {
|
this(context, null);
|
}
|
|
public QSPanel(Context context, AttributeSet attrs) {
|
this(context, attrs, null);
|
}
|
|
@Inject
|
public QSPanel(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
|
DumpController dumpController) {
|
super(context, attrs);
|
mContext = context;
|
|
setOrientation(VERTICAL);
|
|
mBrightnessView = LayoutInflater.from(mContext).inflate(
|
R.layout.quick_settings_brightness_dialog, this, false);
|
addView(mBrightnessView);
|
|
mTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate(
|
R.layout.qs_paged_tile_layout, this, false);
|
mTileLayout.setListening(mListening);
|
addView((View) mTileLayout);
|
|
mQsTileRevealController = new QSTileRevealController(mContext, this,
|
(PagedTileLayout) mTileLayout);
|
|
addDivider();
|
|
mFooter = new QSSecurityFooter(this, context);
|
addView(mFooter.getView());
|
|
updateResources();
|
|
mBrightnessController = new BrightnessController(getContext(),
|
findViewById(R.id.brightness_slider));
|
mDumpController = dumpController;
|
}
|
|
protected void addDivider() {
|
mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
|
mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
|
getColorForState(mContext, Tile.STATE_ACTIVE)));
|
addView(mDivider);
|
}
|
|
@Override
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
// We want all the logic of LinearLayout#onMeasure, and for it to assign the excess space
|
// not used by the other children to PagedTileLayout. However, in this case, LinearLayout
|
// assumes that PagedTileLayout would use all the excess space. This is not the case as
|
// PagedTileLayout height is quantized (because it shows a certain number of rows).
|
// Therefore, after everything is measured, we need to make sure that we add up the correct
|
// total height
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
int height = getPaddingBottom() + getPaddingTop();
|
int numChildren = getChildCount();
|
for (int i = 0; i < numChildren; i++) {
|
View child = getChildAt(i);
|
if (child.getVisibility() != View.GONE) height += child.getMeasuredHeight();
|
}
|
setMeasuredDimension(getMeasuredWidth(), height);
|
}
|
|
public View getDivider() {
|
return mDivider;
|
}
|
|
public QSTileRevealController getQsTileRevealController() {
|
return mQsTileRevealController;
|
}
|
|
public boolean isShowingCustomize() {
|
return mCustomizePanel != null && mCustomizePanel.isCustomizing();
|
}
|
|
@Override
|
protected void onAttachedToWindow() {
|
super.onAttachedToWindow();
|
final TunerService tunerService = Dependency.get(TunerService.class);
|
tunerService.addTunable(this, QS_SHOW_BRIGHTNESS);
|
|
if (mHost != null) {
|
setTiles(mHost.getTiles());
|
}
|
if (mBrightnessMirrorController != null) {
|
mBrightnessMirrorController.addCallback(this);
|
}
|
if (mDumpController != null) mDumpController.addListener(this);
|
}
|
|
@Override
|
protected void onDetachedFromWindow() {
|
Dependency.get(TunerService.class).removeTunable(this);
|
if (mHost != null) {
|
mHost.removeCallback(this);
|
}
|
for (TileRecord record : mRecords) {
|
record.tile.removeCallbacks();
|
}
|
if (mBrightnessMirrorController != null) {
|
mBrightnessMirrorController.removeCallback(this);
|
}
|
if (mDumpController != null) mDumpController.removeListener(this);
|
super.onDetachedFromWindow();
|
}
|
|
@Override
|
public void onTilesChanged() {
|
setTiles(mHost.getTiles());
|
}
|
|
@Override
|
public void onTuningChanged(String key, String newValue) {
|
if (QS_SHOW_BRIGHTNESS.equals(key)) {
|
updateViewVisibilityForTuningValue(mBrightnessView, newValue);
|
}
|
}
|
|
private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) {
|
view.setVisibility(TunerService.parseIntegerSwitch(newValue, true) ? VISIBLE : GONE);
|
}
|
|
public void openDetails(String subPanel) {
|
QSTile tile = getTile(subPanel);
|
// If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory),
|
// QSFactory will not be able to create a tile and getTile will return null
|
if (tile != null) {
|
showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
|
}
|
}
|
|
private QSTile getTile(String subPanel) {
|
for (int i = 0; i < mRecords.size(); i++) {
|
if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
|
return mRecords.get(i).tile;
|
}
|
}
|
return mHost.createTile(subPanel);
|
}
|
|
public void setBrightnessMirror(BrightnessMirrorController c) {
|
if (mBrightnessMirrorController != null) {
|
mBrightnessMirrorController.removeCallback(this);
|
}
|
mBrightnessMirrorController = c;
|
if (mBrightnessMirrorController != null) {
|
mBrightnessMirrorController.addCallback(this);
|
}
|
updateBrightnessMirror();
|
}
|
|
@Override
|
public void onBrightnessMirrorReinflated(View brightnessMirror) {
|
updateBrightnessMirror();
|
}
|
|
View getBrightnessView() {
|
return mBrightnessView;
|
}
|
|
public void setCallback(QSDetail.Callback callback) {
|
mCallback = callback;
|
}
|
|
public void setHost(QSTileHost host, QSCustomizer customizer) {
|
mHost = host;
|
mHost.addCallback(this);
|
setTiles(mHost.getTiles());
|
mFooter.setHostEnvironment(host);
|
mCustomizePanel = customizer;
|
if (mCustomizePanel != null) {
|
mCustomizePanel.setHost(mHost);
|
}
|
}
|
|
/**
|
* Links the footer's page indicator, which is used in landscape orientation to save space.
|
*
|
* @param pageIndicator indicator to use for page scrolling
|
*/
|
public void setFooterPageIndicator(PageIndicator pageIndicator) {
|
if (mTileLayout instanceof PagedTileLayout) {
|
mFooterPageIndicator = pageIndicator;
|
updatePageIndicator();
|
}
|
}
|
|
private void updatePageIndicator() {
|
if (mTileLayout instanceof PagedTileLayout) {
|
if (mFooterPageIndicator != null) {
|
mFooterPageIndicator.setVisibility(View.GONE);
|
|
((PagedTileLayout) mTileLayout).setPageIndicator(mFooterPageIndicator);
|
}
|
}
|
}
|
|
public QSTileHost getHost() {
|
return mHost;
|
}
|
|
public void updateResources() {
|
final Resources res = mContext.getResources();
|
setPadding(0, res.getDimensionPixelSize(R.dimen.qs_panel_padding_top), 0, res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom));
|
|
updatePageIndicator();
|
|
if (mListening) {
|
refreshAllTiles();
|
}
|
if (mTileLayout != null) {
|
mTileLayout.updateResources();
|
}
|
}
|
|
@Override
|
protected void onConfigurationChanged(Configuration newConfig) {
|
super.onConfigurationChanged(newConfig);
|
mFooter.onConfigurationChanged();
|
updateResources();
|
|
updateBrightnessMirror();
|
}
|
|
public void updateBrightnessMirror() {
|
if (mBrightnessMirrorController != null) {
|
ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider);
|
ToggleSliderView mirrorSlider = mBrightnessMirrorController.getMirror()
|
.findViewById(R.id.brightness_slider);
|
brightnessSlider.setMirror(mirrorSlider);
|
brightnessSlider.setMirrorController(mBrightnessMirrorController);
|
}
|
}
|
|
public void onCollapse() {
|
if (mCustomizePanel != null && mCustomizePanel.isShown()) {
|
mCustomizePanel.hide();
|
}
|
}
|
|
public void setExpanded(boolean expanded) {
|
if (mExpanded == expanded) return;
|
mExpanded = expanded;
|
if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
|
((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
|
}
|
mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded);
|
if (!mExpanded) {
|
closeDetail();
|
} else {
|
logTiles();
|
}
|
}
|
|
public void setPageListener(final PagedTileLayout.PageListener pageListener) {
|
if (mTileLayout instanceof PagedTileLayout) {
|
((PagedTileLayout) mTileLayout).setPageListener(pageListener);
|
}
|
}
|
|
public boolean isExpanded() {
|
return mExpanded;
|
}
|
|
public void setListening(boolean listening) {
|
if (mListening == listening) return;
|
mListening = listening;
|
if (mTileLayout != null) {
|
mTileLayout.setListening(listening);
|
}
|
if (mListening) {
|
refreshAllTiles();
|
}
|
}
|
|
public void setListening(boolean listening, boolean expanded) {
|
setListening(listening && expanded);
|
getFooter().setListening(listening);
|
// Set the listening as soon as the QS fragment starts listening regardless of the expansion,
|
// so it will update the current brightness before the slider is visible.
|
setBrightnessListening(listening);
|
}
|
|
public void setBrightnessListening(boolean listening) {
|
if (listening) {
|
mBrightnessController.registerCallbacks();
|
} else {
|
mBrightnessController.unregisterCallbacks();
|
}
|
}
|
|
public void refreshAllTiles() {
|
mBrightnessController.checkRestrictionAndSetEnabled();
|
for (TileRecord r : mRecords) {
|
r.tile.refreshState();
|
}
|
mFooter.refreshState();
|
}
|
|
public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) {
|
int xInWindow = locationInWindow[0];
|
int yInWindow = locationInWindow[1];
|
((View) getParent()).getLocationInWindow(locationInWindow);
|
|
Record r = new Record();
|
r.detailAdapter = adapter;
|
r.x = xInWindow - locationInWindow[0];
|
r.y = yInWindow - locationInWindow[1];
|
|
locationInWindow[0] = xInWindow;
|
locationInWindow[1] = yInWindow;
|
|
showDetail(show, r);
|
}
|
|
protected void showDetail(boolean show, Record r) {
|
mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
|
}
|
|
public void setTiles(Collection<QSTile> tiles) {
|
setTiles(tiles, false);
|
}
|
|
public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
|
if (!collapsedView) {
|
mQsTileRevealController.updateRevealedTiles(tiles);
|
}
|
for (TileRecord record : mRecords) {
|
mTileLayout.removeTile(record);
|
record.tile.removeCallback(record.callback);
|
}
|
mRecords.clear();
|
for (QSTile tile : tiles) {
|
addTile(tile, collapsedView);
|
}
|
}
|
|
protected void drawTile(TileRecord r, QSTile.State state) {
|
r.tileView.onStateChanged(state);
|
}
|
|
protected QSTileView createTileView(QSTile tile, boolean collapsedView) {
|
return mHost.createTileView(tile, collapsedView);
|
}
|
|
protected boolean shouldShowDetail() {
|
return mExpanded;
|
}
|
|
protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
|
final TileRecord r = new TileRecord();
|
r.tile = tile;
|
r.tileView = createTileView(tile, collapsedView);
|
final QSTile.Callback callback = new QSTile.Callback() {
|
@Override
|
public void onStateChanged(QSTile.State state) {
|
drawTile(r, state);
|
}
|
|
@Override
|
public void onShowDetail(boolean show) {
|
// Both the collapsed and full QS panels get this callback, this check determines
|
// which one should handle showing the detail.
|
if (shouldShowDetail()) {
|
QSPanel.this.showDetail(show, r);
|
}
|
}
|
|
@Override
|
public void onToggleStateChanged(boolean state) {
|
if (mDetailRecord == r) {
|
fireToggleStateChanged(state);
|
}
|
}
|
|
@Override
|
public void onScanStateChanged(boolean state) {
|
r.scanState = state;
|
if (mDetailRecord == r) {
|
fireScanStateChanged(r.scanState);
|
}
|
}
|
|
@Override
|
public void onAnnouncementRequested(CharSequence announcement) {
|
if (announcement != null) {
|
mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
|
.sendToTarget();
|
}
|
}
|
};
|
r.tile.addCallback(callback);
|
r.callback = callback;
|
r.tileView.init(r.tile);
|
r.tile.refreshState();
|
mRecords.add(r);
|
|
if (mTileLayout != null) {
|
mTileLayout.addTile(r);
|
}
|
|
return r;
|
}
|
|
|
public void showEdit(final View v) {
|
v.post(new Runnable() {
|
@Override
|
public void run() {
|
if (mCustomizePanel != null) {
|
if (!mCustomizePanel.isCustomizing()) {
|
int[] loc = v.getLocationOnScreen();
|
int x = loc[0] + v.getWidth() / 2;
|
int y = loc[1] + v.getHeight() / 2;
|
mCustomizePanel.show(x, y);
|
}
|
}
|
|
}
|
});
|
}
|
|
public void closeDetail() {
|
if (mCustomizePanel != null && mCustomizePanel.isShown()) {
|
// Treat this as a detail panel for now, to make things easy.
|
mCustomizePanel.hide();
|
return;
|
}
|
showDetail(false, mDetailRecord);
|
}
|
|
public int getGridHeight() {
|
return getMeasuredHeight();
|
}
|
|
protected void handleShowDetail(Record r, boolean show) {
|
if (r instanceof TileRecord) {
|
handleShowDetailTile((TileRecord) r, show);
|
} else {
|
int x = 0;
|
int y = 0;
|
if (r != null) {
|
x = r.x;
|
y = r.y;
|
}
|
handleShowDetailImpl(r, show, x, y);
|
}
|
}
|
|
private void handleShowDetailTile(TileRecord r, boolean show) {
|
if ((mDetailRecord != null) == show && mDetailRecord == r) return;
|
|
if (show) {
|
r.detailAdapter = r.tile.getDetailAdapter();
|
if (r.detailAdapter == null) return;
|
}
|
r.tile.setDetailListening(show);
|
int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
|
int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop();
|
handleShowDetailImpl(r, show, x, y);
|
}
|
|
private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
|
setDetailRecord(show ? r : null);
|
fireShowingDetail(show ? r.detailAdapter : null, x, y);
|
}
|
|
protected void setDetailRecord(Record r) {
|
if (r == mDetailRecord) return;
|
mDetailRecord = r;
|
final boolean scanState = mDetailRecord instanceof TileRecord
|
&& ((TileRecord) mDetailRecord).scanState;
|
fireScanStateChanged(scanState);
|
}
|
|
void setGridContentVisibility(boolean visible) {
|
int newVis = visible ? VISIBLE : INVISIBLE;
|
setVisibility(newVis);
|
if (mGridContentVisible != visible) {
|
mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis);
|
}
|
mGridContentVisible = visible;
|
}
|
|
private void logTiles() {
|
for (int i = 0; i < mRecords.size(); i++) {
|
QSTile tile = mRecords.get(i).tile;
|
mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory())
|
.setType(MetricsEvent.TYPE_OPEN)));
|
}
|
}
|
|
private void fireShowingDetail(DetailAdapter detail, int x, int y) {
|
if (mCallback != null) {
|
mCallback.onShowingDetail(detail, x, y);
|
}
|
}
|
|
private void fireToggleStateChanged(boolean state) {
|
if (mCallback != null) {
|
mCallback.onToggleStateChanged(state);
|
}
|
}
|
|
private void fireScanStateChanged(boolean state) {
|
if (mCallback != null) {
|
mCallback.onScanStateChanged(state);
|
}
|
}
|
|
public void clickTile(ComponentName tile) {
|
final String spec = CustomTile.toSpec(tile);
|
final int N = mRecords.size();
|
for (int i = 0; i < N; i++) {
|
if (mRecords.get(i).tile.getTileSpec().equals(spec)) {
|
mRecords.get(i).tile.click();
|
break;
|
}
|
}
|
}
|
|
QSTileLayout getTileLayout() {
|
return mTileLayout;
|
}
|
|
QSTileView getTileView(QSTile tile) {
|
for (TileRecord r : mRecords) {
|
if (r.tile == tile) {
|
return r.tileView;
|
}
|
}
|
return null;
|
}
|
|
public QSSecurityFooter getFooter() {
|
return mFooter;
|
}
|
|
public void showDeviceMonitoringDialog() {
|
mFooter.showDeviceMonitoringDialog();
|
}
|
|
public void setMargins(int sideMargins) {
|
for (int i = 0; i < getChildCount(); i++) {
|
View view = getChildAt(i);
|
if (view != mTileLayout) {
|
LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
lp.leftMargin = sideMargins;
|
lp.rightMargin = sideMargins;
|
}
|
}
|
}
|
|
private class H extends Handler {
|
private static final int SHOW_DETAIL = 1;
|
private static final int SET_TILE_VISIBILITY = 2;
|
private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3;
|
|
@Override
|
public void handleMessage(Message msg) {
|
if (msg.what == SHOW_DETAIL) {
|
handleShowDetail((Record) msg.obj, msg.arg1 != 0);
|
} else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
|
announceForAccessibility((CharSequence) msg.obj);
|
}
|
}
|
}
|
|
@Override
|
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
pw.println(getClass().getSimpleName() + ":");
|
pw.println(" Tile records:");
|
for (TileRecord record : mRecords) {
|
if (record.tile instanceof Dumpable) {
|
pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args);
|
pw.print(" "); pw.println(record.tileView.toString());
|
}
|
}
|
}
|
|
protected static class Record {
|
DetailAdapter detailAdapter;
|
int x;
|
int y;
|
}
|
|
public static final class TileRecord extends Record {
|
public QSTile tile;
|
public com.android.systemui.plugins.qs.QSTileView tileView;
|
public boolean scanState;
|
public QSTile.Callback callback;
|
}
|
|
public interface QSTileLayout {
|
|
default void saveInstanceState(Bundle outState) {}
|
|
default void restoreInstanceState(Bundle savedInstanceState) {}
|
|
void addTile(TileRecord tile);
|
|
void removeTile(TileRecord tile);
|
|
int getOffsetTop(TileRecord tile);
|
|
boolean updateResources();
|
|
void setListening(boolean listening);
|
|
default void setExpansion(float expansion) {}
|
|
int getNumVisibleTiles();
|
}
|
}
|