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 android.hardware.radio; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemApi; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.util.ArrayMap; 26 27 import com.android.internal.annotations.GuardedBy; 28 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.Iterator; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Objects; 35 import java.util.Set; 36 import java.util.concurrent.Executor; 37 import java.util.stream.Collectors; 38 39 /** 40 * @hide 41 */ 42 @SystemApi 43 public final class ProgramList implements AutoCloseable { 44 45 private final Object mLock = new Object(); 46 47 @GuardedBy("mLock") 48 private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms = 49 new ArrayMap<>(); 50 51 @GuardedBy("mLock") 52 private final List<ListCallback> mListCallbacks = new ArrayList<>(); 53 54 @GuardedBy("mLock") 55 private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>(); 56 57 @GuardedBy("mLock") 58 private OnCloseListener mOnCloseListener; 59 60 @GuardedBy("mLock") 61 private boolean mIsClosed; 62 63 @GuardedBy("mLock") 64 private boolean mIsComplete; 65 ProgramList()66 ProgramList() {} 67 68 /** 69 * Callback for list change operations. 70 */ 71 public abstract static class ListCallback { 72 /** 73 * Called when item was modified or added to the list. 74 */ onItemChanged(@onNull ProgramSelector.Identifier id)75 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { } 76 77 /** 78 * Called when item was removed from the list. 79 */ onItemRemoved(@onNull ProgramSelector.Identifier id)80 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { } 81 } 82 83 /** 84 * Listener of list complete event. 85 */ 86 public interface OnCompleteListener { 87 /** 88 * Called when the list turned complete (i.e. when the scan process 89 * came to an end). 90 */ onComplete()91 void onComplete(); 92 } 93 94 interface OnCloseListener { onClose()95 void onClose(); 96 } 97 98 /** 99 * Registers list change callback with executor. 100 */ registerListCallback(@onNull @allbackExecutor Executor executor, @NonNull ListCallback callback)101 public void registerListCallback(@NonNull @CallbackExecutor Executor executor, 102 @NonNull ListCallback callback) { 103 registerListCallback(new ListCallback() { 104 public void onItemChanged(@NonNull ProgramSelector.Identifier id) { 105 executor.execute(() -> callback.onItemChanged(id)); 106 } 107 108 public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { 109 executor.execute(() -> callback.onItemRemoved(id)); 110 } 111 }); 112 } 113 114 /** 115 * Registers list change callback. 116 */ registerListCallback(@onNull ListCallback callback)117 public void registerListCallback(@NonNull ListCallback callback) { 118 synchronized (mLock) { 119 if (mIsClosed) return; 120 mListCallbacks.add(Objects.requireNonNull(callback)); 121 } 122 } 123 124 /** 125 * Unregisters list change callback. 126 */ unregisterListCallback(@onNull ListCallback callback)127 public void unregisterListCallback(@NonNull ListCallback callback) { 128 synchronized (mLock) { 129 if (mIsClosed) return; 130 mListCallbacks.remove(Objects.requireNonNull(callback)); 131 } 132 } 133 134 /** 135 * Adds list complete event listener with executor. 136 */ addOnCompleteListener(@onNull @allbackExecutor Executor executor, @NonNull OnCompleteListener listener)137 public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor, 138 @NonNull OnCompleteListener listener) { 139 addOnCompleteListener(() -> executor.execute(listener::onComplete)); 140 } 141 142 /** 143 * Adds list complete event listener. 144 */ addOnCompleteListener(@onNull OnCompleteListener listener)145 public void addOnCompleteListener(@NonNull OnCompleteListener listener) { 146 synchronized (mLock) { 147 if (mIsClosed) return; 148 mOnCompleteListeners.add(Objects.requireNonNull(listener)); 149 if (mIsComplete) listener.onComplete(); 150 } 151 } 152 153 /** 154 * Removes list complete event listener. 155 */ removeOnCompleteListener(@onNull OnCompleteListener listener)156 public void removeOnCompleteListener(@NonNull OnCompleteListener listener) { 157 synchronized (mLock) { 158 if (mIsClosed) return; 159 mOnCompleteListeners.remove(Objects.requireNonNull(listener)); 160 } 161 } 162 setOnCloseListener(@ullable OnCloseListener listener)163 void setOnCloseListener(@Nullable OnCloseListener listener) { 164 synchronized (mLock) { 165 if (mOnCloseListener != null) { 166 throw new IllegalStateException("Close callback is already set"); 167 } 168 mOnCloseListener = listener; 169 } 170 } 171 172 /** 173 * Disables list updates and releases all resources. 174 */ close()175 public void close() { 176 OnCloseListener onCompleteListenersCopied = null; 177 synchronized (mLock) { 178 if (mIsClosed) return; 179 mIsClosed = true; 180 mPrograms.clear(); 181 mListCallbacks.clear(); 182 mOnCompleteListeners.clear(); 183 if (mOnCloseListener != null) { 184 onCompleteListenersCopied = mOnCloseListener; 185 mOnCloseListener = null; 186 } 187 } 188 189 if (onCompleteListenersCopied != null) { 190 onCompleteListenersCopied.onClose(); 191 } 192 } 193 apply(Chunk chunk)194 void apply(Chunk chunk) { 195 List<ProgramSelector.Identifier> removedList = new ArrayList<>(); 196 List<ProgramSelector.Identifier> changedList = new ArrayList<>(); 197 List<ProgramList.ListCallback> listCallbacksCopied; 198 List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>(); 199 synchronized (mLock) { 200 if (mIsClosed) return; 201 202 mIsComplete = false; 203 listCallbacksCopied = new ArrayList<>(mListCallbacks); 204 205 if (chunk.isPurge()) { 206 Iterator<Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo>> 207 programsIterator = mPrograms.entrySet().iterator(); 208 while (programsIterator.hasNext()) { 209 RadioManager.ProgramInfo removed = programsIterator.next().getValue(); 210 if (removed != null) { 211 removedList.add(removed.getSelector().getPrimaryId()); 212 } 213 programsIterator.remove(); 214 } 215 } 216 217 chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList)); 218 chunk.getModified().stream().forEach(info -> putLocked(info, changedList)); 219 220 if (chunk.isComplete()) { 221 mIsComplete = true; 222 onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners); 223 } 224 } 225 226 for (int i = 0; i < removedList.size(); i++) { 227 for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { 228 listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i)); 229 } 230 } 231 for (int i = 0; i < changedList.size(); i++) { 232 for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) { 233 listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i)); 234 } 235 } 236 if (chunk.isComplete()) { 237 for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) { 238 onCompleteListenersCopied.get(cbIndex).onComplete(); 239 } 240 } 241 } 242 243 @GuardedBy("mLock") putLocked(RadioManager.ProgramInfo value, List<ProgramSelector.Identifier> changedIdentifierList)244 private void putLocked(RadioManager.ProgramInfo value, 245 List<ProgramSelector.Identifier> changedIdentifierList) { 246 ProgramSelector.Identifier key = value.getSelector().getPrimaryId(); 247 mPrograms.put(Objects.requireNonNull(key), value); 248 ProgramSelector.Identifier sel = value.getSelector().getPrimaryId(); 249 changedIdentifierList.add(sel); 250 } 251 252 @GuardedBy("mLock") removeLocked(ProgramSelector.Identifier key, List<ProgramSelector.Identifier> removedIdentifierList)253 private void removeLocked(ProgramSelector.Identifier key, 254 List<ProgramSelector.Identifier> removedIdentifierList) { 255 RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key)); 256 if (removed == null) return; 257 ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId(); 258 removedIdentifierList.add(sel); 259 } 260 261 /** 262 * Converts the program list in its current shape to the static List<>. 263 * 264 * @return the new List<> object; it won't receive any further updates 265 */ toList()266 public @NonNull List<RadioManager.ProgramInfo> toList() { 267 synchronized (mLock) { 268 return mPrograms.values().stream().collect(Collectors.toList()); 269 } 270 } 271 272 /** 273 * Returns the program with a specified primary identifier. 274 * 275 * @param id primary identifier of a program to fetch 276 * @return the program info, or null if there is no such program on the list 277 */ get(@onNull ProgramSelector.Identifier id)278 public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) { 279 synchronized (mLock) { 280 return mPrograms.get(Objects.requireNonNull(id)); 281 } 282 } 283 284 /** 285 * Filter for the program list. 286 */ 287 public static final class Filter implements Parcelable { 288 private final @NonNull Set<Integer> mIdentifierTypes; 289 private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers; 290 private final boolean mIncludeCategories; 291 private final boolean mExcludeModifications; 292 private final @Nullable Map<String, String> mVendorFilter; 293 294 /** 295 * Constructor of program list filter. 296 * 297 * Arrays passed to this constructor become owned by this object, do not modify them later. 298 * 299 * @param identifierTypes see getIdentifierTypes() 300 * @param identifiers see getIdentifiers() 301 * @param includeCategories see areCategoriesIncluded() 302 * @param excludeModifications see areModificationsExcluded() 303 */ Filter(@onNull Set<Integer> identifierTypes, @NonNull Set<ProgramSelector.Identifier> identifiers, boolean includeCategories, boolean excludeModifications)304 public Filter(@NonNull Set<Integer> identifierTypes, 305 @NonNull Set<ProgramSelector.Identifier> identifiers, 306 boolean includeCategories, boolean excludeModifications) { 307 mIdentifierTypes = Objects.requireNonNull(identifierTypes); 308 mIdentifiers = Objects.requireNonNull(identifiers); 309 mIncludeCategories = includeCategories; 310 mExcludeModifications = excludeModifications; 311 mVendorFilter = null; 312 } 313 314 /** 315 * @hide for framework use only 316 */ Filter()317 public Filter() { 318 mIdentifierTypes = Collections.emptySet(); 319 mIdentifiers = Collections.emptySet(); 320 mIncludeCategories = false; 321 mExcludeModifications = false; 322 mVendorFilter = null; 323 } 324 325 /** 326 * @hide for framework use only 327 */ Filter(@ullable Map<String, String> vendorFilter)328 public Filter(@Nullable Map<String, String> vendorFilter) { 329 mIdentifierTypes = Collections.emptySet(); 330 mIdentifiers = Collections.emptySet(); 331 mIncludeCategories = false; 332 mExcludeModifications = false; 333 mVendorFilter = vendorFilter; 334 } 335 Filter(@onNull Parcel in)336 private Filter(@NonNull Parcel in) { 337 mIdentifierTypes = Utils.createIntSet(in); 338 mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); 339 mIncludeCategories = in.readByte() != 0; 340 mExcludeModifications = in.readByte() != 0; 341 mVendorFilter = Utils.readStringMap(in); 342 } 343 344 @Override writeToParcel(Parcel dest, int flags)345 public void writeToParcel(Parcel dest, int flags) { 346 Utils.writeIntSet(dest, mIdentifierTypes); 347 Utils.writeSet(dest, mIdentifiers); 348 dest.writeByte((byte) (mIncludeCategories ? 1 : 0)); 349 dest.writeByte((byte) (mExcludeModifications ? 1 : 0)); 350 Utils.writeStringMap(dest, mVendorFilter); 351 } 352 353 @Override describeContents()354 public int describeContents() { 355 return 0; 356 } 357 358 public static final @android.annotation.NonNull Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() { 359 public Filter createFromParcel(Parcel in) { 360 return new Filter(in); 361 } 362 363 public Filter[] newArray(int size) { 364 return new Filter[size]; 365 } 366 }; 367 368 /** 369 * @hide for framework use only 370 */ getVendorFilter()371 public Map<String, String> getVendorFilter() { 372 return mVendorFilter; 373 } 374 375 /** 376 * Returns the list of identifier types that satisfy the filter. 377 * 378 * If the program list entry contains at least one identifier of the type 379 * listed, it satisfies this condition. 380 * 381 * Empty list means no filtering on identifier type. 382 * 383 * @return the list of accepted identifier types, must not be modified 384 */ getIdentifierTypes()385 public @NonNull Set<Integer> getIdentifierTypes() { 386 return mIdentifierTypes; 387 } 388 389 /** 390 * Returns the list of identifiers that satisfy the filter. 391 * 392 * If the program list entry contains at least one listed identifier, 393 * it satisfies this condition. 394 * 395 * Empty list means no filtering on identifier. 396 * 397 * @return the list of accepted identifiers, must not be modified 398 */ getIdentifiers()399 public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() { 400 return mIdentifiers; 401 } 402 403 /** 404 * Checks, if non-tunable entries that define tree structure on the 405 * program list (i.e. DAB ensembles) should be included. 406 * 407 * @see {@link ProgramSelector.Identifier#isCategory()} 408 */ areCategoriesIncluded()409 public boolean areCategoriesIncluded() { 410 return mIncludeCategories; 411 } 412 413 /** 414 * Checks, if updates on entry modifications should be disabled. 415 * 416 * If true, 'modified' vector of ProgramListChunk must contain list 417 * additions only. Once the program is added to the list, it's not 418 * updated anymore. 419 */ areModificationsExcluded()420 public boolean areModificationsExcluded() { 421 return mExcludeModifications; 422 } 423 424 @Override hashCode()425 public int hashCode() { 426 return Objects.hash(mIdentifierTypes, mIdentifiers, mIncludeCategories, 427 mExcludeModifications); 428 } 429 430 @Override equals(@ullable Object obj)431 public boolean equals(@Nullable Object obj) { 432 if (this == obj) return true; 433 if (!(obj instanceof Filter)) return false; 434 Filter other = (Filter) obj; 435 436 if (mIncludeCategories != other.mIncludeCategories) return false; 437 if (mExcludeModifications != other.mExcludeModifications) return false; 438 if (!Objects.equals(mIdentifierTypes, other.mIdentifierTypes)) return false; 439 if (!Objects.equals(mIdentifiers, other.mIdentifiers)) return false; 440 return true; 441 } 442 443 @NonNull 444 @Override toString()445 public String toString() { 446 return "Filter [mIdentifierTypes=" + mIdentifierTypes 447 + ", mIdentifiers=" + mIdentifiers 448 + ", mIncludeCategories=" + mIncludeCategories 449 + ", mExcludeModifications=" + mExcludeModifications + "]"; 450 } 451 } 452 453 /** 454 * @hide This is a transport class used for internal communication between 455 * Broadcast Radio Service and RadioManager. 456 * Do not use it directly. 457 */ 458 public static final class Chunk implements Parcelable { 459 private final boolean mPurge; 460 private final boolean mComplete; 461 private final @NonNull Set<RadioManager.ProgramInfo> mModified; 462 private final @NonNull Set<ProgramSelector.Identifier> mRemoved; 463 Chunk(boolean purge, boolean complete, @Nullable Set<RadioManager.ProgramInfo> modified, @Nullable Set<ProgramSelector.Identifier> removed)464 public Chunk(boolean purge, boolean complete, 465 @Nullable Set<RadioManager.ProgramInfo> modified, 466 @Nullable Set<ProgramSelector.Identifier> removed) { 467 mPurge = purge; 468 mComplete = complete; 469 mModified = (modified != null) ? modified : Collections.emptySet(); 470 mRemoved = (removed != null) ? removed : Collections.emptySet(); 471 } 472 Chunk(@onNull Parcel in)473 private Chunk(@NonNull Parcel in) { 474 mPurge = in.readByte() != 0; 475 mComplete = in.readByte() != 0; 476 mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR); 477 mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR); 478 } 479 480 @Override writeToParcel(Parcel dest, int flags)481 public void writeToParcel(Parcel dest, int flags) { 482 dest.writeByte((byte) (mPurge ? 1 : 0)); 483 dest.writeByte((byte) (mComplete ? 1 : 0)); 484 Utils.writeSet(dest, mModified); 485 Utils.writeSet(dest, mRemoved); 486 } 487 488 @Override describeContents()489 public int describeContents() { 490 return 0; 491 } 492 493 public static final @android.annotation.NonNull Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() { 494 public Chunk createFromParcel(Parcel in) { 495 return new Chunk(in); 496 } 497 498 public Chunk[] newArray(int size) { 499 return new Chunk[size]; 500 } 501 }; 502 isPurge()503 public boolean isPurge() { 504 return mPurge; 505 } 506 isComplete()507 public boolean isComplete() { 508 return mComplete; 509 } 510 getModified()511 public @NonNull Set<RadioManager.ProgramInfo> getModified() { 512 return mModified; 513 } 514 getRemoved()515 public @NonNull Set<ProgramSelector.Identifier> getRemoved() { 516 return mRemoved; 517 } 518 519 @Override equals(@ullable Object obj)520 public boolean equals(@Nullable Object obj) { 521 if (this == obj) return true; 522 if (!(obj instanceof Chunk)) return false; 523 Chunk other = (Chunk) obj; 524 525 if (mPurge != other.mPurge) return false; 526 if (mComplete != other.mComplete) return false; 527 if (!Objects.equals(mModified, other.mModified)) return false; 528 if (!Objects.equals(mRemoved, other.mRemoved)) return false; 529 return true; 530 } 531 532 @Override toString()533 public String toString() { 534 return "Chunk [mPurge=" + mPurge + ", mComplete=" + mComplete 535 + ", mModified=" + mModified + ", mRemoved=" + mRemoved + "]"; 536 } 537 } 538 } 539