| /* |
| * 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.util.InjectionInflationController.VIEW_CONTEXT; |
| import static com.android.systemui.util.Utils.useQsMediaPlayer; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.PointF; |
| import android.metrics.LogMaker; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.LinearLayout; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.UiEventLogger; |
| import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| import com.android.internal.widget.RemeasuringLinearLayout; |
| import com.android.systemui.Dependency; |
| import com.android.systemui.Dumpable; |
| import com.android.systemui.R; |
| import com.android.systemui.broadcast.BroadcastDispatcher; |
| import com.android.systemui.dump.DumpManager; |
| import com.android.systemui.media.MediaHierarchyManager; |
| import com.android.systemui.media.MediaHost; |
| 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.qs.logging.QSLogger; |
| 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 com.android.systemui.util.animation.DisappearParameters; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| |
| 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<>(); |
| private final BroadcastDispatcher mBroadcastDispatcher; |
| protected final MediaHost mMediaHost; |
| |
| /** |
| * The index where the content starts that needs to be moved between parents |
| */ |
| private final int mMovableContentStartIndex; |
| private String mCachedSpecs = ""; |
| |
| @Nullable |
| protected View mBrightnessView; |
| @Nullable |
| private BrightnessController mBrightnessController; |
| |
| private final H mHandler = new H(); |
| private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); |
| private QSTileRevealController mQsTileRevealController; |
| /** Whether or not the QS media player feature is enabled. */ |
| protected boolean mUsingMediaPlayer; |
| private int mVisualMarginStart; |
| private int mVisualMarginEnd; |
| |
| protected boolean mExpanded; |
| protected boolean mListening; |
| |
| private QSDetail.Callback mCallback; |
| private final DumpManager mDumpManager; |
| private final QSLogger mQSLogger; |
| protected final UiEventLogger mUiEventLogger; |
| protected QSTileHost mHost; |
| |
| @Nullable |
| protected QSSecurityFooter mSecurityFooter; |
| |
| @Nullable |
| protected View mFooter; |
| @Nullable |
| protected View mDivider; |
| |
| @Nullable |
| private ViewGroup mHeaderContainer; |
| private PageIndicator mFooterPageIndicator; |
| private boolean mGridContentVisible = true; |
| private int mContentMarginStart; |
| private int mContentMarginEnd; |
| private int mVisualTilePadding; |
| private boolean mUsingHorizontalLayout; |
| |
| private QSCustomizer mCustomizePanel; |
| private Record mDetailRecord; |
| |
| private BrightnessMirrorController mBrightnessMirrorController; |
| private LinearLayout mHorizontalLinearLayout; |
| private LinearLayout mHorizontalContentContainer; |
| |
| // Only used with media |
| private QSTileLayout mHorizontalTileLayout; |
| protected QSTileLayout mRegularTileLayout; |
| protected QSTileLayout mTileLayout; |
| private int mLastOrientation = -1; |
| private int mMediaTotalBottomMargin; |
| private int mFooterMarginStartHorizontal; |
| private Consumer<Boolean> mMediaVisibilityChangedListener; |
| |
| |
| @Inject |
| public QSPanel( |
| @Named(VIEW_CONTEXT) Context context, |
| AttributeSet attrs, |
| DumpManager dumpManager, |
| BroadcastDispatcher broadcastDispatcher, |
| QSLogger qsLogger, |
| MediaHost mediaHost, |
| UiEventLogger uiEventLogger |
| ) { |
| super(context, attrs); |
| mUsingMediaPlayer = useQsMediaPlayer(context); |
| mMediaTotalBottomMargin = getResources().getDimensionPixelSize( |
| R.dimen.quick_settings_bottom_margin_media); |
| mMediaHost = mediaHost; |
| mMediaHost.addVisibilityChangeListener((visible) -> { |
| onMediaVisibilityChanged(visible); |
| return null; |
| }); |
| mContext = context; |
| mQSLogger = qsLogger; |
| mDumpManager = dumpManager; |
| mBroadcastDispatcher = broadcastDispatcher; |
| mUiEventLogger = uiEventLogger; |
| |
| setOrientation(VERTICAL); |
| |
| addViewsAboveTiles(); |
| mMovableContentStartIndex = getChildCount(); |
| mRegularTileLayout = createRegularTileLayout(); |
| |
| if (mUsingMediaPlayer) { |
| mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext); |
| mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL); |
| mHorizontalLinearLayout.setClipChildren(false); |
| mHorizontalLinearLayout.setClipToPadding(false); |
| |
| mHorizontalContentContainer = new RemeasuringLinearLayout(mContext); |
| mHorizontalContentContainer.setOrientation(LinearLayout.VERTICAL); |
| mHorizontalContentContainer.setClipChildren(false); |
| mHorizontalContentContainer.setClipToPadding(false); |
| |
| mHorizontalTileLayout = createHorizontalTileLayout(); |
| LayoutParams lp = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1); |
| int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing); |
| lp.setMarginStart(0); |
| lp.setMarginEnd(marginSize); |
| lp.gravity = Gravity.CENTER_VERTICAL; |
| mHorizontalLinearLayout.addView(mHorizontalContentContainer, lp); |
| |
| lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1); |
| addView(mHorizontalLinearLayout, lp); |
| |
| initMediaHostState(); |
| } |
| addSecurityFooter(); |
| if (mRegularTileLayout instanceof PagedTileLayout) { |
| mQsTileRevealController = new QSTileRevealController(mContext, this, |
| (PagedTileLayout) mRegularTileLayout); |
| } |
| mQSLogger.logAllTilesChangeListening(mListening, getDumpableTag(), mCachedSpecs); |
| updateResources(); |
| } |
| |
| protected void onMediaVisibilityChanged(Boolean visible) { |
| switchTileLayout(); |
| if (mMediaVisibilityChangedListener != null) { |
| mMediaVisibilityChangedListener.accept(visible); |
| } |
| } |
| |
| protected void addSecurityFooter() { |
| mSecurityFooter = new QSSecurityFooter(this, mContext); |
| } |
| |
| protected void addViewsAboveTiles() { |
| mBrightnessView = LayoutInflater.from(mContext).inflate( |
| R.layout.quick_settings_brightness_dialog, this, false); |
| addView(mBrightnessView); |
| mBrightnessController = new BrightnessController(getContext(), |
| findViewById(R.id.brightness_slider), mBroadcastDispatcher); |
| } |
| |
| protected QSTileLayout createRegularTileLayout() { |
| if (mRegularTileLayout == null) { |
| mRegularTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate( |
| R.layout.qs_paged_tile_layout, this, false); |
| } |
| return mRegularTileLayout; |
| } |
| |
| |
| protected QSTileLayout createHorizontalTileLayout() { |
| return createRegularTileLayout(); |
| } |
| |
| protected void initMediaHostState() { |
| mMediaHost.setExpansion(1.0f); |
| mMediaHost.setShowsOnlyActiveMedia(false); |
| updateMediaDisappearParameters(); |
| mMediaHost.init(MediaHierarchyManager.LOCATION_QS); |
| } |
| |
| /** |
| * Update the way the media disappears based on if we're using the horizontal layout |
| */ |
| private void updateMediaDisappearParameters() { |
| if (!mUsingMediaPlayer) { |
| return; |
| } |
| DisappearParameters parameters = mMediaHost.getDisappearParameters(); |
| if (mUsingHorizontalLayout) { |
| // Only height remaining |
| parameters.getDisappearSize().set(0.0f, 0.4f); |
| // Disappearing on the right side on the bottom |
| parameters.getGonePivot().set(1.0f, 1.0f); |
| // translating a bit horizontal |
| parameters.getContentTranslationFraction().set(0.25f, 1.0f); |
| parameters.setDisappearEnd(0.6f); |
| } else { |
| // Only width remaining |
| parameters.getDisappearSize().set(1.0f, 0.0f); |
| // Disappearing on the bottom |
| parameters.getGonePivot().set(0.0f, 1.0f); |
| // translating a bit vertical |
| parameters.getContentTranslationFraction().set(0.0f, 1.05f); |
| parameters.setDisappearEnd(0.95f); |
| } |
| parameters.setFadeStartPosition(0.95f); |
| parameters.setDisappearStart(0.0f); |
| mMediaHost.setDisappearParameters(parameters); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| if (mTileLayout instanceof PagedTileLayout) { |
| // Since PageIndicator gets measured before PagedTileLayout, we preemptively set the |
| // # of pages before the measurement pass so PageIndicator is measured appropriately |
| if (mFooterPageIndicator != null) { |
| mFooterPageIndicator.setNumPages(((PagedTileLayout) mTileLayout).getNumPages()); |
| } |
| |
| // Allow the UI to be as big as it want's to, we're in a scroll view |
| int newHeight = 10000; |
| int availableHeight = MeasureSpec.getSize(heightMeasureSpec); |
| int excessHeight = newHeight - availableHeight; |
| // Measure with EXACTLY. That way, The content will only use excess height and will |
| // be measured last, after other views and padding is accounted for. This only |
| // works because our Layouts in here remeasure themselves with the exact content |
| // height. |
| heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); |
| ((PagedTileLayout) mTileLayout).setExcessHeight(excessHeight); |
| } |
| super.onMeasure(widthMeasureSpec, 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 |
| 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(); |
| MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); |
| height += layoutParams.topMargin + layoutParams.bottomMargin; |
| } |
| } |
| setMeasuredDimension(getMeasuredWidth(), height); |
| } |
| |
| 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); |
| } |
| mDumpManager.registerDumpable(getDumpableTag(), this); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| Dependency.get(TunerService.class).removeTunable(this); |
| if (mHost != null) { |
| mHost.removeCallback(this); |
| } |
| if (mTileLayout != null) { |
| mTileLayout.setListening(false); |
| } |
| for (TileRecord record : mRecords) { |
| record.tile.removeCallbacks(); |
| } |
| mRecords.clear(); |
| if (mBrightnessMirrorController != null) { |
| mBrightnessMirrorController.removeCallback(this); |
| } |
| mDumpManager.unregisterDumpable(getDumpableTag()); |
| super.onDetachedFromWindow(); |
| } |
| |
| protected String getDumpableTag() { |
| return TAG; |
| } |
| |
| @Override |
| public void onTilesChanged() { |
| setTiles(mHost.getTiles()); |
| } |
| |
| @Override |
| public void onTuningChanged(String key, String newValue) { |
| if (QS_SHOW_BRIGHTNESS.equals(key) && mBrightnessView != null) { |
| 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(); |
| } |
| |
| @Nullable |
| 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()); |
| if (mSecurityFooter != null) { |
| mSecurityFooter.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 (mRegularTileLayout instanceof PagedTileLayout) { |
| mFooterPageIndicator = pageIndicator; |
| updatePageIndicator(); |
| } |
| } |
| |
| private void updatePageIndicator() { |
| if (mRegularTileLayout instanceof PagedTileLayout) { |
| if (mFooterPageIndicator != null) { |
| mFooterPageIndicator.setVisibility(View.GONE); |
| |
| ((PagedTileLayout) mRegularTileLayout).setPageIndicator(mFooterPageIndicator); |
| } |
| } |
| } |
| |
| public QSTileHost getHost() { |
| return mHost; |
| } |
| |
| public void updateResources() { |
| int tileSize = getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size); |
| int tileBg = getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size); |
| mFooterMarginStartHorizontal = getResources().getDimensionPixelSize( |
| R.dimen.qs_footer_horizontal_margin); |
| mVisualTilePadding = (int) ((tileSize - tileBg) / 2.0f); |
| updatePadding(); |
| |
| updatePageIndicator(); |
| |
| for (TileRecord r : mRecords) { |
| r.tile.clearState(); |
| } |
| if (mListening) { |
| refreshAllTiles(); |
| } |
| if (mTileLayout != null) { |
| mTileLayout.updateResources(); |
| } |
| } |
| |
| protected void updatePadding() { |
| final Resources res = mContext.getResources(); |
| int padding = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top); |
| if (mUsingHorizontalLayout) { |
| // When using the horizontal layout, our space is quite constrained. We therefore |
| // reduce some of the padding on the top, which makes the brightness bar overlapp, |
| // but since that has naturally quite a bit of built in padding, that's fine. |
| padding = (int) (padding * 0.6f); |
| } |
| setPaddingRelative(getPaddingStart(), |
| padding, |
| getPaddingEnd(), |
| res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom)); |
| } |
| |
| @Override |
| protected void onConfigurationChanged(Configuration newConfig) { |
| super.onConfigurationChanged(newConfig); |
| if (mSecurityFooter != null) { |
| mSecurityFooter.onConfigurationChanged(); |
| } |
| updateResources(); |
| |
| updateBrightnessMirror(); |
| |
| if (newConfig.orientation != mLastOrientation) { |
| mLastOrientation = newConfig.orientation; |
| switchTileLayout(); |
| } |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mFooter = findViewById(R.id.qs_footer); |
| mDivider = findViewById(R.id.divider); |
| switchTileLayout(true /* force */); |
| } |
| |
| boolean switchTileLayout() { |
| return switchTileLayout(false /* force */); |
| } |
| |
| private boolean switchTileLayout(boolean force) { |
| /** Whether or not the QuickQSPanel currently contains a media player. */ |
| boolean horizontal = shouldUseHorizontalLayout(); |
| if (mDivider != null) { |
| if (!horizontal && mUsingMediaPlayer && mMediaHost.getVisible()) { |
| mDivider.setVisibility(View.VISIBLE); |
| } else { |
| mDivider.setVisibility(View.GONE); |
| } |
| } |
| if (horizontal != mUsingHorizontalLayout || force) { |
| mUsingHorizontalLayout = horizontal; |
| View visibleView = horizontal ? mHorizontalLinearLayout : (View) mRegularTileLayout; |
| View hiddenView = horizontal ? (View) mRegularTileLayout : mHorizontalLinearLayout; |
| ViewGroup newParent = horizontal ? mHorizontalContentContainer : this; |
| QSTileLayout newLayout = horizontal ? mHorizontalTileLayout : mRegularTileLayout; |
| if (hiddenView != null && |
| (mRegularTileLayout != mHorizontalTileLayout || |
| hiddenView != mRegularTileLayout)) { |
| // Only hide the view if the horizontal and the regular view are different, |
| // otherwise its reattached. |
| hiddenView.setVisibility(View.GONE); |
| } |
| visibleView.setVisibility(View.VISIBLE); |
| switchAllContentToParent(newParent, newLayout); |
| reAttachMediaHost(); |
| if (mTileLayout != null) { |
| mTileLayout.setListening(false); |
| for (TileRecord record : mRecords) { |
| mTileLayout.removeTile(record); |
| record.tile.removeCallback(record.callback); |
| } |
| } |
| mTileLayout = newLayout; |
| if (mHost != null) setTiles(mHost.getTiles()); |
| newLayout.setListening(mListening); |
| if (needsDynamicRowsAndColumns()) { |
| newLayout.setMinRows(horizontal ? 2 : 1); |
| // Let's use 3 columns to match the current layout |
| newLayout.setMaxColumns(horizontal ? 3 : TileLayout.NO_MAX_COLUMNS); |
| } |
| updateTileLayoutMargins(); |
| updateFooterMargin(); |
| updateDividerMargin(); |
| updateMediaDisappearParameters(); |
| updateMediaHostContentMargins(); |
| updateHorizontalLinearLayoutMargins(); |
| updatePadding(); |
| return true; |
| } |
| return false; |
| } |
| |
| private void updateHorizontalLinearLayoutMargins() { |
| if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { |
| LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); |
| lp.bottomMargin = mMediaTotalBottomMargin - getPaddingBottom(); |
| mHorizontalLinearLayout.setLayoutParams(lp); |
| } |
| } |
| |
| /** |
| * @return true if the margin bottom of the media view should be on the media host or false |
| * if they should be on the HorizontalLinearLayout. Returning {@code false} is useful |
| * to visually center the tiles in the Media view, which doesn't work when the |
| * expanded panel actually scrolls. |
| */ |
| protected boolean displayMediaMarginsOnMedia() { |
| return true; |
| } |
| |
| protected boolean needsDynamicRowsAndColumns() { |
| return true; |
| } |
| |
| private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) { |
| int index = parent == this ? mMovableContentStartIndex : 0; |
| |
| // Let's first move the tileLayout to the new parent, since that should come first. |
| switchToParent((View) newLayout, parent, index); |
| index++; |
| |
| if (mSecurityFooter != null) { |
| View view = mSecurityFooter.getView(); |
| LinearLayout.LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); |
| if (mUsingHorizontalLayout && mHeaderContainer != null) { |
| // Adding the security view to the header, that enables us to avoid scrolling |
| layoutParams.width = 0; |
| layoutParams.weight = 1.6f; |
| switchToParent(view, mHeaderContainer, 1 /* always in second place */); |
| } else { |
| layoutParams.width = LayoutParams.WRAP_CONTENT; |
| layoutParams.weight = 0; |
| switchToParent(view, parent, index); |
| index++; |
| } |
| view.setLayoutParams(layoutParams); |
| } |
| |
| if (mFooter != null) { |
| // Then the footer with the settings |
| switchToParent(mFooter, parent, index); |
| } |
| } |
| |
| private void switchToParent(View child, ViewGroup parent, int index) { |
| ViewGroup currentParent = (ViewGroup) child.getParent(); |
| if (currentParent != parent || currentParent.indexOfChild(child) != index) { |
| if (currentParent != null) { |
| currentParent.removeView(child); |
| } |
| parent.addView(child, index); |
| } |
| } |
| |
| private boolean shouldUseHorizontalLayout() { |
| return mUsingMediaPlayer && mMediaHost.getVisible() |
| && getResources().getConfiguration().orientation |
| == Configuration.ORIENTATION_LANDSCAPE; |
| } |
| |
| protected void reAttachMediaHost() { |
| if (!mUsingMediaPlayer) { |
| return; |
| } |
| boolean horizontal = shouldUseHorizontalLayout(); |
| ViewGroup host = mMediaHost.getHostView(); |
| ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this; |
| ViewGroup currentParent = (ViewGroup) host.getParent(); |
| if (currentParent != newParent) { |
| if (currentParent != null) { |
| currentParent.removeView(host); |
| } |
| newParent.addView(host); |
| LinearLayout.LayoutParams layoutParams = (LayoutParams) host.getLayoutParams(); |
| layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; |
| layoutParams.width = horizontal ? 0 : ViewGroup.LayoutParams.MATCH_PARENT; |
| layoutParams.weight = horizontal ? 1.2f : 0; |
| // Add any bottom margin, such that the total spacing is correct. This is only |
| // necessary if the view isn't horizontal, since otherwise the padding is |
| // carried in the parent of this view (to ensure correct vertical alignment) |
| layoutParams.bottomMargin = !horizontal || displayMediaMarginsOnMedia() |
| ? mMediaTotalBottomMargin - getPaddingBottom() : 0; |
| } |
| } |
| |
| 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; |
| mQSLogger.logPanelExpanded(expanded, getDumpableTag()); |
| mExpanded = expanded; |
| if (!mExpanded && mTileLayout instanceof PagedTileLayout) { |
| ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); |
| } |
| mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded); |
| if (!mExpanded) { |
| mUiEventLogger.log(closePanelEvent()); |
| closeDetail(); |
| } else { |
| mUiEventLogger.log(openPanelEvent()); |
| 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) { |
| mQSLogger.logAllTilesChangeListening(listening, getDumpableTag(), mCachedSpecs); |
| mTileLayout.setListening(listening); |
| } |
| if (mListening) { |
| refreshAllTiles(); |
| } |
| } |
| |
| private String getTilesSpecs() { |
| return mRecords.stream() |
| .map(tileRecord -> tileRecord.tile.getTileSpec()) |
| .collect(Collectors.joining(",")); |
| } |
| |
| public void setListening(boolean listening, boolean expanded) { |
| setListening(listening && expanded); |
| if (mSecurityFooter != null) { |
| mSecurityFooter.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 (mBrightnessController == null) { |
| return; |
| } |
| if (listening) { |
| mBrightnessController.registerCallbacks(); |
| } else { |
| mBrightnessController.unregisterCallbacks(); |
| } |
| } |
| |
| public void refreshAllTiles() { |
| if (mBrightnessController != null) { |
| mBrightnessController.checkRestrictionAndSetEnabled(); |
| } |
| for (TileRecord r : mRecords) { |
| r.tile.refreshState(); |
| } |
| if (mSecurityFooter != null) { |
| mSecurityFooter.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(); |
| mCachedSpecs = ""; |
| 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 QSEvent openPanelEvent() { |
| return QSEvent.QS_PANEL_EXPANDED; |
| } |
| |
| protected QSEvent closePanelEvent() { |
| return QSEvent.QS_PANEL_COLLAPSED; |
| } |
| |
| protected QSEvent tileVisibleEvent() { |
| return QSEvent.QS_TILE_VISIBLE; |
| } |
| |
| 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); |
| mCachedSpecs = getTilesSpecs(); |
| |
| 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; |
| } |
| |
| @Nullable |
| public QSSecurityFooter getSecurityFooter() { |
| return mSecurityFooter; |
| } |
| |
| @Nullable |
| public View getDivider() { |
| return mDivider; |
| } |
| |
| public void showDeviceMonitoringDialog() { |
| if (mSecurityFooter != null) { |
| mSecurityFooter.showDeviceMonitoringDialog(); |
| } |
| } |
| |
| public void setContentMargins(int startMargin, int endMargin) { |
| // Only some views actually want this content padding, others want to go all the way |
| // to the edge like the brightness slider |
| mContentMarginStart = startMargin; |
| mContentMarginEnd = endMargin; |
| updateTileLayoutMargins(mContentMarginStart - mVisualTilePadding, |
| mContentMarginEnd - mVisualTilePadding); |
| updateMediaHostContentMargins(); |
| updateFooterMargin(); |
| updateDividerMargin(); |
| } |
| |
| private void updateFooterMargin() { |
| if (mFooter != null) { |
| int footerMargin = 0; |
| int indicatorMargin = 0; |
| if (mUsingHorizontalLayout) { |
| footerMargin = mFooterMarginStartHorizontal; |
| indicatorMargin = footerMargin - mVisualMarginEnd; |
| } |
| updateMargins(mFooter, footerMargin, 0); |
| // The page indicator isn't centered anymore because of the visual positioning. |
| // Let's fix it by adding some margin |
| if (mFooterPageIndicator != null) { |
| updateMargins(mFooterPageIndicator, 0, indicatorMargin); |
| } |
| } |
| } |
| |
| /** |
| * Update the margins of all tile Layouts. |
| * |
| * @param visualMarginStart the visual start margin of the tile, adjusted for local insets |
| * to the tile. This can be set on a tileLayout |
| * @param visualMarginEnd the visual end margin of the tile, adjusted for local insets |
| * to the tile. This can be set on a tileLayout |
| */ |
| private void updateTileLayoutMargins(int visualMarginStart, int visualMarginEnd) { |
| mVisualMarginStart = visualMarginStart; |
| mVisualMarginEnd = visualMarginEnd; |
| updateTileLayoutMargins(); |
| } |
| |
| private void updateTileLayoutMargins() { |
| int marginEnd = mVisualMarginEnd; |
| if (mUsingHorizontalLayout) { |
| marginEnd = 0; |
| } |
| updateMargins((View) mTileLayout, mVisualMarginStart, marginEnd); |
| } |
| |
| private void updateDividerMargin() { |
| if (mDivider == null) return; |
| updateMargins(mDivider, mContentMarginStart, mContentMarginEnd); |
| } |
| |
| /** |
| * Update the margins of the media hosts |
| */ |
| protected void updateMediaHostContentMargins() { |
| if (mUsingMediaPlayer) { |
| int marginStart = mContentMarginStart; |
| if (mUsingHorizontalLayout) { |
| marginStart = 0; |
| } |
| updateMargins(mMediaHost.getHostView(), marginStart, mContentMarginEnd); |
| } |
| } |
| |
| /** |
| * Update the margins of a view. |
| * |
| * @param view the view to adjust |
| * @param start the start margin to set |
| * @param end the end margin to set |
| */ |
| protected void updateMargins(View view, int start, int end) { |
| LayoutParams lp = (LayoutParams) view.getLayoutParams(); |
| lp.setMarginStart(start); |
| lp.setMarginEnd(end); |
| view.setLayoutParams(lp); |
| } |
| |
| public MediaHost getMediaHost() { |
| return mMediaHost; |
| } |
| |
| /** |
| * Set the header container of quick settings. |
| */ |
| public void setHeaderContainer(@NonNull ViewGroup headerContainer) { |
| mHeaderContainer = headerContainer; |
| } |
| |
| public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { |
| mMediaVisibilityChangedListener = visibilityChangedListener; |
| } |
| |
| 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); |
| |
| /** |
| * Set the minimum number of rows to show |
| * |
| * @param minRows the minimum. |
| */ |
| default boolean setMinRows(int minRows) { |
| return false; |
| } |
| |
| /** |
| * Set the max number of collums to show |
| * |
| * @param maxColumns the maximum |
| * |
| * @return true if the number of visible columns has changed. |
| */ |
| default boolean setMaxColumns(int maxColumns) { |
| return false; |
| } |
| |
| default void setExpansion(float expansion) {} |
| |
| int getNumVisibleTiles(); |
| } |
| } |