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