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