1 package com.android.systemui.qs;
2 
3 import static com.android.systemui.util.Utils.useQsMediaPlayer;
4 
5 import android.content.Context;
6 import android.content.res.Resources;
7 import android.provider.Settings;
8 import android.util.AttributeSet;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.view.accessibility.AccessibilityNodeInfo;
12 import android.widget.TextView;
13 
14 import androidx.annotation.Nullable;
15 
16 import com.android.internal.logging.UiEventLogger;
17 import com.android.systemui.FontSizeUtils;
18 import com.android.systemui.R;
19 import com.android.systemui.qs.QSPanel.QSTileLayout;
20 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
21 import com.android.systemui.qs.tileimpl.HeightOverrideable;
22 import com.android.systemui.qs.tileimpl.QSTileViewImplKt;
23 
24 import java.util.ArrayList;
25 
26 public class TileLayout extends ViewGroup implements QSTileLayout {
27 
28     public static final int NO_MAX_COLUMNS = 100;
29 
30     private static final String TAG = "TileLayout";
31 
32     protected int mColumns;
33     protected int mCellWidth;
34     protected int mResourceCellHeightResId = R.dimen.qs_tile_height;
35     protected int mResourceCellHeight;
36     protected int mEstimatedCellHeight;
37     protected int mCellHeight;
38     protected int mCellMarginHorizontal;
39     protected int mCellMarginVertical;
40     protected int mSidePadding;
41     protected int mRows = 1;
42 
43     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
44     protected boolean mListening;
45     protected int mMaxAllowedRows = 3;
46 
47     // Prototyping with less rows
48     private final boolean mLessRows;
49     private int mMinRows = 1;
50     private int mMaxColumns = NO_MAX_COLUMNS;
51     protected int mResourceColumns;
52     private float mSquishinessFraction = 1f;
53     protected int mLastTileBottom;
54 
55     protected TextView mTempTextView;
56 
TileLayout(Context context)57     public TileLayout(Context context) {
58         this(context, null);
59     }
60 
TileLayout(Context context, @Nullable AttributeSet attrs)61     public TileLayout(Context context, @Nullable AttributeSet attrs) {
62         super(context, attrs);
63         mLessRows = ((Settings.System.getInt(context.getContentResolver(), "qs_less_rows", 0) != 0)
64                 || useQsMediaPlayer(context));
65         mTempTextView = new TextView(context);
66         updateResources();
67     }
68 
69     @Override
getOffsetTop(TileRecord tile)70     public int getOffsetTop(TileRecord tile) {
71         return getTop();
72     }
73 
setListening(boolean listening)74     public void setListening(boolean listening) {
75         setListening(listening, null);
76     }
77 
78     @Override
setListening(boolean listening, @Nullable UiEventLogger uiEventLogger)79     public void setListening(boolean listening, @Nullable UiEventLogger uiEventLogger) {
80         if (mListening == listening) return;
81         mListening = listening;
82         for (TileRecord record : mRecords) {
83             record.tile.setListening(this, mListening);
84         }
85     }
86 
87     @Override
setMinRows(int minRows)88     public boolean setMinRows(int minRows) {
89         if (mMinRows != minRows) {
90             mMinRows = minRows;
91             updateResources();
92             return true;
93         }
94         return false;
95     }
96 
97     @Override
setMaxColumns(int maxColumns)98     public boolean setMaxColumns(int maxColumns) {
99         mMaxColumns = maxColumns;
100         return updateColumns();
101     }
102 
addTile(TileRecord tile)103     public void addTile(TileRecord tile) {
104         mRecords.add(tile);
105         tile.tile.setListening(this, mListening);
106         addTileView(tile);
107     }
108 
addTileView(TileRecord tile)109     protected void addTileView(TileRecord tile) {
110         addView(tile.tileView);
111     }
112 
113     @Override
removeTile(TileRecord tile)114     public void removeTile(TileRecord tile) {
115         mRecords.remove(tile);
116         tile.tile.setListening(this, false);
117         removeView(tile.tileView);
118     }
119 
removeAllViews()120     public void removeAllViews() {
121         for (TileRecord record : mRecords) {
122             record.tile.setListening(this, false);
123         }
124         mRecords.clear();
125         super.removeAllViews();
126     }
127 
updateResources()128     public boolean updateResources() {
129         Resources res = getResources();
130         mResourceColumns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
131         mResourceCellHeight = res.getDimensionPixelSize(mResourceCellHeightResId);
132         mCellMarginHorizontal = res.getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal);
133         mSidePadding = useSidePadding() ? mCellMarginHorizontal / 2 : 0;
134         mCellMarginVertical= res.getDimensionPixelSize(R.dimen.qs_tile_margin_vertical);
135         mMaxAllowedRows = Math.max(1, getResources().getInteger(R.integer.quick_settings_max_rows));
136         if (mLessRows) {
137             mMaxAllowedRows = Math.max(mMinRows, mMaxAllowedRows - 1);
138         }
139         // update estimated cell height under current font scaling
140         mTempTextView.dispatchConfigurationChanged(mContext.getResources().getConfiguration());
141         estimateCellHeight();
142         if (updateColumns()) {
143             requestLayout();
144             return true;
145         }
146         return false;
147     }
148 
useSidePadding()149     protected boolean useSidePadding() {
150         return true;
151     }
152 
updateColumns()153     private boolean updateColumns() {
154         int oldColumns = mColumns;
155         mColumns = Math.min(mResourceColumns, mMaxColumns);
156         return oldColumns != mColumns;
157     }
158 
159     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)160     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
161         // If called with AT_MOST, it will limit the number of rows. If called with UNSPECIFIED
162         // it will show all its tiles. In this case, the tiles have to be entered before the
163         // container is measured. Any change in the tiles, should trigger a remeasure.
164         final int numTiles = mRecords.size();
165         final int width = MeasureSpec.getSize(widthMeasureSpec);
166         final int availableWidth = width - getPaddingStart() - getPaddingEnd();
167         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
168         if (heightMode == MeasureSpec.UNSPECIFIED) {
169             mRows = (numTiles + mColumns - 1) / mColumns;
170         }
171         final int gaps = mColumns - 1;
172         mCellWidth =
173                 (availableWidth - (mCellMarginHorizontal * gaps) - mSidePadding * 2) / mColumns;
174 
175         // Measure each QS tile.
176         View previousView = this;
177         int verticalMeasure = exactly(getCellHeight());
178         for (TileRecord record : mRecords) {
179             if (record.tileView.getVisibility() == GONE) continue;
180             record.tileView.measure(exactly(mCellWidth), verticalMeasure);
181             previousView = record.tileView.updateAccessibilityOrder(previousView);
182             mCellHeight = record.tileView.getMeasuredHeight();
183         }
184 
185         int height = (mCellHeight + mCellMarginVertical) * mRows;
186         height -= mCellMarginVertical;
187 
188         if (height < 0) height = 0;
189 
190         setMeasuredDimension(width, height);
191     }
192 
193     /**
194      * Determines the maximum number of rows that can be shown based on height. Clips at a minimum
195      * of 1 and a maximum of mMaxAllowedRows.
196      *
197      * @param allowedHeight The height this view has visually available
198      * @param tilesCount Upper limit on the number of tiles to show. to prevent empty rows.
199      */
updateMaxRows(int allowedHeight, int tilesCount)200     public boolean updateMaxRows(int allowedHeight, int tilesCount) {
201         // Add the cell margin in order to divide easily by the height + the margin below
202         final int availableHeight =  allowedHeight + mCellMarginVertical;
203         final int previousRows = mRows;
204         mRows = availableHeight / (getCellHeight() + mCellMarginVertical);
205         if (mRows < mMinRows) {
206             mRows = mMinRows;
207         } else if (mRows >= mMaxAllowedRows) {
208             mRows = mMaxAllowedRows;
209         }
210         if (mRows > (tilesCount + mColumns - 1) / mColumns) {
211             mRows = (tilesCount + mColumns - 1) / mColumns;
212         }
213         return previousRows != mRows;
214     }
215 
216     @Override
hasOverlappingRendering()217     public boolean hasOverlappingRendering() {
218         return false;
219     }
220 
exactly(int size)221     protected static int exactly(int size) {
222         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
223     }
224 
225     // Estimate the height for the tile with 2 labels (general case) under current font scaling.
estimateCellHeight()226     protected void estimateCellHeight() {
227         FontSizeUtils.updateFontSize(mTempTextView, R.dimen.qs_tile_text_size);
228         int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
229         mTempTextView.measure(unspecifiedSpec, unspecifiedSpec);
230         int padding = mContext.getResources().getDimensionPixelSize(R.dimen.qs_tile_padding);
231         mEstimatedCellHeight = mTempTextView.getMeasuredHeight() * 2 + padding * 2;
232     }
233 
getCellHeight()234     protected int getCellHeight() {
235         // Compare estimated height with resource height and return the larger one.
236         // If estimated height > resource height, it means the resource height is not enough
237         // for the tile content under current font scaling. Therefore, we need to use the estimated
238         // height to have a full tile content view.
239         // If estimated height <= resource height, we can use the resource height for tile to keep
240         // the same UI as original behavior.
241         return Math.max(mResourceCellHeight, mEstimatedCellHeight);
242     }
243 
layoutTileRecords(int numRecords, boolean forLayout)244     private void layoutTileRecords(int numRecords, boolean forLayout) {
245         final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
246         int row = 0;
247         int column = 0;
248         mLastTileBottom = 0;
249 
250         // Layout each QS tile.
251         final int tilesToLayout = Math.min(numRecords, mRows * mColumns);
252         for (int i = 0; i < tilesToLayout; i++, column++) {
253             // If we reached the last column available to layout a tile, wrap back to the next row.
254             if (column == mColumns) {
255                 column = 0;
256                 row++;
257             }
258 
259             final TileRecord record = mRecords.get(i);
260             final int top = getRowTop(row);
261             final int left = getColumnStart(isRtl ? mColumns - column - 1 : column);
262             final int right = left + mCellWidth;
263             final int bottom = top + record.tileView.getMeasuredHeight();
264             if (forLayout) {
265                 record.tileView.layout(left, top, right, bottom);
266             } else {
267                 record.tileView.setLeftTopRightBottom(left, top, right, bottom);
268             }
269             record.tileView.setPosition(i);
270 
271             // Set the bottom to the unoverriden squished bottom. This is to avoid fake bottoms that
272             // are only used for QQS -> QS expansion animations
273             float scale = QSTileViewImplKt.constrainSquishiness(mSquishinessFraction);
274             mLastTileBottom = top + (int) (record.tileView.getMeasuredHeight() * scale);
275         }
276     }
277 
278     @Override
onLayout(boolean changed, int l, int t, int r, int b)279     protected void onLayout(boolean changed, int l, int t, int r, int b) {
280         layoutTileRecords(mRecords.size(), true /* forLayout */);
281     }
282 
getRowTop(int row)283     protected int getRowTop(int row) {
284         float scale = QSTileViewImplKt.constrainSquishiness(mSquishinessFraction);
285         return (int) (row * (mCellHeight * scale + mCellMarginVertical));
286     }
287 
getColumnStart(int column)288     protected int getColumnStart(int column) {
289         return getPaddingStart() + mSidePadding
290                 + column *  (mCellWidth + mCellMarginHorizontal);
291     }
292 
293     @Override
getNumVisibleTiles()294     public int getNumVisibleTiles() {
295         return mRecords.size();
296     }
297 
isFull()298     public boolean isFull() {
299         return false;
300     }
301 
302     /**
303      * @return The maximum number of tiles this layout can hold
304      */
maxTiles()305     public int maxTiles() {
306         // Each layout should be able to hold at least one tile. If there's not enough room to
307         // show even 1 or there are no tiles, it probably means we are in the middle of setting
308         // up.
309         return Math.max(mColumns * mRows, 1);
310     }
311 
312     @Override
getTilesHeight()313     public int getTilesHeight() {
314         return mLastTileBottom + getPaddingBottom();
315     }
316 
317     @Override
setSquishinessFraction(float squishinessFraction)318     public void setSquishinessFraction(float squishinessFraction) {
319         if (Float.compare(mSquishinessFraction, squishinessFraction) == 0) {
320             return;
321         }
322         mSquishinessFraction = squishinessFraction;
323         layoutTileRecords(mRecords.size(), false /* forLayout */);
324 
325         for (TileRecord record : mRecords) {
326             if (record.tileView instanceof HeightOverrideable) {
327                 ((HeightOverrideable) record.tileView).setSquishinessFraction(mSquishinessFraction);
328             }
329         }
330     }
331 
332     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)333     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
334         super.onInitializeAccessibilityNodeInfoInternal(info);
335         info.setCollectionInfo(
336                 new AccessibilityNodeInfo.CollectionInfo(mRecords.size(), 1, false));
337     }
338 }
339