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