-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFunctify.java
More file actions
381 lines (331 loc) · 11.3 KB
/
Functify.java
File metadata and controls
381 lines (331 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package com.mindtheapps.functify;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.util.Log;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import static java.lang.annotation.RetentionPolicy.SOURCE;
/**
* Functify
* --------
* <p>
* Enables you to write any series of operation to execute one after the other.
* An asynchronous func will work on a worker thread
* And UI - on the main thread.
* "ANY" means that it will use the last thread used.
* <p>
* Current implementation uses one worker thread, and all work will be queued on this one thread.
* If the work can be paralleled, for example - receive multiple files from a web server - consider
* changing the code to use a thread pool.
* <p>
* <p>
* License is granted to use and/or modify this class in any project, commercial or not.
* Keep this attribution here and in any credit page you have in your app
* (c) Amir Uval amir@mindtheapps.com
*/
public final class Functify {
/**
* @THREAD_TYPE
*/
public static final int TH_ANY = 0;
public static final int TH_UI = 1;
public static final int TH_WORKER = 2;
public static final int TH_POOL_WORKER = 3;
// static final CallingBackRunnable cbr = new CallingBackRunnable();
private static final String TAG = Functify.class.getSimpleName();
private static final Handler sAsyncHandler;
private static final Handler sMainHandler;
private static final HandlerThread sAsyncHandlerThread = new HandlerThread("WorkerThread");
private static ArrayList<WeakReference<FuncFlow>> mDesroyables = new ArrayList<>();
static {
sAsyncHandlerThread.start();
sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper());
sMainHandler = new Handler(Looper.getMainLooper());
}
public static FuncFlow newFlow() {
return new FuncFlow();
}
/**
* just a useful method
*
* @return
*/
public static
@THREAD_TYPE
int getThreadType() {
return Looper.myLooper().equals(Looper.getMainLooper()) ? TH_UI : TH_WORKER;
}
public static void printDebug() {
Log.d(TAG, ">> [running on " + (getThreadType() == TH_UI ? " the main thread]:" : " a worker thread @" + Looper.myLooper().hashCode() +
"]:"));
}
/**
* To prevent memory leaks, call this in your onDestroy(), and all pending work will be canceled.
*/
public static void onDestroy() {
for (WeakReference<FuncFlow> fb : mDesroyables) {
FuncFlow funcFlow = fb.get();
if (funcFlow != null)
funcFlow.clean();
}
mDesroyables.clear();
}
/**
* quick way to run async any runnable now.
*
* @param r
*/
public static void justRunAsync(Runnable r) {
sAsyncHandler.post(r);
}
/**
* Session wrapper id
*/
@Retention(SOURCE)
@IntDef({
TH_ANY,//0
TH_UI,//1
TH_WORKER,//2
TH_POOL_WORKER, //3
})
public @interface THREAD_TYPE {
}
private interface FCallback {
void onComplete();
void onFException(FException e);
}
public interface FExceptionHandler {
void onFException(Throwable e);
}
public static class FuncFlow implements Runnable {
// one per flow
final CallingBackRunnable cbr = new CallingBackRunnable();
ArrayList<Func> runnables = new ArrayList<>();
private WeakReference<FExceptionHandler> fExceptionHandler;
private boolean mDestroyable = true;
private Bundle bundle = null;
/**
* called once a flow is complete
*/
private void clean() {
// just stay on the safe side
sMainHandler.removeCallbacks(cbr);
if (mDestroyable) {
runnables.clear();
sAsyncHandler.removeCallbacks(cbr);
} // if marked not to be destroyed: keep it. We're not even supposed to be here
if (fExceptionHandler != null) {
fExceptionHandler.clear();
}
}
/**
* Optional call: if not called, it's like calling with 'true'.
* <p>
* when onDestroyed() is called (you must call it in your onDestry()), all the destroyable flows will be cleared.
* call this with 'true' only if you need a task to outlive the Activity, and that has no reference to Context.
* <p>
*
* @param b
*/
public FuncFlow setIsDestroyable(boolean b) {
mDestroyable = b;
return this;
}
/**
* same as executeDelayed(0); (no delay)
*/
public void execute() {
executeDelayed(0);
}
private boolean isDestroyable() {
return mDestroyable;
}
/**
* execute the flow later.
* <p>
* beware:
* it will not fire while in deep sleep. (as handler depends on
* uptimeMillis = not advancing while off).
* deep sleep will postpone post.
* If you must fire even in deep sleep - use the AlarmManager
* <p>
* also note:
* calling Fuctify.onDestroy() will cancel this, if not executed yet. (Unless marked as "setIsDestroyable(false)")
*
* @param delay
*/
public void executeDelayed(long delay) {
if (runnables.size() == 0) {
return;
}
int whereToRun = runnables.get(0).whereToRun;
if (whereToRun == TH_ANY || whereToRun == TH_WORKER) {
sAsyncHandler.postDelayed(this, delay);
} else {
sMainHandler.postDelayed(this, delay);
}
}
/**
* can be safely called as a simple method call from any thread
* but it's best to use execute() or executeDelayed() because they take into account the thread of the first function
*/
@Override
public void run() {
if (runnables.size() == 0) {
// empty execution list, nothing to do here
return;
}
int index = 0;
if (bundle == null) {
bundle = new Bundle();
}
if (isDestroyable()) {
mDesroyables.add(new WeakReference<>(this));
}
operateOnLoop(bundle, index);
}
/**
* a pseudo-recursive call
* pseudo - because it doesn't actually enter the call stack
* recursive - because it calls itself if there's more functions in queue).
*
* @param b
* @param i
*/
private void operateOnLoop(final Bundle b, final int i) {
// starting with an empty bundle
if (i >= runnables.size()) {
// flow is complete
clean();
return;
}
cbr.func = runnables.get(i);
cbr.b = b;
cbr.cb = new MyFCallback(i + 1);
if (cbr.func.whereToRun == TH_ANY) {
// just run on the current thread
cbr.run();
} else if (cbr.func.whereToRun == TH_WORKER) {
sAsyncHandler.post(cbr);
} else if (cbr.func.whereToRun == TH_UI) {
sMainHandler.post(cbr);
} else { // pool
// run on a thread pool
// todo
throw new RuntimeException("todo");
}
}
public FuncFlow runAsync(Func func) {
// run the runnable on a worker thread and return this FuncFlow when done
func.whereToRun = TH_WORKER;
runnables.add(func);
return this;
}
public FuncFlow runAllAsync(Func... funcs) {
// run the runnable on a worker thread and return this FuncFlow when done
for (Func f : funcs) {
f.whereToRun = TH_POOL_WORKER;
runnables.add(f);
}
return this;
}
public FuncFlow runOnMain(Func func) {
if (!mDestroyable) {
// avoid that: this will lead to memory leaks
throw new IllegalStateException("Trying to run an un-destroyable flow on the main thread");
}
func.whereToRun = TH_UI;
runnables.add(func);
return this;
}
public FuncFlow runOnAny(Func func) {
func.whereToRun = TH_ANY;
runnables.add(func);
return this;
}
public void setExceptionHandler(FExceptionHandler eh) {
this.fExceptionHandler = new WeakReference<>(eh);
}
/**
* will start the execution flow upon a user click (using setOnClickListener on the View passed
* as argument.
*
* @param btn
*/
public void runOnClick(View btn) {
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
execute();
}
});
}
public Bundle getBundle() {
return bundle;
}
public void setBundle(Bundle bundle) {
this.bundle = bundle;
}
class MyFCallback implements FCallback {
Bundle b;
int i;
public MyFCallback(int i) {
this.i = i;
}
@Override
public void onComplete() {
// go to next
operateOnLoop(b, i);
}
@Override
public void onFException(FException e) {
if (fExceptionHandler != null && fExceptionHandler.get() != null) {
fExceptionHandler.get().onFException(e);
// stop the flow (could it be useful to have other options, such as retry/backoff wait&retry/ignore and continue/...?)
} else {
throw new RuntimeException(e);
}
}
}
}
public abstract static class Func {
private
@THREAD_TYPE
int whereToRun;
public abstract void onExecute(Bundle b);
}
private static class CallingBackRunnable implements Runnable {
Func func;
Bundle b;
FCallback cb;
@Override
public void run() {
try {
// The Bundle b is there as the data link between the logic flow
// it can be modified inside the onExecute
func.onExecute(b);
cb.onComplete();
} catch (Exception e) {
cb.onFException(new FException(e, func, b));
}
}
}
public static class FException extends Exception {
private final Func f;
private final Bundle b;
public FException(Exception e, Func f, Bundle b) {
super(e);
this.f = f;
this.b = b;
}
@Override
public String toString() {
return super.toString() + " (On Func:" + f + ", With Bundle:" + b + ")";
}
}
}