blob: 08cb4a9d44b60036a4518794bb66750dd5def86d [file] [log] [blame]
/*
* Copyright (C) 2020 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.internal.logging.nano.MetricsProto.MetricsEvent;
import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.res.Configuration;
import android.metrics.LogMaker;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.Dumpable;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.util.Utils;
import com.android.systemui.util.ViewController;
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.Named;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
/**
* Controller for QSPanel views.
*
* @param <T> Type of QSPanel.
*/
public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T>
implements Dumpable{
protected final QSTileHost mHost;
private final QSCustomizerController mQsCustomizerController;
private final boolean mUsingMediaPlayer;
protected final MediaHost mMediaHost;
protected final MetricsLogger mMetricsLogger;
private final UiEventLogger mUiEventLogger;
private final QSLogger mQSLogger;
private final DumpManager mDumpManager;
private final FeatureFlags mFeatureFlags;
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
private boolean mShouldUseSplitNotificationShade;
@Nullable
private Consumer<Boolean> mMediaVisibilityChangedListener;
private int mLastOrientation;
private String mCachedSpecs = "";
private QSTileRevealController mQsTileRevealController;
private float mRevealExpansion;
private final QSHost.Callback mQSHostCallback = this::setTiles;
@VisibleForTesting
protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
new QSPanel.OnConfigurationChangedListener() {
@Override
public void onConfigurationChange(Configuration newConfig) {
mShouldUseSplitNotificationShade =
Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources());
if (newConfig.orientation != mLastOrientation) {
mLastOrientation = newConfig.orientation;
switchTileLayout(false);
}
}
};
private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> {
if (mMediaVisibilityChangedListener != null) {
mMediaVisibilityChangedListener.accept(visible);
}
switchTileLayout(false);
return null;
};
private boolean mUsingHorizontalLayout;
@Nullable
private Runnable mUsingHorizontalLayoutChangedListener;
protected QSPanelControllerBase(
T view,
QSTileHost host,
QSCustomizerController qsCustomizerController,
@Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
MediaHost mediaHost,
MetricsLogger metricsLogger,
UiEventLogger uiEventLogger,
QSLogger qsLogger,
DumpManager dumpManager,
FeatureFlags featureFlags
) {
super(view);
mHost = host;
mQsCustomizerController = qsCustomizerController;
mUsingMediaPlayer = usingMediaPlayer;
mMediaHost = mediaHost;
mMetricsLogger = metricsLogger;
mUiEventLogger = uiEventLogger;
mQSLogger = qsLogger;
mDumpManager = dumpManager;
mFeatureFlags = featureFlags;
mShouldUseSplitNotificationShade =
Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources());
}
@Override
protected void onInit() {
mView.initialize();
mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
}
/**
* @return the media host for this panel
*/
public MediaHost getMediaHost() {
return mMediaHost;
}
@Override
protected void onViewAttached() {
mQsTileRevealController = createTileRevealController();
if (mQsTileRevealController != null) {
mQsTileRevealController.setExpansion(mRevealExpansion);
}
mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);
mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
mHost.addCallback(mQSHostCallback);
setTiles();
mLastOrientation = getResources().getConfiguration().orientation;
switchTileLayout(true);
mDumpManager.registerDumpable(mView.getDumpableTag(), this);
}
@Override
protected void onViewDetached() {
mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
mHost.removeCallback(mQSHostCallback);
mView.getTileLayout().setListening(false, mUiEventLogger);
mMediaHost.removeVisibilityChangeListener(mMediaHostVisibilityListener);
for (TileRecord record : mRecords) {
record.tile.removeCallbacks();
}
mRecords.clear();
mDumpManager.unregisterDumpable(mView.getDumpableTag());
}
protected QSTileRevealController createTileRevealController() {
return null;
}
/** */
public void setTiles() {
setTiles(mHost.getTiles(), false);
}
/** */
public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
// TODO(b/168904199): move this logic into QSPanelController.
if (!collapsedView && mQsTileRevealController != null) {
mQsTileRevealController.updateRevealedTiles(tiles);
}
for (QSPanelControllerBase.TileRecord record : mRecords) {
mView.removeTile(record);
record.tile.removeCallback(record.callback);
}
mRecords.clear();
mCachedSpecs = "";
for (QSTile tile : tiles) {
addTile(tile, collapsedView);
}
}
/** */
public void refreshAllTiles() {
for (QSPanelControllerBase.TileRecord r : mRecords) {
r.tile.refreshState();
}
}
private void addTile(final QSTile tile, boolean collapsedView) {
final TileRecord r = new TileRecord();
r.tile = tile;
r.tileView = mHost.createTileView(getContext(), tile, collapsedView);
mView.addTile(r);
mRecords.add(r);
mCachedSpecs = getTilesSpecs();
}
/** */
public void clickTile(ComponentName tile) {
final String spec = CustomTile.toSpec(tile);
for (TileRecord record : mRecords) {
if (record.tile.getTileSpec().equals(spec)) {
record.tile.click(null /* view */);
break;
}
}
}
protected 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);
}
boolean areThereTiles() {
return !mRecords.isEmpty();
}
QSTileView getTileView(QSTile tile) {
for (QSPanelControllerBase.TileRecord r : mRecords) {
if (r.tile == tile) {
return r.tileView;
}
}
return null;
}
private String getTilesSpecs() {
return mRecords.stream()
.map(tileRecord -> tileRecord.tile.getTileSpec())
.collect(Collectors.joining(","));
}
/** */
public void setExpanded(boolean expanded) {
if (mView.isExpanded() == expanded) {
return;
}
mQSLogger.logPanelExpanded(expanded, mView.getDumpableTag());
mView.setExpanded(expanded);
mMetricsLogger.visibility(MetricsEvent.QS_PANEL, expanded);
if (!expanded) {
mUiEventLogger.log(mView.closePanelEvent());
closeDetail();
} else {
mUiEventLogger.log(mView.openPanelEvent());
logTiles();
}
}
/** */
public void closeDetail() {
if (mQsCustomizerController.isShown()) {
mQsCustomizerController.hide();
return;
}
mView.closeDetail();
}
/** */
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) {
mView.showDetailAdapter(
true, tile.getDetailAdapter(), new int[]{mView.getWidth() / 2, 0});
}
}
void setListening(boolean listening) {
mView.setListening(listening);
if (mView.getTileLayout() != null) {
mQSLogger.logAllTilesChangeListening(listening, mView.getDumpableTag(), mCachedSpecs);
mView.getTileLayout().setListening(listening, mUiEventLogger);
}
}
boolean switchTileLayout(boolean force) {
/* Whether or not the panel currently contains a media player. */
boolean horizontal = shouldUseHorizontalLayout();
if (horizontal != mUsingHorizontalLayout || force) {
mUsingHorizontalLayout = horizontal;
mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force);
updateMediaDisappearParameters();
if (mUsingHorizontalLayoutChangedListener != null) {
mUsingHorizontalLayoutChangedListener.run();
}
return true;
}
return false;
}
/**
* Update the way the media disappears based on if we're using the horizontal layout
*/
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);
}
boolean shouldUseHorizontalLayout() {
if (mShouldUseSplitNotificationShade) {
return false;
}
return mUsingMediaPlayer && mMediaHost.getVisible()
&& mLastOrientation == Configuration.ORIENTATION_LANDSCAPE;
}
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)));
}
}
/** Set the expansion on the associated {@link QSTileRevealController}. */
public void setRevealExpansion(float expansion) {
mRevealExpansion = expansion;
if (mQsTileRevealController != null) {
mQsTileRevealController.setExpansion(expansion);
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println(getClass().getSimpleName() + ":");
pw.println(" Tile records:");
for (QSPanelControllerBase.TileRecord record : mRecords) {
if (record.tile instanceof Dumpable) {
pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args);
pw.print(" "); pw.println(record.tileView.toString());
}
}
}
public QSPanel.QSTileLayout getTileLayout() {
return mView.getTileLayout();
}
/**
* Add a listener for when the media visibility changes.
*/
public void setMediaVisibilityChangedListener(@NonNull Consumer<Boolean> listener) {
mMediaVisibilityChangedListener = listener;
}
/**
* Add a listener when the horizontal layout changes
*/
public void setUsingHorizontalLayoutChangeListener(Runnable listener) {
mUsingHorizontalLayoutChangedListener = listener;
}
/** */
public static final class TileRecord extends QSPanel.Record {
public QSTile tile;
public com.android.systemui.plugins.qs.QSTileView tileView;
public boolean scanState;
public QSTile.Callback callback;
}
}