1 package com.android.car.media.widgets; 2 3 import android.annotation.Nullable; 4 import android.content.Context; 5 import android.graphics.drawable.Drawable; 6 import android.util.AttributeSet; 7 import android.util.Log; 8 import android.view.LayoutInflater; 9 import android.view.View; 10 import android.view.ViewGroup; 11 import android.widget.ImageView; 12 import android.widget.TextView; 13 14 import androidx.constraintlayout.widget.ConstraintLayout; 15 16 import com.android.car.apps.common.UxrButton; 17 import com.android.car.apps.common.widget.CarTabLayout; 18 import com.android.car.media.R; 19 import com.android.car.media.common.MediaAppSelectorWidget; 20 import com.android.car.media.common.MediaItemMetadata; 21 22 import java.util.List; 23 import java.util.Objects; 24 25 /** 26 * Media template application bar. The callers should set properties via the public methods (e.g., 27 * {@link setItems()}, {@link setTitle()}, {@link setHasSettings()}), and set the visibility of the 28 * views via {@link setState()}. A detailed explanation of all possible states of this application 29 * bar can be seen at {@link AppBarView.State}. 30 */ 31 public class AppBarView extends ConstraintLayout { 32 private static final String TAG = "AppBarView"; 33 34 private CarTabLayout<MediaItemTab> mTabsContainer; 35 private ImageView mNavIcon; 36 private ViewGroup mNavIconContainer; 37 private TextView mTitle; 38 /** Visible if mHasSettings && mShowSettings. */ 39 private UxrButton mSettingsButton; 40 private boolean mHasSettings; 41 private boolean mShowSettings; 42 private View mSearchButton; 43 private SearchBar mSearchBar; 44 private MediaAppSelectorWidget mAppSelector; 45 private Context mContext; 46 private int mMaxTabs; 47 private Drawable mArrowBack; 48 private Drawable mCollapse; 49 private State mState = State.BROWSING; 50 private AppBarListener mListener; 51 private int mFadeDuration; 52 private String mMediaAppTitle; 53 private boolean mSearchSupported; 54 private int mMaxRows; 55 56 public interface AppBarProvider { getAppBar()57 AppBarView getAppBar(); 58 } 59 60 /** 61 * Application bar listener 62 */ 63 public interface AppBarListener { 64 /** 65 * Invoked when the user selects an item from the tabs 66 */ onTabSelected(MediaItemMetadata item)67 void onTabSelected(MediaItemMetadata item); 68 69 /** 70 * Invoked when the user clicks on the back button 71 */ onBack()72 void onBack(); 73 74 /** 75 * Invoked when the user clicks on the settings button. 76 */ onSettingsSelection()77 void onSettingsSelection(); 78 79 /** 80 * Invoked when the user submits a search query. 81 */ onSearch(String query)82 void onSearch(String query); 83 84 /** 85 * Invoked when the user clicks on the search button 86 */ onSearchSelection()87 void onSearchSelection(); 88 } 89 90 /** 91 * Possible states of this application bar 92 */ 93 public enum State { 94 /** 95 * Normal application state. If we are able to obtain media items from the media 96 * source application, we display them as tabs. Otherwise we show the application name. 97 */ 98 BROWSING, 99 /** 100 * Indicates that the user has navigated into an element. In this case we show 101 * the name of the element and we disable the back button. 102 */ 103 STACKED, 104 /** 105 * Indicates that the user is currently entering a search query. We show the search bar and 106 * a collapse icon 107 */ 108 SEARCHING, 109 /** 110 * Used whenever the app bar should not display any information such as when MediaCenter 111 * is in an error state 112 */ 113 EMPTY 114 } 115 AppBarView(Context context)116 public AppBarView(Context context) { 117 this(context, null); 118 } 119 AppBarView(Context context, AttributeSet attrs)120 public AppBarView(Context context, AttributeSet attrs) { 121 this(context, attrs, 0); 122 } 123 AppBarView(Context context, AttributeSet attrs, int defStyleAttr)124 public AppBarView(Context context, AttributeSet attrs, int defStyleAttr) { 125 super(context, attrs, defStyleAttr); 126 init(context, attrs, defStyleAttr); 127 } 128 init(Context context, AttributeSet attrs, int defStyleAttr)129 private void init(Context context, AttributeSet attrs, int defStyleAttr) { 130 mMaxTabs = context.getResources().getInteger(R.integer.max_tabs); 131 132 LayoutInflater inflater = (LayoutInflater) context 133 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 134 inflater.inflate(R.layout.appbar_view, this, true); 135 136 mContext = context; 137 mMaxRows = mContext.getResources().getInteger(R.integer.num_app_bar_view_rows); 138 139 mTabsContainer = findViewById(R.id.tabs); 140 mTabsContainer.addOnCarTabSelectedListener( 141 new CarTabLayout.SimpleOnCarTabSelectedListener<MediaItemTab>() { 142 @Override 143 public void onCarTabSelected(MediaItemTab mediaItemTab) { 144 if (mListener != null) { 145 mListener.onTabSelected(mediaItemTab.getItem()); 146 } 147 } 148 }); 149 mNavIcon = findViewById(R.id.nav_icon); 150 mNavIconContainer = findViewById(R.id.nav_icon_container); 151 mNavIconContainer.setOnClickListener(view -> onNavIconClicked()); 152 mAppSelector = findViewById(R.id.app_switch_container); 153 mSettingsButton = findViewById(R.id.settings); 154 mSettingsButton.setOnClickListener(view -> onSettingsClicked()); 155 mSearchButton = findViewById(R.id.search); 156 mSearchButton.setOnClickListener(view -> onSearchClicked()); 157 mSearchBar = findViewById(R.id.search_bar_container); 158 159 mTitle = findViewById(R.id.title); 160 mArrowBack = getResources().getDrawable(R.drawable.ic_arrow_back, null); 161 mCollapse = getResources().getDrawable(R.drawable.ic_expand_more, null); 162 mFadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration); 163 mMediaAppTitle = getResources().getString(R.string.media_app_title); 164 165 setState(State.BROWSING); 166 } 167 openAppSelector()168 public void openAppSelector() { 169 mAppSelector.open(); 170 } 171 closeAppSelector()172 public void closeAppSelector() { 173 mAppSelector.close(); 174 } 175 onNavIconClicked()176 private void onNavIconClicked() { 177 if (mListener == null) { 178 return; 179 } 180 switch (mState) { 181 case BROWSING: 182 case STACKED: 183 mListener.onBack(); 184 break; 185 case SEARCHING: 186 mSearchBar.showSearchBar(false); 187 mListener.onBack(); 188 break; 189 } 190 } 191 onSettingsClicked()192 private void onSettingsClicked() { 193 if (mListener == null) { 194 return; 195 } 196 mListener.onSettingsSelection(); 197 } 198 onSearchClicked()199 private void onSearchClicked() { 200 if (mListener == null) { 201 return; 202 } 203 mListener.onSearchSelection(); 204 } 205 206 /** 207 * Sets a listener of this application bar events. In order to avoid memory leaks, consumers 208 * must reset this reference by setting the listener to null. 209 */ setListener(AppBarListener listener)210 public void setListener(AppBarListener listener) { 211 mListener = listener; 212 } 213 214 /** 215 * Updates the list of items to show in the application bar tabs. 216 * 217 * @param items list of tabs to show, or null if no tabs should be shown. 218 */ setItems(@ullable List<MediaItemMetadata> items)219 public void setItems(@Nullable List<MediaItemMetadata> items) { 220 mTabsContainer.clearAllCarTabs(); 221 222 if (items != null && !items.isEmpty()) { 223 int count = 0; 224 for (MediaItemMetadata item : items) { 225 MediaItemTab tab = new MediaItemTab(mContext, item); 226 mTabsContainer.addCarTab(tab); 227 228 count++; 229 if (count >= mMaxTabs) { 230 break; 231 } 232 } 233 } 234 235 // Refresh the views visibility 236 setState(mState); 237 } 238 239 /** 240 * Updates the title to display when the bar is not showing tabs. If the provided title is null, 241 * will default to displaying the app name. 242 */ setTitle(CharSequence title)243 public void setTitle(CharSequence title) { 244 mTitle.setText(title != null ? title : mMediaAppTitle); 245 } 246 247 /** 248 * Sets the name of the currently displayed media app. This is used as the default title for 249 * playback and the root browse menu. If provided title is null, will use default media center 250 * title. 251 */ setMediaAppTitle(CharSequence appTitle)252 public void setMediaAppTitle(CharSequence appTitle) { 253 mMediaAppTitle = appTitle == null ? getResources().getString(R.string.media_app_title) 254 : appTitle.toString(); 255 } 256 257 /** Sets whether the source has settings (not all screens show it). */ setHasSettings(boolean hasSettings)258 public void setHasSettings(boolean hasSettings) { 259 mHasSettings = hasSettings; 260 updateSettingsVisibility(); 261 } 262 showSettings(boolean showSettings)263 private void showSettings(boolean showSettings) { 264 mShowSettings = showSettings; 265 updateSettingsVisibility(); 266 } 267 updateSettingsVisibility()268 private void updateSettingsVisibility() { 269 mSettingsButton.setVisibility(mHasSettings && mShowSettings ? VISIBLE : GONE); 270 } 271 272 /** 273 * Updates the currently active item 274 */ setActiveItem(MediaItemMetadata item)275 public void setActiveItem(MediaItemMetadata item) { 276 for (int i = 0; i < mTabsContainer.getCarTabCount(); i++) { 277 MediaItemTab mediaItemTab = mTabsContainer.get(i); 278 boolean match = item != null && Objects.equals( 279 item.getId(), 280 mediaItemTab.getItem().getId()); 281 if (match) { 282 mTabsContainer.selectCarTab(mediaItemTab); 283 return; 284 } 285 } 286 } 287 288 /** 289 * Sets whether the search box should be shown 290 */ setSearchSupported(boolean supported)291 public void setSearchSupported(boolean supported) { 292 mSearchSupported = supported; 293 mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE); 294 } 295 296 /** 297 * Sets whether to show tabs or not. The caller should make sure tabs has at least 1 item before 298 * showing tabs. 299 */ setShowTabs(boolean visible)300 private void setShowTabs(boolean visible) { 301 // Refresh state to adjust for new tab visibility 302 mTabsContainer.setVisibility(visible ? View.VISIBLE : View.GONE); 303 } 304 305 /** 306 * Updates the state of the bar. 307 */ setState(State state)308 public void setState(State state) { 309 mState = state; 310 final boolean hasTabs = mTabsContainer.getCarTabCount() > 0; 311 final boolean showTitle = !hasTabs || mMaxRows == 2; 312 Log.d(TAG, "Updating state: " + state + " (has tabs: " + hasTabs + ")"); 313 switch (state) { 314 case EMPTY: 315 mNavIconContainer.setVisibility(View.GONE); 316 setShowTabs(false); 317 mTitle.setVisibility(View.GONE); 318 mSearchBar.showSearchBar(false); 319 showSettings(true); 320 mAppSelector.setVisibility(View.VISIBLE); 321 break; 322 case BROWSING: 323 mNavIcon.setImageDrawable(mArrowBack); 324 mNavIconContainer.setVisibility(View.GONE); 325 setShowTabs(hasTabs); 326 mTitle.setVisibility(showTitle ? View.VISIBLE : View.GONE); 327 mSearchBar.showSearchBar(false); 328 mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE); 329 showSettings(true); 330 mAppSelector.setVisibility(View.VISIBLE); 331 break; 332 case STACKED: 333 mNavIcon.setImageDrawable(mArrowBack); 334 mNavIconContainer.setVisibility(View.VISIBLE); 335 setShowTabs(false); 336 mTitle.setVisibility(View.VISIBLE); 337 mSearchBar.showSearchBar(false); 338 mSearchButton.setVisibility(mSearchSupported ? View.VISIBLE : View.GONE); 339 showSettings(true); 340 mAppSelector.setVisibility(View.VISIBLE); 341 break; 342 case SEARCHING: 343 mNavIcon.setImageDrawable(mArrowBack); 344 mNavIconContainer.setVisibility(View.VISIBLE); 345 setShowTabs(false); 346 mTitle.setVisibility(View.GONE); 347 mSearchBar.showSearchBar(true); 348 mSearchButton.setVisibility(View.GONE); 349 showSettings(false); 350 mAppSelector.setVisibility(View.GONE); 351 break; 352 } 353 } 354 } 355