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