1 /*
2  * Copyright (C) 2015 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.tv.util;
18 
19 import android.content.Context;
20 import android.content.SharedPreferences;
21 import android.os.AsyncTask;
22 import android.os.Handler;
23 import android.support.annotation.WorkerThread;
24 import android.util.Log;
25 
26 import com.android.tv.common.SharedPreferencesUtils;
27 import com.android.tv.common.SoftPreconditions;
28 
29 import java.util.Date;
30 
31 /**
32  * Repeatedly executes a {@link Runnable}.
33  *
34  * <p>The next execution time is saved to a {@link SharedPreferences}, and used on the next start.
35  * The given {@link Runnable} will run in the main thread.
36  */
37 public final class RecurringRunner {
38     private static final String TAG = "RecurringRunner";
39     private static final boolean DEBUG = false;
40 
41     private final Handler mHandler;
42     private final long mIntervalMs;
43     private final Runnable mRunnable;
44     private final Runnable mOnStopRunnable;
45     private final Context mContext;
46     private final String mName;
47     private boolean mRunning;
48 
RecurringRunner(Context context, long intervalMs, Runnable runnable, Runnable onStopRunnable)49     public RecurringRunner(Context context, long intervalMs, Runnable runnable,
50             Runnable onStopRunnable) {
51         mContext = context.getApplicationContext();
52         mRunnable = runnable;
53         mOnStopRunnable = onStopRunnable;
54         mIntervalMs = intervalMs;
55         if (DEBUG) Log.i(TAG, "Delaying " + (intervalMs / 1000.0) + " seconds");
56         mName = runnable.getClass().getCanonicalName();
57         mHandler = new Handler(mContext.getMainLooper());
58     }
59 
start()60     public void start() {
61         SoftPreconditions.checkState(!mRunning, TAG, "start is called twice.");
62         if (mRunning) {
63             return;
64         }
65         mRunning = true;
66         new AsyncTask<Void, Void, Long>() {
67             @Override
68             protected Long doInBackground(Void... params) {
69                 return getNextRunTime();
70             }
71 
72             @Override
73             protected void onPostExecute(Long nextRunTime) {
74                 postAt(nextRunTime);
75             }
76         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
77     }
78 
stop()79     public void stop() {
80         mRunning = false;
81         mHandler.removeCallbacksAndMessages(null);
82         if (mOnStopRunnable != null) {
83             mOnStopRunnable.run();
84         }
85     }
86 
postAt(long next)87     private void postAt(long next) {
88         if (!mRunning) {
89             return;
90         }
91         long now = System.currentTimeMillis();
92         // Run it anyways even if it is in the past
93         if (DEBUG) Log.i(TAG, "Next run of " + mName + " at " + new Date(next));
94         long delay = Math.max(next - now, 0);
95         boolean posted = mHandler.postDelayed(new Runnable() {
96             @Override
97             public void run() {
98                 try {
99                     if (DEBUG) Log.i(TAG, "Starting " + mName);
100                     mRunnable.run();
101                 } catch (Exception e) {
102                     Log.w(TAG, "Error running " + mName, e);
103                 }
104                 postAt(resetNextRunTime());
105             }
106         }, delay);
107         if (!posted) {
108             Log.w(TAG, "Scheduling a future run of " + mName + " at " + new Date(next) + "failed");
109         }
110         if (DEBUG) Log.i(TAG, "Actual delay is " + (delay / 1000.0) + " seconds.");
111     }
112 
getSharedPreferences()113     private SharedPreferences getSharedPreferences() {
114         return mContext.getSharedPreferences(SharedPreferencesUtils.SHARED_PREF_RECURRING_RUNNER,
115                 Context.MODE_PRIVATE);
116     }
117 
118     @WorkerThread
getNextRunTime()119     private long getNextRunTime() {
120         // The access to SharedPreferences is done by an AsyncTask thread because
121         // SharedPreferences reads to disk at first time.
122         long next = getSharedPreferences().getLong(mName, System.currentTimeMillis());
123         if (next > System.currentTimeMillis() + mIntervalMs) {
124             next = resetNextRunTime();
125         }
126         return next;
127     }
128 
resetNextRunTime()129     private long resetNextRunTime() {
130         long next = System.currentTimeMillis() + mIntervalMs;
131         getSharedPreferences().edit().putLong(mName, next).apply();
132         return next;
133     }
134 }
135