• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.volume;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorInflater;
21 import android.animation.AnimatorSet;
22 import android.annotation.DrawableRes;
23 import android.annotation.Nullable;
24 import android.app.Dialog;
25 import android.app.KeyguardManager;
26 import android.car.Car;
27 import android.car.CarNotConnectedException;
28 import android.car.media.CarAudioManager;
29 import android.car.media.ICarVolumeCallback;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.DialogInterface;
33 import android.content.ServiceConnection;
34 import android.content.res.TypedArray;
35 import android.content.res.XmlResourceParser;
36 import android.graphics.Color;
37 import android.graphics.drawable.ColorDrawable;
38 import android.graphics.PixelFormat;
39 import android.graphics.drawable.Drawable;
40 import android.media.AudioAttributes;
41 import android.media.AudioManager;
42 import android.os.Debug;
43 import android.os.Handler;
44 import android.os.IBinder;
45 import android.os.Looper;
46 import android.os.Message;
47 import android.util.AttributeSet;
48 import android.util.Log;
49 import android.util.SparseArray;
50 import android.util.Xml;
51 import android.view.ContextThemeWrapper;
52 import android.view.Gravity;
53 import android.view.MotionEvent;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.view.Window;
57 import android.view.WindowManager;
58 import android.widget.SeekBar;
59 import android.widget.SeekBar.OnSeekBarChangeListener;
60 
61 import androidx.car.widget.ListItem;
62 import androidx.car.widget.ListItemAdapter;
63 import androidx.car.widget.ListItemAdapter.BackgroundStyle;
64 import androidx.car.widget.ListItemProvider.ListProvider;
65 import androidx.car.widget.PagedListView;
66 import androidx.car.widget.SeekbarListItem;
67 
68 import java.util.Iterator;
69 import org.xmlpull.v1.XmlPullParserException;
70 
71 import java.io.IOException;
72 import java.io.PrintWriter;
73 import java.util.ArrayList;
74 import java.util.List;
75 
76 import com.android.systemui.R;
77 import com.android.systemui.plugins.VolumeDialog;
78 
79 /**
80  * Car version of the volume dialog.
81  *
82  * Methods ending in "H" must be called on the (ui) handler.
83  */
84 public class CarVolumeDialogImpl implements VolumeDialog {
85   private static final String TAG = Util.logTag(CarVolumeDialogImpl.class);
86 
87   private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems";
88   private static final String XML_TAG_VOLUME_ITEM = "item";
89   private static final int HOVERING_TIMEOUT = 16000;
90   private static final int NORMAL_TIMEOUT = 3000;
91   private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250;
92   private static final int DISMISS_DELAY_IN_MILLIS = 50;
93   private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100;
94 
95   private final Context mContext;
96   private final H mHandler = new H();
97 
98   private Window mWindow;
99   private CustomDialog mDialog;
100   private PagedListView mListView;
101   private ListItemAdapter mPagedListAdapter;
102   // All the volume items.
103   private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>();
104   // Available volume items in car audio manager.
105   private final List<VolumeItem> mAvailableVolumeItems = new ArrayList<>();
106   // Volume items in the PagedListView.
107   private final List<ListItem> mVolumeLineItems = new ArrayList<>();
108   private final KeyguardManager mKeyguard;
109 
110   private Car mCar;
111   private CarAudioManager mCarAudioManager;
112 
113   private boolean mHovering;
114   private boolean mShowing;
115   private boolean mExpanded;
116 
CarVolumeDialogImpl(Context context)117   public CarVolumeDialogImpl(Context context) {
118     mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme);
119     mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
120     mCar = Car.createCar(mContext, mServiceConnection);
121   }
122 
init(int windowType, Callback callback)123   public void init(int windowType, Callback callback) {
124     initDialog();
125 
126     mCar.connect();
127   }
128 
129   @Override
destroy()130   public void destroy() {
131     mHandler.removeCallbacksAndMessages(null);
132 
133     cleanupAudioManager();
134     // unregisterVolumeCallback is not being called when disconnect car, so we manually cleanup
135     // audio manager beforehand.
136     mCar.disconnect();
137   }
138 
initDialog()139   private void initDialog() {
140     loadAudioUsageItems();
141     mVolumeLineItems.clear();
142     mDialog = new CustomDialog(mContext);
143 
144     mHovering = false;
145     mShowing = false;
146     mExpanded = false;
147     mWindow = mDialog.getWindow();
148     mWindow.requestFeature(Window.FEATURE_NO_TITLE);
149     mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
150     mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
151         | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
152     mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
153         | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
154         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
155         | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
156         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
157         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
158     mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
159     mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast);
160     final WindowManager.LayoutParams lp = mWindow.getAttributes();
161     lp.format = PixelFormat.TRANSLUCENT;
162     lp.setTitle(VolumeDialogImpl.class.getSimpleName());
163     lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
164     lp.windowAnimations = -1;
165     mWindow.setAttributes(lp);
166     mWindow.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
167 
168     mDialog.setCanceledOnTouchOutside(true);
169     mDialog.setContentView(R.layout.car_volume_dialog);
170     mDialog.setOnShowListener(dialog -> {
171       mListView.setTranslationY(-mListView.getHeight());
172       mListView.setAlpha(0);
173       mListView.animate()
174           .alpha(1)
175           .translationY(0)
176           .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
177           .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
178           .start();
179     });
180     mListView = (PagedListView) mWindow.findViewById(R.id.volume_list);
181     mListView.setOnHoverListener((v, event) -> {
182       int action = event.getActionMasked();
183       mHovering = (action == MotionEvent.ACTION_HOVER_ENTER)
184           || (action == MotionEvent.ACTION_HOVER_MOVE);
185       rescheduleTimeoutH();
186       return true;
187     });
188 
189     mPagedListAdapter = new ListItemAdapter(mContext, new ListProvider(mVolumeLineItems),
190         BackgroundStyle.PANEL);
191     mListView.setAdapter(mPagedListAdapter);
192     mListView.setMaxPages(PagedListView.UNLIMITED_PAGES);
193   }
194 
show(int reason)195   public void show(int reason) {
196     mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget();
197   }
198 
dismiss(int reason)199   public void dismiss(int reason) {
200     mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
201   }
202 
showH(int reason)203   private void showH(int reason) {
204     if (D.BUG) {
205       Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
206     }
207 
208     mHandler.removeMessages(H.SHOW);
209     mHandler.removeMessages(H.DISMISS);
210     rescheduleTimeoutH();
211     // Refresh the data set before showing.
212     mPagedListAdapter.notifyDataSetChanged();
213     if (mShowing) {
214       return;
215     }
216     mShowing = true;
217 
218     mDialog.show();
219     Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
220   }
221 
rescheduleTimeoutH()222   protected void rescheduleTimeoutH() {
223     mHandler.removeMessages(H.DISMISS);
224     final int timeout = computeTimeoutH();
225     mHandler.sendMessageDelayed(mHandler
226         .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout);
227 
228     if (D.BUG) {
229       Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller());
230     }
231   }
232 
computeTimeoutH()233   private int computeTimeoutH() {
234     return mHovering ? HOVERING_TIMEOUT : NORMAL_TIMEOUT;
235   }
236 
dismissH(int reason)237   protected void dismissH(int reason) {
238     if (D.BUG) {
239       Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]);
240     }
241 
242     mHandler.removeMessages(H.DISMISS);
243     mHandler.removeMessages(H.SHOW);
244     if (!mShowing) {
245       return;
246     }
247 
248     mListView.animate().cancel();
249     mShowing = false;
250 
251     mListView.setTranslationY(0);
252     mListView.setAlpha(1);
253     mListView.animate()
254         .alpha(0)
255         .translationY(-mListView.getHeight())
256         .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS)
257         .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
258         .withEndAction(() -> mHandler.postDelayed(() -> {
259           if (D.BUG) {
260             Log.d(TAG, "mDialog.dismiss()");
261           }
262           mDialog.dismiss();
263         }, DISMISS_DELAY_IN_MILLIS))
264         .start();
265 
266     Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason);
267   }
268 
dump(PrintWriter writer)269   public void dump(PrintWriter writer) {
270     writer.println(VolumeDialogImpl.class.getSimpleName() + " state:");
271     writer.print("  mShowing: "); writer.println(mShowing);
272   }
273 
loadAudioUsageItems()274   private void loadAudioUsageItems() {
275     try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) {
276       AttributeSet attrs = Xml.asAttributeSet(parser);
277       int type;
278       // Traverse to the first start tag
279       while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT
280           && type != XmlResourceParser.START_TAG) {
281       }
282 
283       if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) {
284         throw new RuntimeException("Meta-data does not start with carVolumeItems tag");
285       }
286       int outerDepth = parser.getDepth();
287       int rank = 0;
288       while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT
289           && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) {
290         if (type == XmlResourceParser.END_TAG) {
291           continue;
292         }
293         if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) {
294           TypedArray item = mContext.getResources().obtainAttributes(
295               attrs, R.styleable.carVolumeItems_item);
296           int usage = item.getInt(R.styleable.carVolumeItems_item_usage, -1);
297           if (usage >= 0) {
298             VolumeItem volumeItem = new VolumeItem();
299             volumeItem.usage = usage;
300             volumeItem.rank = rank;
301             volumeItem.icon = item.getResourceId(R.styleable.carVolumeItems_item_icon, 0);
302             mVolumeItems.put(usage, volumeItem);
303             rank++;
304           }
305           item.recycle();
306         }
307       }
308     } catch (XmlPullParserException | IOException e) {
309       Log.e(TAG, "Error parsing volume groups configuration", e);
310     }
311   }
312 
getVolumeItemForUsages(int[] usages)313   private VolumeItem getVolumeItemForUsages(int[] usages) {
314     int rank = Integer.MAX_VALUE;
315     VolumeItem result = null;
316     for (int usage : usages) {
317       VolumeItem volumeItem = mVolumeItems.get(usage);
318       if (volumeItem.rank < rank) {
319         rank = volumeItem.rank;
320         result = volumeItem;
321       }
322     }
323     return result;
324   }
325 
getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId)326   private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
327     try {
328       return carAudioManager.getGroupVolume(volumeGroupId);
329     } catch (CarNotConnectedException e) {
330       Log.e(TAG, "Car is not connected!", e);
331     }
332     return 0;
333   }
334 
getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId)335   private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) {
336     try {
337       return carAudioManager.getGroupMaxVolume(volumeGroupId);
338     } catch (CarNotConnectedException e) {
339       Log.e(TAG, "Car is not connected!", e);
340     }
341     return 0;
342   }
343 
addSeekbarListItem(VolumeItem volumeItem, int volumeGroupId, int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener)344   private SeekbarListItem addSeekbarListItem(VolumeItem volumeItem, int volumeGroupId,
345       int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener) {
346     SeekbarListItem listItem = new SeekbarListItem(mContext);
347     listItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeGroupId));
348     int color = mContext.getResources().getColor(R.color.car_volume_dialog_tint);
349     int progress = getSeekbarValue(mCarAudioManager, volumeGroupId);
350     listItem.setProgress(progress);
351     listItem.setOnSeekBarChangeListener(
352         new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeGroupId, mCarAudioManager));
353     Drawable primaryIcon = mContext.getResources().getDrawable(volumeItem.icon);
354     primaryIcon.setTint(color);
355     listItem.setPrimaryActionIcon(primaryIcon);
356     if (supplementalIconId != 0) {
357       Drawable supplementalIcon = mContext.getResources().getDrawable(supplementalIconId);
358       supplementalIcon.setTint(color);
359       listItem.setSupplementalIcon(supplementalIcon, true,
360           supplementalIconOnClickListener);
361     } else {
362       listItem.setSupplementalEmptyIcon(true);
363     }
364 
365     mVolumeLineItems.add(listItem);
366     volumeItem.listItem = listItem;
367     volumeItem.progress = progress;
368     return listItem;
369   }
370 
findVolumeItem(SeekbarListItem targetItem)371   private VolumeItem findVolumeItem(SeekbarListItem targetItem) {
372     for (int i = 0; i < mVolumeItems.size(); ++i) {
373       VolumeItem volumeItem = mVolumeItems.valueAt(i);
374       if (volumeItem.listItem == targetItem) {
375         return volumeItem;
376       }
377     }
378     return null;
379   }
380 
cleanupAudioManager()381   private void cleanupAudioManager() {
382     try {
383       mCarAudioManager.unregisterVolumeCallback(mVolumeChangeCallback.asBinder());
384     } catch (CarNotConnectedException e) {
385       Log.e(TAG, "Car is not connected!", e);
386     }
387     mVolumeLineItems.clear();
388     mCarAudioManager = null;
389   }
390 
391   private final class H extends Handler {
392     private static final int SHOW = 1;
393     private static final int DISMISS = 2;
394 
H()395     public H() {
396       super(Looper.getMainLooper());
397     }
398 
399     @Override
handleMessage(Message msg)400     public void handleMessage(Message msg) {
401       switch (msg.what) {
402         case SHOW:
403           showH(msg.arg1);
404           break;
405         case DISMISS:
406           dismissH(msg.arg1);
407           break;
408         default:
409       }
410     }
411   }
412 
413   private final class CustomDialog extends Dialog implements DialogInterface {
CustomDialog(Context context)414     public CustomDialog(Context context) {
415       super(context, com.android.systemui.R.style.qs_theme);
416     }
417 
418     @Override
dispatchTouchEvent(MotionEvent ev)419     public boolean dispatchTouchEvent(MotionEvent ev) {
420       rescheduleTimeoutH();
421       return super.dispatchTouchEvent(ev);
422     }
423 
424     @Override
onStart()425     protected void onStart() {
426       super.setCanceledOnTouchOutside(true);
427       super.onStart();
428     }
429 
430     @Override
onStop()431     protected void onStop() {
432       super.onStop();
433     }
434 
435     @Override
onTouchEvent(MotionEvent event)436     public boolean onTouchEvent(MotionEvent event) {
437       if (isShowing()) {
438         if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
439           dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
440           return true;
441         }
442       }
443       return false;
444     }
445   }
446 
447   private final class ExpandIconListener implements View.OnClickListener {
448     @Override
onClick(final View v)449     public void onClick(final View v) {
450       mExpanded = !mExpanded;
451       Animator inAnimator;
452       if (mExpanded) {
453         for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) {
454           // Adding the items which are not coming from the default item.
455           VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
456           if (volumeItem.defaultItem) {
457             // Set progress here due to the progress of seekbar may not be updated.
458             volumeItem.listItem.setProgress(volumeItem.progress);
459           } else {
460             addSeekbarListItem(volumeItem, groupId, 0, null);
461           }
462         }
463         inAnimator = AnimatorInflater.loadAnimator(
464             mContext, R.anim.car_arrow_fade_in_rotate_up);
465       } else {
466         // Only keeping the default stream if it is not expended.
467         Iterator itr = mVolumeLineItems.iterator();
468         while (itr.hasNext()) {
469           SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next();
470           VolumeItem volumeItem = findVolumeItem(seekbarListItem);
471           if (!volumeItem.defaultItem) {
472             itr.remove();
473           } else {
474             // Set progress here due to the progress of seekbar may not be updated.
475             seekbarListItem.setProgress(volumeItem.progress);
476           }
477         }
478         inAnimator = AnimatorInflater.loadAnimator(
479             mContext, R.anim.car_arrow_fade_in_rotate_down);
480       }
481 
482       Animator outAnimator = AnimatorInflater.loadAnimator(
483           mContext, R.anim.car_arrow_fade_out);
484       inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS);
485       AnimatorSet animators = new AnimatorSet();
486       animators.playTogether(outAnimator, inAnimator);
487       animators.setTarget(v);
488       animators.start();
489       mPagedListAdapter.notifyDataSetChanged();
490     }
491   }
492 
493   private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener {
494     private final int mVolumeGroupId;
495     private final CarAudioManager mCarAudioManager;
496 
VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager)497     private VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager) {
498       mVolumeGroupId = volumeGroupId;
499       mCarAudioManager = carAudioManager;
500     }
501 
502     @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)503     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
504       if (!fromUser) {
505         // For instance, if this event is originated from AudioService,
506         // we can ignore it as it has already been handled and doesn't need to be
507         // sent back down again.
508         return;
509       }
510       try {
511         if (mCarAudioManager == null) {
512           Log.w(TAG, "Ignoring volume change event because the car isn't connected");
513           return;
514         }
515         mAvailableVolumeItems.get(mVolumeGroupId).progress = progress;
516         mCarAudioManager.setGroupVolume(mVolumeGroupId, progress, 0);
517       } catch (CarNotConnectedException e) {
518         Log.e(TAG, "Car is not connected!", e);
519       }
520     }
521 
522     @Override
onStartTrackingTouch(SeekBar seekBar)523     public void onStartTrackingTouch(SeekBar seekBar) {}
524 
525     @Override
onStopTrackingTouch(SeekBar seekBar)526     public void onStopTrackingTouch(SeekBar seekBar) {}
527   }
528 
529   private final ICarVolumeCallback mVolumeChangeCallback = new ICarVolumeCallback.Stub() {
530     @Override
531     public void onGroupVolumeChanged(int groupId, int flags) {
532       VolumeItem volumeItem = mAvailableVolumeItems.get(groupId);
533       int value = getSeekbarValue(mCarAudioManager, groupId);
534       // Do not update the progress if it is the same as before. When car audio manager sets its
535       // group volume caused by the seekbar progress changed, it also triggers this callback.
536       // Updating the seekbar at the same time could block the continuous seeking.
537       if (value != volumeItem.progress) {
538         volumeItem.listItem.setProgress(value);
539         volumeItem.progress = value;
540         if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
541           show(Events.SHOW_REASON_VOLUME_CHANGED);
542         }
543       }
544     }
545 
546     @Override
547     public void onMasterMuteChanged(int flags) {
548       // ignored
549     }
550   };
551 
552   private final ServiceConnection mServiceConnection = new ServiceConnection() {
553     @Override
554     public void onServiceConnected(ComponentName name, IBinder service) {
555       try {
556         mExpanded = false;
557         mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE);
558         int volumeGroupCount = mCarAudioManager.getVolumeGroupCount();
559         // Populates volume slider items from volume groups to UI.
560         for (int groupId = 0; groupId < volumeGroupCount; groupId++) {
561           VolumeItem volumeItem = getVolumeItemForUsages(
562               mCarAudioManager.getUsagesForVolumeGroupId(groupId));
563           mAvailableVolumeItems.add(volumeItem);
564           // The first one is the default item.
565           if (groupId == 0) {
566             volumeItem.defaultItem = true;
567             addSeekbarListItem(volumeItem, groupId, R.drawable.car_ic_keyboard_arrow_down,
568                 new ExpandIconListener());
569           }
570         }
571 
572         // If list is already initiated, update its content.
573         if (mPagedListAdapter != null) {
574           mPagedListAdapter.notifyDataSetChanged();
575         }
576         mCarAudioManager.registerVolumeCallback(mVolumeChangeCallback.asBinder());
577       } catch (CarNotConnectedException e) {
578         Log.e(TAG, "Car is not connected!", e);
579       }
580     }
581 
582     /**
583      * This does not get called when service is properly disconnected.
584      * So we need to also handle cleanups in destroy().
585      */
586     @Override
587     public void onServiceDisconnected(ComponentName name) {
588       cleanupAudioManager();
589     }
590   };
591 
592   /**
593    * Wrapper class which contains information of each volume group.
594    */
595   private static class VolumeItem {
596     private @AudioAttributes.AttributeUsage int usage;
597     private int rank;
598     private boolean defaultItem = false;
599     private @DrawableRes int icon;
600     private SeekbarListItem listItem;
601     private int progress;
602   }
603 }
604