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