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