package com.rafali.flickruploader.service; import java.io.File; import java.io.FileNotFoundException; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.androidannotations.api.BackgroundExecutor; import org.slf4j.LoggerFactory; import se.emilsjolander.sprinkles.Transaction; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.net.ConnectivityManager; import android.os.BatteryManager; import android.os.Handler; import android.os.IBinder; import android.provider.MediaStore.Images; import android.provider.MediaStore.Video; import com.googlecode.flickrjandroid.FlickrException; import com.googlecode.flickrjandroid.REST; import com.rafali.common.STR; import com.rafali.common.ToolString; import com.rafali.flickruploader.FlickrUploader; import com.rafali.flickruploader.api.FlickrApi; import com.rafali.flickruploader.broadcast.AlarmBroadcastReceiver; import com.rafali.flickruploader.enums.CAN_UPLOAD; import com.rafali.flickruploader.enums.MEDIA_TYPE; import com.rafali.flickruploader.enums.STATUS; import com.rafali.flickruploader.model.Folder; import com.rafali.flickruploader.model.Media; import com.rafali.flickruploader.tool.Notifications; import com.rafali.flickruploader.tool.Utils; import com.rafali.flickruploader.tool.Utils.Callback; import com.rafali.flickruploader.ui.activity.FlickrUploaderActivity; import com.rafali.flickruploader.ui.activity.PreferencesActivity; public class UploadService extends Service { static final org.slf4j.Logger LOG = LoggerFactory.getLogger(UploadService.class); private static final Set<UploadProgressListener> uploadProgressListeners = new HashSet<UploadService.UploadProgressListener>(); public static interface UploadProgressListener { void onProgress(final Media media); void onProcessed(final Media media); void onFinished(final int nbUploaded, final int nbErrors); void onQueued(final int nbQueued, final int nbAlreadyUploaded, final int nbAlreadyQueued); void onDequeued(final int nbDequeued); } public static class BasicUploadProgressListener implements UploadProgressListener { @Override public void onProgress(Media media) { } @Override public void onProcessed(Media media) { } @Override public void onFinished(int nbUploaded, int nbErrors) { } @Override public void onQueued(int nbQueued, int nbAlreadyUploaded, int nbAlreadyQueued) { } @Override public void onDequeued(int nbDequeued) { } } public static void register(UploadProgressListener uploadProgressListener) { if (uploadProgressListener != null) uploadProgressListeners.add(uploadProgressListener); else LOG.warn("uploadProgressListener is null"); } public static void unregister(UploadProgressListener uploadProgressListener) { if (uploadProgressListener != null) uploadProgressListeners.remove(uploadProgressListener); else LOG.warn("uploadProgressListener is null"); } @Override public IBinder onBind(Intent intent) { return null; } private static UploadService instance; @Override public void onCreate() { super.onCreate(); instance = this; LOG.debug("Service created…"); running = true; getContentResolver().registerContentObserver(Images.Media.EXTERNAL_CONTENT_URI, true, imageTableObserver); getContentResolver().registerContentObserver(Video.Media.EXTERNAL_CONTENT_URI, true, imageTableObserver); if (thread == null || !thread.isAlive()) { thread = new Thread(new UploadRunnable()); thread.start(); } IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); registerReceiver(batteryReceiver, filter); checkNewFiles(); Notifications.init(); } ContentObserver imageTableObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean change) { UploadService.checkNewFiles(); } }; BroadcastReceiver batteryReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1); boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; Utils.setCharging(charging); // LOG.debug("charging : " + charging + ", status : " + status); if (charging) wake(); } }; private long started = System.currentTimeMillis(); private boolean destroyed = false; @Override public void onDestroy() { super.onDestroy(); destroyed = true; LOG.debug("Service destroyed… started " + ToolString.formatDuration(System.currentTimeMillis() - started) + " ago"); if (instance == this) { instance = null; } running = false; unregisterReceiver(batteryReceiver); getContentResolver().unregisterContentObserver(imageTableObserver); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (Utils.canAutoUploadBool()) { // We want this service to continue running until it is explicitly // stopped, so return sticky. return START_STICKY; } else { return super.onStartCommand(intent, flags, startId); } } boolean running = false; public static int enqueue(boolean auto, Collection<Media> medias, String photoSetTitle) { int nbQueued = 0; int nbAlreadyQueued = 0; int nbAlreadyUploaded = 0; Transaction t = new Transaction(); try { for (Media media : medias) { if (media.isQueued()) { nbAlreadyQueued++; } else if (media.isUploaded()) { nbAlreadyUploaded++; } else if (auto && media.getRetries() > 3) { LOG.debug("not auto enqueueing file with too many retries : " + media); } else { nbQueued++; LOG.debug("enqueueing " + media); media.setFlickrSetTitle(photoSetTitle); media.setStatus(STATUS.QUEUED, t); } } t.setSuccessful(true); } finally { t.finish(); } if (nbQueued > 0) { checkQueue(); } for (UploadProgressListener uploadProgressListener : uploadProgressListeners) { uploadProgressListener.onQueued(nbQueued, nbAlreadyUploaded, nbAlreadyQueued); } wake(nbQueued > 0); return nbQueued; } public static void enqueueRetry(Iterable<Media> medias) { int nbQueued = 0; Transaction t = new Transaction(); try { for (Media media : medias) { if (!media.isQueued() && media.getTimestampRetry() < Long.MAX_VALUE) { nbQueued++; media.setStatus(STATUS.QUEUED, t); } } t.setSuccessful(true); } finally { t.finish(); } if (nbQueued > 0) { checkQueue(); } wake(nbQueued > 0); } public static void dequeue(Collection<Media> medias) { int nbDequeued = 0; Transaction t = new Transaction(); try { for (final Media media : medias) { if (media.isQueued()) { LOG.debug("dequeueing " + media); media.setStatus(STATUS.PAUSED, t); nbDequeued++; if (media.equals(mediaCurrentlyUploading)) { REST.kill(media); } } } t.setSuccessful(true); } finally { t.finish(); } if (nbDequeued > 0) { checkQueue(); for (UploadProgressListener uploadProgressListener : uploadProgressListeners) { uploadProgressListener.onDequeued(nbDequeued); } } wake(); } private static boolean paused = true; public static boolean isPaused() { return paused; } private static Media mediaCurrentlyUploading; private static Media mediaPreviouslyUploading; private static long lastUpload = 0; public static Media getMediaCurrentlyUploading() { return mediaCurrentlyUploading; } static int nbNetworkRetries = 0; class UploadRunnable implements Runnable { @SuppressWarnings("deprecation") @Override public void run() { while (running) { try { mediaCurrentlyUploading = checkQueue(); if (mediaPreviouslyUploading != null) { for (UploadProgressListener uploadProgressListener : uploadProgressListeners) { uploadProgressListener.onProcessed(mediaPreviouslyUploading); } mediaPreviouslyUploading = null; if (mediaCurrentlyUploading == null) { onUploadFinished(); FlickrUploader.cleanLogs(); } } CAN_UPLOAD canUploadNow = Utils.canUploadNow(); if (mediaCurrentlyUploading == null || canUploadNow != CAN_UPLOAD.ok) { paused = true; synchronized (mPauseLock) { // LOG.debug("waiting for work"); if (mediaCurrentlyUploading == null) { if ((FlickrUploaderActivity.getInstance() == null || FlickrUploaderActivity.getInstance().isPaused()) && !Utils.canAutoUploadBool() && System.currentTimeMillis() - lastUpload > 5 * 60 * 1000) { running = false; LOG.debug("stopping service after waiting for 5 minutes"); checkForFilesToDelete(); } else { if (Utils.canAutoUploadBool()) { mPauseLock.wait(); } else { LOG.debug("will stop the service if no more upload " + ToolString.formatDuration(System.currentTimeMillis() - started)); mPauseLock.wait(60000); } } } else { if (FlickrUploaderActivity.getInstance() != null && !FlickrUploaderActivity.getInstance().isPaused()) { mPauseLock.wait(2000); } else { mPauseLock.wait(60000); } } } } else { paused = false; if (FlickrApi.isAuthentified()) { long start = System.currentTimeMillis(); mediaCurrentlyUploading.setProgress(0); onUploadProgress(mediaCurrentlyUploading); ConnectivityManager cm = (ConnectivityManager) FlickrUploader.getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE); if (STR.wifionly.equals(Utils.getStringProperty(PreferencesActivity.UPLOAD_NETWORK))) { cm.setNetworkPreference(ConnectivityManager.TYPE_WIFI); } else { cm.setNetworkPreference(ConnectivityManager.DEFAULT_NETWORK_PREFERENCE); } while (mediaCurrentlyUploading.getTimestampRetry() < Long.MAX_VALUE && System.currentTimeMillis() < mediaCurrentlyUploading.getTimestampRetry()) { synchronized (mPauseLock) { long pausems = Math.max(1000, mediaCurrentlyUploading.getTimestampRetry() - System.currentTimeMillis()); LOG.debug("pausing for " + (pausems / 1000) + "s before uploading"); mPauseLock.wait(pausems); Media topQueued = checkQueue(); if (topQueued == null || !topQueued.equals(mediaCurrentlyUploading)) { LOG.info("topQueued:" + topQueued + ", mediaCurrentlyUploading:" + mediaCurrentlyUploading); mediaCurrentlyUploading = null; break; } } } if (mediaCurrentlyUploading == null) { continue; } if (mediaCurrentlyUploading.getRetries() > 0) { boolean networkOk = FlickrApi.isNetworkOk(); LOG.debug("retry=" + mediaCurrentlyUploading.getRetries() + ", networkOk=" + networkOk); if (!networkOk) { nbNetworkRetries++; long waitingtime = (long) Math.min(3600 * 1000L, Math.max(10000L, Math.pow(2, nbNetworkRetries) * 1000L)); LOG.warn("network not ready yet, retrying in " + (waitingtime / 1000) + "s, nbNetworkRetries=" + nbNetworkRetries); mediaCurrentlyUploading.setTimestampRetry(System.currentTimeMillis() + waitingtime); mediaCurrentlyUploading.save(); continue; } } UploadException exc = null; try { LOG.debug("Starting upload : " + mediaCurrentlyUploading); mediaCurrentlyUploading.setTimestampUploadStarted(start); FlickrApi.upload(mediaCurrentlyUploading); } catch (UploadException e) { LOG.error(e.toString()); exc = e; } long time = System.currentTimeMillis() - start; if (exc == null) { nbNetworkRetries = 0; lastUpload = System.currentTimeMillis(); LOG.debug("Upload success : " + time + "ms " + mediaCurrentlyUploading); mediaCurrentlyUploading.setStatus(STATUS.UPLOADED); } else { mediaCurrentlyUploading.setTimestampUploadStarted(0); mediaCurrentlyUploading.setErrorMessage(exc.getMessage()); int newretries = mediaCurrentlyUploading.getRetries() + 1; mediaCurrentlyUploading.setRetries(newretries); if (exc.isRetryable()) { LOG.warn("Upload fail in " + time + "ms : " + mediaCurrentlyUploading + ", newretries=" + newretries); if (newretries >= 9) { mediaCurrentlyUploading.setStatus(STATUS.FAILED); } } else { mediaCurrentlyUploading.setStatus(STATUS.FAILED); } } mediaCurrentlyUploading.save(); } else { Notifications.clear(); running = false; } } } catch (InterruptedException e) { LOG.warn("Thread interrupted"); } catch (Throwable e) { LOG.error(ToolString.stack2string(e)); } finally { if (mediaCurrentlyUploading != null) { mediaPreviouslyUploading = mediaCurrentlyUploading; mediaCurrentlyUploading = null; } } } stopSelf(); } } public static void wake() { wake(false); } public static class UploadException extends RuntimeException { private static final long serialVersionUID = 1L; private Boolean retryable; public UploadException(String message, boolean retryable) { super(message); this.retryable = retryable; } @Override public String toString() { return "UploadException : " + getMessage() + " : " + (getCause() == null ? "" : "cause : " + getCause().getClass().getSimpleName() + " : " + getCause().getMessage() + " : ") + "isRetryable=" + isRetryable() + ", isNetworkError=" + isNetworkError(); } public UploadException(String message, Throwable cause) { super(message, cause); } public boolean isRetryable() { if (retryable != null) { return retryable; } else { return isRetryable(getCause()); } } private static boolean isRetryable(Throwable e) { if (e == null) { return false; } else if (e instanceof FlickrException) { return false; } else if (e instanceof FileNotFoundException) { return false; } else if (e instanceof RuntimeException && e.getCause() != null) { return isRetryable(e.getCause()); } return true; } public boolean isNetworkError() { return isNetworkError(getCause()); } private boolean isNetworkError(Throwable e) { if (e instanceof UnknownHostException) { return true; } else if (e instanceof SocketException) { return true; } return false; } } public static void wake(final boolean force) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { if ((instance == null || instance.destroyed) && (force || Utils.canAutoUploadBool() || checkQueue() != null)) { Context context = FlickrUploader.getAppContext(); context.startService(new Intent(context, UploadService.class)); AlarmBroadcastReceiver.initAlarm(); } checkForFilesToDelete(); synchronized (mPauseLock) { mPauseLock.notifyAll(); } } }); } private static final Object mPauseLock = new Object(); private Thread thread; public static synchronized Media checkQueue() { List<Media> medias = Utils.loadMedia(false); recentlyUploaded.clear(); failed.clear(); currentlyQueued.clear(); Media oldestCreatedMedia = null; long yesterday = System.currentTimeMillis() - 24 * 3600 * 1000L; for (Media media : medias) { if (media.isQueued()) { currentlyQueued.add(media); if (oldestCreatedMedia == null || oldestCreatedMedia.getTimestampCreated() > media.getTimestampCreated()) { oldestCreatedMedia = media; } } if (media.isUploaded() && media.getTimestampQueued() > yesterday) { recentlyUploaded.add(media); } if (media.isFailed()) { failed.add(media); } } return oldestCreatedMedia; } static Set<Media> recentlyUploaded = new HashSet<Media>(); static Set<Media> failed = new HashSet<Media>(); static Set<Media> currentlyQueued = new HashSet<Media>(); public static void onUploadProgress(Media media) { for (UploadProgressListener uploadProgressListener : uploadProgressListeners) { uploadProgressListener.onProgress(media); } } public static void onUploadFinished() { for (UploadProgressListener uploadProgressListener : uploadProgressListeners) { uploadProgressListener.onFinished(recentlyUploaded.size(), failed.size()); } } public static void clear(final int status, final Callback<Void> callback) { if (status == STATUS.FAILED || status == STATUS.QUEUED) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { int nbModified = 0; List<Media> medias = Utils.loadMedia(false); Transaction t = new Transaction(); try { for (final Media media : medias) { if (media.getStatus() == status) { if (media.isQueued() && media.equals(mediaCurrentlyUploading)) { REST.kill(media); } media.setStatus(STATUS.PAUSED, t); nbModified++; } } t.setSuccessful(true); } finally { t.finish(); } if (nbModified > 0) { checkQueue(); } if (callback != null) callback.onResult(null); } }); } else { LOG.error("status " + status + " is not supported"); } } static long lastLoad = 0; public static void checkNewFiles() { BackgroundExecutor.execute(new Runnable() { @Override public void run() { try { final List<Media> medias; if (System.currentTimeMillis() - lastLoad > 5000) { medias = Utils.loadMedia(true); lastLoad = System.currentTimeMillis(); } else { medias = Utils.loadMedia(false); } if (medias == null || medias.isEmpty()) { LOG.info("no media found"); return; } Map<String, Folder> pathFolders = Utils.getFolders(false); if (pathFolders.isEmpty()) { LOG.info("no folder monitored"); return; } String canAutoUpload = Utils.canAutoUpload(); boolean autoUpload = "true".equals(canAutoUpload); final long uploadDelayMs = Utils.getUploadDelayMs(); long newestFileAge = 0; for (Media media : medias) { if (media.isImported()) { if (!autoUpload) { LOG.debug("not uploading " + media + " because " + canAutoUpload); media.setStatus(STATUS.PAUSED); continue; } else if (media.getMediaType() == MEDIA_TYPE.PHOTO && !Utils.isAutoUpload(MEDIA_TYPE.PHOTO)) { LOG.debug("not uploading " + media + " because photo upload disabled"); media.setStatus(STATUS.PAUSED); continue; } else if (media.getMediaType() == MEDIA_TYPE.VIDEO && !Utils.isAutoUpload(MEDIA_TYPE.VIDEO)) { LOG.debug("not uploading " + media + " because video upload disabled"); media.setStatus(STATUS.PAUSED); continue; } else { File file = new File(media.getPath()); if (file.exists()) { boolean uploaded = media.isUploaded(); LOG.debug("uploaded : " + uploaded + ", " + media); if (!uploaded) { Folder folder = pathFolders.get(media.getFolderPath()); if (folder == null || !folder.isAutoUploaded()) { media.setStatus(STATUS.PAUSED); LOG.debug("not uploading " + media + " because " + media.getFolderPath() + " is not monitored"); } else { int sleep = 0; while (file.length() < 100 && sleep < 5) { LOG.debug("sleeping a bit"); sleep++; Thread.sleep(1000); } long fileAge = System.currentTimeMillis() - file.lastModified(); LOG.debug("uploadDelayMs:" + uploadDelayMs + ", fileAge:" + fileAge + ", newestFileAge:" + newestFileAge); if (uploadDelayMs > 0) { media.setTimestampRetry(System.currentTimeMillis() + uploadDelayMs); } enqueue(true, Arrays.asList(media), folder.getFlickrSetTitle()); } } } else { LOG.debug("Deleted : " + file); media.deleteAsync(); } } } } } catch (Throwable e) { LOG.error(ToolString.stack2string(e)); } } }, "checkNewFiles", "checkNewFiles"); } public static Set<Media> getCurrentlyQueued() { return currentlyQueued; } public static Set<Media> getRecentlyUploaded() { return recentlyUploaded; } public static int getNbTotal() { return currentlyQueued.size() + recentlyUploaded.size() + failed.size(); } public static Set<Media> getFailed() { return failed; } static long lastDeleteCheck = Utils.getLongProperty("lastDeleteCheck"); static void checkForFilesToDelete() { if (Utils.isAutoDelete() && System.currentTimeMillis() - lastDeleteCheck > 3600 * 1000L) { lastDeleteCheck = System.currentTimeMillis(); Utils.setLongProperty("lastDeleteCheck", lastDeleteCheck); BackgroundExecutor.execute(new Runnable() { @Override public void run() { try { long firstInstallTime = FlickrUploader.getAppContext().getPackageManager().getPackageInfo(FlickrUploader.getAppContext().getPackageName(), 0).firstInstallTime; long yesterday = System.currentTimeMillis() - 24 * 3600 * 1000L; List<Media> loadMedia = Utils.loadMedia(false); int nbFileDeleted = 0; for (Media media : loadMedia) { if (media.isUploaded() && media.getTimestampUploaded() > firstInstallTime && media.getTimestampUploaded() < yesterday) { boolean stillOnFlickr = FlickrApi.isStillOnFlickr(media); LOG.info("poundering deletion of : " + media + " : stillOnFlickr=" + stillOnFlickr); if (stillOnFlickr) { boolean deleted = new File(media.getPath()).delete(); LOG.warn("Deleting " + media + " " + ToolString.formatDuration(System.currentTimeMillis() - media.getTimestampUploaded()) + " after upload : deleted=" + deleted); media.delete(); nbFileDeleted++; } else if (FlickrApi.isNetworkOk()) { media.setFlickrId(null); media.save2(null); } } } if (nbFileDeleted > 0) { LOG.warn(nbFileDeleted + " files deleted"); Utils.loadMedia(true); } } catch (Throwable e) { LOG.error(ToolString.stack2string(e)); } } }); } } }