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