1 /* 2 * Copyright (C) 2021 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.car.volume; 18 19 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_EVENTS; 20 import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING; 21 import static android.car.media.CarAudioManager.INVALID_AUDIO_ZONE; 22 import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE; 23 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_MUTE_CHANGED; 24 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED; 25 import static android.car.media.CarVolumeGroupEvent.EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED; 26 import static android.car.media.CarVolumeGroupEvent.EXTRA_INFO_SHOW_UI; 27 import static android.car.media.CarVolumeGroupEvent.EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorInflater; 31 import android.animation.AnimatorSet; 32 import android.animation.ObjectAnimator; 33 import android.animation.PropertyValuesHolder; 34 import android.annotation.DrawableRes; 35 import android.annotation.Nullable; 36 import android.app.Dialog; 37 import android.app.KeyguardManager; 38 import android.car.Car; 39 import android.car.CarOccupantZoneManager; 40 import android.car.media.CarAudioManager; 41 import android.car.media.CarVolumeGroupEvent; 42 import android.car.media.CarVolumeGroupEventCallback; 43 import android.car.media.CarVolumeGroupInfo; 44 import android.content.BroadcastReceiver; 45 import android.content.Context; 46 import android.content.DialogInterface; 47 import android.content.Intent; 48 import android.content.IntentFilter; 49 import android.content.res.Configuration; 50 import android.content.res.TypedArray; 51 import android.content.res.XmlResourceParser; 52 import android.graphics.Color; 53 import android.graphics.PixelFormat; 54 import android.graphics.drawable.ColorDrawable; 55 import android.graphics.drawable.Drawable; 56 import android.os.Build; 57 import android.os.Debug; 58 import android.os.Handler; 59 import android.os.Looper; 60 import android.os.Message; 61 import android.util.AttributeSet; 62 import android.util.Log; 63 import android.util.SparseArray; 64 import android.util.Xml; 65 import android.view.Gravity; 66 import android.view.MotionEvent; 67 import android.view.View; 68 import android.view.ViewGroup; 69 import android.view.Window; 70 import android.view.WindowManager; 71 import android.widget.SeekBar; 72 import android.widget.SeekBar.OnSeekBarChangeListener; 73 74 import androidx.recyclerview.widget.LinearLayoutManager; 75 import androidx.recyclerview.widget.RecyclerView; 76 77 import com.android.systemui.R; 78 import com.android.systemui.car.CarServiceProvider; 79 import com.android.systemui.plugins.VolumeDialog; 80 import com.android.systemui.plugins.VolumeDialogController; 81 import com.android.systemui.settings.UserTracker; 82 import com.android.systemui.statusbar.policy.ConfigurationController; 83 import com.android.systemui.volume.Events; 84 import com.android.systemui.volume.SystemUIInterpolators; 85 import com.android.systemui.volume.VolumeDialogImpl; 86 87 import org.xmlpull.v1.XmlPullParserException; 88 89 import java.io.IOException; 90 import java.util.ArrayList; 91 import java.util.List; 92 import java.util.concurrent.Executor; 93 94 /** 95 * Car version of the volume dialog. 96 * 97 * Methods ending in "H" must be called on the (ui) handler. 98 */ 99 public class CarVolumeDialogImpl 100 implements VolumeDialog, ConfigurationController.ConfigurationListener { 101 102 private static final String TAG = "CarVolumeDialog"; 103 private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG; 104 105 private static final String XML_TAG_VOLUME_ITEMS = "carVolumeItems"; 106 private static final String XML_TAG_VOLUME_ITEM = "item"; 107 private static final int LISTVIEW_ANIMATION_DURATION_IN_MILLIS = 250; 108 private static final int DISMISS_DELAY_IN_MILLIS = 50; 109 private static final int ARROW_FADE_IN_START_DELAY_IN_MILLIS = 100; 110 private static final int INVALID_INDEX = -1; 111 112 private final Context mContext; 113 private final H mHandler = new H(); 114 // All the volume items. 115 private final SparseArray<VolumeItem> mVolumeItems = new SparseArray<>(); 116 // Available volume items in car audio manager. 117 private final List<VolumeItem> mAvailableVolumeItems = new ArrayList<>(); 118 // Volume items in the RecyclerView. 119 private final List<CarVolumeItem> mCarVolumeLineItems = new ArrayList<>(); 120 private final KeyguardManager mKeyguard; 121 private final int mNormalTimeout; 122 private final int mHoveringTimeout; 123 private final int mExpNormalTimeout; 124 private final int mExpHoveringTimeout; 125 private final CarServiceProvider mCarServiceProvider; 126 private final VolumeDialogController mController; 127 private final ConfigurationController mConfigurationController; 128 private final UserTracker mUserTracker; 129 private final Executor mExecutor; 130 131 private Window mWindow; 132 private CustomDialog mDialog; 133 private RecyclerView mListView; 134 private CarVolumeItemAdapter mVolumeItemsAdapter; 135 private CarAudioManager mCarAudioManager; 136 private int mAudioZoneId = INVALID_AUDIO_ZONE; 137 private boolean mHovering; 138 private int mCurrentlyDisplayingGroupId; 139 private int mPreviouslyDisplayingGroupId; 140 private boolean mDismissing; 141 private boolean mExpanded; 142 private View mExpandIcon; 143 private boolean mHomeButtonPressedBroadcastReceiverRegistered; 144 private boolean mIsUiModeNight; 145 146 private final CarAudioManager.CarVolumeCallback mVolumeChangeCallback = 147 new CarAudioManager.CarVolumeCallback() { 148 @Override 149 public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { 150 updateVolumeAndMute(zoneId, groupId, flags, 151 EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED); 152 } 153 154 @Override 155 public void onMasterMuteChanged(int zoneId, int flags) { 156 // ignored 157 } 158 159 @Override 160 public void onGroupMuteChanged(int zoneId, int groupId, int flags) { 161 updateVolumeAndMute(zoneId, groupId, flags, EVENT_TYPE_MUTE_CHANGED); 162 } 163 164 private void updateVolumeAndMute(int zoneId, int groupId, int flags, 165 int eventTypes) { 166 if (zoneId != mAudioZoneId) { 167 return; 168 } 169 List<Integer> extraInfos = CarVolumeGroupEvent.convertFlagsToExtraInfo(flags, 170 eventTypes); 171 if (mCarAudioManager != null) { 172 CarVolumeGroupInfo carVolumeGroupInfo = 173 mCarAudioManager.getVolumeGroupInfo(zoneId, groupId); 174 boolean isMuted; 175 int currentIndex; 176 int maxIndex = INVALID_INDEX; 177 if (carVolumeGroupInfo != null) { 178 isMuted = carVolumeGroupInfo.isMuted(); 179 maxIndex = carVolumeGroupInfo.getMaxVolumeGainIndex(); 180 currentIndex = carVolumeGroupInfo.getVolumeGainIndex(); 181 } else { 182 isMuted = isGroupMuted(mCarAudioManager, zoneId, groupId); 183 currentIndex = getSeekbarValue(mCarAudioManager, zoneId, groupId); 184 } 185 updateVolumePreference(groupId, maxIndex, currentIndex, isMuted, eventTypes, 186 extraInfos); 187 } 188 } 189 }; 190 191 private final CarVolumeGroupEventCallback mCarVolumeGroupEventCallback = 192 new CarVolumeGroupEventCallback() { 193 @Override 194 public void onVolumeGroupEvent(List<CarVolumeGroupEvent> volumeGroupEvents) { 195 updateVolumeGroupForEvents(volumeGroupEvents); 196 } 197 }; 198 199 private final CarServiceProvider.CarServiceOnConnectedListener mCarServiceOnConnectedListener = 200 new CarServiceProvider.CarServiceOnConnectedListener() { 201 @Override 202 public void onConnected(Car car) { 203 mExpanded = false; 204 CarOccupantZoneManager carOccupantZoneManager = 205 (CarOccupantZoneManager) car.getCarManager( 206 Car.CAR_OCCUPANT_ZONE_SERVICE); 207 if (carOccupantZoneManager != null) { 208 CarOccupantZoneManager.OccupantZoneInfo info = 209 carOccupantZoneManager.getOccupantZoneForUser( 210 mUserTracker.getUserHandle()); 211 if (info != null) { 212 mAudioZoneId = carOccupantZoneManager.getAudioZoneIdForOccupant(info); 213 } 214 } 215 if (mAudioZoneId == INVALID_AUDIO_ZONE) { 216 // No audio zone found in occupant zone mapping - default to primary zone 217 mAudioZoneId = PRIMARY_AUDIO_ZONE; 218 } 219 mCarAudioManager = (CarAudioManager) car.getCarManager(Car.AUDIO_SERVICE); 220 if (mCarAudioManager != null) { 221 int volumeGroupCount = mCarAudioManager.getVolumeGroupCount(mAudioZoneId); 222 List<VolumeItem> availableVolumeItems = new ArrayList<>(); 223 // Populates volume slider items from volume groups to UI. 224 for (int groupId = 0; groupId < volumeGroupCount; groupId++) { 225 VolumeItem volumeItem = getVolumeItemForUsages( 226 mCarAudioManager.getUsagesForVolumeGroupId(mAudioZoneId, 227 groupId)); 228 availableVolumeItems.add(volumeItem); 229 } 230 mAvailableVolumeItems.clear(); 231 mAvailableVolumeItems.addAll(availableVolumeItems); 232 // The first one is the default item. 233 clearAllAndSetupDefaultCarVolumeLineItem(0); 234 235 // If list is already initiated, update its content. 236 if (mVolumeItemsAdapter != null) { 237 mVolumeItemsAdapter.notifyDataSetChanged(); 238 } 239 240 // if volume group events are enabled, use it. Else fallback to the legacy 241 // volume group callbacks. 242 if (mCarAudioManager.isAudioFeatureEnabled( 243 AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) { 244 mCarAudioManager.registerCarVolumeGroupEventCallback(mExecutor, 245 mCarVolumeGroupEventCallback); 246 } else { 247 mCarAudioManager.registerCarVolumeCallback(mVolumeChangeCallback); 248 } 249 } 250 } 251 }; 252 253 private final BroadcastReceiver mHomeButtonPressedBroadcastReceiver = new BroadcastReceiver() { 254 @Override 255 public void onReceive(Context context, Intent intent) { 256 if (!intent.getAction().equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) { 257 return; 258 } 259 260 dismissH(Events.DISMISS_REASON_VOLUME_CONTROLLER); 261 } 262 }; 263 264 private final UserTracker.Callback mUserTrackerCallback = new UserTracker.Callback() { 265 @Override 266 public void onUserChanged(int newUser, Context userContext) { 267 if (mHomeButtonPressedBroadcastReceiverRegistered) { 268 mContext.unregisterReceiver(mHomeButtonPressedBroadcastReceiver); 269 mContext.registerReceiverAsUser(mHomeButtonPressedBroadcastReceiver, 270 mUserTracker.getUserHandle(), 271 new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), 272 /* broadcastPermission= */ null, /* scheduler= */ null, 273 Context.RECEIVER_EXPORTED); 274 } 275 } 276 }; 277 CarVolumeDialogImpl( Context context, CarServiceProvider carServiceProvider, VolumeDialogController volumeDialogController, ConfigurationController configurationController, UserTracker userTracker)278 public CarVolumeDialogImpl( 279 Context context, 280 CarServiceProvider carServiceProvider, 281 VolumeDialogController volumeDialogController, 282 ConfigurationController configurationController, 283 UserTracker userTracker) { 284 mContext = context; 285 mCarServiceProvider = carServiceProvider; 286 mUserTracker = userTracker; 287 mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); 288 mNormalTimeout = mContext.getResources().getInteger( 289 R.integer.car_volume_dialog_display_normal_timeout); 290 mHoveringTimeout = mContext.getResources().getInteger( 291 R.integer.car_volume_dialog_display_hovering_timeout); 292 mExpNormalTimeout = mContext.getResources().getInteger( 293 R.integer.car_volume_dialog_display_expanded_normal_timeout); 294 mExpHoveringTimeout = mContext.getResources().getInteger( 295 R.integer.car_volume_dialog_display_expanded_hovering_timeout); 296 mController = volumeDialogController; 297 mConfigurationController = configurationController; 298 mIsUiModeNight = mContext.getResources().getConfiguration().isNightModeActive(); 299 mExecutor = context.getMainExecutor(); 300 } 301 getSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)302 private static int getSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, 303 int volumeGroupId) { 304 return carAudioManager.getGroupVolume(volumeZoneId, volumeGroupId); 305 } 306 isGroupMuted(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)307 private static boolean isGroupMuted(CarAudioManager carAudioManager, int volumeZoneId, 308 int volumeGroupId) { 309 if (!carAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING)) { 310 return false; 311 } 312 return carAudioManager.isVolumeGroupMuted(volumeZoneId, volumeGroupId); 313 } 314 getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, int volumeGroupId)315 private static int getMaxSeekbarValue(CarAudioManager carAudioManager, int volumeZoneId, 316 int volumeGroupId) { 317 return carAudioManager.getGroupMaxVolume(volumeZoneId, volumeGroupId); 318 } 319 320 /** 321 * Build the volume window and connect to the CarService which registers with car audio 322 * manager. 323 */ 324 @Override init(int windowType, Callback callback)325 public void init(int windowType, Callback callback) { 326 initDialog(); 327 328 // The VolumeDialog is not initialized until the first volume change for a particular zone 329 // (to improve boot time by deferring initialization). Therefore, the dialog should be shown 330 // on init to handle the first audio change. 331 mHandler.obtainMessage(H.SHOW, Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget(); 332 333 mCarServiceProvider.addListener(mCarServiceOnConnectedListener); 334 mContext.registerReceiverAsUser(mHomeButtonPressedBroadcastReceiver, 335 mUserTracker.getUserHandle(), new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), 336 /* broadcastPermission= */ null, /* scheduler= */ null, Context.RECEIVER_EXPORTED); 337 mHomeButtonPressedBroadcastReceiverRegistered = true; 338 mUserTracker.addCallback(mUserTrackerCallback, mContext.getMainExecutor()); 339 mConfigurationController.addCallback(this); 340 } 341 342 @Override destroy()343 public void destroy() { 344 mController.notifyVisible(false); 345 mHandler.removeCallbacksAndMessages(/* token= */ null); 346 347 mUserTracker.removeCallback(mUserTrackerCallback); 348 mContext.unregisterReceiver(mHomeButtonPressedBroadcastReceiver); 349 mHomeButtonPressedBroadcastReceiverRegistered = false; 350 351 cleanupAudioManager(); 352 mConfigurationController.removeCallback(this); 353 } 354 355 @Override onLayoutDirectionChanged(boolean isLayoutRtl)356 public void onLayoutDirectionChanged(boolean isLayoutRtl) { 357 if (mListView != null) { 358 mListView.setLayoutDirection( 359 isLayoutRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); 360 } 361 } 362 363 @Override onConfigChanged(Configuration newConfig)364 public void onConfigChanged(Configuration newConfig) { 365 ConfigurationController.ConfigurationListener.super.onConfigChanged(newConfig); 366 boolean isConfigNightMode = newConfig.isNightModeActive(); 367 368 if (isConfigNightMode != mIsUiModeNight) { 369 mIsUiModeNight = isConfigNightMode; 370 // Call notifyDataSetChanged to force trigger the mVolumeItemsAdapter#onBindViewHolder 371 // and reset items background color. notify() or invalidate() don't work here. 372 mVolumeItemsAdapter.notifyDataSetChanged(); 373 } 374 } 375 376 /** 377 * Reveals volume dialog. 378 */ show(int reason)379 public void show(int reason) { 380 mHandler.obtainMessage(H.SHOW, reason).sendToTarget(); 381 } 382 383 /** 384 * Hides volume dialog. 385 */ dismiss(int reason)386 public void dismiss(int reason) { 387 mHandler.obtainMessage(H.DISMISS, reason).sendToTarget(); 388 } 389 initDialog()390 private void initDialog() { 391 loadAudioUsageItems(); 392 mCarVolumeLineItems.clear(); 393 mDialog = new CustomDialog(mContext); 394 395 mHovering = false; 396 mDismissing = false; 397 mExpanded = false; 398 mWindow = mDialog.getWindow(); 399 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 400 mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 401 mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND 402 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); 403 mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 404 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 405 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 406 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 407 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 408 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); 409 mWindow.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); 410 mWindow.setWindowAnimations(com.android.internal.R.style.Animation_Toast); 411 final WindowManager.LayoutParams lp = mWindow.getAttributes(); 412 lp.format = PixelFormat.TRANSLUCENT; 413 lp.setTitle(VolumeDialogImpl.class.getSimpleName()); 414 lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; 415 lp.windowAnimations = -1; 416 mWindow.setAttributes(lp); 417 418 mDialog.setContentView(R.layout.car_volume_dialog); 419 mWindow.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 420 421 mDialog.setCanceledOnTouchOutside(true); 422 mDialog.setOnShowListener(dialog -> { 423 mListView.setTranslationY(-mListView.getHeight()); 424 mListView.setAlpha(0); 425 PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat(View.ALPHA, 1f); 426 PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f); 427 ObjectAnimator showAnimator = ObjectAnimator.ofPropertyValuesHolder(mListView, pvhAlpha, 428 pvhY); 429 showAnimator.setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS); 430 showAnimator.setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator()); 431 showAnimator.start(); 432 }); 433 mListView = mWindow.findViewById(R.id.volume_list); 434 mListView.setOnHoverListener((v, event) -> { 435 int action = event.getActionMasked(); 436 mHovering = (action == MotionEvent.ACTION_HOVER_ENTER) 437 || (action == MotionEvent.ACTION_HOVER_MOVE); 438 rescheduleTimeoutH(); 439 return true; 440 }); 441 442 mVolumeItemsAdapter = new CarVolumeItemAdapter(mContext, mCarVolumeLineItems); 443 mListView.setAdapter(mVolumeItemsAdapter); 444 mListView.setLayoutManager(new LinearLayoutManager(mContext)); 445 } 446 447 showH(int reason)448 private void showH(int reason) { 449 if (mCarAudioManager == null) { 450 Log.w(TAG, "cannot show dialog - car audio manager is null"); 451 return; 452 } 453 454 if (DEBUG) { 455 Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]); 456 } 457 458 mHandler.removeMessages(H.SHOW); 459 mHandler.removeMessages(H.DISMISS); 460 461 rescheduleTimeoutH(); 462 463 // Refresh the data set before showing. 464 mVolumeItemsAdapter.notifyDataSetChanged(); 465 466 if (mDialog.isShowing()) { 467 if (mPreviouslyDisplayingGroupId == mCurrentlyDisplayingGroupId || mExpanded) { 468 return; 469 } 470 471 clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId); 472 return; 473 } 474 475 clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId); 476 mDismissing = false; 477 mDialog.show(); 478 mController.notifyVisible(true); 479 Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked()); 480 } 481 clearAllAndSetupDefaultCarVolumeLineItem(int groupId)482 private void clearAllAndSetupDefaultCarVolumeLineItem(int groupId) { 483 mCarVolumeLineItems.clear(); 484 if (groupId >= mAvailableVolumeItems.size()) { 485 Log.w(TAG, "group id not in available volume items"); 486 return; 487 } 488 VolumeItem volumeItem = mAvailableVolumeItems.get(groupId); 489 volumeItem.mDefaultItem = true; 490 addCarVolumeListItem(volumeItem, mAudioZoneId, /* volumeGroupId = */ groupId, 491 R.drawable.car_ic_keyboard_arrow_down, new ExpandIconListener()); 492 } 493 rescheduleTimeoutH()494 protected void rescheduleTimeoutH() { 495 mHandler.removeMessages(H.DISMISS); 496 final int timeout = computeTimeoutH(); 497 mHandler.sendMessageDelayed(mHandler 498 .obtainMessage(H.DISMISS, Events.DISMISS_REASON_TIMEOUT), timeout); 499 500 if (DEBUG) { 501 Log.d(TAG, "rescheduleTimeout " + timeout + " " + Debug.getCaller()); 502 } 503 } 504 computeTimeoutH()505 private int computeTimeoutH() { 506 if (mExpanded) { 507 return mHovering ? mExpHoveringTimeout : mExpNormalTimeout; 508 } else { 509 return mHovering ? mHoveringTimeout : mNormalTimeout; 510 } 511 } 512 dismissH(int reason)513 private void dismissH(int reason) { 514 if (DEBUG) { 515 Log.d(TAG, "dismissH r=" + Events.DISMISS_REASONS[reason]); 516 } 517 518 mHandler.removeMessages(H.DISMISS); 519 mHandler.removeMessages(H.SHOW); 520 if (!mDialog.isShowing() || mDismissing) { 521 return; 522 } 523 524 PropertyValuesHolder pvhAlpha = PropertyValuesHolder.ofFloat(View.ALPHA, 0f); 525 PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 526 (float) -mListView.getHeight()); 527 ObjectAnimator dismissAnimator = ObjectAnimator.ofPropertyValuesHolder(mListView, pvhAlpha, 528 pvhY); 529 dismissAnimator.setDuration(LISTVIEW_ANIMATION_DURATION_IN_MILLIS); 530 dismissAnimator.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator()); 531 dismissAnimator.addListener(new DismissAnimationListener()); 532 dismissAnimator.start(); 533 534 Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason); 535 } 536 loadAudioUsageItems()537 private void loadAudioUsageItems() { 538 if (DEBUG) { 539 Log.i(TAG, "loadAudioUsageItems start"); 540 } 541 542 try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.car_volume_items)) { 543 AttributeSet attrs = Xml.asAttributeSet(parser); 544 int type; 545 // Traverse to the first start tag 546 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 547 && type != XmlResourceParser.START_TAG) { 548 // Do Nothing (moving parser to start element) 549 } 550 551 if (!XML_TAG_VOLUME_ITEMS.equals(parser.getName())) { 552 throw new RuntimeException("Meta-data does not start with carVolumeItems tag"); 553 } 554 int outerDepth = parser.getDepth(); 555 int rank = 0; 556 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 557 && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) { 558 if (type == XmlResourceParser.END_TAG) { 559 continue; 560 } 561 if (XML_TAG_VOLUME_ITEM.equals(parser.getName())) { 562 TypedArray item = mContext.getResources().obtainAttributes( 563 attrs, R.styleable.carVolumeItems_item); 564 int usage = item.getInt(R.styleable.carVolumeItems_item_usage, 565 /* defValue= */ -1); 566 if (usage >= 0) { 567 VolumeItem volumeItem = new VolumeItem(); 568 volumeItem.mRank = rank; 569 volumeItem.mIcon = item.getResourceId( 570 R.styleable.carVolumeItems_item_icon, /* defValue= */ 0); 571 volumeItem.mMuteIcon = item.getResourceId( 572 R.styleable.carVolumeItems_item_mute_icon, /* defValue= */ 0); 573 mVolumeItems.put(usage, volumeItem); 574 rank++; 575 } 576 item.recycle(); 577 } 578 } 579 } catch (XmlPullParserException | IOException e) { 580 Log.e(TAG, "Error parsing volume groups configuration", e); 581 } 582 583 if (DEBUG) { 584 Log.i(TAG, 585 "loadAudioUsageItems finished. Number of volume items: " + mVolumeItems.size()); 586 } 587 } 588 getVolumeItemForUsages(int[] usages)589 private VolumeItem getVolumeItemForUsages(int[] usages) { 590 int rank = Integer.MAX_VALUE; 591 VolumeItem result = null; 592 for (int usage : usages) { 593 VolumeItem volumeItem = mVolumeItems.get(usage); 594 if (DEBUG) { 595 Log.i(TAG, "getVolumeItemForUsage: " + usage + ": " + volumeItem); 596 } 597 if (volumeItem.mRank < rank) { 598 rank = volumeItem.mRank; 599 result = volumeItem; 600 } 601 } 602 return result; 603 } 604 createCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, int volumeGroupId, Drawable supplementalIcon, int seekbarProgressValue, boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener)605 private CarVolumeItem createCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, 606 int volumeGroupId, Drawable supplementalIcon, int seekbarProgressValue, 607 boolean isMuted, @Nullable View.OnClickListener supplementalIconOnClickListener) { 608 CarVolumeItem carVolumeItem = new CarVolumeItem(); 609 carVolumeItem.setMax(getMaxSeekbarValue(mCarAudioManager, volumeZoneId, volumeGroupId)); 610 carVolumeItem.setProgress(seekbarProgressValue); 611 carVolumeItem.setIsMuted(isMuted); 612 carVolumeItem.setOnSeekBarChangeListener( 613 new CarVolumeDialogImpl.VolumeSeekBarChangeListener(volumeZoneId, volumeGroupId, 614 mCarAudioManager)); 615 carVolumeItem.setGroupId(volumeGroupId); 616 617 int color = mContext.getResources().getColor(R.color.car_volume_dialog_tint, 618 mContext.getTheme()); 619 Drawable primaryIcon = mContext.getDrawable(volumeItem.mIcon); 620 primaryIcon.mutate().setTint(color); 621 carVolumeItem.setPrimaryIcon(primaryIcon); 622 623 Drawable primaryMuteIcon = mContext.getDrawable(volumeItem.mMuteIcon); 624 primaryMuteIcon.mutate().setTint(color); 625 carVolumeItem.setPrimaryMuteIcon(primaryMuteIcon); 626 627 if (supplementalIcon != null) { 628 supplementalIcon.mutate().setTint(color); 629 carVolumeItem.setSupplementalIcon(supplementalIcon, 630 /* showSupplementalIconDivider= */ true); 631 carVolumeItem.setSupplementalIconListener(supplementalIconOnClickListener); 632 } else { 633 carVolumeItem.setSupplementalIcon(/* drawable= */ null, 634 /* showSupplementalIconDivider= */ false); 635 } 636 637 volumeItem.mCarVolumeItem = carVolumeItem; 638 volumeItem.mProgress = seekbarProgressValue; 639 640 return carVolumeItem; 641 } 642 addCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, int volumeGroupId, int supplementalIconId, @Nullable View.OnClickListener supplementalIconOnClickListener)643 private CarVolumeItem addCarVolumeListItem(VolumeItem volumeItem, int volumeZoneId, 644 int volumeGroupId, int supplementalIconId, 645 @Nullable View.OnClickListener supplementalIconOnClickListener) { 646 int seekbarProgressValue = getSeekbarValue(mCarAudioManager, volumeZoneId, volumeGroupId); 647 boolean isMuted = isGroupMuted(mCarAudioManager, volumeZoneId, volumeGroupId); 648 Drawable supplementalIcon = supplementalIconId == 0 ? null : mContext.getDrawable( 649 supplementalIconId); 650 CarVolumeItem carVolumeItem = createCarVolumeListItem(volumeItem, volumeZoneId, 651 volumeGroupId, supplementalIcon, seekbarProgressValue, isMuted, 652 supplementalIconOnClickListener); 653 mCarVolumeLineItems.add(carVolumeItem); 654 return carVolumeItem; 655 } 656 cleanupAudioManager()657 private void cleanupAudioManager() { 658 if (mCarAudioManager != null) { 659 if (mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_EVENTS)) { 660 mCarAudioManager.unregisterCarVolumeGroupEventCallback( 661 mCarVolumeGroupEventCallback); 662 } else { 663 mCarAudioManager.unregisterCarVolumeCallback(mVolumeChangeCallback); 664 } 665 mCarAudioManager = null; 666 } 667 mCarVolumeLineItems.clear(); 668 } 669 670 /** 671 * Wrapper class which contains information of each volume group. 672 */ 673 private static class VolumeItem { 674 private int mRank; 675 private boolean mDefaultItem = false; 676 @DrawableRes 677 private int mIcon; 678 @DrawableRes 679 private int mMuteIcon; 680 private CarVolumeItem mCarVolumeItem; 681 private int mProgress; 682 private boolean mIsMuted; 683 } 684 685 private final class H extends Handler { 686 687 private static final int SHOW = 1; 688 private static final int DISMISS = 2; 689 H()690 private H() { 691 super(Looper.getMainLooper()); 692 } 693 694 @Override handleMessage(Message msg)695 public void handleMessage(Message msg) { 696 switch (msg.what) { 697 case SHOW: 698 showH(msg.arg1); 699 break; 700 case DISMISS: 701 dismissH(msg.arg1); 702 break; 703 default: 704 } 705 } 706 } 707 708 private final class CustomDialog extends Dialog implements DialogInterface { 709 CustomDialog(Context context)710 private CustomDialog(Context context) { 711 super(context, com.android.systemui.R.style.Theme_SystemUI); 712 } 713 714 @Override dispatchTouchEvent(MotionEvent ev)715 public boolean dispatchTouchEvent(MotionEvent ev) { 716 rescheduleTimeoutH(); 717 return super.dispatchTouchEvent(ev); 718 } 719 720 @Override onStart()721 protected void onStart() { 722 super.setCanceledOnTouchOutside(true); 723 super.onStart(); 724 } 725 726 @Override onStop()727 protected void onStop() { 728 super.onStop(); 729 } 730 731 @Override onTouchEvent(MotionEvent event)732 public boolean onTouchEvent(MotionEvent event) { 733 if (isShowing()) { 734 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 735 mHandler.obtainMessage( 736 H.DISMISS, Events.DISMISS_REASON_TOUCH_OUTSIDE).sendToTarget(); 737 return true; 738 } 739 } 740 return false; 741 } 742 } 743 744 private final class DismissAnimationListener implements Animator.AnimatorListener { 745 @Override onAnimationStart(Animator animation)746 public void onAnimationStart(Animator animation) { 747 mDismissing = true; 748 } 749 750 @Override onAnimationEnd(Animator animation)751 public void onAnimationEnd(Animator animation) { 752 mHandler.postDelayed(() -> { 753 if (DEBUG) { 754 Log.d(TAG, "mDialog.dismiss()"); 755 } 756 mDialog.dismiss(); 757 mDismissing = false; 758 // if mExpandIcon is null that means user never clicked on the expanded arrow 759 // which implies that the dialog is still not expanded. In that case we do 760 // not want to reset the state 761 if (mExpandIcon != null && mExpanded) { 762 toggleDialogExpansion(/* isClicked = */ false); 763 } 764 }, DISMISS_DELAY_IN_MILLIS); 765 } 766 767 @Override onAnimationCancel(Animator animation)768 public void onAnimationCancel(Animator animation) { 769 // A canceled animation will also call onAnimationEnd so any necessary cleanup will 770 // already happen there 771 if (DEBUG) { 772 Log.d(TAG, "dismiss animation canceled"); 773 } 774 } 775 776 @Override onAnimationRepeat(Animator animation)777 public void onAnimationRepeat(Animator animation) { 778 // no-op 779 } 780 } 781 782 private final class ExpandIconListener implements View.OnClickListener { 783 @Override onClick(final View v)784 public void onClick(final View v) { 785 mExpandIcon = v; 786 toggleDialogExpansion(true); 787 rescheduleTimeoutH(); 788 } 789 } 790 toggleDialogExpansion(boolean isClicked)791 private void toggleDialogExpansion(boolean isClicked) { 792 mExpanded = !mExpanded; 793 Animator inAnimator; 794 if (mExpanded) { 795 for (int groupId = 0; groupId < mAvailableVolumeItems.size(); ++groupId) { 796 if (groupId != mCurrentlyDisplayingGroupId) { 797 VolumeItem volumeItem = mAvailableVolumeItems.get(groupId); 798 addCarVolumeListItem(volumeItem, mAudioZoneId, groupId, 799 /* supplementalIconId= */ 0, 800 /* supplementalIconOnClickListener= */ null); 801 } 802 } 803 inAnimator = AnimatorInflater.loadAnimator( 804 mContext, R.anim.car_arrow_fade_in_rotate_up); 805 806 } else { 807 clearAllAndSetupDefaultCarVolumeLineItem(mCurrentlyDisplayingGroupId); 808 inAnimator = AnimatorInflater.loadAnimator( 809 mContext, R.anim.car_arrow_fade_in_rotate_down); 810 } 811 812 Animator outAnimator = AnimatorInflater.loadAnimator( 813 mContext, R.anim.car_arrow_fade_out); 814 inAnimator.setStartDelay(ARROW_FADE_IN_START_DELAY_IN_MILLIS); 815 AnimatorSet animators = new AnimatorSet(); 816 animators.playTogether(outAnimator, inAnimator); 817 if (!isClicked) { 818 // Do not animate when the state is called to reset the dialogs view and not clicked 819 // by user. 820 animators.setDuration(0); 821 } 822 animators.setTarget(mExpandIcon); 823 animators.start(); 824 mVolumeItemsAdapter.notifyDataSetChanged(); 825 } 826 827 private final class VolumeSeekBarChangeListener implements OnSeekBarChangeListener { 828 829 private final int mVolumeZoneId; 830 private final int mVolumeGroupId; 831 private final CarAudioManager mCarAudioManager; 832 VolumeSeekBarChangeListener(int volumeZoneId, int volumeGroupId, CarAudioManager carAudioManager)833 private VolumeSeekBarChangeListener(int volumeZoneId, int volumeGroupId, 834 CarAudioManager carAudioManager) { 835 mVolumeZoneId = volumeZoneId; 836 mVolumeGroupId = volumeGroupId; 837 mCarAudioManager = carAudioManager; 838 } 839 840 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)841 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 842 if (!fromUser) { 843 // For instance, if this event is originated from AudioService, 844 // we can ignore it as it has already been handled and doesn't need to be 845 // sent back down again. 846 return; 847 } 848 if (mCarAudioManager == null) { 849 Log.w(TAG, "Ignoring volume change event because the car isn't connected"); 850 return; 851 } 852 mAvailableVolumeItems.get(mVolumeGroupId).mProgress = progress; 853 mAvailableVolumeItems.get( 854 mVolumeGroupId).mCarVolumeItem.setProgress(progress); 855 mCarAudioManager.setGroupVolume(mVolumeZoneId, mVolumeGroupId, progress, 0); 856 } 857 858 @Override onStartTrackingTouch(SeekBar seekBar)859 public void onStartTrackingTouch(SeekBar seekBar) { 860 } 861 862 @Override onStopTrackingTouch(SeekBar seekBar)863 public void onStopTrackingTouch(SeekBar seekBar) { 864 } 865 } 866 updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents)867 private void updateVolumeGroupForEvents(List<CarVolumeGroupEvent> volumeGroupEvents) { 868 List<CarVolumeGroupEvent> filteredEvents = 869 filterVolumeGroupEventForZoneId(mAudioZoneId, volumeGroupEvents); 870 for (int index = 0; index < filteredEvents.size(); index++) { 871 CarVolumeGroupEvent event = filteredEvents.get(index); 872 int eventTypes = event.getEventTypes(); 873 List<Integer> extraInfos = event.getExtraInfos(); 874 List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos(); 875 for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) { 876 CarVolumeGroupInfo carVolumeGroupInfo = infos.get(infoIndex); 877 updateVolumePreference(carVolumeGroupInfo.getId(), 878 carVolumeGroupInfo.getMaxVolumeGainIndex(), 879 carVolumeGroupInfo.getVolumeGainIndex(), carVolumeGroupInfo.isMuted(), 880 eventTypes, extraInfos); 881 } 882 } 883 } 884 filterVolumeGroupEventForZoneId(int zoneId, List<CarVolumeGroupEvent> volumeGroupEvents)885 private List<CarVolumeGroupEvent> filterVolumeGroupEventForZoneId(int zoneId, 886 List<CarVolumeGroupEvent> volumeGroupEvents) { 887 List<CarVolumeGroupEvent> filteredEvents = new ArrayList<>(); 888 for (int index = 0; index < volumeGroupEvents.size(); index++) { 889 CarVolumeGroupEvent event = volumeGroupEvents.get(index); 890 List<CarVolumeGroupInfo> infos = event.getCarVolumeGroupInfos(); 891 for (int infoIndex = 0; infoIndex < infos.size(); infoIndex++) { 892 if (infos.get(infoIndex).getZoneId() == zoneId) { 893 filteredEvents.add(event); 894 break; 895 } 896 } 897 } 898 return filteredEvents; 899 } 900 updateVolumePreference(int groupId, int maxIndex, int currentIndex, boolean isMuted, int eventTypes, List<Integer> extraInfos)901 private void updateVolumePreference(int groupId, int maxIndex, int currentIndex, 902 boolean isMuted, int eventTypes, List<Integer> extraInfos) { 903 VolumeItem volumeItem = mAvailableVolumeItems.get(groupId); 904 boolean isShowing = mCarVolumeLineItems.stream().anyMatch( 905 item -> item.getGroupId() == groupId); 906 907 if (isShowing) { 908 if ((eventTypes & EVENT_TYPE_VOLUME_GAIN_INDEX_CHANGED) != 0) { 909 volumeItem.mCarVolumeItem.setProgress(currentIndex); 910 volumeItem.mProgress = currentIndex; 911 } 912 if ((eventTypes & EVENT_TYPE_MUTE_CHANGED) != 0) { 913 volumeItem.mCarVolumeItem.setIsMuted(isMuted); 914 volumeItem.mIsMuted = isMuted; 915 } 916 if ((eventTypes & EVENT_TYPE_VOLUME_MAX_INDEX_CHANGED) != 0 917 && maxIndex != INVALID_INDEX) { 918 volumeItem.mCarVolumeItem.setMax(maxIndex); 919 } 920 } 921 922 if (extraInfos.contains(EXTRA_INFO_SHOW_UI) 923 || extraInfos.contains(EXTRA_INFO_VOLUME_INDEX_CHANGED_BY_AUDIO_SYSTEM)) { 924 mPreviouslyDisplayingGroupId = mCurrentlyDisplayingGroupId; 925 mCurrentlyDisplayingGroupId = groupId; 926 mHandler.obtainMessage(H.SHOW, 927 Events.SHOW_REASON_VOLUME_CHANGED).sendToTarget(); 928 } 929 } 930 } 931