1 /* 2 * Copyright (c) 2016, 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 package com.android.car.overview; 17 18 import android.content.Context; 19 import android.support.annotation.Nullable; 20 import android.support.car.ui.PagedListView; 21 import android.support.v7.widget.CardView; 22 import android.support.v7.widget.RecyclerView; 23 import android.util.Log; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import com.android.car.stream.AbstractBundleable; 28 import com.android.car.stream.MediaPlaybackExtension; 29 import com.android.car.stream.StreamCard; 30 import com.android.car.stream.StreamConstants; 31 32 import java.util.ArrayList; 33 import java.util.Iterator; 34 35 /** 36 * A {@link RecyclerView.Adapter} that binds {@link StreamCard} to their respective views. 37 */ 38 public class StreamAdapter extends RecyclerView.Adapter<StreamViewHolder> 39 implements PagedListView.ItemCap { 40 41 private static final int BASIC_CARD_LAYOUT_TYPE = 0; 42 private static final int CURRENT_CALL_CARD_LAYOUT_TYPE = 1; 43 private static final int MEDIA_CARD_LAYOUT_TYPE = 2; 44 45 private static final String TAG = "StreamAdapter"; 46 47 private static final int MAX_NUMBER_ITEMS = 25; 48 private int mMaxItems = MAX_NUMBER_ITEMS; 49 50 private final ArrayList<StreamCard> mStreamCards = new ArrayList<>(mMaxItems); 51 private final Context mContext; 52 StreamAdapter(Context context)53 public StreamAdapter(Context context) { 54 mContext = context; 55 } 56 57 @Override onCreateViewHolder(ViewGroup parent, int viewType)58 public StreamViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 59 View view = LayoutInflater.from(mContext) 60 .inflate(R.layout.stream_card_container, parent, false); 61 CardView container = (CardView) view.findViewById(R.id.stream_item_container); 62 switch (viewType) { 63 case CURRENT_CALL_CARD_LAYOUT_TYPE: 64 container.addView(LayoutInflater.from(parent.getContext()) 65 .inflate(R.layout.stream_card_current_call, container, false)); 66 return new CurrentCallStreamViewHolder(mContext, view); 67 case MEDIA_CARD_LAYOUT_TYPE: 68 container.addView(LayoutInflater.from(parent.getContext()) 69 .inflate(R.layout.stream_card_media, container, false)); 70 return new MediaStreamViewHolder(mContext, view); 71 default: 72 // For all cards that do not have their own view holder/layout, use the basic stream 73 // card layout. 74 View contentView = LayoutInflater.from(parent.getContext()) 75 .inflate(R.layout.stream_card_simple, container, false); 76 container.addView(contentView); 77 78 return new SimpleStreamViewHolder(mContext, view); 79 } 80 } 81 82 @Override onBindViewHolder(StreamViewHolder holder, int position)83 public void onBindViewHolder(StreamViewHolder holder, int position) { 84 if (Log.isLoggable(TAG, Log.DEBUG)) { 85 Log.d(TAG, "Stream Card being bound: " + mStreamCards.get(position)); 86 } 87 holder.bindStreamCard(mStreamCards.get(position)); 88 } 89 90 @Override getItemViewType(int position)91 public int getItemViewType(int position) { 92 StreamCard card = mStreamCards.get(position); 93 94 // If the card has no extensions, then render as a basic card. 95 if (card.getCardExtension() == null) { 96 return BASIC_CARD_LAYOUT_TYPE; 97 } 98 99 switch (card.getType()) { 100 case StreamConstants.CARD_TYPE_CURRENT_CALL: 101 return CURRENT_CALL_CARD_LAYOUT_TYPE; 102 case StreamConstants.CARD_TYPE_MEDIA: 103 return MEDIA_CARD_LAYOUT_TYPE; 104 default: 105 return BASIC_CARD_LAYOUT_TYPE; 106 } 107 } 108 109 @Override getItemCount()110 public int getItemCount() { 111 return mStreamCards.size(); 112 } 113 114 @Override setMaxItems(int max)115 public void setMaxItems(int max) { 116 if (max < 1) { 117 return; 118 } 119 mMaxItems = Math.min(max, MAX_NUMBER_ITEMS); 120 } 121 122 /** 123 * Remove all {@link StreamCard} in the adapter. 124 */ removeAllCards()125 public void removeAllCards() { 126 mStreamCards.clear(); 127 notifyDataSetChanged(); 128 } 129 addCard(StreamCard card)130 public void addCard(StreamCard card) { 131 // There should only be one card in the stream that is of type MEDIA. As a result, handle 132 // this case specially. Otherwise, check if the card matches a stream card that already 133 // exists and replace it. 134 if (card.getType() == StreamConstants.CARD_TYPE_MEDIA && !canAddMediaCard(card)) { 135 if (Log.isLoggable(TAG, Log.DEBUG)) { 136 Log.d(TAG, "Card: " + card + " does not have focus, so will not be added."); 137 } 138 return; 139 } else if (maybeReplaceCard(card)) { 140 if (Log.isLoggable(TAG, Log.DEBUG)) { 141 Log.d(TAG, "Card: " + card + " was replaced in stream"); 142 } 143 return; 144 } 145 146 if (mStreamCards.size() >= mMaxItems) { 147 StreamCard removedCard = mStreamCards.remove(mStreamCards.size() - 1); 148 if (Log.isLoggable(TAG, Log.DEBUG)) { 149 Log.d(TAG, "Card: " + removedCard + " was pushed out the stream"); 150 } 151 } 152 153 int size = mStreamCards.size(); 154 for (int i = 0; i < size; i++) { 155 if (mStreamCards.get(i).getPriority() <= card.getPriority()) { 156 if (Log.isLoggable(TAG, Log.DEBUG)) { 157 Log.d(TAG, "Card: " + card + " was inserted at i: " + i); 158 } 159 mStreamCards.add(i, card); 160 notifyDataSetChanged(); 161 return; 162 } 163 } 164 165 // The card had lower priority than all existing cards, add to the end. 166 if (Log.isLoggable(TAG, Log.DEBUG)) { 167 Log.d(TAG, "Card: " + card + " was inserted at the end"); 168 } 169 mStreamCards.add(card); 170 notifyDataSetChanged(); 171 } 172 removeCard(StreamCard card)173 public void removeCard(StreamCard card) { 174 for (Iterator<StreamCard> iterator = mStreamCards.iterator(); iterator.hasNext();) { 175 StreamCard existingCard = iterator.next(); 176 if (existingCard.getType() == card.getType() && existingCard.getId() == card.getId()) { 177 iterator.remove(); 178 notifyDataSetChanged(); 179 break; 180 } 181 } 182 } 183 184 /** 185 * Replaces a card in the adapter if the new card has the same priority. Otherwise it removes 186 * the card from the adapter. 187 */ maybeReplaceCard(StreamCard newCard)188 private boolean maybeReplaceCard(StreamCard newCard) { 189 for (int i = 0, size = mStreamCards.size(); i < size; i++) { 190 StreamCard existingCard = mStreamCards.get(i); 191 192 if (existingCard.getType() == newCard.getType() 193 && existingCard.getId() == newCard.getId()) { 194 mStreamCards.set(i, newCard); 195 if (existingCard.getPriority() == newCard.getPriority()) { 196 mStreamCards.set(i, newCard); 197 notifyDataSetChanged(); 198 return true; 199 } else { 200 // If the priority is no longer the same, just remove the card 201 // and let it be added again. 202 mStreamCards.remove(i); 203 return false; 204 } 205 } 206 } 207 return false; 208 } 209 210 /** 211 * Searches through {@link #mStreamCards} and returns the first card in the list that has a 212 * card type of {@link StreamConstants#CARD_TYPE_MEDIA}. If none is found, then {@code null} 213 * is returned. 214 */ 215 @Nullable getExistingMediaCard()216 private StreamCard getExistingMediaCard() { 217 for (StreamCard streamCard : mStreamCards) { 218 if (streamCard.getType() == StreamConstants.CARD_TYPE_MEDIA) { 219 return streamCard; 220 } 221 } 222 223 return null; 224 } 225 226 /** 227 * Returns {@code true} if the given {@link StreamCard} of type 228 * {@link StreamConstants#CARD_TYPE_MEDIA} can be added to the set of stream cards. This method 229 * is responsible for ensuring that there is only a single instance of a media card at all 230 * times. 231 */ canAddMediaCard(StreamCard card)232 private boolean canAddMediaCard(StreamCard card) { 233 StreamCard existingMediaCard = getExistingMediaCard(); 234 235 // If there is no other media StreamCard, then it is ok to add. 236 if (existingMediaCard == null) { 237 return true; 238 } 239 240 // If this update is coming from the same application, then add the StreamCard. 241 if (existingMediaCard.getId() == card.getId()) { 242 return true; 243 } 244 245 AbstractBundleable cardExtension = card.getCardExtension(); 246 247 // Cannot infer play state from the card to be added, so just add it. 248 if (!(cardExtension instanceof MediaPlaybackExtension)) { 249 return true; 250 } 251 252 // Otherwise, ensure only the application that currently has focus has the ability to show 253 // their card. When a card is currently playing, it implies that it has focus. 254 boolean hasFocus = ((MediaPlaybackExtension) cardExtension).isPlaying(); 255 256 // Since this new card has focus, remove the existing card from the list. 257 if (hasFocus) { 258 mStreamCards.remove(existingMediaCard); 259 } 260 261 return hasFocus; 262 } 263 } 264