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