1/**
2 * Copyright (C) 2009 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 android.util.Log;
20import android.util.Pair;
21import android.text.TextUtils;
22
23import java.util.Random;
24import java.util.ArrayList;
25
26/**
27 * Retry manager allows a simple way to declare a series of
28 * retry timeouts. After creating a RetryManager the configure
29 * method is used to define the sequence. A simple linear series
30 * may be initialized using configure with three integer parameters
31 * The other configure method allows a series to be declared using
32 * a string.
33 *<p>
34 * The format of the configuration string is a series of parameters
35 * separated by a comma. There are two name value pair parameters plus a series
36 * of delay times. The units of of these delay times is unspecified.
37 * The name value pairs which may be specified are:
38 *<ul>
39 *<li>max_retries=<value>
40 *<li>default_randomizationTime=<value>
41 *</ul>
42 *<p>
43 * max_retries is the number of times that incrementRetryCount
44 * maybe called before isRetryNeeded will return false. if value
45 * is infinite then isRetryNeeded will always return true.
46 *
47 * default_randomizationTime will be used as the randomizationTime
48 * for delay times which have no supplied randomizationTime. If
49 * default_randomizationTime is not defined it defaults to 0.
50 *<p>
51 * The other parameters define The series of delay times and each
52 * may have an optional randomization value separated from the
53 * delay time by a colon.
54 *<p>
55 * Examples:
56 * <ul>
57 * <li>3 retries with no randomization value which means its 0:
58 * <ul><li><code>"1000, 2000, 3000"</code></ul>
59 *
60 * <li>10 retries with a 500 default randomization value for each and
61 * the 4..10 retries all using 3000 as the delay:
62 * <ul><li><code>"max_retries=10, default_randomization=500, 1000, 2000, 3000"</code></ul>
63 *
64 * <li>4 retries with a 100 as the default randomization value for the first 2 values and
65 * the other two having specified values of 500:
66 * <ul><li><code>"default_randomization=100, 1000, 2000, 4000:500, 5000:500"</code></ul>
67 *
68 * <li>Infinite number of retries with the first one at 1000, the second at 2000 all
69 * others will be at 3000.
70 * <ul><li><code>"max_retries=infinite,1000,2000,3000</code></ul>
71 * </ul>
72 *
73 * {@hide}
74 */
75public class RetryManager {
76    static public final String LOG_TAG = "RetryManager";
77    static public final boolean DBG = false;
78
79    /**
80     * Retry record with times in milli-seconds
81     */
82    private static class RetryRec {
83        RetryRec(int delayTime, int randomizationTime) {
84            mDelayTime = delayTime;
85            mRandomizationTime = randomizationTime;
86        }
87
88        int mDelayTime;
89        int mRandomizationTime;
90    }
91
92    /** The array of retry records */
93    private ArrayList<RetryRec> mRetryArray = new ArrayList<RetryRec>();
94
95    /** When true isRetryNeeded() will always return true */
96    private boolean mRetryForever;
97
98    /**
99     * The maximum number of retries to attempt before
100     * isRetryNeeded returns false
101     */
102    private int mMaxRetryCount;
103
104    /** The current number of retries */
105    private int mRetryCount;
106
107    /** Random number generator */
108    private Random rng = new Random();
109
110    /** Constructor */
111    public RetryManager() {
112        if (DBG) log("constructor");
113    }
114
115    /**
116     * Configure for a simple linear sequence of times plus
117     * a random value.
118     *
119     * @param maxRetryCount is the maximum number of retries
120     *        before isRetryNeeded returns false.
121     * @param retryTime is a time that will be returned by getRetryTime.
122     * @param randomizationTime a random value between 0 and
123     *        randomizationTime will be added to retryTime. this
124     *        parameter may be 0.
125     * @return true if successful
126     */
127    public boolean configure(int maxRetryCount, int retryTime, int randomizationTime) {
128        Pair<Boolean, Integer> value;
129
130        if (DBG) log("configure: " + maxRetryCount + ", " + retryTime + "," + randomizationTime);
131
132        if (!validateNonNegativeInt("maxRetryCount", maxRetryCount)) {
133            return false;
134        }
135
136        if (!validateNonNegativeInt("retryTime", retryTime)) {
137            return false;
138        }
139
140        if (!validateNonNegativeInt("randomizationTime", randomizationTime)) {
141            return false;
142        }
143
144        mMaxRetryCount = maxRetryCount;
145        resetRetryCount();
146        mRetryArray.clear();
147        mRetryArray.add(new RetryRec(retryTime, randomizationTime));
148
149        return true;
150    }
151
152    /**
153     * Configure for using string which allow arbitrary
154     * sequences of times. See class comments for the
155     * string format.
156     *
157     * @return true if successful
158     */
159    public boolean configure(String configStr) {
160        // Strip quotes if present.
161        if ((configStr.startsWith("\"") && configStr.endsWith("\""))) {
162            configStr = configStr.substring(1, configStr.length()-1);
163        }
164        if (DBG) log("configure: '" + configStr + "'");
165
166        if (!TextUtils.isEmpty(configStr)) {
167            int defaultRandomization = 0;
168
169            if (DBG) log("configure: not empty");
170
171            mMaxRetryCount = 0;
172            resetRetryCount();
173            mRetryArray.clear();
174
175            String strArray[] = configStr.split(",");
176            for (int i = 0; i < strArray.length; i++) {
177                if (DBG) log("configure: strArray[" + i + "]='" + strArray[i] + "'");
178                Pair<Boolean, Integer> value;
179                String splitStr[] = strArray[i].split("=", 2);
180                splitStr[0] = splitStr[0].trim();
181                if (DBG) log("configure: splitStr[0]='" + splitStr[0] + "'");
182                if (splitStr.length > 1) {
183                    splitStr[1] = splitStr[1].trim();
184                    if (DBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
185                    if (TextUtils.equals(splitStr[0], "default_randomization")) {
186                        value = parseNonNegativeInt(splitStr[0], splitStr[1]);
187                        if (!value.first) return false;
188                        defaultRandomization = value.second;
189                    } else if (TextUtils.equals(splitStr[0], "max_retries")) {
190                        if (TextUtils.equals("infinite",splitStr[1])) {
191                            mRetryForever = true;
192                        } else {
193                            value = parseNonNegativeInt(splitStr[0], splitStr[1]);
194                            if (!value.first) return false;
195                            mMaxRetryCount = value.second;
196                        }
197                    } else {
198                        Log.e(LOG_TAG, "Unrecognized configuration name value pair: "
199                                        + strArray[i]);
200                        return false;
201                    }
202                } else {
203                    /**
204                     * Assume a retry time with an optional randomization value
205                     * following a ":"
206                     */
207                    splitStr = strArray[i].split(":", 2);
208                    splitStr[0] = splitStr[0].trim();
209                    RetryRec rr = new RetryRec(0, 0);
210                    value = parseNonNegativeInt("delayTime", splitStr[0]);
211                    if (!value.first) return false;
212                    rr.mDelayTime = value.second;
213
214                    // Check if optional randomization value present
215                    if (splitStr.length > 1) {
216                        splitStr[1] = splitStr[1].trim();
217                        if (DBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
218                        value = parseNonNegativeInt("randomizationTime", splitStr[1]);
219                        if (!value.first) return false;
220                        rr.mRandomizationTime = value.second;
221                    } else {
222                        rr.mRandomizationTime = defaultRandomization;
223                    }
224                    mRetryArray.add(rr);
225                }
226            }
227            if (mRetryArray.size() > mMaxRetryCount) {
228                mMaxRetryCount = mRetryArray.size();
229                if (DBG) log("configure: setting mMaxRetryCount=" + mMaxRetryCount);
230            }
231            if (DBG) log("configure: true");
232            return true;
233        } else {
234            if (DBG) log("configure: false it's empty");
235            return false;
236        }
237    }
238
239    /**
240     * Report whether data reconnection should be retried
241     *
242     * @return {@code true} if the max retries has not been reached. {@code
243     *         false} otherwise.
244     */
245    public boolean isRetryNeeded() {
246        boolean retVal = mRetryForever || (mRetryCount < mMaxRetryCount);
247        if (DBG) log("isRetryNeeded: " + retVal);
248        return retVal;
249    }
250
251    /**
252     * Return the timer that should be used to trigger the data reconnection
253     */
254    public int getRetryTimer() {
255        int index;
256        if (mRetryCount < mRetryArray.size()) {
257            index = mRetryCount;
258        } else {
259            index = mRetryArray.size() - 1;
260        }
261
262        int retVal;
263        if ((index >= 0) && (index < mRetryArray.size())) {
264            retVal = mRetryArray.get(index).mDelayTime + nextRandomizationTime(index);
265        } else {
266            retVal = 0;
267        }
268
269        if (DBG) log("getRetryTimer: " + retVal);
270        return retVal;
271    }
272
273    /**
274     * @return retry count
275     */
276    public int getRetryCount() {
277        if (DBG) log("getRetryCount: " + mRetryCount);
278        return mRetryCount;
279    }
280
281    /**
282     * Increase the retry counter, does not change retry forever.
283     */
284    public void increaseRetryCount() {
285        mRetryCount++;
286        if (mRetryCount > mMaxRetryCount) {
287            mRetryCount = mMaxRetryCount;
288        }
289        if (DBG) log("increaseRetryCount: " + mRetryCount);
290    }
291
292    /**
293     * Set retry count to the specified value
294     * and turns off retrying forever.
295     */
296    public void setRetryCount(int count) {
297        mRetryCount = count;
298        if (mRetryCount > mMaxRetryCount) {
299            mRetryCount = mMaxRetryCount;
300        }
301
302        if (mRetryCount < 0) {
303            mRetryCount = 0;
304        }
305
306        mRetryForever = false;
307        if (DBG) log("setRetryCount: " + mRetryCount);
308    }
309
310    /**
311     * Reset network re-registration indicator and clear the data-retry counter
312     * and turns off retrying forever.
313     */
314    public void resetRetryCount() {
315        mRetryCount = 0;
316        mRetryForever = false;
317        if (DBG) log("resetRetryCount: " + mRetryCount);
318    }
319
320    /**
321     * Retry forever using last timeout time.
322     */
323    public void retryForeverUsingLastTimeout() {
324        mRetryCount = mMaxRetryCount;
325        mRetryForever = true;
326        if (DBG) log("retryForeverUsingLastTimeout: " + mRetryForever + ", " + mRetryCount);
327    }
328
329    /**
330     * @return true if retrying forever
331     */
332    public boolean isRetryForever() {
333        if (DBG) log("isRetryForever: " + mRetryForever);
334        return mRetryForever;
335    }
336
337    /**
338     * Parse an integer validating the value is not negative.
339     *
340     * @param name
341     * @param stringValue
342     * @return Pair.first == true if stringValue an integer >= 0
343     */
344    private Pair<Boolean, Integer> parseNonNegativeInt(String name, String stringValue) {
345        int value;
346        Pair<Boolean, Integer> retVal;
347        try {
348            value = Integer.parseInt(stringValue);
349            retVal = new Pair<Boolean, Integer>(validateNonNegativeInt(name, value), value);
350        } catch (NumberFormatException e) {
351            Log.e(LOG_TAG, name + " bad value: " + stringValue, e);
352            retVal = new Pair<Boolean, Integer>(false, 0);
353        }
354        if (DBG) log("parseNonNetativeInt: " + name + ", " + stringValue + ", "
355                    + retVal.first + ", " + retVal.second);
356        return retVal;
357    }
358
359    /**
360     * Validate an integer is >= 0 and logs an error if not
361     *
362     * @param name
363     * @param value
364     * @return Pair.first
365     */
366    private boolean validateNonNegativeInt(String name, int value) {
367        boolean retVal;
368        if (value < 0) {
369            Log.e(LOG_TAG, name + " bad value: is < 0");
370            retVal = false;
371        } else {
372            retVal = true;
373        }
374        if (DBG) log("validateNonNegative: " + name + ", " + value + ", " + retVal);
375        return retVal;
376    }
377
378    /**
379     * Return next random number for the index
380     */
381    private int nextRandomizationTime(int index) {
382        int randomTime = mRetryArray.get(index).mRandomizationTime;
383        if (randomTime == 0) {
384            return 0;
385        } else {
386            return rng.nextInt(randomTime);
387        }
388    }
389
390    private void log(String s) {
391        Log.d(LOG_TAG, s);
392    }
393}
394