1 /* 2 * Copyright (C) 2011 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 com.android.internal.telephony; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.os.Build; 21 22 import java.util.ArrayList; 23 import java.util.Iterator; 24 import java.util.stream.Collectors; 25 26 /** 27 * Clients can enable reception of SMS-CB messages for specific ranges of 28 * message identifiers (channels). This class keeps track of the currently 29 * enabled message identifiers and calls abstract methods to update the 30 * radio when the range of enabled message identifiers changes. 31 * 32 * An update is a call to {@link #startUpdate} followed by zero or more 33 * calls to {@link #addRange} followed by a call to {@link #finishUpdate}. 34 * Calls to {@link #enableRange} and {@link #disableRange} will perform 35 * an incremental update operation if the enabled ranges have changed. 36 * A full update operation (i.e. after a radio reset) can be performed 37 * by a call to {@link #updateRanges}. 38 * 39 * Clients are identified by String (the name associated with the User ID 40 * of the caller) so that a call to remove a range can be mapped to the 41 * client that enabled that range (or else rejected). 42 */ 43 public abstract class IntRangeManager { 44 45 /** 46 * Initial capacity for IntRange clients array list. There will be 47 * few cell broadcast listeners on a typical device, so this can be small. 48 */ 49 private static final int INITIAL_CLIENTS_ARRAY_SIZE = 4; 50 51 /** 52 * One or more clients forming the continuous range [startId, endId]. 53 * <p>When a client is added, the IntRange may merge with one or more 54 * adjacent IntRanges to form a single combined IntRange. 55 * <p>When a client is removed, the IntRange may divide into several 56 * non-contiguous IntRanges. 57 */ 58 private class IntRange { 59 int mStartId; 60 int mEndId; 61 // sorted by earliest start id 62 final ArrayList<ClientRange> mClients; 63 64 /** 65 * Create a new IntRange with a single client. 66 * @param startId the first id included in the range 67 * @param endId the last id included in the range 68 * @param client the client requesting the enabled range 69 */ IntRange(int startId, int endId, String client)70 IntRange(int startId, int endId, String client) { 71 mStartId = startId; 72 mEndId = endId; 73 mClients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE); 74 mClients.add(new ClientRange(startId, endId, client)); 75 } 76 77 /** 78 * Create a new IntRange for an existing ClientRange. 79 * @param clientRange the initial ClientRange to add 80 */ IntRange(ClientRange clientRange)81 IntRange(ClientRange clientRange) { 82 mStartId = clientRange.mStartId; 83 mEndId = clientRange.mEndId; 84 mClients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE); 85 mClients.add(clientRange); 86 } 87 88 /** 89 * Create a new IntRange from an existing IntRange. This is used for 90 * removing a ClientRange, because new IntRanges may need to be created 91 * for any gaps that open up after the ClientRange is removed. A copy 92 * is made of the elements of the original IntRange preceding the element 93 * that is being removed. The following elements will be added to this 94 * IntRange or to a new IntRange when a gap is found. 95 * @param intRange the original IntRange to copy elements from 96 * @param numElements the number of elements to copy from the original 97 */ IntRange(IntRange intRange, int numElements)98 IntRange(IntRange intRange, int numElements) { 99 mStartId = intRange.mStartId; 100 mEndId = intRange.mEndId; 101 mClients = new ArrayList<ClientRange>(intRange.mClients.size()); 102 for (int i=0; i < numElements; i++) { 103 mClients.add(intRange.mClients.get(i)); 104 } 105 } 106 107 /** 108 * Insert new ClientRange in order by start id, then by end id 109 * <p>If the new ClientRange is known to be sorted before or after the 110 * existing ClientRanges, or at a particular index, it can be added 111 * to the clients array list directly, instead of via this method. 112 * <p>Note that this can be changed from linear to binary search if the 113 * number of clients grows large enough that it would make a difference. 114 * @param range the new ClientRange to insert 115 */ insert(ClientRange range)116 void insert(ClientRange range) { 117 int len = mClients.size(); 118 int insert = -1; 119 for (int i=0; i < len; i++) { 120 ClientRange nextRange = mClients.get(i); 121 if (range.mStartId <= nextRange.mStartId) { 122 // ignore duplicate ranges from the same client 123 if (!range.equals(nextRange)) { 124 // check if same startId, then order by endId 125 if (range.mStartId == nextRange.mStartId 126 && range.mEndId > nextRange.mEndId) { 127 insert = i + 1; 128 if (insert < len) { 129 // there may be more client following with same startId 130 // new [1, 5] existing [1, 2] [1, 4] [1, 7] 131 continue; 132 } 133 break; 134 } 135 mClients.add(i, range); 136 } 137 return; 138 } 139 } 140 if (insert != -1 && insert < len) { 141 mClients.add(insert, range); 142 return; 143 } 144 mClients.add(range); // append to end of list 145 } 146 147 @Override toString()148 public String toString() { 149 return "[" + mStartId + "-" + mEndId + "]"; 150 } 151 } 152 /** 153 * The message id range for a single client. 154 */ 155 private class ClientRange { 156 final int mStartId; 157 final int mEndId; 158 final String mClient; 159 ClientRange(int startId, int endId, String client)160 ClientRange(int startId, int endId, String client) { 161 mStartId = startId; 162 mEndId = endId; 163 mClient = client; 164 } 165 166 @Override equals(Object o)167 public boolean equals(Object o) { 168 if (o != null && o instanceof ClientRange) { 169 ClientRange other = (ClientRange) o; 170 return mStartId == other.mStartId && 171 mEndId == other.mEndId && 172 mClient.equals(other.mClient); 173 } else { 174 return false; 175 } 176 } 177 178 @Override hashCode()179 public int hashCode() { 180 return (mStartId * 31 + mEndId) * 31 + mClient.hashCode(); 181 } 182 } 183 184 /** 185 * List of integer ranges, one per client, sorted by start id. 186 */ 187 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 188 private ArrayList<IntRange> mRanges = new ArrayList<IntRange>(); 189 IntRangeManager()190 protected IntRangeManager() {} 191 192 /** 193 * Clear all the ranges. 194 */ clearRanges()195 public synchronized void clearRanges() { 196 mRanges.clear(); 197 } 198 199 /** 200 * Enable a range for the specified client and update ranges 201 * if necessary. If {@link #finishUpdate} returns failure, 202 * false is returned and the range is not added. 203 * 204 * @param startId the first id included in the range 205 * @param endId the last id included in the range 206 * @param client the client requesting the enabled range 207 * @return true if successful, false otherwise 208 */ enableRange(int startId, int endId, String client)209 public synchronized boolean enableRange(int startId, int endId, String client) { 210 int len = mRanges.size(); 211 212 // empty range list: add the initial IntRange 213 if (len == 0) { 214 if (tryAddRanges(startId, endId, true)) { 215 mRanges.add(new IntRange(startId, endId, client)); 216 return true; 217 } else { 218 return false; // failed to update radio 219 } 220 } 221 222 for (int startIndex = 0; startIndex < len; startIndex++) { 223 IntRange range = mRanges.get(startIndex); 224 if ((startId) >= range.mStartId && (endId) <= range.mEndId) { 225 // exact same range: new [1, 1] existing [1, 1] 226 // range already enclosed in existing: new [3, 3], [1,3] 227 // no radio update necessary. 228 // duplicate "client" check is done in insert, attempt to insert. 229 range.insert(new ClientRange(startId, endId, client)); 230 return true; 231 } else if ((startId - 1) == range.mEndId) { 232 // new [3, x] existing [1, 2] OR new [2, 2] existing [1, 1] 233 // found missing link? check if next range can be joined 234 int newRangeEndId = endId; 235 IntRange nextRange = null; 236 if ((startIndex + 1) < len) { 237 nextRange = mRanges.get(startIndex + 1); 238 if ((nextRange.mStartId - 1) <= endId) { 239 // new [3, x] existing [1, 2] [5, 7] OR new [2 , 2] existing [1, 1] [3, 5] 240 if (endId <= nextRange.mEndId) { 241 // new [3, 6] existing [1, 2] [5, 7] 242 newRangeEndId = nextRange.mStartId - 1; // need to enable [3, 4] 243 } 244 } else { 245 // mark nextRange to be joined as null. 246 nextRange = null; 247 } 248 } 249 if (tryAddRanges(startId, newRangeEndId, true)) { 250 range.mEndId = endId; 251 range.insert(new ClientRange(startId, endId, client)); 252 253 // found missing link? check if next range can be joined 254 if (nextRange != null) { 255 if (range.mEndId < nextRange.mEndId) { 256 // new [3, 6] existing [1, 2] [5, 10] 257 range.mEndId = nextRange.mEndId; 258 } 259 range.mClients.addAll(nextRange.mClients); 260 mRanges.remove(nextRange); 261 } 262 return true; 263 } else { 264 return false; // failed to update radio 265 } 266 } else if (startId < range.mStartId) { 267 // new [1, x] , existing [5, y] 268 // test if new range completely precedes this range 269 // note that [1, 4] and [5, 6] coalesce to [1, 6] 270 if ((endId + 1) < range.mStartId) { 271 // new [1, 3] existing [5, 6] non contiguous case 272 // insert new int range before previous first range 273 if (tryAddRanges(startId, endId, true)) { 274 mRanges.add(startIndex, new IntRange(startId, endId, client)); 275 return true; 276 } else { 277 return false; // failed to update radio 278 } 279 } else if (endId <= range.mEndId) { 280 // new [1, 4] existing [5, 6] or new [1, 1] existing [2, 2] 281 // extend the start of this range 282 if (tryAddRanges(startId, range.mStartId - 1, true)) { 283 range.mStartId = startId; 284 range.mClients.add(0, new ClientRange(startId, endId, client)); 285 return true; 286 } else { 287 return false; // failed to update radio 288 } 289 } else { 290 // find last range that can coalesce into the new combined range 291 for (int endIndex = startIndex+1; endIndex < len; endIndex++) { 292 IntRange endRange = mRanges.get(endIndex); 293 if ((endId + 1) < endRange.mStartId) { 294 // new [1, 10] existing [2, 3] [14, 15] 295 // try to add entire new range 296 if (tryAddRanges(startId, endId, true)) { 297 range.mStartId = startId; 298 range.mEndId = endId; 299 // insert new ClientRange before existing ranges 300 range.mClients.add(0, new ClientRange(startId, endId, client)); 301 // coalesce range with following ranges up to endIndex-1 302 // remove each range after adding its elements, so the index 303 // of the next range to join is always startIndex+1. 304 // i is the index if no elements were removed: we only care 305 // about the number of loop iterations, not the value of i. 306 int joinIndex = startIndex + 1; 307 for (int i = joinIndex; i < endIndex; i++) { 308 // new [1, 10] existing [2, 3] [5, 6] [14, 15] 309 IntRange joinRange = mRanges.get(joinIndex); 310 range.mClients.addAll(joinRange.mClients); 311 mRanges.remove(joinRange); 312 } 313 return true; 314 } else { 315 return false; // failed to update radio 316 } 317 } else if (endId <= endRange.mEndId) { 318 // new [1, 10] existing [2, 3] [5, 15] 319 // add range from start id to start of last overlapping range, 320 // values from endRange.startId to endId are already enabled 321 if (tryAddRanges(startId, endRange.mStartId - 1, true)) { 322 range.mStartId = startId; 323 range.mEndId = endRange.mEndId; 324 // insert new ClientRange before existing ranges 325 range.mClients.add(0, new ClientRange(startId, endId, client)); 326 // coalesce range with following ranges up to endIndex 327 // remove each range after adding its elements, so the index 328 // of the next range to join is always startIndex+1. 329 // i is the index if no elements were removed: we only care 330 // about the number of loop iterations, not the value of i. 331 int joinIndex = startIndex + 1; 332 for (int i = joinIndex; i <= endIndex; i++) { 333 IntRange joinRange = mRanges.get(joinIndex); 334 range.mClients.addAll(joinRange.mClients); 335 mRanges.remove(joinRange); 336 } 337 return true; 338 } else { 339 return false; // failed to update radio 340 } 341 } 342 } 343 344 // new [1, 10] existing [2, 3] 345 // endId extends past all existing IntRanges: combine them all together 346 if (tryAddRanges(startId, endId, true)) { 347 range.mStartId = startId; 348 range.mEndId = endId; 349 // insert new ClientRange before existing ranges 350 range.mClients.add(0, new ClientRange(startId, endId, client)); 351 // coalesce range with following ranges up to len-1 352 // remove each range after adding its elements, so the index 353 // of the next range to join is always startIndex+1. 354 // i is the index if no elements were removed: we only care 355 // about the number of loop iterations, not the value of i. 356 int joinIndex = startIndex + 1; 357 for (int i = joinIndex; i < len; i++) { 358 // new [1, 10] existing [2, 3] [5, 6] 359 IntRange joinRange = mRanges.get(joinIndex); 360 range.mClients.addAll(joinRange.mClients); 361 mRanges.remove(joinRange); 362 } 363 return true; 364 } else { 365 return false; // failed to update radio 366 } 367 } 368 } else if ((startId + 1) <= range.mEndId) { 369 // new [2, x] existing [1, 4] 370 if (endId <= range.mEndId) { 371 // new [2, 3] existing [1, 4] 372 // completely contained in existing range; no radio changes 373 range.insert(new ClientRange(startId, endId, client)); 374 return true; 375 } else { 376 // new [2, 5] existing [1, 4] 377 // find last range that can coalesce into the new combined range 378 int endIndex = startIndex; 379 for (int testIndex = startIndex+1; testIndex < len; testIndex++) { 380 IntRange testRange = mRanges.get(testIndex); 381 if ((endId + 1) < testRange.mStartId) { 382 break; 383 } else { 384 endIndex = testIndex; 385 } 386 } 387 // no adjacent IntRanges to combine 388 if (endIndex == startIndex) { 389 // new [2, 5] existing [1, 4] 390 // add range from range.endId+1 to endId, 391 // values from startId to range.endId are already enabled 392 if (tryAddRanges(range.mEndId + 1, endId, true)) { 393 range.mEndId = endId; 394 range.insert(new ClientRange(startId, endId, client)); 395 return true; 396 } else { 397 return false; // failed to update radio 398 } 399 } 400 // get last range to coalesce into start range 401 IntRange endRange = mRanges.get(endIndex); 402 // Values from startId to range.endId have already been enabled. 403 // if endId > endRange.endId, then enable range from range.endId+1 to endId, 404 // else enable range from range.endId+1 to endRange.startId-1, because 405 // values from endRange.startId to endId have already been added. 406 int newRangeEndId = (endId <= endRange.mEndId) ? endRange.mStartId - 1 : endId; 407 // new [2, 10] existing [1, 4] [7, 8] OR 408 // new [2, 10] existing [1, 4] [7, 15] 409 if (tryAddRanges(range.mEndId + 1, newRangeEndId, true)) { 410 newRangeEndId = (endId <= endRange.mEndId) ? endRange.mEndId : endId; 411 range.mEndId = newRangeEndId; 412 // insert new ClientRange in place 413 range.insert(new ClientRange(startId, endId, client)); 414 // coalesce range with following ranges up to endIndex 415 // remove each range after adding its elements, so the index 416 // of the next range to join is always startIndex+1 (joinIndex). 417 // i is the index if no elements had been removed: we only care 418 // about the number of loop iterations, not the value of i. 419 int joinIndex = startIndex + 1; 420 for (int i = joinIndex; i <= endIndex; i++) { 421 IntRange joinRange = mRanges.get(joinIndex); 422 range.mClients.addAll(joinRange.mClients); 423 mRanges.remove(joinRange); 424 } 425 return true; 426 } else { 427 return false; // failed to update radio 428 } 429 } 430 } 431 } 432 433 // new [5, 6], existing [1, 3] 434 // append new range after existing IntRanges 435 if (tryAddRanges(startId, endId, true)) { 436 mRanges.add(new IntRange(startId, endId, client)); 437 return true; 438 } else { 439 return false; // failed to update radio 440 } 441 } 442 443 /** 444 * Disable a range for the specified client and update ranges 445 * if necessary. If {@link #finishUpdate} returns failure, 446 * false is returned and the range is not removed. 447 * 448 * @param startId the first id included in the range 449 * @param endId the last id included in the range 450 * @param client the client requesting to disable the range 451 * @return true if successful, false otherwise 452 */ disableRange(int startId, int endId, String client)453 public synchronized boolean disableRange(int startId, int endId, String client) { 454 int len = mRanges.size(); 455 456 for (int i=0; i < len; i++) { 457 IntRange range = mRanges.get(i); 458 if (startId < range.mStartId) { 459 return false; // not found 460 } else if (endId <= range.mEndId) { 461 // found the IntRange that encloses the client range, if any 462 // search for it in the clients list 463 ArrayList<ClientRange> clients = range.mClients; 464 465 // handle common case of IntRange containing one ClientRange 466 int crLength = clients.size(); 467 if (crLength == 1) { 468 ClientRange cr = clients.get(0); 469 if (cr.mStartId == startId && cr.mEndId == endId && cr.mClient.equals(client)) { 470 // mRange contains only what's enabled. 471 // remove the range from mRange then update the radio 472 mRanges.remove(i); 473 if (updateRanges()) { 474 return true; 475 } else { 476 // failed to update radio. insert back the range 477 mRanges.add(i, range); 478 return false; 479 } 480 } else { 481 return false; // not found 482 } 483 } 484 485 // several ClientRanges: remove one, potentially splitting into many IntRanges. 486 // Save the original start and end id for the original IntRange 487 // in case the radio update fails and we have to revert it. If the 488 // update succeeds, we remove the client range and insert the new IntRanges. 489 // clients are ordered by startId then by endId, so client with largest endId 490 // can be anywhere. Need to loop thru to find largestEndId. 491 int largestEndId = Integer.MIN_VALUE; // largest end identifier found 492 boolean updateStarted = false; 493 494 // crlength >= 2 495 for (int crIndex=0; crIndex < crLength; crIndex++) { 496 ClientRange cr = clients.get(crIndex); 497 if (cr.mStartId == startId && cr.mEndId == endId && cr.mClient.equals(client)) { 498 // found the ClientRange to remove, check if it's the last in the list 499 if (crIndex == crLength - 1) { 500 if (range.mEndId == largestEndId) { 501 // remove [2, 5] from [1, 7] [2, 5] 502 // no channels to remove from radio; return success 503 clients.remove(crIndex); 504 return true; 505 } else { 506 // disable the channels at the end and lower the end id 507 clients.remove(crIndex); 508 range.mEndId = largestEndId; 509 if (updateRanges()) { 510 return true; 511 } else { 512 clients.add(crIndex, cr); 513 range.mEndId = cr.mEndId; 514 return false; 515 } 516 } 517 } 518 519 // copy the IntRange so that we can remove elements and modify the 520 // start and end id's in the copy, leaving the original unmodified 521 // until after the radio update succeeds 522 IntRange rangeCopy = new IntRange(range, crIndex); 523 524 if (crIndex == 0) { 525 // removing the first ClientRange, so we may need to increase 526 // the start id of the IntRange. 527 // We know there are at least two ClientRanges in the list, 528 // because check for just one ClientRanges case is already handled 529 // so clients.get(1) should always succeed. 530 int nextStartId = clients.get(1).mStartId; 531 if (nextStartId != range.mStartId) { 532 updateStarted = true; 533 rangeCopy.mStartId = nextStartId; 534 } 535 // init largestEndId 536 largestEndId = clients.get(1).mEndId; 537 } 538 539 // go through remaining ClientRanges, creating new IntRanges when 540 // there is a gap in the sequence. After radio update succeeds, 541 // remove the original IntRange and append newRanges to mRanges. 542 // Otherwise, leave the original IntRange in mRanges and return false. 543 ArrayList<IntRange> newRanges = new ArrayList<IntRange>(); 544 545 IntRange currentRange = rangeCopy; 546 for (int nextIndex = crIndex + 1; nextIndex < crLength; nextIndex++) { 547 ClientRange nextCr = clients.get(nextIndex); 548 if (nextCr.mStartId > largestEndId + 1) { 549 updateStarted = true; 550 currentRange.mEndId = largestEndId; 551 newRanges.add(currentRange); 552 currentRange = new IntRange(nextCr); 553 } else { 554 if (currentRange.mEndId < nextCr.mEndId) { 555 currentRange.mEndId = nextCr.mEndId; 556 } 557 currentRange.mClients.add(nextCr); 558 } 559 if (nextCr.mEndId > largestEndId) { 560 largestEndId = nextCr.mEndId; 561 } 562 } 563 564 // remove any channels between largestEndId and endId 565 if (largestEndId < endId) { 566 updateStarted = true; 567 currentRange.mEndId = largestEndId; 568 } 569 newRanges.add(currentRange); 570 571 // replace the original IntRange with newRanges 572 mRanges.remove(i); 573 mRanges.addAll(i, newRanges); 574 if (updateStarted && !updateRanges()) { 575 // failed to update radio. revert back mRange. 576 mRanges.removeAll(newRanges); 577 mRanges.add(i, range); 578 return false; 579 } 580 581 return true; 582 } else { 583 // not the ClientRange to remove; save highest end ID seen so far 584 if (cr.mEndId > largestEndId) { 585 largestEndId = cr.mEndId; 586 } 587 } 588 } 589 } 590 } 591 592 return false; // not found 593 } 594 595 /** 596 * Perform a complete update operation (enable all ranges). Useful 597 * after a radio reset. Calls {@link #startUpdate}, followed by zero or 598 * more calls to {@link #addRange}, followed by {@link #finishUpdate}. 599 * @return true if successful, false otherwise 600 */ updateRanges()601 public boolean updateRanges() { 602 startUpdate(); 603 604 populateAllRanges(); 605 return finishUpdate(); 606 } 607 608 /** 609 * Enable or disable a single range of message identifiers. 610 * @param startId the first id included in the range 611 * @param endId the last id included in the range 612 * @param selected true to enable range, false to disable range 613 * @return true if successful, false otherwise 614 */ tryAddRanges(int startId, int endId, boolean selected)615 protected boolean tryAddRanges(int startId, int endId, boolean selected) { 616 617 startUpdate(); 618 populateAllRanges(); 619 // This is the new range to be enabled 620 addRange(startId, endId, selected); // adds to mConfigList 621 return finishUpdate(); 622 } 623 624 /** 625 * Returns whether the list of ranges is completely empty. 626 * @return true if there are no enabled ranges 627 */ isEmpty()628 public boolean isEmpty() { 629 return mRanges.isEmpty(); 630 } 631 632 /** 633 * Called when attempting to add a single range of message identifiers 634 * Populate all ranges of message identifiers. 635 */ populateAllRanges()636 private void populateAllRanges() { 637 Iterator<IntRange> itr = mRanges.iterator(); 638 // Populate all ranges from mRanges 639 while (itr.hasNext()) { 640 IntRange currRange = (IntRange) itr.next(); 641 addRange(currRange.mStartId, currRange.mEndId, true); 642 } 643 } 644 645 /** 646 * Called when attempting to add a single range of message identifiers 647 * Populate all ranges of message identifiers using clients' ranges. 648 */ populateAllClientRanges()649 private void populateAllClientRanges() { 650 int len = mRanges.size(); 651 for (int i = 0; i < len; i++) { 652 IntRange range = mRanges.get(i); 653 654 int clientLen = range.mClients.size(); 655 for (int j=0; j < clientLen; j++) { 656 ClientRange nextRange = range.mClients.get(j); 657 addRange(nextRange.mStartId, nextRange.mEndId, true); 658 } 659 } 660 } 661 662 /** 663 * Called when the list of enabled ranges has changed. This will be 664 * followed by zero or more calls to {@link #addRange} followed by 665 * a call to {@link #finishUpdate}. 666 */ startUpdate()667 protected abstract void startUpdate(); 668 669 /** 670 * Called after {@link #startUpdate} to indicate a range of enabled 671 * or disabled values. 672 * 673 * @param startId the first id included in the range 674 * @param endId the last id included in the range 675 * @param selected true to enable range, false to disable range 676 */ addRange(int startId, int endId, boolean selected)677 protected abstract void addRange(int startId, int endId, boolean selected); 678 679 /** 680 * Called to indicate the end of a range update started by the 681 * previous call to {@link #startUpdate}. 682 * @return true if successful, false otherwise 683 */ finishUpdate()684 protected abstract boolean finishUpdate(); 685 686 @Override toString()687 public String toString() { 688 return mRanges.stream().map(IntRange::toString).collect(Collectors.joining(",")); 689 } 690 } 691