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