diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index d53f1a02b5149..99d65111ec3ea 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -208,6 +208,9 @@ + + + - - - - - - \ No newline at end of file diff --git a/packages/SystemUI/res/values/attrs_car.xml b/packages/SystemUI/res/values/attrs_car.xml index 99d242591e92e..41e078699647b 100644 --- a/packages/SystemUI/res/values/attrs_car.xml +++ b/packages/SystemUI/res/values/attrs_car.xml @@ -63,4 +63,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/xml/car_volume_items.xml b/packages/SystemUI/res/xml/car_volume_items.xml new file mode 100644 index 0000000000000..742dfdda73c82 --- /dev/null +++ b/packages/SystemUI/res/xml/car_volume_items.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java index f1a7183e1602e..927b0e744238e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/CarVolumeDialogImpl.java @@ -22,20 +22,30 @@ import android.animation.AnimatorSet; import android.annotation.Nullable; import android.app.Dialog; import android.app.KeyguardManager; +import android.car.Car; +import android.car.CarNotConnectedException; +import android.car.media.CarAudioManager; +import android.car.media.ICarVolumeCallback; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; +import android.content.ServiceConnection; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; -import android.media.AudioManager; -import android.media.AudioSystem; +import android.media.AudioAttributes; import android.os.Debug; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.util.AttributeSet; import android.util.Log; -import android.util.SparseBooleanArray; +import android.util.SparseArray; +import android.util.Xml; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.MotionEvent; @@ -46,6 +56,7 @@ import android.view.WindowManager; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; +import androidx.annotation.DrawableRes; import androidx.car.widget.ListItem; import androidx.car.widget.ListItemAdapter; import androidx.car.widget.ListItemAdapter.BackgroundStyle; @@ -53,626 +64,533 @@ import androidx.car.widget.ListItemProvider.ListProvider; import androidx.car.widget.PagedListView; import androidx.car.widget.SeekbarListItem; +import java.util.Iterator; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.VolumeDialog; -import com.android.systemui.plugins.VolumeDialogController; -import com.android.systemui.plugins.VolumeDialogController.State; -import com.android.systemui.plugins.VolumeDialogController.StreamState; /** * Car version of the volume dialog. * - * A client of VolumeDialogControllerImpl and its state model. - * * Methods ending in "H" must be called on the (ui) handler. */ public class CarVolumeDialogImpl implements VolumeDialog { - private static final String TAG = Util.logTag(CarVolumeDialogImpl.class); + private static final String TAG = Util.logTag(CarVolumeDialogImpl.class); - private static final long USER_ATTEMPT_GRACE_PERIOD = 1000; + private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems"; + private static final String XML_TAG_VOLUME_ITEM = "item"; + private static final int HOVERING_TIMEOUT = 16000; + private static final int NORMAL_TIMEOUT = 3000; + private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250; + private static final int DISMISS_DELAY_IN_MILLIS = 50; + private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100; - private final Context mContext; - private final H mHandler = new H(); - private final VolumeDialogController mController; - private final AudioManager mAudioManager; + private final Context mContext; + private final H mHandler = new H(); - private Window mWindow; - private CustomDialog mDialog; - private PagedListView mListView; - private ListItemAdapter mPagedListAdapter; - private final List mVolumeLineItems = new ArrayList<>(); - private final List mRows = new ArrayList<>(); - private ConfigurableTexts mConfigurableTexts; - private final SparseBooleanArray mDynamic = new SparseBooleanArray(); - private final KeyguardManager mKeyguard; - private final Object mSafetyWarningLock = new Object(); + private Window mWindow; + private CustomDialog mDialog; + private PagedListView mListView; + private ListItemAdapter mPagedListAdapter; + // All the volume items. + private final SparseArray mVolumeItems = new SparseArray<>(); + // Available volume items in car audio manager. + private final List mAvailableVolumeItems = new ArrayList<>(); + // Volume items in the PagedListView. + private final List mVolumeLineItems = new ArrayList<>(); + private final KeyguardManager mKeyguard; - private boolean mShowing; + private Car mCar; + private CarAudioManager mCarAudioManager; - private boolean mAutomute = VolumePrefs.DEFAULT_ENABLE_AUTOMUTE; - private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE; - private State mState; - private SafetyWarningDialog mSafetyWarning; - private boolean mHovering = false; - private boolean mExpanded; + private boolean mHovering; + private boolean mShowing; + private boolean mExpanded; - public CarVolumeDialogImpl(Context context) { - mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme); - mController = Dependency.get(VolumeDialogController.class); - mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); - mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + public CarVolumeDialogImpl(Context context) { + mContext = new ContextThemeWrapper(context, com.android.systemui.R.style.qs_theme); + mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); + mCar = Car.createCar(mContext, mServiceConnection); + } + + public void init(int windowType, Callback callback) { + initDialog(); + + mCar.connect(); + } + + @Override + public void destroy() { + mHandler.removeCallbacksAndMessages(null); + + cleanupAudioManager(); + // unregisterVolumeCallback is not being called when disconnect car, so we manually cleanup + // audio manager beforehand. + mCar.disconnect(); + } + + private void initDialog() { + loadAudioUsageItems(); + mVolumeLineItems.clear(); + mDialog = new CustomDialog(mContext); + + mHovering = false; + mShowing = false; + mExpanded = false; + mWindow = mDialog.getWindow(); + mWindow.requestFeature(Window.FEATURE_NO_TITLE); + mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); + mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); + mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); + final WindowManager.LayoutParams lp = mWindow.getAttributes(); + lp.format = PixelFormat.TRANSLUCENT; + lp.setTitle(VolumeDialogImpl.class.getSimpleName()); + lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + lp.windowAnimations = -1; + mWindow.setAttributes(lp); + mWindow.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + mDialog.setCanceledOnTouchOutside(true); + mDialog.setContentView(R.layout.car_volume_dialog); + mDialog.setOnShowListener(dialog -> { + mListView.setTranslationY(-mListView.getHeight()); + mListView.setAlpha(0); + mListView.animate() + .alpha(1) + .translationY(0) + .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS) + .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator()) + .start(); + }); + mListView = (PagedListView) mWindow.findViewById(R.id.volume_list); + mListView.setOnHoverListener((v, event) -> { + int action = event.getActionMasked(); + mHovering = (action == MotionEvent.ACTION_HOVER_ENTER) + || (action == MotionEvent.ACTION_HOVER_MOVE); + rescheduleTimeoutH(); + return true; + }); + + mPagedListAdapter = new ListItemAdapter(mContext, new ListProvider(mVolumeLineItems), + BackgroundStyle.PANEL); + mListView.setAdapter(mPagedListAdapter); + mListView.setMaxPages(PagedListView.UNLIMITED_PAGES); + } + + public void show(int reason) { + mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); + } + + public void dismiss(int reason) { + mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); + } + + private void showH(int reason) { + if (D.BUG) { + Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]); } - public void init(int windowType, Callback callback) { - initDialog(); + mHandler.removeMessages(H.SHOW); + mHandler.removeMessages(H.DISMISS); + rescheduleTimeoutH(); + // Refresh the data set before showing. + mPagedListAdapter.notifyDataSetChanged(); + if (mShowing) { + return; + } + mShowing = true; - mController.addCallback(mControllerCallbackH, mHandler); - mController.getState(); + mDialog.show(); + Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); + } + + protected void rescheduleTimeoutH() { + mHandler.removeMessages(H.DISMISS); + final int timeout = computeTimeoutH(); + mHandler.sendMessageDelayed(mHandler + .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); + + if (D.BUG) { + Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); + } + } + + private int computeTimeoutH() { + return mHovering ? HOVERING_TIMEOUT : NORMAL_TIMEOUT; + } + + protected void dismissH(int reason) { + if (D.BUG) { + Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]); + } + + mHandler.removeMessages(H.DISMISS); + mHandler.removeMessages(H.SHOW); + if (!mShowing) { + return; + } + + mListView.animate().cancel(); + mShowing = false; + + mListView.setTranslationY(0); + mListView.setAlpha(1); + mListView.animate() + .alpha(0) + .translationY(-mListView.getHeight()) + .setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS) + .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) + .withEndAction(() -> mHandler.postDelayed(() -> { + if (D.BUG) { + Log.d(TAG, "mDialog.dismiss()"); + } + mDialog.dismiss(); + }, DISMISS_DELAY_IN_MILLIS)) + .start(); + + Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); + } + + public void dump(PrintWriter writer) { + writer.println(VolumeDialogImpl.class.getSimpleName() + " state:"); + writer.print(" mShowing: "); writer.println(mShowing); + } + + private void loadAudioUsageItems() { + try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) { + AttributeSet attrs = Xml.asAttributeSet(parser); + int type; + // Traverse to the first start tag + while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT + && type != XmlResourceParser.START_TAG) { + } + + if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) { + throw new RuntimeException("Meta-data does not start with carVolumeItems tag"); + } + int outerDepth = parser.getDepth(); + int rank = 0; + while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT + && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlResourceParser.END_TAG) { + continue; + } + if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) { + TypedArray item = mContext.getResources().obtainAttributes( + attrs, R.styleable.carVolumeItems_item); + int usage = item.getInt(R.styleable.carVolumeItems_item_usage, -1); + if (usage >= 0) { + VolumeItem volumeItem = new VolumeItem(); + volumeItem.usage = usage; + volumeItem.rank = rank; + volumeItem.icon = item.getResourceId(R.styleable.carVolumeItems_item_icon, 0); + mVolumeItems.put(usage, volumeItem); + rank++; + } + item.recycle(); + } + } + } catch (XmlPullParserException | IOException e) { + Log.e(TAG, "Error parsing volume groups configuration", e); + } + } + + private VolumeItem getVolumeItemForUsages(int[] usages) { + int rank = Integer.MAX_VALUE; + VolumeItem result = null; + for (int usage : usages) { + VolumeItem volumeItem = mVolumeItems.get(usage); + if (volumeItem.rank < rank) { + rank = volumeItem.rank; + result = volumeItem; + } + } + return result; + } + + private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) { + try { + return carAudioManager.getGroupVolume(volumeGroupId); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } + return 0; + } + + private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeGroupId) { + try { + return carAudioManager.getGroupMaxVolume(volumeGroupId); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } + return 0; + } + + private SeekbarListItem addSeekbarListItem(VolumeItem volumeItem, int volumeGroupId, + int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener) { + SeekbarListItem listItem = new SeekbarListItem(mContext); + listItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeGroupId)); + int progress = getSeekbarValue(mCarAudioManager, volumeGroupId); + listItem.setProgress(progress); + listItem.setOnSeekBarChangeListener( + new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeGroupId, mCarAudioManager)); + listItem.setPrimaryActionIcon(mContext.getResources().getDrawable(volumeItem.icon)); + if (supplementalIconId != 0) { + Drawable supplementalIcon = mContext.getResources().getDrawable(supplementalIconId); + listItem.setSupplementalIcon(supplementalIcon, true, + supplementalIconOnClickListener); + } else { + listItem.setSupplementalEmptyIcon(true); + } + + mVolumeLineItems.add(listItem); + volumeItem.listItem = listItem; + volumeItem.progress = progress; + return listItem; + } + + private VolumeItem findVolumeItem(SeekbarListItem targetItem) { + for (int i = 0; i < mVolumeItems.size(); ++i) { + VolumeItem volumeItem = mVolumeItems.valueAt(i); + if (volumeItem.listItem == targetItem) { + return volumeItem; + } + } + return null; + } + + private void cleanupAudioManager() { + try { + mCarAudioManager.unregisterVolumeCallback(mVolumeChangeCallback.asBinder()); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } + mVolumeLineItems.clear(); + mCarAudioManager = null; + } + + private final class H extends Handler { + private static final int SHOW = 1; + private static final int DISMISS = 2; + + public H() { + super(Looper.getMainLooper()); } @Override - public void destroy() { - mController.removeCallback(mControllerCallbackH); - mHandler.removeCallbacksAndMessages(null); + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW: + showH(msg.arg1); + break; + case DISMISS: + dismissH(msg.arg1); + break; + default: + } + } + } + + private final class CustomDialog extends Dialog implements DialogInterface { + public CustomDialog(Context context) { + super(context, com.android.systemui.R.style.qs_theme); } - private void initDialog() { - mRows.clear(); - mVolumeLineItems.clear(); - mDialog = new CustomDialog(mContext); + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + rescheduleTimeoutH(); + return super.dispatchTouchEvent(ev); + } - mConfigurableTexts = new ConfigurableTexts(mContext); - mHovering = false; - mShowing = false; + @Override + protected void onStart() { + super.setCanceledOnTouchOutside(true); + super.onStart(); + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (isShowing()) { + if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); + return true; + } + } + return false; + } + } + + private final class ExpandIconListener implements View.OnClickListener { + @Override + public void onClick(final View v) { + mExpanded = !mExpanded; + Animator inAnimator; + if (mExpanded) { + for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) { + // Adding the items which are not coming from the default item. + VolumeItem volumeItem = mAvailableVolumeItems.get(groupId); + if (volumeItem.defaultItem) { + // Set progress here due to the progress of seekbar may not be updated. + volumeItem.listItem.setProgress(volumeItem.progress); + } else { + addSeekbarListItem(volumeItem, groupId, 0, null); + } + } + inAnimator = AnimatorInflater.loadAnimator( + mContext, R.anim.car_arrow_fade_in_rotate_up); + } else { + // Only keeping the default stream if it is not expended. + Iterator itr = mVolumeLineItems.iterator(); + while (itr.hasNext()) { + SeekbarListItem seekbarListItem = (SeekbarListItem) itr.next(); + VolumeItem volumeItem = findVolumeItem(seekbarListItem); + if (!volumeItem.defaultItem) { + itr.remove(); + } else { + // Set progress here due to the progress of seekbar may not be updated. + seekbarListItem.setProgress(volumeItem.progress); + } + } + inAnimator = AnimatorInflater.loadAnimator( + mContext, R.anim.car_arrow_fade_in_rotate_down); + } + + Animator outAnimator = AnimatorInflater.loadAnimator( + mContext, R.anim.car_arrow_fade_out); + inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS); + AnimatorSet animators = new AnimatorSet(); + animators.playTogether(outAnimator, inAnimator); + animators.setTarget(v); + animators.start(); + mPagedListAdapter.notifyDataSetChanged(); + } + } + + private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { + private final int mVolumeGroupId; + private final CarAudioManager mCarAudioManager; + + private VolumeSeekBarChangeListener(int volumeGroupId, CarAudioManager carAudioManager) { + mVolumeGroupId = volumeGroupId; + mCarAudioManager = carAudioManager; + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + // For instance, if this event is originated from AudioService, + // we can ignore it as it has already been handled and doesn't need to be + // sent back down again. + return; + } + try { + if (mCarAudioManager == null) { + Log.w(TAG, "Ignoring volume change event because the car isn't connected"); + return; + } + mAvailableVolumeItems.get(mVolumeGroupId).progress = progress; + mCarAudioManager.setGroupVolume(mVolumeGroupId, progress, 0); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + } + + private final ICarVolumeCallback mVolumeChangeCallback = new ICarVolumeCallback.Stub() { + @Override + public void onGroupVolumeChanged(int groupId) { + VolumeItem volumeItem = mAvailableVolumeItems.get(groupId); + int value = getSeekbarValue(mCarAudioManager, groupId); + // Do not update the progress if it is the same as before. When car audio manager sets its + // group volume caused by the seekbar progress changed, it also triggers this callback. + // Updating the seekbar at the same time could block the continuous seeking. + if (value != volumeItem.progress) { + volumeItem.listItem.setProgress(value); + volumeItem.progress = value; + show(Events.SHOW_REASON_VOLUME_CHANGED); + } + } + + @Override + public void onMasterMuteChanged() { + // ignored + } + }; + + private final ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + try { mExpanded = false; - mWindow = mDialog.getWindow(); - mWindow.requestFeature(Window.FEATURE_NO_TITLE); - mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND - | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); - mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN - | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH - | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); - mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); - mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); - final WindowManager.LayoutParams lp = mWindow.getAttributes(); - lp.format = PixelFormat.TRANSLUCENT; - lp.setTitle(VolumeDialogImpl.class.getSimpleName()); - lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; - lp.windowAnimations = -1; - mWindow.setAttributes(lp); - mWindow.setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mCarAudioManager = (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE); + int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(); + // Populates volume slider items from volume groups to UI. + for (int groupId = 0; groupId < volumeGroupCount; groupId++) { + VolumeItem volumeItem = getVolumeItemForUsages( + mCarAudioManager.getUsagesForVolumeGroupId(groupId)); + mAvailableVolumeItems.add(volumeItem); + // The first one is the default item. + if (groupId == 0) { + volumeItem.defaultItem = true; + addSeekbarListItem(volumeItem, groupId, R.drawable.car_ic_keyboard_arrow_down, + new ExpandIconListener()); + } + } - mDialog.setCanceledOnTouchOutside(true); - mDialog.setContentView(R.layout.car_volume_dialog); - mDialog.setOnShowListener(dialog -> { - mListView.setTranslationY(-mListView.getHeight()); - mListView.setAlpha(0); - mListView.animate() - .alpha(1) - .translationY(0) - .setDuration(300) - .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator()) - .start(); - }); - mListView = (PagedListView) mWindow.findViewById(R.id.volume_list); - mListView.setOnHoverListener((v, event) -> { - int action = event.getActionMasked(); - mHovering = (action == MotionEvent.ACTION_HOVER_ENTER) - || (action == MotionEvent.ACTION_HOVER_MOVE); - rescheduleTimeoutH(); - return true; - }); - - addSeekbarListItem(addVolumeRow(AudioManager.STREAM_MUSIC, R.drawable.car_ic_music, - R.drawable.car_ic_keyboard_arrow_down, true, true), - new ExpandIconListener()); - // We map AudioManager.STREAM_RING to a navigation icon for demo. - addVolumeRow(AudioManager.STREAM_RING, R.drawable.car_ic_navigation, 0, - true, false); - addVolumeRow(AudioManager.STREAM_NOTIFICATION, R.drawable.car_ic_notification_2, 0, - true, false); - - mPagedListAdapter = new ListItemAdapter(mContext, new ListProvider(mVolumeLineItems), - BackgroundStyle.PANEL); - mListView.setAdapter(mPagedListAdapter); - mListView.setMaxPages(PagedListView.UNLIMITED_PAGES); + // If list is already initiated, update its content. + if (mPagedListAdapter != null) { + mPagedListAdapter.notifyDataSetChanged(); + } + mCarAudioManager.registerVolumeCallback(mVolumeChangeCallback.asBinder()); + } catch (CarNotConnectedException e) { + Log.e(TAG, "Car is not connected!", e); + } } - public void setStreamImportant(int stream, boolean important) { - mHandler.obtainMessage(H.SET_STREAM_IMPORTANT, stream, important ? 1 : 0).sendToTarget(); + /** + * This does not get called when service is properly disconnected. + * So we need to also handle cleanups in destroy(). + */ + @Override + public void onServiceDisconnected(ComponentName name) { + cleanupAudioManager(); } + }; - public void setAutomute(boolean automute) { - if (mAutomute == automute) { - return; - } - mAutomute = automute; - mHandler.sendEmptyMessage(H.RECHECK_ALL); - } - - public void setSilentMode(boolean silentMode) { - if (mSilentMode == silentMode) { - return; - } - mSilentMode = silentMode; - mHandler.sendEmptyMessage(H.RECHECK_ALL); - } - - private VolumeRow addVolumeRow(int stream, int primaryActionIcon, int supplementalIcon, - boolean important, boolean defaultStream) { - VolumeRow volumeRow = new VolumeRow(); - volumeRow.stream = stream; - volumeRow.primaryActionIcon = primaryActionIcon; - volumeRow.supplementalIcon = supplementalIcon; - volumeRow.important = important; - volumeRow.defaultStream = defaultStream; - volumeRow.listItem = null; - mRows.add(volumeRow); - return volumeRow; - } - - private SeekbarListItem addSeekbarListItem( - VolumeRow volumeRow, @Nullable View.OnClickListener supplementalIconOnClickListener) { - int volumeMax = mAudioManager.getStreamMaxVolume(volumeRow.stream); - int currentVolume = mAudioManager.getStreamVolume(volumeRow.stream); - SeekbarListItem listItem = - new SeekbarListItem(mContext, volumeMax, currentVolume, - new VolumeSeekBarChangeListener(volumeRow), null); - Drawable primaryIcon = mContext.getResources().getDrawable(volumeRow.primaryActionIcon); - listItem.setPrimaryActionIcon(primaryIcon); - if (volumeRow.supplementalIcon != 0) { - Drawable supplementalIcon = mContext.getResources() - .getDrawable(volumeRow.supplementalIcon); - listItem.setSupplementalIcon(supplementalIcon, true, - supplementalIconOnClickListener); - } else { - listItem.setSupplementalEmptyIcon(true); - } - - mVolumeLineItems.add(listItem); - volumeRow.listItem = listItem; - - return listItem; - } - - private static int getImpliedLevel(SeekBar seekBar, int progress) { - final int m = seekBar.getMax(); - final int n = m / 100 - 1; - final int level = progress == 0 ? 0 - : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n)); - return level; - } - - public void show(int reason) { - mHandler.obtainMessage(H.SHOW, reason, 0).sendToTarget(); - } - - public void dismiss(int reason) { - mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget(); - } - - private void showH(int reason) { - if (D.BUG) Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]); - mHandler.removeMessages(H.SHOW); - mHandler.removeMessages(H.DISMISS); - rescheduleTimeoutH(); - if (mShowing) return; - mShowing = true; - - mDialog.show(); - Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); - mController.notifyVisible(true); - } - - protected void rescheduleTimeoutH() { - mHandler.removeMessages(H.DISMISS); - final int timeout = computeTimeoutH(); - mHandler.sendMessageDelayed(mHandler - .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT, 0), timeout); - if (D.BUG) Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); - mController.userActivity(); - } - - private int computeTimeoutH() { - if (mHovering) return 16000; - if (mSafetyWarning != null) return 5000; - return 3000; - } - - protected void dismissH(int reason) { - mHandler.removeMessages(H.DISMISS); - mHandler.removeMessages(H.SHOW); - if (!mShowing) return; - mListView.animate().cancel(); - mShowing = false; - - mListView.setTranslationY(0); - mListView.setAlpha(1); - mListView.animate() - .alpha(0) - .translationY(-mListView.getHeight()) - .setDuration(250) - .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()) - .withEndAction(() -> mHandler.postDelayed(() -> { - if (D.BUG) Log.d(TAG, "mDialog.dismiss()"); - mDialog.dismiss(); - }, 50)) - .start(); - - Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason); - mController.notifyVisible(false); - synchronized (mSafetyWarningLock) { - if (mSafetyWarning != null) { - if (D.BUG) Log.d(TAG, "SafetyWarning dismissed"); - mSafetyWarning.dismiss(); - } - } - } - - private void trimObsoleteH() { - int initialVolumeItemSize = mVolumeLineItems.size(); - for (int i = mRows.size() - 1; i >= 0; i--) { - final VolumeRow row = mRows.get(i); - if (row.ss == null || !row.ss.dynamic) continue; - if (!mDynamic.get(row.stream)) { - mRows.remove(i); - mVolumeLineItems.remove(row.listItem); - } - } - - if (mVolumeLineItems.size() != initialVolumeItemSize) { - mPagedListAdapter.notifyDataSetChanged(); - } - } - - private void onStateChangedH(State state) { - mState = state; - mDynamic.clear(); - // add any new dynamic rows - for (int i = 0; i < state.states.size(); i++) { - final int stream = state.states.keyAt(i); - final StreamState ss = state.states.valueAt(i); - if (!ss.dynamic) { - continue; - } - mDynamic.put(stream, true); - if (findRow(stream) == null) { - VolumeRow row = addVolumeRow(stream, R.drawable.ic_volume_remote, - 0, true,false); - if (mExpanded) { - addSeekbarListItem(row, null); - } - } - } - - for (VolumeRow row : mRows) { - updateVolumeRowH(row); - } - } - - private void updateVolumeRowH(VolumeRow row) { - if (D.BUG) Log.d(TAG, "updateVolumeRowH s=" + row.stream); - if (mState == null) { - return; - } - final StreamState ss = mState.states.get(row.stream); - if (ss == null) { - return; - } - row.ss = ss; - if (ss.level == row.requestedLevel) { - row.requestedLevel = -1; - } - // TODO: update Seekbar progress and change the mute icon if necessary. - } - - private VolumeRow findRow(int stream) { - for (VolumeRow row : mRows) { - if (row.stream == stream) { - return row; - } - } - return null; - } - - private VolumeRow findRow(SeekbarListItem targetItem) { - for (VolumeRow row : mRows) { - if (row.listItem == targetItem) { - return row; - } - } - return null; - } - - public void dump(PrintWriter writer) { - writer.println(VolumeDialogImpl.class.getSimpleName() + " state:"); - writer.print(" mShowing: "); writer.println(mShowing); - writer.print(" mDynamic: "); writer.println(mDynamic); - writer.print(" mAutomute: "); writer.println(mAutomute); - writer.print(" mSilentMode: "); writer.println(mSilentMode); - } - - private void recheckH(VolumeRow row) { - if (row == null) { - if (D.BUG) Log.d(TAG, "recheckH ALL"); - trimObsoleteH(); - for (VolumeRow r : mRows) { - updateVolumeRowH(r); - } - } else { - if (D.BUG) Log.d(TAG, "recheckH " + row.stream); - updateVolumeRowH(row); - } - } - - private void setStreamImportantH(int stream, boolean important) { - for (VolumeRow row : mRows) { - if (row.stream == stream) { - row.important = important; - return; - } - } - } - - private void showSafetyWarningH(int flags) { - if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0 - || mShowing) { - synchronized (mSafetyWarningLock) { - if (mSafetyWarning != null) { - return; - } - mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) { - @Override - protected void cleanUp() { - synchronized (mSafetyWarningLock) { - mSafetyWarning = null; - } - recheckH(null); - } - }; - mSafetyWarning.show(); - } - recheckH(null); - } - rescheduleTimeoutH(); - } - - private final VolumeDialogController.Callbacks mControllerCallbackH - = new VolumeDialogController.Callbacks() { - @Override - public void onShowRequested(int reason) { - showH(reason); - } - - @Override - public void onDismissRequested(int reason) { - dismissH(reason); - } - - @Override - public void onScreenOff() { - dismissH(Events.DISMISS_REASON_SCREEN_OFF); - } - - @Override - public void onStateChanged(State state) { - onStateChangedH(state); - } - - @Override - public void onLayoutDirectionChanged(int layoutDirection) { - mListView.setLayoutDirection(layoutDirection); - } - - @Override - public void onConfigurationChanged() { - mDialog.dismiss(); - initDialog(); - mConfigurableTexts.update(); - } - - @Override - public void onShowVibrateHint() { - if (mSilentMode) { - mController.setRingerMode(AudioManager.RINGER_MODE_SILENT, false); - } - } - - @Override - public void onShowSilentHint() { - if (mSilentMode) { - mController.setRingerMode(AudioManager.RINGER_MODE_NORMAL, false); - } - } - - @Override - public void onShowSafetyWarning(int flags) { - showSafetyWarningH(flags); - } - - @Override - public void onAccessibilityModeChanged(Boolean showA11yStream) { - } - }; - - private final class H extends Handler { - private static final int SHOW = 1; - private static final int DISMISS = 2; - private static final int RECHECK = 3; - private static final int RECHECK_ALL = 4; - private static final int SET_STREAM_IMPORTANT = 5; - private static final int RESCHEDULE_TIMEOUT = 6; - private static final int STATE_CHANGED = 7; - - public H() { - super(Looper.getMainLooper()); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case SHOW: showH(msg.arg1); break; - case DISMISS: dismissH(msg.arg1); break; - case RECHECK: recheckH((VolumeRow) msg.obj); break; - case RECHECK_ALL: recheckH(null); break; - case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break; - case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break; - case STATE_CHANGED: onStateChangedH(mState); break; - } - } - } - - private final class CustomDialog extends Dialog implements DialogInterface { - public CustomDialog(Context context) { - super(context, com.android.systemui.R.style.qs_theme); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - rescheduleTimeoutH(); - return super.dispatchTouchEvent(ev); - } - - @Override - protected void onStart() { - super.setCanceledOnTouchOutside(true); - super.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - mHandler.sendEmptyMessage(H.RECHECK_ALL); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (isShowing()) { - if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { - dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE); - return true; - } - } - return false; - } - } - - private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { - private final VolumeRow mRow; - - private VolumeSeekBarChangeListener(VolumeRow volumeRow) { - mRow = volumeRow; - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (mRow.ss == null) { - return; - } - if (D.BUG) { - Log.d(TAG, AudioSystem.streamToString(mRow.stream) - + " onProgressChanged " + progress + " fromUser=" + fromUser); - } - if (!fromUser) { - return; - } - if (mRow.ss.levelMin > 0) { - final int minProgress = mRow.ss.levelMin; - if (progress < minProgress) { - seekBar.setProgress(minProgress); - progress = minProgress; - } - } - final int userLevel = getImpliedLevel(seekBar, progress); - if (mRow.ss.level != userLevel || mRow.ss.muted && userLevel > 0) { - if (mRow.requestedLevel != userLevel) { - mController.setStreamVolume(mRow.stream, userLevel); - mRow.requestedLevel = userLevel; - Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_CHANGED, mRow.stream, - userLevel); - } - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - if (D.BUG) { - Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); - } - mController.setActiveStream(mRow.stream); - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - if (D.BUG) { - Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); - } - final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); - Events.writeEvent(mContext, Events.EVENT_TOUCH_LEVEL_DONE, mRow.stream, userLevel); - if (mRow.ss.level != userLevel) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(H.RECHECK, mRow), - USER_ATTEMPT_GRACE_PERIOD); - } - } - } - - private final class ExpandIconListener implements View.OnClickListener { - @Override - public void onClick(final View v) { - mExpanded = !mExpanded; - Animator inAnimator; - if (mExpanded) { - for (VolumeRow row : mRows) { - // Adding the items which are not coming from default stream. - if (!row.defaultStream) { - addSeekbarListItem(row, null); - } - } - inAnimator = AnimatorInflater.loadAnimator( - mContext, R.anim.car_arrow_fade_in_rotate_up); - } else { - // Only keeping the default stream if it is not expended. - Iterator itr = mVolumeLineItems.iterator(); - while (itr.hasNext()) { - SeekbarListItem item = (SeekbarListItem) itr.next(); - VolumeRow row = findRow(item); - if (!row.defaultStream) { - itr.remove(); - } - } - inAnimator = AnimatorInflater.loadAnimator( - mContext, R.anim.car_arrow_fade_in_rotate_down); - } - - Animator outAnimator = AnimatorInflater.loadAnimator( - mContext, R.anim.car_arrow_fade_out); - inAnimator.setStartDelay(100); - AnimatorSet animators = new AnimatorSet(); - animators.playTogether(outAnimator, inAnimator); - animators.setTarget(v); - animators.start(); - mPagedListAdapter.notifyDataSetChanged(); - } - } - - private static class VolumeRow { - private int stream; - private StreamState ss; - private boolean important; - private boolean defaultStream; - private int primaryActionIcon; - private int supplementalIcon; - private SeekbarListItem listItem; - private int requestedLevel = -1; // pending user-requested level via progress changed - } -} \ No newline at end of file + /** + * Wrapper class which contains information of each volume group. + */ + private static class VolumeItem { + private @AudioAttributes.AttributeUsage int usage; + private int rank; + private boolean defaultItem = false; + private @DrawableRes int icon; + private SeekbarListItem listItem; + private int progress; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java index 6e5b5484cabea..dd552646955a8 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java @@ -103,11 +103,7 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna } private VolumeDialog createCarDefault() { - CarVolumeDialogImpl impl = new CarVolumeDialogImpl(mContext); - impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); - impl.setAutomute(true); - impl.setSilentMode(false); - return impl; + return new CarVolumeDialogImpl(mContext); } @Override