package com.amaze.filemanager.utils; /** * Created by vishal on 4/1/17. * * Helper class providing helper methods to manage Service startup and it's progress * Be advised - this class can only handle progress with one object at a time. Hence, class also provides * convenience methods to serialize the service startup. */ import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.HandlerThread; import android.support.v4.app.NotificationCompat; import com.amaze.filemanager.R; import java.util.ArrayList; public class ServiceWatcherUtil { private Handler handler; private static HandlerThread handlerThread; private ProgressHandler progressHandler; long totalSize; private Runnable runnable; private static ArrayList<Intent> pendingIntents = new ArrayList<>(); // position of byte in total byte size to be copied public static long POSITION = 0L; private static int HAULT_COUNTER = -1; public static final int ID_NOTIFICATION_WAIT = 9248; /** * * @param progressHandler to publish progress after certain delay * @param totalSize total size of files to be performed, so we know when to halt the watcher */ public ServiceWatcherUtil(ProgressHandler progressHandler, long totalSize) { this.progressHandler = progressHandler; this.totalSize = totalSize; POSITION = 0L; HAULT_COUNTER = -1; handlerThread = new HandlerThread("service_progress_watcher"); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); } /** * Watches over the service progress without interrupting the worker thread in respective services * Method frees up all the resources and handlers after operation completes. */ public void watch() { runnable = new Runnable() { @Override public void run() { // we don't have a file name yet, wait for service to set if (progressHandler.getFileName()==null) handler.postDelayed(this, 1000); progressHandler.addWrittenLength(POSITION); if (POSITION == totalSize || progressHandler.getCancelled()) { // process complete, free up resources // we've finished the work or process cancelled handler.removeCallbacks(this); handlerThread.quit(); return; } if (POSITION == progressHandler.getWrittenSize()) { HAULT_COUNTER++; if (HAULT_COUNTER>10) { // we suspect the progress has been haulted for some reason, stop the watcher // workaround for decryption when we have a length retreived by // CipherInputStream less than the orginal stream, and hence the total size // we passed at the beginning is never reached progressHandler.addWrittenLength(totalSize); handler.removeCallbacks(this); handlerThread.quit(); return; } } handler.postDelayed(this, 1000); } }; handler.postDelayed(runnable, 1000); } /** * Manually call runnable, before the delay. Fixes race condition which can arise when * service has finished execution and stopping self, but the runnable is yet scheduled to be posted. * Thus avoids posting any callback after service has stopped. */ public void stopWatch() { if (handlerThread.isAlive()) handler.post(runnable); } /** * Convenience method to check whether another service is working in background * If a service is found working (by checking {@link #handlerThread} for it's state) * then we wait for an interval of 5 secs, before checking on it again. * * Be advised - this method is not sure to start a new service, especially when app has been closed * as there are higher chances for android system to GC the thread when it is running low on memory * * @param context * @param intent */ public static synchronized void runService(final Context context, final Intent intent) { /*if (handlerThread==null || !handlerThread.isAlive()) { // we're not bound, no need to proceed further and waste up resources // start the service directly *//** * We can actually end up racing at this point with the {@link HandlerThread} started * in {@link #init(Context)}. If older service has returned, we already have the runnable * waiting to execute in #init, and user is in app, and starts another service, and * as this block executes the {@link android.app.Service#onStartCommand(Intent, int, int)} * we end up with a context switch to 'service_startup_watcher' in #init, it also starts * a new service (as {@link #progressHandler} is not alive yet). * Though chances are very slim, but even if this condition occurs, only the progress will * be flawed, but the actual operation will go fine, due to android's native serial service * execution. #nough' said! *//* context.startService(intent); return; }*/ if (pendingIntents.size()==0) { init(context); } pendingIntents.add(intent); } /** * Helper method to {@link #runService(Context, Intent)} * Starts the wait watcher thread if not already started. * Halting condition depends on the state of {@link #handlerThread} * @param context */ private static synchronized void init(final Context context) { final HandlerThread waitingThread = new HandlerThread("service_startup_watcher"); waitingThread.start(); final Handler handler = new Handler(waitingThread.getLooper()); final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationCompat.Builder mBuilder=new NotificationCompat.Builder(context); mBuilder.setContentTitle(context.getString(R.string.waiting_title)); mBuilder.setContentText(context.getString(R.string.waiting_content)); mBuilder.setAutoCancel(false); mBuilder.setSmallIcon(R.drawable.ic_all_inclusive_white_36dp); mBuilder.setProgress(0, 0, true); Runnable runnable = new Runnable() { @Override public void run() { if (handlerThread==null || !handlerThread.isAlive()) { // service is been finished, let's start this one // pop recent intent from pendingIntents context.startService(pendingIntents.remove(pendingIntents.size()-1)); if (pendingIntents.size()==0) { // we've done all the work, free up resources (if not already killed by system) notificationManager.cancel(ID_NOTIFICATION_WAIT); handler.removeCallbacks(this); waitingThread.quit(); return; } else { notificationManager.notify(ID_NOTIFICATION_WAIT, mBuilder.build()); } } handler.postDelayed(this, 5000); } }; handler.postDelayed(runnable, 0); } }