• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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