1 /* 2 * Copyright (C) 2020 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.qs; 18 19 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent; 20 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.ComponentName; 25 import android.content.res.Configuration; 26 import android.metrics.LogMaker; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 import com.android.internal.logging.MetricsLogger; 30 import com.android.internal.logging.UiEventLogger; 31 import com.android.systemui.Dumpable; 32 import com.android.systemui.dump.DumpManager; 33 import com.android.systemui.media.MediaHost; 34 import com.android.systemui.plugins.qs.QSTile; 35 import com.android.systemui.plugins.qs.QSTileView; 36 import com.android.systemui.qs.customize.QSCustomizerController; 37 import com.android.systemui.qs.external.CustomTile; 38 import com.android.systemui.qs.logging.QSLogger; 39 import com.android.systemui.statusbar.FeatureFlags; 40 import com.android.systemui.util.Utils; 41 import com.android.systemui.util.ViewController; 42 import com.android.systemui.util.animation.DisappearParameters; 43 44 import java.io.FileDescriptor; 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.Collection; 48 import java.util.function.Consumer; 49 import java.util.stream.Collectors; 50 51 import javax.inject.Named; 52 53 import kotlin.Unit; 54 import kotlin.jvm.functions.Function1; 55 56 /** 57 * Controller for QSPanel views. 58 * 59 * @param <T> Type of QSPanel. 60 */ 61 public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T> 62 implements Dumpable{ 63 protected final QSTileHost mHost; 64 private final QSCustomizerController mQsCustomizerController; 65 private final boolean mUsingMediaPlayer; 66 protected final MediaHost mMediaHost; 67 protected final MetricsLogger mMetricsLogger; 68 private final UiEventLogger mUiEventLogger; 69 private final QSLogger mQSLogger; 70 private final DumpManager mDumpManager; 71 private final FeatureFlags mFeatureFlags; 72 protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); 73 private boolean mShouldUseSplitNotificationShade; 74 75 @Nullable 76 private Consumer<Boolean> mMediaVisibilityChangedListener; 77 private int mLastOrientation; 78 private String mCachedSpecs = ""; 79 private QSTileRevealController mQsTileRevealController; 80 private float mRevealExpansion; 81 82 private final QSHost.Callback mQSHostCallback = this::setTiles; 83 84 @VisibleForTesting 85 protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = 86 new QSPanel.OnConfigurationChangedListener() { 87 @Override 88 public void onConfigurationChange(Configuration newConfig) { 89 mShouldUseSplitNotificationShade = 90 Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources()); 91 if (newConfig.orientation != mLastOrientation) { 92 mLastOrientation = newConfig.orientation; 93 switchTileLayout(false); 94 } 95 } 96 }; 97 98 private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> { 99 if (mMediaVisibilityChangedListener != null) { 100 mMediaVisibilityChangedListener.accept(visible); 101 } 102 switchTileLayout(false); 103 return null; 104 }; 105 106 private boolean mUsingHorizontalLayout; 107 108 @Nullable 109 private Runnable mUsingHorizontalLayoutChangedListener; 110 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 )111 protected QSPanelControllerBase( 112 T view, 113 QSTileHost host, 114 QSCustomizerController qsCustomizerController, 115 @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer, 116 MediaHost mediaHost, 117 MetricsLogger metricsLogger, 118 UiEventLogger uiEventLogger, 119 QSLogger qsLogger, 120 DumpManager dumpManager, 121 FeatureFlags featureFlags 122 ) { 123 super(view); 124 mHost = host; 125 mQsCustomizerController = qsCustomizerController; 126 mUsingMediaPlayer = usingMediaPlayer; 127 mMediaHost = mediaHost; 128 mMetricsLogger = metricsLogger; 129 mUiEventLogger = uiEventLogger; 130 mQSLogger = qsLogger; 131 mDumpManager = dumpManager; 132 mFeatureFlags = featureFlags; 133 mShouldUseSplitNotificationShade = 134 Utils.shouldUseSplitNotificationShade(mFeatureFlags, getResources()); 135 } 136 137 @Override onInit()138 protected void onInit() { 139 mView.initialize(); 140 mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); 141 } 142 143 /** 144 * @return the media host for this panel 145 */ getMediaHost()146 public MediaHost getMediaHost() { 147 return mMediaHost; 148 } 149 150 @Override onViewAttached()151 protected void onViewAttached() { 152 mQsTileRevealController = createTileRevealController(); 153 if (mQsTileRevealController != null) { 154 mQsTileRevealController.setExpansion(mRevealExpansion); 155 } 156 157 mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); 158 mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); 159 mHost.addCallback(mQSHostCallback); 160 setTiles(); 161 mLastOrientation = getResources().getConfiguration().orientation; 162 switchTileLayout(true); 163 164 mDumpManager.registerDumpable(mView.getDumpableTag(), this); 165 } 166 167 @Override onViewDetached()168 protected void onViewDetached() { 169 mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener); 170 mHost.removeCallback(mQSHostCallback); 171 172 mView.getTileLayout().setListening(false, mUiEventLogger); 173 174 mMediaHost.removeVisibilityChangeListener(mMediaHostVisibilityListener); 175 176 for (TileRecord record : mRecords) { 177 record.tile.removeCallbacks(); 178 } 179 mRecords.clear(); 180 mDumpManager.unregisterDumpable(mView.getDumpableTag()); 181 } 182 createTileRevealController()183 protected QSTileRevealController createTileRevealController() { 184 return null; 185 } 186 187 /** */ setTiles()188 public void setTiles() { 189 setTiles(mHost.getTiles(), false); 190 } 191 192 /** */ setTiles(Collection<QSTile> tiles, boolean collapsedView)193 public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { 194 // TODO(b/168904199): move this logic into QSPanelController. 195 if (!collapsedView && mQsTileRevealController != null) { 196 mQsTileRevealController.updateRevealedTiles(tiles); 197 } 198 199 for (QSPanelControllerBase.TileRecord record : mRecords) { 200 mView.removeTile(record); 201 record.tile.removeCallback(record.callback); 202 } 203 mRecords.clear(); 204 mCachedSpecs = ""; 205 for (QSTile tile : tiles) { 206 addTile(tile, collapsedView); 207 } 208 } 209 210 /** */ refreshAllTiles()211 public void refreshAllTiles() { 212 for (QSPanelControllerBase.TileRecord r : mRecords) { 213 r.tile.refreshState(); 214 } 215 } 216 addTile(final QSTile tile, boolean collapsedView)217 private void addTile(final QSTile tile, boolean collapsedView) { 218 final TileRecord r = new TileRecord(); 219 r.tile = tile; 220 r.tileView = mHost.createTileView(getContext(), tile, collapsedView); 221 mView.addTile(r); 222 mRecords.add(r); 223 mCachedSpecs = getTilesSpecs(); 224 } 225 226 /** */ clickTile(ComponentName tile)227 public void clickTile(ComponentName tile) { 228 final String spec = CustomTile.toSpec(tile); 229 for (TileRecord record : mRecords) { 230 if (record.tile.getTileSpec().equals(spec)) { 231 record.tile.click(null /* view */); 232 break; 233 } 234 } 235 } getTile(String subPanel)236 protected QSTile getTile(String subPanel) { 237 for (int i = 0; i < mRecords.size(); i++) { 238 if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) { 239 return mRecords.get(i).tile; 240 } 241 } 242 return mHost.createTile(subPanel); 243 } 244 areThereTiles()245 boolean areThereTiles() { 246 return !mRecords.isEmpty(); 247 } 248 getTileView(QSTile tile)249 QSTileView getTileView(QSTile tile) { 250 for (QSPanelControllerBase.TileRecord r : mRecords) { 251 if (r.tile == tile) { 252 return r.tileView; 253 } 254 } 255 return null; 256 } 257 getTilesSpecs()258 private String getTilesSpecs() { 259 return mRecords.stream() 260 .map(tileRecord -> tileRecord.tile.getTileSpec()) 261 .collect(Collectors.joining(",")); 262 } 263 264 /** */ setExpanded(boolean expanded)265 public void setExpanded(boolean expanded) { 266 if (mView.isExpanded() == expanded) { 267 return; 268 } 269 mQSLogger.logPanelExpanded(expanded, mView.getDumpableTag()); 270 271 mView.setExpanded(expanded); 272 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, expanded); 273 if (!expanded) { 274 mUiEventLogger.log(mView.closePanelEvent()); 275 closeDetail(); 276 } else { 277 mUiEventLogger.log(mView.openPanelEvent()); 278 logTiles(); 279 } 280 } 281 282 /** */ closeDetail()283 public void closeDetail() { 284 if (mQsCustomizerController.isShown()) { 285 mQsCustomizerController.hide(); 286 return; 287 } 288 mView.closeDetail(); 289 } 290 291 /** */ openDetails(String subPanel)292 public void openDetails(String subPanel) { 293 QSTile tile = getTile(subPanel); 294 // If there's no tile with that name (as defined in QSFactoryImpl or other QSFactory), 295 // QSFactory will not be able to create a tile and getTile will return null 296 if (tile != null) { 297 mView.showDetailAdapter( 298 true, tile.getDetailAdapter(), new int[]{mView.getWidth() / 2, 0}); 299 } 300 } 301 302 setListening(boolean listening)303 void setListening(boolean listening) { 304 mView.setListening(listening); 305 306 if (mView.getTileLayout() != null) { 307 mQSLogger.logAllTilesChangeListening(listening, mView.getDumpableTag(), mCachedSpecs); 308 mView.getTileLayout().setListening(listening, mUiEventLogger); 309 } 310 } 311 switchTileLayout(boolean force)312 boolean switchTileLayout(boolean force) { 313 /* Whether or not the panel currently contains a media player. */ 314 boolean horizontal = shouldUseHorizontalLayout(); 315 if (horizontal != mUsingHorizontalLayout || force) { 316 mUsingHorizontalLayout = horizontal; 317 mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force); 318 updateMediaDisappearParameters(); 319 if (mUsingHorizontalLayoutChangedListener != null) { 320 mUsingHorizontalLayoutChangedListener.run(); 321 } 322 return true; 323 } 324 return false; 325 } 326 327 /** 328 * Update the way the media disappears based on if we're using the horizontal layout 329 */ updateMediaDisappearParameters()330 void updateMediaDisappearParameters() { 331 if (!mUsingMediaPlayer) { 332 return; 333 } 334 DisappearParameters parameters = mMediaHost.getDisappearParameters(); 335 if (mUsingHorizontalLayout) { 336 // Only height remaining 337 parameters.getDisappearSize().set(0.0f, 0.4f); 338 // Disappearing on the right side on the bottom 339 parameters.getGonePivot().set(1.0f, 1.0f); 340 // translating a bit horizontal 341 parameters.getContentTranslationFraction().set(0.25f, 1.0f); 342 parameters.setDisappearEnd(0.6f); 343 } else { 344 // Only width remaining 345 parameters.getDisappearSize().set(1.0f, 0.0f); 346 // Disappearing on the bottom 347 parameters.getGonePivot().set(0.0f, 1.0f); 348 // translating a bit vertical 349 parameters.getContentTranslationFraction().set(0.0f, 1.05f); 350 parameters.setDisappearEnd(0.95f); 351 } 352 parameters.setFadeStartPosition(0.95f); 353 parameters.setDisappearStart(0.0f); 354 mMediaHost.setDisappearParameters(parameters); 355 } 356 shouldUseHorizontalLayout()357 boolean shouldUseHorizontalLayout() { 358 if (mShouldUseSplitNotificationShade) { 359 return false; 360 } 361 return mUsingMediaPlayer && mMediaHost.getVisible() 362 && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE; 363 } 364 logTiles()365 private void logTiles() { 366 for (int i = 0; i < mRecords.size(); i++) { 367 QSTile tile = mRecords.get(i).tile; 368 mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) 369 .setType(MetricsEvent.TYPE_OPEN))); 370 } 371 } 372 373 /** Set the expansion on the associated {@link QSTileRevealController}. */ setRevealExpansion(float expansion)374 public void setRevealExpansion(float expansion) { 375 mRevealExpansion = expansion; 376 if (mQsTileRevealController != null) { 377 mQsTileRevealController.setExpansion(expansion); 378 } 379 } 380 381 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)382 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 383 pw.println(getClass().getSimpleName() + ":"); 384 pw.println(" Tile records:"); 385 for (QSPanelControllerBase.TileRecord record : mRecords) { 386 if (record.tile instanceof Dumpable) { 387 pw.print(" "); ((Dumpable) record.tile).dump(fd, pw, args); 388 pw.print(" "); pw.println(record.tileView.toString()); 389 } 390 } 391 } 392 getTileLayout()393 public QSPanel.QSTileLayout getTileLayout() { 394 return mView.getTileLayout(); 395 } 396 397 /** 398 * Add a listener for when the media visibility changes. 399 */ setMediaVisibilityChangedListener(@onNull Consumer<Boolean> listener)400 public void setMediaVisibilityChangedListener(@NonNull Consumer<Boolean> listener) { 401 mMediaVisibilityChangedListener = listener; 402 } 403 404 /** 405 * Add a listener when the horizontal layout changes 406 */ setUsingHorizontalLayoutChangeListener(Runnable listener)407 public void setUsingHorizontalLayoutChangeListener(Runnable listener) { 408 mUsingHorizontalLayoutChangedListener = listener; 409 } 410 411 /** */ 412 public static final class TileRecord extends QSPanel.Record { 413 public QSTile tile; 414 public com.android.systemui.plugins.qs.QSTileView tileView; 415 public boolean scanState; 416 public QSTile.Callback callback; 417 } 418 } 419