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