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.content.res.Configuration.Orientation; 27 import android.metrics.LogMaker; 28 import android.util.Log; 29 import android.view.View; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.logging.MetricsLogger; 33 import com.android.internal.logging.UiEventLogger; 34 import com.android.systemui.Dumpable; 35 import com.android.systemui.dump.DumpManager; 36 import com.android.systemui.media.controls.ui.MediaHost; 37 import com.android.systemui.plugins.qs.QSTile; 38 import com.android.systemui.plugins.qs.QSTileView; 39 import com.android.systemui.qs.customize.QSCustomizerController; 40 import com.android.systemui.qs.external.CustomTile; 41 import com.android.systemui.qs.logging.QSLogger; 42 import com.android.systemui.qs.tileimpl.QSTileViewImpl; 43 import com.android.systemui.util.LargeScreenUtils; 44 import com.android.systemui.util.ViewController; 45 import com.android.systemui.util.animation.DisappearParameters; 46 47 import java.io.PrintWriter; 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.Objects; 51 import java.util.function.Consumer; 52 import java.util.stream.Collectors; 53 54 import javax.inject.Named; 55 56 import kotlin.Unit; 57 import kotlin.jvm.functions.Function1; 58 59 /** 60 * Controller for QSPanel views. 61 * 62 * @param <T> Type of QSPanel. 63 */ 64 public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewController<T> 65 implements Dumpable{ 66 private static final String TAG = "QSPanelControllerBase"; 67 protected final QSHost mHost; 68 private final QSCustomizerController mQsCustomizerController; 69 private final boolean mUsingMediaPlayer; 70 protected final MediaHost mMediaHost; 71 protected final MetricsLogger mMetricsLogger; 72 private final UiEventLogger mUiEventLogger; 73 protected final QSLogger mQSLogger; 74 private final DumpManager mDumpManager; 75 protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); 76 protected boolean mShouldUseSplitNotificationShade; 77 78 @Nullable 79 private Consumer<Boolean> mMediaVisibilityChangedListener; 80 @Orientation 81 private int mLastOrientation; 82 private String mCachedSpecs = ""; 83 @Nullable 84 private QSTileRevealController mQsTileRevealController; 85 private float mRevealExpansion; 86 87 private final QSHost.Callback mQSHostCallback = this::setTiles; 88 89 @VisibleForTesting 90 protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener = 91 new QSPanel.OnConfigurationChangedListener() { 92 @Override 93 public void onConfigurationChange(Configuration newConfig) { 94 final boolean previousSplitShadeState = mShouldUseSplitNotificationShade; 95 final int previousOrientation = mLastOrientation; 96 mShouldUseSplitNotificationShade = 97 LargeScreenUtils.shouldUseSplitNotificationShade(getResources()); 98 mLastOrientation = newConfig.orientation; 99 100 mQSLogger.logOnConfigurationChanged( 101 /* oldOrientation= */ previousOrientation, 102 /* newOrientation= */ mLastOrientation, 103 /* oldShouldUseSplitShade= */ previousSplitShadeState, 104 /* newShouldUseSplitShade= */ mShouldUseSplitNotificationShade, 105 /* containerName= */ mView.getDumpableTag()); 106 107 switchTileLayoutIfNeeded(); 108 onConfigurationChanged(); 109 if (previousSplitShadeState != mShouldUseSplitNotificationShade) { 110 onSplitShadeChanged(mShouldUseSplitNotificationShade); 111 } 112 } 113 }; 114 onConfigurationChanged()115 protected void onConfigurationChanged() { } 116 onSplitShadeChanged(boolean shouldUseSplitNotificationShade)117 protected void onSplitShadeChanged(boolean shouldUseSplitNotificationShade) { } 118 119 private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> { 120 if (mMediaVisibilityChangedListener != null) { 121 mMediaVisibilityChangedListener.accept(visible); 122 } 123 switchTileLayout(false); 124 return null; 125 }; 126 127 private boolean mUsingHorizontalLayout; 128 129 @Nullable 130 private Runnable mUsingHorizontalLayoutChangedListener; 131 QSPanelControllerBase( T view, QSHost host, QSCustomizerController qsCustomizerController, @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer, MediaHost mediaHost, MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger, DumpManager dumpManager )132 protected QSPanelControllerBase( 133 T view, 134 QSHost host, 135 QSCustomizerController qsCustomizerController, 136 @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer, 137 MediaHost mediaHost, 138 MetricsLogger metricsLogger, 139 UiEventLogger uiEventLogger, 140 QSLogger qsLogger, 141 DumpManager dumpManager 142 ) { 143 super(view); 144 mHost = host; 145 mQsCustomizerController = qsCustomizerController; 146 mUsingMediaPlayer = usingMediaPlayer; 147 mMediaHost = mediaHost; 148 mMetricsLogger = metricsLogger; 149 mUiEventLogger = uiEventLogger; 150 mQSLogger = qsLogger; 151 mDumpManager = dumpManager; 152 mShouldUseSplitNotificationShade = 153 LargeScreenUtils.shouldUseSplitNotificationShade(getResources()); 154 } 155 156 @Override onInit()157 protected void onInit() { 158 mView.initialize(mQSLogger); 159 mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); 160 } 161 162 /** 163 * @return the media host for this panel 164 */ getMediaHost()165 public MediaHost getMediaHost() { 166 return mMediaHost; 167 } 168 setSquishinessFraction(float squishinessFraction)169 public void setSquishinessFraction(float squishinessFraction) { 170 mView.setSquishinessFraction(squishinessFraction); 171 } 172 173 @Override onViewAttached()174 protected void onViewAttached() { 175 mQsTileRevealController = createTileRevealController(); 176 if (mQsTileRevealController != null) { 177 mQsTileRevealController.setExpansion(mRevealExpansion); 178 } 179 180 mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener); 181 mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); 182 mHost.addCallback(mQSHostCallback); 183 setTiles(); 184 mLastOrientation = getResources().getConfiguration().orientation; 185 mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag()); 186 switchTileLayout(true); 187 188 mDumpManager.registerDumpable(mView.getDumpableTag(), this); 189 } 190 191 @Override onViewDetached()192 protected void onViewDetached() { 193 mQSLogger.logOnViewDetached(mLastOrientation, mView.getDumpableTag()); 194 mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener); 195 mHost.removeCallback(mQSHostCallback); 196 197 mView.getTileLayout().setListening(false, mUiEventLogger); 198 199 mMediaHost.removeVisibilityChangeListener(mMediaHostVisibilityListener); 200 201 for (TileRecord record : mRecords) { 202 record.tile.removeCallbacks(); 203 } 204 mRecords.clear(); 205 mDumpManager.unregisterDumpable(mView.getDumpableTag()); 206 } 207 208 @Nullable createTileRevealController()209 protected QSTileRevealController createTileRevealController() { 210 return null; 211 } 212 213 /** */ setTiles()214 public void setTiles() { 215 setTiles(mHost.getTiles(), false); 216 } 217 218 /** */ setTiles(Collection<QSTile> tiles, boolean collapsedView)219 public void setTiles(Collection<QSTile> tiles, boolean collapsedView) { 220 // TODO(b/168904199): move this logic into QSPanelController. 221 if (!collapsedView && mQsTileRevealController != null) { 222 mQsTileRevealController.updateRevealedTiles(tiles); 223 } 224 225 for (QSPanelControllerBase.TileRecord record : mRecords) { 226 mView.removeTile(record); 227 record.tile.removeCallback(record.callback); 228 } 229 mRecords.clear(); 230 mCachedSpecs = ""; 231 for (QSTile tile : tiles) { 232 addTile(tile, collapsedView); 233 } 234 } 235 236 /** */ refreshAllTiles()237 public void refreshAllTiles() { 238 for (QSPanelControllerBase.TileRecord r : mRecords) { 239 if (!r.tile.isListening()) { 240 // Only refresh tiles that were not already in the listening state. Tiles that are 241 // already listening is as if they are already expanded (for example, tiles that 242 // are both in QQS and QS). 243 r.tile.refreshState(); 244 } 245 } 246 } 247 addTile(final QSTile tile, boolean collapsedView)248 private void addTile(final QSTile tile, boolean collapsedView) { 249 final TileRecord r = 250 new TileRecord(tile, mHost.createTileView(getContext(), tile, collapsedView)); 251 // TODO(b/250618218): Remove the QSLogger in QSTileViewImpl once we know the root cause of 252 // b/250618218. 253 try { 254 QSTileViewImpl qsTileView = (QSTileViewImpl) (r.tileView); 255 if (qsTileView != null) { 256 qsTileView.setQsLogger(mQSLogger); 257 } 258 } catch (ClassCastException e) { 259 Log.e(TAG, "Failed to cast QSTileView to QSTileViewImpl", e); 260 } 261 mView.addTile(r); 262 mRecords.add(r); 263 mCachedSpecs = getTilesSpecs(); 264 } 265 266 /** */ clickTile(ComponentName tile)267 public void clickTile(ComponentName tile) { 268 final String spec = CustomTile.toSpec(tile); 269 for (TileRecord record : mRecords) { 270 if (record.tile.getTileSpec().equals(spec)) { 271 record.tile.click(null /* view */); 272 break; 273 } 274 } 275 } 276 areThereTiles()277 boolean areThereTiles() { 278 return !mRecords.isEmpty(); 279 } 280 281 @Nullable getTileView(QSTile tile)282 QSTileView getTileView(QSTile tile) { 283 for (QSPanelControllerBase.TileRecord r : mRecords) { 284 if (r.tile == tile) { 285 return r.tileView; 286 } 287 } 288 return null; 289 } 290 getTileView(String spec)291 QSTileView getTileView(String spec) { 292 for (QSPanelControllerBase.TileRecord r : mRecords) { 293 if (Objects.equals(r.tile.getTileSpec(), spec)) { 294 return r.tileView; 295 } 296 } 297 return null; 298 } 299 getTilesSpecs()300 private String getTilesSpecs() { 301 return mRecords.stream() 302 .map(tileRecord -> tileRecord.tile.getTileSpec()) 303 .collect(Collectors.joining(",")); 304 } 305 306 /** */ setExpanded(boolean expanded)307 public void setExpanded(boolean expanded) { 308 if (mView.isExpanded() == expanded) { 309 return; 310 } 311 mQSLogger.logPanelExpanded(expanded, mView.getDumpableTag()); 312 313 mView.setExpanded(expanded); 314 mMetricsLogger.visibility(MetricsEvent.QS_PANEL, expanded); 315 if (!expanded) { 316 mUiEventLogger.log(mView.closePanelEvent()); 317 closeDetail(); 318 } else { 319 mUiEventLogger.log(mView.openPanelEvent()); 320 logTiles(); 321 } 322 } 323 324 /** */ closeDetail()325 public void closeDetail() { 326 if (mQsCustomizerController.isShown()) { 327 mQsCustomizerController.hide(); 328 return; 329 } 330 } 331 setListening(boolean listening)332 void setListening(boolean listening) { 333 if (mView.isListening() == listening) return; 334 mView.setListening(listening); 335 336 if (mView.getTileLayout() != null) { 337 mQSLogger.logAllTilesChangeListening(listening, mView.getDumpableTag(), mCachedSpecs); 338 mView.getTileLayout().setListening(listening, mUiEventLogger); 339 } 340 341 if (mView.isListening()) { 342 refreshAllTiles(); 343 } 344 } 345 switchTileLayoutIfNeeded()346 private void switchTileLayoutIfNeeded() { 347 switchTileLayout(/* force= */ false); 348 } 349 switchTileLayout(boolean force)350 boolean switchTileLayout(boolean force) { 351 /* Whether or not the panel currently contains a media player. */ 352 boolean horizontal = shouldUseHorizontalLayout(); 353 if (horizontal != mUsingHorizontalLayout || force) { 354 mQSLogger.logSwitchTileLayout(horizontal, mUsingHorizontalLayout, force, 355 mView.getDumpableTag()); 356 mUsingHorizontalLayout = horizontal; 357 mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force); 358 updateMediaDisappearParameters(); 359 if (mUsingHorizontalLayoutChangedListener != null) { 360 mUsingHorizontalLayoutChangedListener.run(); 361 } 362 return true; 363 } 364 return false; 365 } 366 367 /** 368 * Update the way the media disappears based on if we're using the horizontal layout 369 */ updateMediaDisappearParameters()370 void updateMediaDisappearParameters() { 371 if (!mUsingMediaPlayer) { 372 return; 373 } 374 DisappearParameters parameters = mMediaHost.getDisappearParameters(); 375 if (mUsingHorizontalLayout) { 376 // Only height remaining 377 parameters.getDisappearSize().set(0.0f, 0.4f); 378 // Disappearing on the right side on the top 379 parameters.getGonePivot().set(1.0f, 0.0f); 380 // translating a bit horizontal 381 parameters.getContentTranslationFraction().set(0.25f, 1.0f); 382 parameters.setDisappearEnd(0.6f); 383 } else { 384 // Only width remaining 385 parameters.getDisappearSize().set(1.0f, 0.0f); 386 // Disappearing on the top 387 parameters.getGonePivot().set(0.0f, 0.0f); 388 // translating a bit vertical 389 parameters.getContentTranslationFraction().set(0.0f, 1f); 390 parameters.setDisappearEnd(0.95f); 391 } 392 parameters.setFadeStartPosition(0.95f); 393 parameters.setDisappearStart(0.0f); 394 mMediaHost.setDisappearParameters(parameters); 395 } 396 shouldUseHorizontalLayout()397 boolean shouldUseHorizontalLayout() { 398 if (mShouldUseSplitNotificationShade) { 399 return false; 400 } 401 return mUsingMediaPlayer && mMediaHost.getVisible() 402 && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE; 403 } 404 logTiles()405 private void logTiles() { 406 for (int i = 0; i < mRecords.size(); i++) { 407 QSTile tile = mRecords.get(i).tile; 408 mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory()) 409 .setType(MetricsEvent.TYPE_OPEN))); 410 } 411 } 412 413 /** Set the expansion on the associated {@link QSTileRevealController}. */ setRevealExpansion(float expansion)414 public void setRevealExpansion(float expansion) { 415 mRevealExpansion = expansion; 416 if (mQsTileRevealController != null) { 417 mQsTileRevealController.setExpansion(expansion); 418 } 419 } 420 421 @Override dump(PrintWriter pw, String[] args)422 public void dump(PrintWriter pw, String[] args) { 423 pw.println(getClass().getSimpleName() + ":"); 424 pw.println(" Tile records:"); 425 for (QSPanelControllerBase.TileRecord record : mRecords) { 426 if (record.tile instanceof Dumpable) { 427 pw.print(" "); ((Dumpable) record.tile).dump(pw, args); 428 pw.print(" "); pw.println(record.tileView.toString()); 429 } 430 } 431 if (mMediaHost != null) { 432 pw.println(" media bounds: " + mMediaHost.getCurrentBounds()); 433 pw.println(" horizontal layout: " + mUsingHorizontalLayout); 434 pw.println(" last orientation: " + mLastOrientation); 435 } 436 pw.println(" mShouldUseSplitNotificationShade: " + mShouldUseSplitNotificationShade); 437 } 438 getTileLayout()439 public QSPanel.QSTileLayout getTileLayout() { 440 return mView.getTileLayout(); 441 } 442 443 /** 444 * Add a listener for when the media visibility changes. 445 */ setMediaVisibilityChangedListener(@onNull Consumer<Boolean> listener)446 public void setMediaVisibilityChangedListener(@NonNull Consumer<Boolean> listener) { 447 mMediaVisibilityChangedListener = listener; 448 } 449 450 /** 451 * Add a listener when the horizontal layout changes 452 */ setUsingHorizontalLayoutChangeListener(Runnable listener)453 public void setUsingHorizontalLayoutChangeListener(Runnable listener) { 454 mUsingHorizontalLayoutChangedListener = listener; 455 } 456 457 @Nullable getBrightnessView()458 public View getBrightnessView() { 459 return mView.getBrightnessView(); 460 } 461 462 /** 463 * Set a listener to collapse/expand QS. 464 * @param action 465 */ setCollapseExpandAction(Runnable action)466 public void setCollapseExpandAction(Runnable action) { 467 mView.setCollapseExpandAction(action); 468 } 469 470 /** Sets whether we are currently on lock screen. */ setIsOnKeyguard(boolean isOnKeyguard)471 public void setIsOnKeyguard(boolean isOnKeyguard) { 472 boolean isOnSplitShadeLockscreen = mShouldUseSplitNotificationShade && isOnKeyguard; 473 // When the split shade is expanding on lockscreen, the media container transitions from the 474 // lockscreen to QS. 475 // We have to prevent the media container position from moving during the transition to have 476 // a smooth translation animation without stuttering. 477 mView.setShouldMoveMediaOnExpansion(!isOnSplitShadeLockscreen); 478 } 479 480 /** */ 481 public static final class TileRecord { TileRecord(QSTile tile, com.android.systemui.plugins.qs.QSTileView tileView)482 public TileRecord(QSTile tile, com.android.systemui.plugins.qs.QSTileView tileView) { 483 this.tile = tile; 484 this.tileView = tileView; 485 } 486 487 public QSTile tile; 488 public com.android.systemui.plugins.qs.QSTileView tileView; 489 public boolean scanState; 490 @Nullable 491 public QSTile.Callback callback; 492 } 493 } 494