1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.radio; 18 19 import android.hardware.radio.ProgramSelector; 20 import android.hardware.radio.RadioManager.ProgramInfo; 21 import android.view.LayoutInflater; 22 import android.view.View; 23 import android.view.ViewGroup; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import androidx.lifecycle.LifecycleOwner; 28 import androidx.lifecycle.LiveData; 29 import androidx.recyclerview.widget.RecyclerView; 30 31 import com.android.car.broadcastradio.support.Program; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 import java.util.stream.Collectors; 38 39 40 /** 41 * Adapter that will display a list of radio stations that represent the user's presets. 42 */ 43 public class BrowseAdapter extends RecyclerView.Adapter<ProgramViewHolder> { 44 // Only one type of view in this adapter. 45 private static final int PRESETS_VIEW_TYPE = 0; 46 47 private final Object mLock = new Object(); 48 49 private @NonNull List<Entry> mPrograms = new ArrayList<>(); 50 private @Nullable ProgramInfo mCurrentProgram; 51 52 private OnItemClickListener mItemClickListener; 53 private OnItemFavoriteListener mItemFavoriteListener; 54 55 /** 56 * Interface for a listener that will be notified when an item in the program list has been 57 * clicked. 58 */ 59 public interface OnItemClickListener { 60 /** 61 * Method called when an item in the list has been clicked. 62 * 63 * @param selector The {@link ProgramSelector} corresponding to the clicked preset. 64 */ onItemClicked(ProgramSelector selector)65 void onItemClicked(ProgramSelector selector); 66 } 67 68 /** 69 * Interface for a listener that will be notified when a favorite in the list has been 70 * toggled. 71 */ 72 public interface OnItemFavoriteListener { 73 74 /** 75 * Method called when an item's favorite status has been toggled 76 * 77 * @param program The {@link Program} corresponding to the clicked item. 78 * @param saveAsFavorite Whether the program should be saved or removed as a favorite. 79 */ onItemFavoriteChanged(Program program, boolean saveAsFavorite)80 void onItemFavoriteChanged(Program program, boolean saveAsFavorite); 81 } 82 83 private class Entry { 84 public Program program; 85 public boolean isFavorite; 86 public boolean wasFavorite; 87 Entry(Program program, boolean isFavorite)88 Entry(Program program, boolean isFavorite) { 89 this.program = program; 90 this.isFavorite = isFavorite; 91 this.wasFavorite = isFavorite; 92 } 93 } 94 BrowseAdapter(@onNull LifecycleOwner lifecycleOwner, @NonNull LiveData<ProgramInfo> currentProgram, @NonNull LiveData<List<Program>> favorites)95 public BrowseAdapter(@NonNull LifecycleOwner lifecycleOwner, 96 @NonNull LiveData<ProgramInfo> currentProgram, 97 @NonNull LiveData<List<Program>> favorites) { 98 favorites.observe(lifecycleOwner, this::onFavoritesChanged); 99 currentProgram.observe(lifecycleOwner, this::onCurrentProgramChanged); 100 } 101 102 /** 103 * Set a listener to be notified whenever a program card is pressed. 104 */ setOnItemClickListener(@onNull OnItemClickListener listener)105 public void setOnItemClickListener(@NonNull OnItemClickListener listener) { 106 synchronized (mLock) { 107 mItemClickListener = Objects.requireNonNull(listener); 108 } 109 } 110 111 /** 112 * Set a listener to be notified whenever a program favorite is changed. 113 */ setOnItemFavoriteListener(@onNull OnItemFavoriteListener listener)114 public void setOnItemFavoriteListener(@NonNull OnItemFavoriteListener listener) { 115 synchronized (mLock) { 116 mItemFavoriteListener = Objects.requireNonNull(listener); 117 } 118 } 119 120 /** 121 * Sets the given list as the list of programs to display. 122 */ setProgramList(@onNull List<ProgramInfo> programs)123 public void setProgramList(@NonNull List<ProgramInfo> programs) { 124 Map<ProgramSelector.Identifier, ProgramInfo> liveMap = programs.stream().collect( 125 Collectors.toMap(p -> p.getSelector().getPrimaryId(), p -> p)); 126 synchronized (mLock) { 127 // Remove entries no longer on live list, except those which were favorites previously 128 List<Entry> remove = new ArrayList<>(); 129 for (Entry entry : mPrograms) { 130 ProgramSelector.Identifier id = entry.program.getSelector().getPrimaryId(); 131 ProgramInfo liveEntry = liveMap.get(id); 132 if (liveEntry != null) { 133 liveMap.remove(id); // item is already on the list, don't add twice 134 } else if (!entry.wasFavorite) { 135 remove.add(entry); // no longer live and was never favorite - remove it 136 } 137 } 138 mPrograms.removeAll(remove); 139 140 // Add new entries from live list 141 liveMap.values().stream() 142 .map(pi -> new Entry(Program.fromProgramInfo(pi), false)) 143 .forEachOrdered(mPrograms::add); 144 145 notifyDataSetChanged(); 146 } 147 } 148 149 /** 150 * Remove formerly favorite stations from the list of stations, e.g. a station that started as a 151 * favorite, but is no longer a favorite 152 */ removeFormerFavorites()153 public void removeFormerFavorites() { 154 synchronized (mLock) { 155 // Remove all programs that are no longer a favorite, 156 // except those that were never favorites (i.e. currently tuned) 157 mPrograms = mPrograms.stream() 158 .filter(e -> e.isFavorite || !e.wasFavorite) 159 .collect(Collectors.toList()); 160 } 161 notifyDataSetChanged(); 162 } 163 164 /** 165 * Updates the stations that are favorites, while keeping unfavorited stations in the list 166 */ onFavoritesChanged(List<Program> favorites)167 private void onFavoritesChanged(List<Program> favorites) { 168 Map<ProgramSelector.Identifier, Program> favMap = favorites.stream().collect( 169 Collectors.toMap(p -> p.getSelector().getPrimaryId(), p -> p)); 170 synchronized (mLock) { 171 // Mark existing elements as favorites or not 172 for (Entry entry : mPrograms) { 173 ProgramSelector.Identifier id = entry.program.getSelector().getPrimaryId(); 174 entry.isFavorite = favMap.containsKey(id); 175 if (entry.isFavorite) favMap.remove(id); // don't add twice 176 } 177 178 // Add new items 179 favMap.values().stream().map(p -> new Entry(p, true)).forEachOrdered(mPrograms::add); 180 181 notifyDataSetChanged(); 182 } 183 } 184 185 /** 186 * Indicates which radio station is the active one inside the list of programs that are set on 187 * this adapter. This will cause that station to be highlighted in the list. If the station 188 * passed to this method does not match any of the programs, then none will be highlighted. 189 */ onCurrentProgramChanged(@onNull ProgramInfo info)190 private void onCurrentProgramChanged(@NonNull ProgramInfo info) { 191 synchronized (mLock) { 192 mCurrentProgram = Objects.requireNonNull(info); 193 notifyDataSetChanged(); 194 } 195 } 196 197 @Override onCreateViewHolder(ViewGroup parent, int viewType)198 public ProgramViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 199 View view = LayoutInflater.from(parent.getContext()) 200 .inflate(R.layout.radio_browse_item, parent, false); 201 202 return new ProgramViewHolder( 203 view, this::handlePresetClicked, this::handlePresetFavoriteChanged); 204 } 205 206 @Override onBindViewHolder(ProgramViewHolder holder, int position)207 public void onBindViewHolder(ProgramViewHolder holder, int position) { 208 synchronized (mLock) { 209 Entry entry = getEntryLocked(position); 210 boolean isCurrent = mCurrentProgram != null 211 && entry.program.getSelector().equals(mCurrentProgram.getSelector()); 212 holder.bindPreset(entry.program, isCurrent, getItemCount(), entry.isFavorite); 213 } 214 } 215 216 @Override getItemViewType(int position)217 public int getItemViewType(int position) { 218 return PRESETS_VIEW_TYPE; 219 } 220 getEntryLocked(int position)221 private Entry getEntryLocked(int position) { 222 // if there are no elements on the list, return current program 223 if (position == 0 && mPrograms.size() == 0) { 224 return new Entry(Program.fromProgramInfo(mCurrentProgram), false); 225 } 226 return mPrograms.get(position); 227 } 228 229 @Override getItemCount()230 public int getItemCount() { 231 synchronized (mLock) { 232 int cnt = mPrograms.size(); 233 if (cnt == 0 && mCurrentProgram != null) return 1; 234 return cnt; 235 } 236 } 237 handlePresetClicked(int position)238 private void handlePresetClicked(int position) { 239 synchronized (mLock) { 240 if (mItemClickListener == null) return; 241 if (position >= getItemCount()) return; 242 243 mItemClickListener.onItemClicked(getEntryLocked(position).program.getSelector()); 244 } 245 } 246 handlePresetFavoriteChanged(int position, boolean saveAsFavorite)247 private void handlePresetFavoriteChanged(int position, boolean saveAsFavorite) { 248 synchronized (mLock) { 249 if (mItemFavoriteListener == null) return; 250 if (position >= getItemCount()) return; 251 252 mItemFavoriteListener.onItemFavoriteChanged( 253 getEntryLocked(position).program, saveAsFavorite); 254 } 255 } 256 } 257