1 /*
2  * Copyright (C) 2014 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 android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
21 
22 import android.annotation.Nullable;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.content.res.Resources;
27 import android.metrics.LogMaker;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.service.quicksettings.Tile;
31 import android.util.AttributeSet;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.widget.LinearLayout;
35 
36 import com.android.internal.logging.MetricsLogger;
37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
38 import com.android.settingslib.Utils;
39 import com.android.systemui.Dependency;
40 import com.android.systemui.R;
41 import com.android.systemui.plugins.qs.DetailAdapter;
42 import com.android.systemui.plugins.qs.QSTile;
43 import com.android.systemui.plugins.qs.QSTileView;
44 import com.android.systemui.qs.QSHost.Callback;
45 import com.android.systemui.qs.customize.QSCustomizer;
46 import com.android.systemui.qs.external.CustomTile;
47 import com.android.systemui.settings.BrightnessController;
48 import com.android.systemui.settings.ToggleSliderView;
49 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
50 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
51 import com.android.systemui.tuner.TunerService;
52 import com.android.systemui.tuner.TunerService.Tunable;
53 
54 import java.util.ArrayList;
55 import java.util.Collection;
56 
57 /** View that represents the quick settings tile panel (when expanded/pulled down). **/
58 public class QSPanel extends LinearLayout implements Tunable, Callback, BrightnessMirrorListener {
59 
60     public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
61     public static final String QS_SHOW_HEADER = "qs_show_header";
62 
63     protected final Context mContext;
64     protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
65     protected final View mBrightnessView;
66     private final H mHandler = new H();
67     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
68     private final QSTileRevealController mQsTileRevealController;
69 
70     protected boolean mExpanded;
71     protected boolean mListening;
72 
73     private QSDetail.Callback mCallback;
74     private BrightnessController mBrightnessController;
75     protected QSTileHost mHost;
76 
77     protected QSSecurityFooter mFooter;
78     private PageIndicator mPanelPageIndicator;
79     private PageIndicator mFooterPageIndicator;
80     private boolean mGridContentVisible = true;
81 
82     protected QSTileLayout mTileLayout;
83 
84     private QSCustomizer mCustomizePanel;
85     private Record mDetailRecord;
86 
87     private BrightnessMirrorController mBrightnessMirrorController;
88     private View mDivider;
89 
QSPanel(Context context)90     public QSPanel(Context context) {
91         this(context, null);
92     }
93 
QSPanel(Context context, AttributeSet attrs)94     public QSPanel(Context context, AttributeSet attrs) {
95         super(context, attrs);
96         mContext = context;
97 
98         setOrientation(VERTICAL);
99 
100         mBrightnessView = LayoutInflater.from(mContext).inflate(
101             R.layout.quick_settings_brightness_dialog, this, false);
102         addView(mBrightnessView);
103 
104         mTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate(
105                 R.layout.qs_paged_tile_layout, this, false);
106         mTileLayout.setListening(mListening);
107         addView((View) mTileLayout);
108 
109         mPanelPageIndicator = (PageIndicator) LayoutInflater.from(context).inflate(
110                 R.layout.qs_page_indicator, this, false);
111         addView(mPanelPageIndicator);
112 
113         ((PagedTileLayout) mTileLayout).setPageIndicator(mPanelPageIndicator);
114         mQsTileRevealController = new QSTileRevealController(mContext, this,
115                 (PagedTileLayout) mTileLayout);
116 
117         addDivider();
118 
119         mFooter = new QSSecurityFooter(this, context);
120         addView(mFooter.getView());
121 
122         updateResources();
123 
124         mBrightnessController = new BrightnessController(getContext(),
125                 findViewById(R.id.brightness_icon),
126                 findViewById(R.id.brightness_slider));
127     }
128 
addDivider()129     protected void addDivider() {
130         mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
131         mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
132                 getColorForState(mContext, Tile.STATE_ACTIVE)));
133         addView(mDivider);
134     }
135 
getDivider()136     public View getDivider() {
137         return mDivider;
138     }
139 
getPageIndicator()140     public View getPageIndicator() {
141         return mPanelPageIndicator;
142     }
143 
getQsTileRevealController()144     public QSTileRevealController getQsTileRevealController() {
145         return mQsTileRevealController;
146     }
147 
isShowingCustomize()148     public boolean isShowingCustomize() {
149         return mCustomizePanel != null && mCustomizePanel.isCustomizing();
150     }
151 
152     @Override
onAttachedToWindow()153     protected void onAttachedToWindow() {
154         super.onAttachedToWindow();
155         final TunerService tunerService = Dependency.get(TunerService.class);
156         tunerService.addTunable(this, QS_SHOW_BRIGHTNESS);
157 
158         if (mHost != null) {
159             setTiles(mHost.getTiles());
160         }
161         if (mBrightnessMirrorController != null) {
162             mBrightnessMirrorController.addCallback(this);
163         }
164     }
165 
166     @Override
onDetachedFromWindow()167     protected void onDetachedFromWindow() {
168         Dependency.get(TunerService.class).removeTunable(this);
169         if (mHost != null) {
170             mHost.removeCallback(this);
171         }
172         for (TileRecord record : mRecords) {
173             record.tile.removeCallbacks();
174         }
175         if (mBrightnessMirrorController != null) {
176             mBrightnessMirrorController.removeCallback(this);
177         }
178         super.onDetachedFromWindow();
179     }
180 
181     @Override
onTilesChanged()182     public void onTilesChanged() {
183         setTiles(mHost.getTiles());
184     }
185 
186     @Override
onTuningChanged(String key, String newValue)187     public void onTuningChanged(String key, String newValue) {
188         if (QS_SHOW_BRIGHTNESS.equals(key)) {
189             updateViewVisibilityForTuningValue(mBrightnessView, newValue);
190         }
191     }
192 
updateViewVisibilityForTuningValue(View view, @Nullable String newValue)193     private void updateViewVisibilityForTuningValue(View view, @Nullable String newValue) {
194         view.setVisibility(newValue == null || Integer.parseInt(newValue) != 0 ? VISIBLE : GONE);
195     }
196 
openDetails(String subPanel)197     public void openDetails(String subPanel) {
198         QSTile tile = getTile(subPanel);
199         showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
200     }
201 
getTile(String subPanel)202     private QSTile getTile(String subPanel) {
203         for (int i = 0; i < mRecords.size(); i++) {
204             if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
205                 return mRecords.get(i).tile;
206             }
207         }
208         return mHost.createTile(subPanel);
209     }
210 
setBrightnessMirror(BrightnessMirrorController c)211     public void setBrightnessMirror(BrightnessMirrorController c) {
212         if (mBrightnessMirrorController != null) {
213             mBrightnessMirrorController.removeCallback(this);
214         }
215         mBrightnessMirrorController = c;
216         if (mBrightnessMirrorController != null) {
217             mBrightnessMirrorController.addCallback(this);
218         }
219         updateBrightnessMirror();
220     }
221 
222     @Override
onBrightnessMirrorReinflated(View brightnessMirror)223     public void onBrightnessMirrorReinflated(View brightnessMirror) {
224         updateBrightnessMirror();
225     }
226 
getBrightnessView()227     View getBrightnessView() {
228         return mBrightnessView;
229     }
230 
setCallback(QSDetail.Callback callback)231     public void setCallback(QSDetail.Callback callback) {
232         mCallback = callback;
233     }
234 
setHost(QSTileHost host, QSCustomizer customizer)235     public void setHost(QSTileHost host, QSCustomizer customizer) {
236         mHost = host;
237         mHost.addCallback(this);
238         setTiles(mHost.getTiles());
239         mFooter.setHostEnvironment(host);
240         mCustomizePanel = customizer;
241         if (mCustomizePanel != null) {
242             mCustomizePanel.setHost(mHost);
243         }
244     }
245 
246     /**
247      * Links the footer's page indicator, which is used in landscape orientation to save space.
248      *
249      * @param pageIndicator indicator to use for page scrolling
250      */
setFooterPageIndicator(PageIndicator pageIndicator)251     public void setFooterPageIndicator(PageIndicator pageIndicator) {
252         if (mTileLayout instanceof PagedTileLayout) {
253             mFooterPageIndicator = pageIndicator;
254             updatePageIndicator();
255         }
256     }
257 
updatePageIndicator()258     private void updatePageIndicator() {
259         if (mTileLayout instanceof PagedTileLayout) {
260             // If we're in landscape, and we have the footer page indicator (which we should if the
261             // footer has been initialized & linked), then we'll show the footer page indicator to
262             // save space in the main QS tile area. Otherwise, we'll use the default one under the
263             // tiles/above the footer.
264             boolean shouldUseFooterPageIndicator =
265                     getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
266                             && mFooterPageIndicator != null;
267 
268             mPanelPageIndicator.setVisibility(View.GONE);
269             if (mFooterPageIndicator != null) {
270                 mFooterPageIndicator.setVisibility(View.GONE);
271             }
272 
273             ((PagedTileLayout) mTileLayout).setPageIndicator(
274                     shouldUseFooterPageIndicator ? mFooterPageIndicator : mPanelPageIndicator);
275         }
276     }
277 
getHost()278     public QSTileHost getHost() {
279         return mHost;
280     }
281 
updateResources()282     public void updateResources() {
283         final Resources res = mContext.getResources();
284         setPadding(0, res.getDimensionPixelSize(R.dimen.qs_panel_padding_top), 0, res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom));
285 
286         updatePageIndicator();
287 
288         for (TileRecord r : mRecords) {
289             r.tile.clearState();
290         }
291         if (mListening) {
292             refreshAllTiles();
293         }
294         if (mTileLayout != null) {
295             mTileLayout.updateResources();
296         }
297     }
298 
299     @Override
onConfigurationChanged(Configuration newConfig)300     protected void onConfigurationChanged(Configuration newConfig) {
301         super.onConfigurationChanged(newConfig);
302         mFooter.onConfigurationChanged();
303 
304         updateBrightnessMirror();
305     }
306 
updateBrightnessMirror()307     public void updateBrightnessMirror() {
308         if (mBrightnessMirrorController != null) {
309             ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider);
310             ToggleSliderView mirrorSlider = mBrightnessMirrorController.getMirror()
311                     .findViewById(R.id.brightness_slider);
312             brightnessSlider.setMirror(mirrorSlider);
313             brightnessSlider.setMirrorController(mBrightnessMirrorController);
314         }
315     }
316 
onCollapse()317     public void onCollapse() {
318         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
319             mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
320         }
321     }
322 
setExpanded(boolean expanded)323     public void setExpanded(boolean expanded) {
324         if (mExpanded == expanded) return;
325         mExpanded = expanded;
326         if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
327             ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
328         }
329         mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded);
330         if (!mExpanded) {
331             closeDetail();
332         } else {
333             logTiles();
334         }
335     }
336 
setPageListener(final PagedTileLayout.PageListener pageListener)337     public void setPageListener(final PagedTileLayout.PageListener pageListener) {
338         if (mTileLayout instanceof PagedTileLayout) {
339             ((PagedTileLayout) mTileLayout).setPageListener(pageListener);
340         }
341     }
342 
isExpanded()343     public boolean isExpanded() {
344         return mExpanded;
345     }
346 
setListening(boolean listening)347     public void setListening(boolean listening) {
348         if (mListening == listening) return;
349         mListening = listening;
350         if (mTileLayout != null) {
351             mTileLayout.setListening(listening);
352         }
353         mFooter.setListening(mListening);
354         if (mListening) {
355             refreshAllTiles();
356         }
357         if (mBrightnessView.getVisibility() == View.VISIBLE) {
358             if (listening) {
359                 mBrightnessController.registerCallbacks();
360             } else {
361                 mBrightnessController.unregisterCallbacks();
362             }
363         }
364     }
365 
refreshAllTiles()366     public void refreshAllTiles() {
367         mBrightnessController.checkRestrictionAndSetEnabled();
368         for (TileRecord r : mRecords) {
369             r.tile.refreshState();
370         }
371         mFooter.refreshState();
372     }
373 
showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)374     public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) {
375         int xInWindow = locationInWindow[0];
376         int yInWindow = locationInWindow[1];
377         ((View) getParent()).getLocationInWindow(locationInWindow);
378 
379         Record r = new Record();
380         r.detailAdapter = adapter;
381         r.x = xInWindow - locationInWindow[0];
382         r.y = yInWindow - locationInWindow[1];
383 
384         locationInWindow[0] = xInWindow;
385         locationInWindow[1] = yInWindow;
386 
387         showDetail(show, r);
388     }
389 
showDetail(boolean show, Record r)390     protected void showDetail(boolean show, Record r) {
391         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
392     }
393 
setTiles(Collection<QSTile> tiles)394     public void setTiles(Collection<QSTile> tiles) {
395         setTiles(tiles, false);
396     }
397 
setTiles(Collection<QSTile> tiles, boolean collapsedView)398     public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
399         if (!collapsedView) {
400             mQsTileRevealController.updateRevealedTiles(tiles);
401         }
402         for (TileRecord record : mRecords) {
403             mTileLayout.removeTile(record);
404             record.tile.removeCallback(record.callback);
405         }
406         mRecords.clear();
407         for (QSTile tile : tiles) {
408             addTile(tile, collapsedView);
409         }
410     }
411 
drawTile(TileRecord r, QSTile.State state)412     protected void drawTile(TileRecord r, QSTile.State state) {
413         r.tileView.onStateChanged(state);
414     }
415 
createTileView(QSTile tile, boolean collapsedView)416     protected QSTileView createTileView(QSTile tile, boolean collapsedView) {
417         return mHost.createTileView(tile, collapsedView);
418     }
419 
shouldShowDetail()420     protected boolean shouldShowDetail() {
421         return mExpanded;
422     }
423 
addTile(final QSTile tile, boolean collapsedView)424     protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
425         final TileRecord r = new TileRecord();
426         r.tile = tile;
427         r.tileView = createTileView(tile, collapsedView);
428         final QSTile.Callback callback = new QSTile.Callback() {
429             @Override
430             public void onStateChanged(QSTile.State state) {
431                 drawTile(r, state);
432             }
433 
434             @Override
435             public void onShowDetail(boolean show) {
436                 // Both the collapsed and full QS panels get this callback, this check determines
437                 // which one should handle showing the detail.
438                 if (shouldShowDetail()) {
439                     QSPanel.this.showDetail(show, r);
440                 }
441             }
442 
443             @Override
444             public void onToggleStateChanged(boolean state) {
445                 if (mDetailRecord == r) {
446                     fireToggleStateChanged(state);
447                 }
448             }
449 
450             @Override
451             public void onScanStateChanged(boolean state) {
452                 r.scanState = state;
453                 if (mDetailRecord == r) {
454                     fireScanStateChanged(r.scanState);
455                 }
456             }
457 
458             @Override
459             public void onAnnouncementRequested(CharSequence announcement) {
460                 if (announcement != null) {
461                     mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
462                             .sendToTarget();
463                 }
464             }
465         };
466         r.tile.addCallback(callback);
467         r.callback = callback;
468         r.tileView.init(r.tile);
469         r.tile.refreshState();
470         mRecords.add(r);
471 
472         if (mTileLayout != null) {
473             mTileLayout.addTile(r);
474         }
475 
476         return r;
477     }
478 
479 
showEdit(final View v)480     public void showEdit(final View v) {
481         v.post(new Runnable() {
482             @Override
483             public void run() {
484                 if (mCustomizePanel != null) {
485                     if (!mCustomizePanel.isCustomizing()) {
486                         int[] loc = new int[2];
487                         v.getLocationInWindow(loc);
488                         int x = loc[0] + v.getWidth() / 2;
489                         int y = loc[1] + v.getHeight() / 2;
490                         mCustomizePanel.show(x, y);
491                     }
492                 }
493 
494             }
495         });
496     }
497 
closeDetail()498     public void closeDetail() {
499         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
500             // Treat this as a detail panel for now, to make things easy.
501             mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
502             return;
503         }
504         showDetail(false, mDetailRecord);
505     }
506 
getGridHeight()507     public int getGridHeight() {
508         return getMeasuredHeight();
509     }
510 
handleShowDetail(Record r, boolean show)511     protected void handleShowDetail(Record r, boolean show) {
512         if (r instanceof TileRecord) {
513             handleShowDetailTile((TileRecord) r, show);
514         } else {
515             int x = 0;
516             int y = 0;
517             if (r != null) {
518                 x = r.x;
519                 y = r.y;
520             }
521             handleShowDetailImpl(r, show, x, y);
522         }
523     }
524 
handleShowDetailTile(TileRecord r, boolean show)525     private void handleShowDetailTile(TileRecord r, boolean show) {
526         if ((mDetailRecord != null) == show && mDetailRecord == r) return;
527 
528         if (show) {
529             r.detailAdapter = r.tile.getDetailAdapter();
530             if (r.detailAdapter == null) return;
531         }
532         r.tile.setDetailListening(show);
533         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
534         int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop();
535         handleShowDetailImpl(r, show, x, y);
536     }
537 
handleShowDetailImpl(Record r, boolean show, int x, int y)538     private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
539         setDetailRecord(show ? r : null);
540         fireShowingDetail(show ? r.detailAdapter : null, x, y);
541     }
542 
setDetailRecord(Record r)543     protected void setDetailRecord(Record r) {
544         if (r == mDetailRecord) return;
545         mDetailRecord = r;
546         final boolean scanState = mDetailRecord instanceof TileRecord
547                 && ((TileRecord) mDetailRecord).scanState;
548         fireScanStateChanged(scanState);
549     }
550 
setGridContentVisibility(boolean visible)551     void setGridContentVisibility(boolean visible) {
552         int newVis = visible ? VISIBLE : INVISIBLE;
553         setVisibility(newVis);
554         if (mGridContentVisible != visible) {
555             mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis);
556         }
557         mGridContentVisible = visible;
558     }
559 
logTiles()560     private void logTiles() {
561         for (int i = 0; i < mRecords.size(); i++) {
562             QSTile tile = mRecords.get(i).tile;
563             mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory())
564                     .setType(MetricsEvent.TYPE_OPEN)));
565         }
566     }
567 
fireShowingDetail(DetailAdapter detail, int x, int y)568     private void fireShowingDetail(DetailAdapter detail, int x, int y) {
569         if (mCallback != null) {
570             mCallback.onShowingDetail(detail, x, y);
571         }
572     }
573 
fireToggleStateChanged(boolean state)574     private void fireToggleStateChanged(boolean state) {
575         if (mCallback != null) {
576             mCallback.onToggleStateChanged(state);
577         }
578     }
579 
fireScanStateChanged(boolean state)580     private void fireScanStateChanged(boolean state) {
581         if (mCallback != null) {
582             mCallback.onScanStateChanged(state);
583         }
584     }
585 
clickTile(ComponentName tile)586     public void clickTile(ComponentName tile) {
587         final String spec = CustomTile.toSpec(tile);
588         final int N = mRecords.size();
589         for (int i = 0; i < N; i++) {
590             if (mRecords.get(i).tile.getTileSpec().equals(spec)) {
591                 mRecords.get(i).tile.click();
592                 break;
593             }
594         }
595     }
596 
getTileLayout()597     QSTileLayout getTileLayout() {
598         return mTileLayout;
599     }
600 
getTileView(QSTile tile)601     QSTileView getTileView(QSTile tile) {
602         for (TileRecord r : mRecords) {
603             if (r.tile == tile) {
604                 return r.tileView;
605             }
606         }
607         return null;
608     }
609 
getFooter()610     public QSSecurityFooter getFooter() {
611         return mFooter;
612     }
613 
showDeviceMonitoringDialog()614     public void showDeviceMonitoringDialog() {
615         mFooter.showDeviceMonitoringDialog();
616     }
617 
setMargins(int sideMargins)618     public void setMargins(int sideMargins) {
619         for (int i = 0; i < getChildCount(); i++) {
620             View view = getChildAt(i);
621             if (view != mTileLayout) {
622                 LayoutParams lp = (LayoutParams) view.getLayoutParams();
623                 lp.leftMargin = sideMargins;
624                 lp.rightMargin = sideMargins;
625             }
626         }
627     }
628 
629     private class H extends Handler {
630         private static final int SHOW_DETAIL = 1;
631         private static final int SET_TILE_VISIBILITY = 2;
632         private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3;
633 
634         @Override
handleMessage(Message msg)635         public void handleMessage(Message msg) {
636             if (msg.what == SHOW_DETAIL) {
637                 handleShowDetail((Record) msg.obj, msg.arg1 != 0);
638             } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
639                 announceForAccessibility((CharSequence) msg.obj);
640             }
641         }
642     }
643 
644     protected static class Record {
645         DetailAdapter detailAdapter;
646         int x;
647         int y;
648     }
649 
650     public static final class TileRecord extends Record {
651         public QSTile tile;
652         public com.android.systemui.plugins.qs.QSTileView tileView;
653         public boolean scanState;
654         public QSTile.Callback callback;
655     }
656 
657     public interface QSTileLayout {
addTile(TileRecord tile)658         void addTile(TileRecord tile);
659 
removeTile(TileRecord tile)660         void removeTile(TileRecord tile);
661 
getOffsetTop(TileRecord tile)662         int getOffsetTop(TileRecord tile);
663 
updateResources()664         boolean updateResources();
665 
setListening(boolean listening)666         void setListening(boolean listening);
667 
setExpansion(float expansion)668         default void setExpansion(float expansion) {}
669     }
670 }
671