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