1 /* 2 * Copyright (C) 2019 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.radio.service; 17 18 import android.os.RemoteException; 19 20 import androidx.annotation.GuardedBy; 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 import androidx.lifecycle.LiveData; 24 25 import com.android.car.broadcastradio.support.Program; 26 import com.android.car.radio.SkipMode; 27 import com.android.car.radio.util.Log; 28 29 import java.io.PrintWriter; 30 import java.util.List; 31 32 /** 33 * Helper class used to keep track of which station should be toggled next (or prev), based on 34 * {@link SkipMode}. 35 */ 36 final class SkipController { 37 38 private static final String TAG = SkipController.class.getSimpleName(); 39 40 private final Object mLock = new Object(); 41 42 private final IRadioAppService.Stub mService; 43 44 @GuardedBy("mlock") 45 private List<Program> mFavorites; 46 47 @GuardedBy("mlock") 48 private int mCurrentIndex; 49 50 @GuardedBy("mlock") 51 private SkipMode mSkipMode; 52 SkipController(@onNull IRadioAppService.Stub service, @NonNull LiveData<List<Program>> favorites, @NonNull SkipMode initialMode)53 SkipController(@NonNull IRadioAppService.Stub service, 54 @NonNull LiveData<List<Program>> favorites, @NonNull SkipMode initialMode) { 55 mService = service; 56 mSkipMode = initialMode; 57 58 Log.v(TAG, "Initial mode: %s", initialMode); 59 60 // TODO(b/137647889): not really working because they're changed in a different process. 61 // As such, the changes are only effective after the radio service restarts - that's 62 // not ideal, but it's better than nothing :-) 63 // Long term, we need to provide a way to sync them... 64 favorites.observeForever(this::onFavoritesChanged); 65 } 66 setSkipMode(@onNull SkipMode mode)67 void setSkipMode(@NonNull SkipMode mode) { 68 Log.d(TAG, "setSkipMode(%s)", mode); 69 synchronized (mLock) { 70 this.mSkipMode = mode; 71 } 72 } 73 skip(boolean forward, ITuneCallback callback)74 void skip(boolean forward, ITuneCallback callback) throws RemoteException { 75 Log.d(TAG, "skip(%s, %s)", mSkipMode, forward); 76 77 Program program = null; 78 synchronized (mLock) { 79 if (mSkipMode == SkipMode.FAVORITES || mSkipMode == SkipMode.BROWSE) { 80 program = getFavoriteLocked(forward); 81 if (program == null) { 82 Log.d(TAG, "skip(%s): no favorites, seeking instead", forward); 83 } 84 } 85 } 86 87 if (program != null) { 88 Log.d(TAG, "skip(%s): changing to %s", forward, program.getName()); 89 mService.tune(program.getSelector(), callback); 90 } else { 91 mService.seek(forward, callback); 92 } 93 } 94 onFavoritesChanged(List<Program> favorites)95 private void onFavoritesChanged(List<Program> favorites) { 96 Log.v(TAG, "onFavoritesChanged(): %s", favorites); 97 synchronized (this) { 98 mFavorites = favorites; 99 // TODO(b/137647889): try to preserve currentIndex, either pointing to the same station, 100 // or the closest one 101 mCurrentIndex = 0; 102 } 103 } 104 105 @Nullable getFavoriteLocked(boolean next)106 private Program getFavoriteLocked(boolean next) { 107 if (mFavorites == null || mFavorites.isEmpty()) return null; 108 109 // TODO(b/137647889): to keep it simple, we're only interacting through explicit calls 110 // to prev/next, but ideally it should also take in account the current station. 111 // For example, say the favorites are 4, 8, 15, 16, 23, 42 and user skipped from 112 // 15 to 16 but later manually tuned to 5. In this case, if the user skips again we'll 113 // return 23 (next index), but ideally it would be 8 (i.e, next favorite whose value 114 // is higher than 5) 115 if (next) { 116 mCurrentIndex++; 117 if (mCurrentIndex >= mFavorites.size()) { 118 mCurrentIndex = 0; 119 } 120 } else { 121 mCurrentIndex--; 122 if (mCurrentIndex < 0) { 123 mCurrentIndex = mFavorites.size() - 1; 124 } 125 } 126 Program program = mFavorites.get(mCurrentIndex); 127 Log.v(TAG, "getting favorite #" + mCurrentIndex + ": " + program.getName()); 128 return program; 129 } 130 dump(@onNull PrintWriter pw, @NonNull String prefix)131 void dump(@NonNull PrintWriter pw, @NonNull String prefix) { 132 synchronized (mLock) { 133 pw.print(prefix); pw.print("mode: "); pw.println(mSkipMode); 134 pw.print(prefix); pw.print("current index: "); pw.println(mCurrentIndex); 135 if (mFavorites == null || mFavorites.isEmpty()) { 136 pw.print(prefix); pw.println("no favorites"); 137 return; 138 } 139 int size = mFavorites.size(); 140 pw.print(prefix); pw.print(size); pw.println(" favorites: "); 141 String prefix2 = prefix + " "; 142 for (int i = 0; i < size; i++) { 143 pw.print(prefix2); 144 pw.print(i); pw.print(": "); pw.print(mFavorites.get(i).getName()); 145 if (i == mCurrentIndex) { 146 pw.print(" (current)"); 147 } 148 pw.println(); 149 } 150 } 151 } 152 } 153