| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.radio; |
| |
| import android.hardware.radio.ProgramSelector; |
| import android.hardware.radio.RadioManager.ProgramInfo; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.lifecycle.LifecycleOwner; |
| import androidx.lifecycle.LiveData; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.car.broadcastradio.support.Program; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| |
| /** |
| * Adapter that will display a list of radio stations that represent the user's presets. |
| */ |
| public class BrowseAdapter extends RecyclerView.Adapter<ProgramViewHolder> { |
| // Only one type of view in this adapter. |
| private static final int PRESETS_VIEW_TYPE = 0; |
| |
| private final Object mLock = new Object(); |
| |
| private @NonNull List<Entry> mPrograms = new ArrayList<>(); |
| private @Nullable ProgramInfo mCurrentProgram; |
| |
| private OnItemClickListener mItemClickListener; |
| private OnItemFavoriteListener mItemFavoriteListener; |
| |
| /** |
| * Interface for a listener that will be notified when an item in the program list has been |
| * clicked. |
| */ |
| public interface OnItemClickListener { |
| /** |
| * Method called when an item in the list has been clicked. |
| * |
| * @param selector The {@link ProgramSelector} corresponding to the clicked preset. |
| */ |
| void onItemClicked(ProgramSelector selector); |
| } |
| |
| /** |
| * Interface for a listener that will be notified when a favorite in the list has been |
| * toggled. |
| */ |
| public interface OnItemFavoriteListener { |
| |
| /** |
| * Method called when an item's favorite status has been toggled |
| * |
| * @param program The {@link Program} corresponding to the clicked item. |
| * @param saveAsFavorite Whether the program should be saved or removed as a favorite. |
| */ |
| void onItemFavoriteChanged(Program program, boolean saveAsFavorite); |
| } |
| |
| private class Entry { |
| public Program program; |
| public boolean isFavorite; |
| public boolean wasFavorite; |
| |
| Entry(Program program, boolean isFavorite) { |
| this.program = program; |
| this.isFavorite = isFavorite; |
| this.wasFavorite = isFavorite; |
| } |
| } |
| |
| public BrowseAdapter(@NonNull LifecycleOwner lifecycleOwner, |
| @NonNull LiveData<ProgramInfo> currentProgram, |
| @NonNull LiveData<List<Program>> favorites) { |
| favorites.observe(lifecycleOwner, this::onFavoritesChanged); |
| currentProgram.observe(lifecycleOwner, this::onCurrentProgramChanged); |
| } |
| |
| /** |
| * Set a listener to be notified whenever a program card is pressed. |
| */ |
| public void setOnItemClickListener(@NonNull OnItemClickListener listener) { |
| synchronized (mLock) { |
| mItemClickListener = Objects.requireNonNull(listener); |
| } |
| } |
| |
| /** |
| * Set a listener to be notified whenever a program favorite is changed. |
| */ |
| public void setOnItemFavoriteListener(@NonNull OnItemFavoriteListener listener) { |
| synchronized (mLock) { |
| mItemFavoriteListener = Objects.requireNonNull(listener); |
| } |
| } |
| |
| /** |
| * Sets the given list as the list of programs to display. |
| */ |
| public void setProgramList(@NonNull List<ProgramInfo> programs) { |
| Map<ProgramSelector.Identifier, ProgramInfo> liveMap = programs.stream().collect( |
| Collectors.toMap(p -> p.getSelector().getPrimaryId(), p -> p)); |
| synchronized (mLock) { |
| // Remove entries no longer on live list, except those which were favorites previously |
| List<Entry> remove = new ArrayList<>(); |
| for (Entry entry : mPrograms) { |
| ProgramSelector.Identifier id = entry.program.getSelector().getPrimaryId(); |
| ProgramInfo liveEntry = liveMap.get(id); |
| if (liveEntry != null) { |
| liveMap.remove(id); // item is already on the list, don't add twice |
| } else if (!entry.wasFavorite) { |
| remove.add(entry); // no longer live and was never favorite - remove it |
| } |
| } |
| mPrograms.removeAll(remove); |
| |
| // Add new entries from live list |
| liveMap.values().stream() |
| .map(pi -> new Entry(Program.fromProgramInfo(pi), false)) |
| .forEachOrdered(mPrograms::add); |
| |
| notifyDataSetChanged(); |
| } |
| } |
| |
| /** |
| * Remove formerly favorite stations from the list of stations, e.g. a station that started as a |
| * favorite, but is no longer a favorite |
| */ |
| public void removeFormerFavorites() { |
| synchronized (mLock) { |
| // Remove all programs that are no longer a favorite, |
| // except those that were never favorites (i.e. currently tuned) |
| mPrograms = mPrograms.stream() |
| .filter(e -> e.isFavorite || !e.wasFavorite) |
| .collect(Collectors.toList()); |
| } |
| notifyDataSetChanged(); |
| } |
| |
| /** |
| * Updates the stations that are favorites, while keeping unfavorited stations in the list |
| */ |
| private void onFavoritesChanged(List<Program> favorites) { |
| Map<ProgramSelector.Identifier, Program> favMap = favorites.stream().collect( |
| Collectors.toMap(p -> p.getSelector().getPrimaryId(), p -> p)); |
| synchronized (mLock) { |
| // Mark existing elements as favorites or not |
| for (Entry entry : mPrograms) { |
| ProgramSelector.Identifier id = entry.program.getSelector().getPrimaryId(); |
| entry.isFavorite = favMap.containsKey(id); |
| if (entry.isFavorite) favMap.remove(id); // don't add twice |
| } |
| |
| // Add new items |
| favMap.values().stream().map(p -> new Entry(p, true)).forEachOrdered(mPrograms::add); |
| |
| notifyDataSetChanged(); |
| } |
| } |
| |
| /** |
| * Indicates which radio station is the active one inside the list of programs that are set on |
| * this adapter. This will cause that station to be highlighted in the list. If the station |
| * passed to this method does not match any of the programs, then none will be highlighted. |
| */ |
| private void onCurrentProgramChanged(@NonNull ProgramInfo info) { |
| synchronized (mLock) { |
| mCurrentProgram = Objects.requireNonNull(info); |
| notifyDataSetChanged(); |
| } |
| } |
| |
| @Override |
| public ProgramViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| View view = LayoutInflater.from(parent.getContext()) |
| .inflate(R.layout.radio_browse_item, parent, false); |
| |
| return new ProgramViewHolder( |
| view, this::handlePresetClicked, this::handlePresetFavoriteChanged); |
| } |
| |
| @Override |
| public void onBindViewHolder(ProgramViewHolder holder, int position) { |
| synchronized (mLock) { |
| Entry entry = getEntryLocked(position); |
| boolean isCurrent = mCurrentProgram != null |
| && entry.program.getSelector().equals(mCurrentProgram.getSelector()); |
| holder.bindPreset(entry.program, isCurrent, getItemCount(), entry.isFavorite); |
| } |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| return PRESETS_VIEW_TYPE; |
| } |
| |
| private Entry getEntryLocked(int position) { |
| // if there are no elements on the list, return current program |
| if (position == 0 && mPrograms.size() == 0) { |
| return new Entry(Program.fromProgramInfo(mCurrentProgram), false); |
| } |
| return mPrograms.get(position); |
| } |
| |
| @Override |
| public int getItemCount() { |
| synchronized (mLock) { |
| int cnt = mPrograms.size(); |
| if (cnt == 0 && mCurrentProgram != null) return 1; |
| return cnt; |
| } |
| } |
| |
| private void handlePresetClicked(int position) { |
| synchronized (mLock) { |
| if (mItemClickListener == null) return; |
| if (position < 0 || position >= getItemCount()) return; |
| |
| mItemClickListener.onItemClicked(getEntryLocked(position).program.getSelector()); |
| } |
| } |
| |
| private void handlePresetFavoriteChanged(int position, boolean saveAsFavorite) { |
| synchronized (mLock) { |
| if (mItemFavoriteListener == null) return; |
| if (position < 0 || position >= getItemCount()) return; |
| |
| mItemFavoriteListener.onItemFavoriteChanged( |
| getEntryLocked(position).program, saveAsFavorite); |
| } |
| } |
| } |