1 /*
2  * Copyright (C) 2015 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.systemui.util.InjectionInflationController.VIEW_CONTEXT;
20 
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Rect;
24 import android.util.AttributeSet;
25 import android.view.Gravity;
26 import android.view.View;
27 import android.widget.LinearLayout;
28 
29 import com.android.internal.logging.UiEventLogger;
30 import com.android.systemui.Dependency;
31 import com.android.systemui.R;
32 import com.android.systemui.broadcast.BroadcastDispatcher;
33 import com.android.systemui.dump.DumpManager;
34 import com.android.systemui.media.MediaHierarchyManager;
35 import com.android.systemui.media.MediaHost;
36 import com.android.systemui.plugins.qs.QSTile;
37 import com.android.systemui.plugins.qs.QSTile.SignalState;
38 import com.android.systemui.plugins.qs.QSTile.State;
39 import com.android.systemui.qs.customize.QSCustomizer;
40 import com.android.systemui.qs.logging.QSLogger;
41 import com.android.systemui.tuner.TunerService;
42 import com.android.systemui.tuner.TunerService.Tunable;
43 
44 import java.util.ArrayList;
45 import java.util.Collection;
46 
47 import javax.inject.Inject;
48 import javax.inject.Named;
49 
50 /**
51  * Version of QSPanel that only shows N Quick Tiles in the QS Header.
52  */
53 public class QuickQSPanel extends QSPanel {
54 
55     public static final String NUM_QUICK_TILES = "sysui_qqs_count";
56     private static final String TAG = "QuickQSPanel";
57     // Start it at 6 so a non-zero value can be obtained statically.
58     private static int sDefaultMaxTiles = 6;
59 
60     private boolean mDisabledByPolicy;
61     private int mMaxTiles;
62     protected QSPanel mFullPanel;
63 
64 
65     @Inject
QuickQSPanel( @amedVIEW_CONTEXT) Context context, AttributeSet attrs, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, QSLogger qsLogger, MediaHost mediaHost, UiEventLogger uiEventLogger )66     public QuickQSPanel(
67             @Named(VIEW_CONTEXT) Context context,
68             AttributeSet attrs,
69             DumpManager dumpManager,
70             BroadcastDispatcher broadcastDispatcher,
71             QSLogger qsLogger,
72             MediaHost mediaHost,
73             UiEventLogger uiEventLogger
74     ) {
75         super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger);
76         sDefaultMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_columns);
77         applyBottomMargin((View) mRegularTileLayout);
78     }
79 
applyBottomMargin(View view)80     private void applyBottomMargin(View view) {
81         int margin = getResources().getDimensionPixelSize(R.dimen.qs_header_tile_margin_bottom);
82         MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
83         layoutParams.bottomMargin = margin;
84         view.setLayoutParams(layoutParams);
85     }
86 
87     @Override
addSecurityFooter()88     protected void addSecurityFooter() {
89         // No footer needed
90     }
91 
92     @Override
addViewsAboveTiles()93     protected void addViewsAboveTiles() {
94         // Nothing to add above the tiles
95     }
96 
97     @Override
createRegularTileLayout()98     protected TileLayout createRegularTileLayout() {
99         return new QuickQSPanel.HeaderTileLayout(mContext, mUiEventLogger);
100     }
101 
102     @Override
createHorizontalTileLayout()103     protected QSTileLayout createHorizontalTileLayout() {
104         return new DoubleLineTileLayout(mContext, mUiEventLogger);
105     }
106 
107     @Override
initMediaHostState()108     protected void initMediaHostState() {
109         mMediaHost.setExpansion(0.0f);
110         mMediaHost.setShowsOnlyActiveMedia(true);
111         mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
112     }
113 
114     @Override
needsDynamicRowsAndColumns()115     protected boolean needsDynamicRowsAndColumns() {
116         return false; // QQS always have the same layout
117     }
118 
119     @Override
displayMediaMarginsOnMedia()120     protected boolean displayMediaMarginsOnMedia() {
121         // Margins should be on the container to visually center the view
122         return false;
123     }
124 
125     @Override
updatePadding()126     protected void updatePadding() {
127         // QS Panel is setting a top padding by default, which we don't need.
128     }
129 
130     @Override
onAttachedToWindow()131     protected void onAttachedToWindow() {
132         super.onAttachedToWindow();
133         Dependency.get(TunerService.class).addTunable(mNumTiles, NUM_QUICK_TILES);
134     }
135 
136     @Override
onDetachedFromWindow()137     protected void onDetachedFromWindow() {
138         super.onDetachedFromWindow();
139         Dependency.get(TunerService.class).removeTunable(mNumTiles);
140     }
141 
142     @Override
getDumpableTag()143     protected String getDumpableTag() {
144         return TAG;
145     }
146 
setQSPanelAndHeader(QSPanel fullPanel, View header)147     public void setQSPanelAndHeader(QSPanel fullPanel, View header) {
148         mFullPanel = fullPanel;
149     }
150 
151     @Override
shouldShowDetail()152     protected boolean shouldShowDetail() {
153         return !mExpanded;
154     }
155 
156     @Override
drawTile(TileRecord r, State state)157     protected void drawTile(TileRecord r, State state) {
158         if (state instanceof SignalState) {
159             SignalState copy = new SignalState();
160             state.copyTo(copy);
161             // No activity shown in the quick panel.
162             copy.activityIn = false;
163             copy.activityOut = false;
164             state = copy;
165         }
166         super.drawTile(r, state);
167     }
168 
169     @Override
setHost(QSTileHost host, QSCustomizer customizer)170     public void setHost(QSTileHost host, QSCustomizer customizer) {
171         super.setHost(host, customizer);
172         setTiles(mHost.getTiles());
173     }
174 
setMaxTiles(int maxTiles)175     public void setMaxTiles(int maxTiles) {
176         mMaxTiles = maxTiles;
177         if (mHost != null) {
178             setTiles(mHost.getTiles());
179         }
180     }
181 
182     @Override
onTuningChanged(String key, String newValue)183     public void onTuningChanged(String key, String newValue) {
184         if (QS_SHOW_BRIGHTNESS.equals(key)) {
185             // No Brightness or Tooltip for you!
186             super.onTuningChanged(key, "0");
187         }
188     }
189 
190     @Override
setTiles(Collection<QSTile> tiles)191     public void setTiles(Collection<QSTile> tiles) {
192         ArrayList<QSTile> quickTiles = new ArrayList<>();
193         for (QSTile tile : tiles) {
194             quickTiles.add(tile);
195             if (quickTiles.size() == mMaxTiles) {
196                 break;
197             }
198         }
199         super.setTiles(quickTiles, true);
200     }
201 
202     private final Tunable mNumTiles = new Tunable() {
203         @Override
204         public void onTuningChanged(String key, String newValue) {
205             setMaxTiles(parseNumTiles(newValue));
206         }
207     };
208 
getNumQuickTiles()209     public int getNumQuickTiles() {
210         return mMaxTiles;
211     }
212 
213     /**
214      * Parses the String setting into the number of tiles. Defaults to {@code mDefaultMaxTiles}
215      *
216      * @param numTilesValue value of the setting to parse
217      * @return parsed value of numTilesValue OR {@code mDefaultMaxTiles} on error
218      */
parseNumTiles(String numTilesValue)219     public static int parseNumTiles(String numTilesValue) {
220         try {
221             return Integer.parseInt(numTilesValue);
222         } catch (NumberFormatException e) {
223             // Couldn't read an int from the new setting value. Use default.
224             return sDefaultMaxTiles;
225         }
226     }
227 
getDefaultMaxTiles()228     public static int getDefaultMaxTiles() {
229         return sDefaultMaxTiles;
230     }
231 
setDisabledByPolicy(boolean disabled)232     void setDisabledByPolicy(boolean disabled) {
233         if (disabled != mDisabledByPolicy) {
234             mDisabledByPolicy = disabled;
235             setVisibility(disabled ? View.GONE : View.VISIBLE);
236         }
237     }
238 
239     /**
240      * Sets the visibility of this {@link QuickQSPanel}. This method has no effect when this panel
241      * is disabled by policy through {@link #setDisabledByPolicy(boolean)}, and in this case the
242      * visibility will always be {@link View#GONE}. This method is called externally by
243      * {@link QSAnimator} only.
244      */
245     @Override
setVisibility(int visibility)246     public void setVisibility(int visibility) {
247         if (mDisabledByPolicy) {
248             if (getVisibility() == View.GONE) {
249                 return;
250             }
251             visibility = View.GONE;
252         }
253         super.setVisibility(visibility);
254     }
255 
256     @Override
openPanelEvent()257     protected QSEvent openPanelEvent() {
258         return QSEvent.QQS_PANEL_EXPANDED;
259     }
260 
261     @Override
closePanelEvent()262     protected QSEvent closePanelEvent() {
263         return QSEvent.QQS_PANEL_COLLAPSED;
264     }
265 
266     @Override
tileVisibleEvent()267     protected QSEvent tileVisibleEvent() {
268         return QSEvent.QQS_TILE_VISIBLE;
269     }
270 
271     private static class HeaderTileLayout extends TileLayout {
272 
273         private final UiEventLogger mUiEventLogger;
274 
275         private Rect mClippingBounds = new Rect();
276 
HeaderTileLayout(Context context, UiEventLogger uiEventLogger)277         public HeaderTileLayout(Context context, UiEventLogger uiEventLogger) {
278             super(context);
279             mUiEventLogger = uiEventLogger;
280             setClipChildren(false);
281             setClipToPadding(false);
282             LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
283                     LayoutParams.WRAP_CONTENT);
284             lp.gravity = Gravity.CENTER_HORIZONTAL;
285             setLayoutParams(lp);
286         }
287 
288         @Override
onConfigurationChanged(Configuration newConfig)289         protected void onConfigurationChanged(Configuration newConfig) {
290             super.onConfigurationChanged(newConfig);
291             updateResources();
292         }
293 
294         @Override
onFinishInflate()295         public void onFinishInflate(){
296             super.onFinishInflate();
297             updateResources();
298         }
299 
generateTileLayoutParams()300         private LayoutParams generateTileLayoutParams() {
301             LayoutParams lp = new LayoutParams(mCellWidth, mCellHeight);
302             return lp;
303         }
304 
305         @Override
addTileView(TileRecord tile)306         protected void addTileView(TileRecord tile) {
307             addView(tile.tileView, getChildCount(), generateTileLayoutParams());
308         }
309 
310         @Override
onLayout(boolean changed, int l, int t, int r, int b)311         protected void onLayout(boolean changed, int l, int t, int r, int b) {
312             // We only care about clipping on the right side
313             mClippingBounds.set(0, 0, r - l, 10000);
314             setClipBounds(mClippingBounds);
315 
316             calculateColumns();
317 
318             for (int i = 0; i < mRecords.size(); i++) {
319                 mRecords.get(i).tileView.setVisibility( i < mColumns ? View.VISIBLE : View.GONE);
320             }
321 
322             setAccessibilityOrder();
323             layoutTileRecords(mColumns);
324         }
325 
326         @Override
327         public boolean updateResources() {
328             mCellWidth = mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
329             mCellHeight = mCellWidth;
330 
331             return false;
332         }
333 
334         private boolean calculateColumns() {
335             int prevNumColumns = mColumns;
336             int maxTiles = mRecords.size();
337 
338             if (maxTiles == 0){ // Early return during setup
339                 mColumns = 0;
340                 return true;
341             }
342 
343             final int availableWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd();
344             final int leftoverWhitespace = availableWidth - maxTiles * mCellWidth;
345             final int smallestHorizontalMarginNeeded;
346             smallestHorizontalMarginNeeded = leftoverWhitespace / Math.max(1, maxTiles - 1);
347 
348             if (smallestHorizontalMarginNeeded > 0){
349                 mCellMarginHorizontal = smallestHorizontalMarginNeeded;
350                 mColumns = maxTiles;
351             } else{
352                 mColumns = mCellWidth == 0 ? 1 :
353                         Math.min(maxTiles, availableWidth / mCellWidth );
354                 // If we can only fit one column, use mCellMarginHorizontal to center it.
355                 if (mColumns == 1) {
356                     mCellMarginHorizontal = (availableWidth - mCellWidth) / 2;
357                 } else {
358                     mCellMarginHorizontal =
359                             (availableWidth - mColumns * mCellWidth) / (mColumns - 1);
360                 }
361 
362             }
363             return mColumns != prevNumColumns;
364         }
365 
setAccessibilityOrder()366         private void setAccessibilityOrder() {
367             if (mRecords != null && mRecords.size() > 0) {
368                 View previousView = this;
369                 for (TileRecord record : mRecords) {
370                     if (record.tileView.getVisibility() == GONE) continue;
371                     previousView = record.tileView.updateAccessibilityOrder(previousView);
372                 }
373                 mRecords.get(mRecords.size() - 1).tileView.setAccessibilityTraversalBefore(
374                         R.id.expand_indicator);
375             }
376         }
377 
378         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)379         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
380             // Measure each QS tile.
381             for (TileRecord record : mRecords) {
382                 if (record.tileView.getVisibility() == GONE) continue;
383                 record.tileView.measure(exactly(mCellWidth), exactly(mCellHeight));
384             }
385 
386             int height = mCellHeight;
387             if (height < 0) height = 0;
388 
389             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
390         }
391 
392         @Override
getNumVisibleTiles()393         public int getNumVisibleTiles() {
394             return mColumns;
395         }
396 
397         @Override
getColumnStart(int column)398         protected int getColumnStart(int column) {
399             if (mColumns == 1) {
400                 // Only one column/tile. Use the margin to center the tile.
401                 return getPaddingStart() + mCellMarginHorizontal;
402             }
403             return getPaddingStart() + column *  (mCellWidth + mCellMarginHorizontal);
404         }
405 
406         @Override
setListening(boolean listening)407         public void setListening(boolean listening) {
408             boolean startedListening = !mListening && listening;
409             super.setListening(listening);
410             if (startedListening) {
411                 for (int i = 0; i < getNumVisibleTiles(); i++) {
412                     QSTile tile = mRecords.get(i).tile;
413                     mUiEventLogger.logWithInstanceId(QSEvent.QQS_TILE_VISIBLE, 0,
414                             tile.getMetricsSpec(), tile.getInstanceId());
415                 }
416             }
417         }
418     }
419 }
420