Home | History | Annotate | Download | only in telephony
      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 java.util.ArrayList;
     20 import 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  */
     39 public 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.
    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             for (int i=0; i < len; i++) {
    115                 ClientRange nextRange = clients.get(i);
    116                 if (range.startId <= nextRange.startId) {
    117                     // ignore duplicate ranges from the same client
    118                     if (!range.equals(nextRange)) {
    119                         clients.add(i, range);
    120                     }
    121                     return;
    122                 }
    123             }
    124             clients.add(range);    // append to end of list
    125         }
    126     }
    127 
    128     /**
    129      * The message id range for a single client.
    130      */
    131     private class ClientRange {
    132         final int startId;
    133         final int endId;
    134         final String client;
    135 
    136         ClientRange(int startId, int endId, String client) {
    137             this.startId = startId;
    138             this.endId = endId;
    139             this.client = client;
    140         }
    141 
    142         @Override
    143         public boolean equals(Object o) {
    144             if (o != null && o instanceof ClientRange) {
    145                 ClientRange other = (ClientRange) o;
    146                 return startId == other.startId &&
    147                         endId == other.endId &&
    148                         client.equals(other.client);
    149             } else {
    150                 return false;
    151             }
    152         }
    153 
    154         @Override
    155         public int hashCode() {
    156             return (startId * 31 + endId) * 31 + client.hashCode();
    157         }
    158     }
    159 
    160     /**
    161      * List of integer ranges, one per client, sorted by start id.
    162      */
    163     private ArrayList<IntRange> mRanges = new ArrayList<IntRange>();
    164 
    165     protected IntRangeManager() {}
    166 
    167     /**
    168      * Enable a range for the specified client and update ranges
    169      * if necessary. If {@link #finishUpdate} returns failure,
    170      * false is returned and the range is not added.
    171      *
    172      * @param startId the first id included in the range
    173      * @param endId the last id included in the range
    174      * @param client the client requesting the enabled range
    175      * @return true if successful, false otherwise
    176      */
    177     public synchronized boolean enableRange(int startId, int endId, String client) {
    178         int len = mRanges.size();
    179 
    180         // empty range list: add the initial IntRange
    181         if (len == 0) {
    182             if (tryAddSingleRange(startId, endId, true)) {
    183                 mRanges.add(new IntRange(startId, endId, client));
    184                 return true;
    185             } else {
    186                 return false;   // failed to update radio
    187             }
    188         }
    189 
    190         for (int startIndex = 0; startIndex < len; startIndex++) {
    191             IntRange range = mRanges.get(startIndex);
    192             if (startId < range.startId) {
    193                 // test if new range completely precedes this range
    194                 // note that [1, 4] and [5, 6] coalesce to [1, 6]
    195                 if ((endId + 1) < range.startId) {
    196                     // insert new int range before previous first range
    197                     if (tryAddSingleRange(startId, endId, true)) {
    198                         mRanges.add(startIndex, new IntRange(startId, endId, client));
    199                         return true;
    200                     } else {
    201                         return false;   // failed to update radio
    202                     }
    203                 } else if (endId <= range.endId) {
    204                     // extend the start of this range
    205                     if (tryAddSingleRange(startId, range.startId - 1, true)) {
    206                         range.startId = startId;
    207                         range.clients.add(0, new ClientRange(startId, endId, client));
    208                         return true;
    209                     } else {
    210                         return false;   // failed to update radio
    211                     }
    212                 } else {
    213                     // find last range that can coalesce into the new combined range
    214                     for (int endIndex = startIndex+1; endIndex < len; endIndex++) {
    215                         IntRange endRange = mRanges.get(endIndex);
    216                         if ((endId + 1) < endRange.startId) {
    217                             // try to add entire new range
    218                             if (tryAddSingleRange(startId, endId, true)) {
    219                                 range.startId = startId;
    220                                 range.endId = endId;
    221                                 // insert new ClientRange before existing ranges
    222                                 range.clients.add(0, new ClientRange(startId, endId, client));
    223                                 // coalesce range with following ranges up to endIndex-1
    224                                 // remove each range after adding its elements, so the index
    225                                 // of the next range to join is always startIndex+1.
    226                                 // i is the index if no elements were removed: we only care
    227                                 // about the number of loop iterations, not the value of i.
    228                                 int joinIndex = startIndex + 1;
    229                                 for (int i = joinIndex; i < endIndex; i++) {
    230                                     IntRange joinRange = mRanges.get(joinIndex);
    231                                     range.clients.addAll(joinRange.clients);
    232                                     mRanges.remove(joinRange);
    233                                 }
    234                                 return true;
    235                             } else {
    236                                 return false;   // failed to update radio
    237                             }
    238                         } else if (endId <= endRange.endId) {
    239                             // add range from start id to start of last overlapping range,
    240                             // values from endRange.startId to endId are already enabled
    241                             if (tryAddSingleRange(startId, endRange.startId - 1, true)) {
    242                                 range.startId = startId;
    243                                 range.endId = endRange.endId;
    244                                 // insert new ClientRange before existing ranges
    245                                 range.clients.add(0, new ClientRange(startId, endId, client));
    246                                 // coalesce range with following ranges up to endIndex
    247                                 // remove each range after adding its elements, so the index
    248                                 // of the next range to join is always startIndex+1.
    249                                 // i is the index if no elements were removed: we only care
    250                                 // about the number of loop iterations, not the value of i.
    251                                 int joinIndex = startIndex + 1;
    252                                 for (int i = joinIndex; i <= endIndex; i++) {
    253                                     IntRange joinRange = mRanges.get(joinIndex);
    254                                     range.clients.addAll(joinRange.clients);
    255                                     mRanges.remove(joinRange);
    256                                 }
    257                                 return true;
    258                             } else {
    259                                 return false;   // failed to update radio
    260                             }
    261                         }
    262                     }
    263 
    264                     // endId extends past all existing IntRanges: combine them all together
    265                     if (tryAddSingleRange(startId, endId, true)) {
    266                         range.startId = startId;
    267                         range.endId = endId;
    268                         // insert new ClientRange before existing ranges
    269                         range.clients.add(0, new ClientRange(startId, endId, client));
    270                         // coalesce range with following ranges up to len-1
    271                         // remove each range after adding its elements, so the index
    272                         // of the next range to join is always startIndex+1.
    273                         // i is the index if no elements were removed: we only care
    274                         // about the number of loop iterations, not the value of i.
    275                         int joinIndex = startIndex + 1;
    276                         for (int i = joinIndex; i < len; i++) {
    277                             IntRange joinRange = mRanges.get(joinIndex);
    278                             range.clients.addAll(joinRange.clients);
    279                             mRanges.remove(joinRange);
    280                         }
    281                         return true;
    282                     } else {
    283                         return false;   // failed to update radio
    284                     }
    285                 }
    286             } else if ((startId + 1) <= range.endId) {
    287                 if (endId <= range.endId) {
    288                     // completely contained in existing range; no radio changes
    289                     range.insert(new ClientRange(startId, endId, client));
    290                     return true;
    291                 } else {
    292                     // find last range that can coalesce into the new combined range
    293                     int endIndex = startIndex;
    294                     for (int testIndex = startIndex+1; testIndex < len; testIndex++) {
    295                         IntRange testRange = mRanges.get(testIndex);
    296                         if ((endId + 1) < testRange.startId) {
    297                             break;
    298                         } else {
    299                             endIndex = testIndex;
    300                         }
    301                     }
    302                     // no adjacent IntRanges to combine
    303                     if (endIndex == startIndex) {
    304                         // add range from range.endId+1 to endId,
    305                         // values from startId to range.endId are already enabled
    306                         if (tryAddSingleRange(range.endId + 1, endId, true)) {
    307                             range.endId = endId;
    308                             range.insert(new ClientRange(startId, endId, client));
    309                             return true;
    310                         } else {
    311                             return false;   // failed to update radio
    312                         }
    313                     }
    314                     // get last range to coalesce into start range
    315                     IntRange endRange = mRanges.get(endIndex);
    316                     // Values from startId to range.endId have already been enabled.
    317                     // if endId > endRange.endId, then enable range from range.endId+1 to endId,
    318                     // else enable range from range.endId+1 to endRange.startId-1, because
    319                     // values from endRange.startId to endId have already been added.
    320                     int newRangeEndId = (endId <= endRange.endId) ? endRange.startId - 1 : endId;
    321                     if (tryAddSingleRange(range.endId + 1, newRangeEndId, true)) {
    322                         range.endId = endId;
    323                         // insert new ClientRange in place
    324                         range.insert(new ClientRange(startId, endId, client));
    325                         // coalesce range with following ranges up to endIndex-1
    326                         // remove each range after adding its elements, so the index
    327                         // of the next range to join is always startIndex+1 (joinIndex).
    328                         // i is the index if no elements had been removed: we only care
    329                         // about the number of loop iterations, not the value of i.
    330                         int joinIndex = startIndex + 1;
    331                         for (int i = joinIndex; i < endIndex; i++) {
    332                             IntRange joinRange = mRanges.get(joinIndex);
    333                             range.clients.addAll(joinRange.clients);
    334                             mRanges.remove(joinRange);
    335                         }
    336                         return true;
    337                     } else {
    338                         return false;   // failed to update radio
    339                     }
    340                 }
    341             }
    342         }
    343 
    344         // append new range after existing IntRanges
    345         if (tryAddSingleRange(startId, endId, true)) {
    346             mRanges.add(new IntRange(startId, endId, client));
    347             return true;
    348         } else {
    349             return false;   // failed to update radio
    350         }
    351     }
    352 
    353     /**
    354      * Disable a range for the specified client and update ranges
    355      * if necessary. If {@link #finishUpdate} returns failure,
    356      * false is returned and the range is not removed.
    357      *
    358      * @param startId the first id included in the range
    359      * @param endId the last id included in the range
    360      * @param client the client requesting to disable the range
    361      * @return true if successful, false otherwise
    362      */
    363     public synchronized boolean disableRange(int startId, int endId, String client) {
    364         int len = mRanges.size();
    365 
    366         for (int i=0; i < len; i++) {
    367             IntRange range = mRanges.get(i);
    368             if (startId < range.startId) {
    369                 return false;   // not found
    370             } else if (endId <= range.endId) {
    371                 // found the IntRange that encloses the client range, if any
    372                 // search for it in the clients list
    373                 ArrayList<ClientRange> clients = range.clients;
    374 
    375                 // handle common case of IntRange containing one ClientRange
    376                 int crLength = clients.size();
    377                 if (crLength == 1) {
    378                     ClientRange cr = clients.get(0);
    379                     if (cr.startId == startId && cr.endId == endId && cr.client.equals(client)) {
    380                         // disable range in radio then remove the entire IntRange
    381                         if (tryAddSingleRange(startId, endId, false)) {
    382                             mRanges.remove(i);
    383                             return true;
    384                         } else {
    385                             return false;   // failed to update radio
    386                         }
    387                     } else {
    388                         return false;   // not found
    389                     }
    390                 }
    391 
    392                 // several ClientRanges: remove one, potentially splitting into many IntRanges.
    393                 // Save the original start and end id for the original IntRange
    394                 // in case the radio update fails and we have to revert it. If the
    395                 // update succeeds, we remove the client range and insert the new IntRanges.
    396                 int largestEndId = Integer.MIN_VALUE;  // largest end identifier found
    397                 boolean updateStarted = false;
    398 
    399                 for (int crIndex=0; crIndex < crLength; crIndex++) {
    400                     ClientRange cr = clients.get(crIndex);
    401                     if (cr.startId == startId && cr.endId == endId && cr.client.equals(client)) {
    402                         // found the ClientRange to remove, check if it's the last in the list
    403                         if (crIndex == crLength - 1) {
    404                             if (range.endId == largestEndId) {
    405                                 // no channels to remove from radio; return success
    406                                 clients.remove(crIndex);
    407                                 return true;
    408                             } else {
    409                                 // disable the channels at the end and lower the end id
    410                                 if (tryAddSingleRange(largestEndId + 1, range.endId, false)) {
    411                                     clients.remove(crIndex);
    412                                     range.endId = largestEndId;
    413                                     return true;
    414                                 } else {
    415                                     return false;
    416                                 }
    417                             }
    418                         }
    419 
    420                         // copy the IntRange so that we can remove elements and modify the
    421                         // start and end id's in the copy, leaving the original unmodified
    422                         // until after the radio update succeeds
    423                         IntRange rangeCopy = new IntRange(range, crIndex);
    424 
    425                         if (crIndex == 0) {
    426                             // removing the first ClientRange, so we may need to increase
    427                             // the start id of the IntRange.
    428                             // We know there are at least two ClientRanges in the list,
    429                             // so clients.get(1) should always succeed.
    430                             int nextStartId = clients.get(1).startId;
    431                             if (nextStartId != range.startId) {
    432                                 startUpdate();
    433                                 updateStarted = true;
    434                                 addRange(range.startId, nextStartId - 1, false);
    435                                 rangeCopy.startId = nextStartId;
    436                             }
    437                             // init largestEndId
    438                             largestEndId = clients.get(1).endId;
    439                         }
    440 
    441                         // go through remaining ClientRanges, creating new IntRanges when
    442                         // there is a gap in the sequence. After radio update succeeds,
    443                         // remove the original IntRange and append newRanges to mRanges.
    444                         // Otherwise, leave the original IntRange in mRanges and return false.
    445                         ArrayList<IntRange> newRanges = new ArrayList<IntRange>();
    446 
    447                         IntRange currentRange = rangeCopy;
    448                         for (int nextIndex = crIndex + 1; nextIndex < crLength; nextIndex++) {
    449                             ClientRange nextCr = clients.get(nextIndex);
    450                             if (nextCr.startId > largestEndId + 1) {
    451                                 if (!updateStarted) {
    452                                     startUpdate();
    453                                     updateStarted = true;
    454                                 }
    455                                 addRange(largestEndId + 1, nextCr.startId - 1, false);
    456                                 currentRange.endId = largestEndId;
    457                                 newRanges.add(currentRange);
    458                                 currentRange = new IntRange(nextCr);
    459                             } else {
    460                                 currentRange.clients.add(nextCr);
    461                             }
    462                             if (nextCr.endId > largestEndId) {
    463                                 largestEndId = nextCr.endId;
    464                             }
    465                         }
    466 
    467                         // remove any channels between largestEndId and endId
    468                         if (largestEndId < endId) {
    469                             if (!updateStarted) {
    470                                 startUpdate();
    471                                 updateStarted = true;
    472                             }
    473                             addRange(largestEndId + 1, endId, false);
    474                             currentRange.endId = largestEndId;
    475                         }
    476                         newRanges.add(currentRange);
    477 
    478                         if (updateStarted && !finishUpdate()) {
    479                             return false;   // failed to update radio
    480                         }
    481 
    482                         // replace the original IntRange with newRanges
    483                         mRanges.remove(i);
    484                         mRanges.addAll(i, newRanges);
    485                         return true;
    486                     } else {
    487                         // not the ClientRange to remove; save highest end ID seen so far
    488                         if (cr.endId > largestEndId) {
    489                             largestEndId = cr.endId;
    490                         }
    491                     }
    492                 }
    493             }
    494         }
    495 
    496         return false;   // not found
    497     }
    498 
    499     /**
    500      * Perform a complete update operation (enable all ranges). Useful
    501      * after a radio reset. Calls {@link #startUpdate}, followed by zero or
    502      * more calls to {@link #addRange}, followed by {@link #finishUpdate}.
    503      * @return true if successful, false otherwise
    504      */
    505     public boolean updateRanges() {
    506         startUpdate();
    507         Iterator<IntRange> iterator = mRanges.iterator();
    508         if (iterator.hasNext()) {
    509             IntRange range = iterator.next();
    510             int start = range.startId;
    511             int end = range.endId;
    512             // accumulate ranges of [startId, endId]
    513             while (iterator.hasNext()) {
    514                 IntRange nextNode = iterator.next();
    515                 // [startIdA, endIdA], [endIdA + 1, endIdB] -> [startIdA, endIdB]
    516                 if (nextNode.startId <= (end + 1)) {
    517                     if (nextNode.endId > end) {
    518                         end = nextNode.endId;
    519                     }
    520                 } else {
    521                     addRange(start, end, true);
    522                     start = nextNode.startId;
    523                     end = nextNode.endId;
    524                 }
    525             }
    526             // add final range
    527             addRange(start, end, true);
    528         }
    529         return finishUpdate();
    530     }
    531 
    532     /**
    533      * Enable or disable a single range of message identifiers.
    534      * @param startId the first id included in the range
    535      * @param endId the last id included in the range
    536      * @param selected true to enable range, false to disable range
    537      * @return true if successful, false otherwise
    538      */
    539     private boolean tryAddSingleRange(int startId, int endId, boolean selected) {
    540         startUpdate();
    541         addRange(startId, endId, selected);
    542         return finishUpdate();
    543     }
    544 
    545     /**
    546      * Called when the list of enabled ranges has changed. This will be
    547      * followed by zero or more calls to {@link #addRange} followed by
    548      * a call to {@link #finishUpdate}.
    549      */
    550     protected abstract void startUpdate();
    551 
    552     /**
    553      * Called after {@link #startUpdate} to indicate a range of enabled
    554      * or disabled values.
    555      *
    556      * @param startId the first id included in the range
    557      * @param endId the last id included in the range
    558      * @param selected true to enable range, false to disable range
    559      */
    560     protected abstract void addRange(int startId, int endId, boolean selected);
    561 
    562     /**
    563      * Called to indicate the end of a range update started by the
    564      * previous call to {@link #startUpdate}.
    565      * @return true if successful, false otherwise
    566      */
    567     protected abstract boolean finishUpdate();
    568 }
    569