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