package com.seafile.seadroid2.monitor; import android.os.Handler; import android.util.Log; import com.google.common.collect.ConcurrentHashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.seafile.seadroid2.SeafException; import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.data.SeafCachedFile; import com.seafile.seadroid2.transfer.TransferService; import com.seafile.seadroid2.util.ConcurrentAsyncTask; import com.seafile.seadroid2.util.Utils; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Update modified files, retry until success */ public class AutoUpdateManager implements Runnable, CachedFileChangedListener { private static final String DEBUG_TAG = "AutoUpdateManager"; private static final int CHECK_INTERVAL_MILLI = 3000; private TransferService txService; private Thread thread; private volatile boolean running; private final Handler mHandler = new Handler(); private Set<AutoUpdateInfo> infos = Sets.newHashSet(); private MonitorDBHelper db = MonitorDBHelper.getMonitorDBHelper(); public void onTransferServiceConnected(TransferService txService) { this.txService = txService; running = true; thread = new Thread(this); thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { Log.e(DEBUG_TAG, "Uncaught exception", ex); } }); thread.start(); } public void stop() { running = false; } /** * This method is called by file monitor, so it would be executed in the file monitor thread */ @Override public void onCachedBlocksChanged(final Account account, final SeafCachedFile cachedFile, final File localFile, int version) { addTask(account, cachedFile, localFile, version); } /** * This method is called by file monitor, so it would be executed in the file monitor thread */ @Override public void onCachedFileChanged(final Account account, final SeafCachedFile cachedFile, final File localFile) { addTask(account, cachedFile, localFile); } public void addTask(Account account, SeafCachedFile cachedFile, File localFile) { addTask(account, cachedFile, localFile, -1); } public void addTask(Account account, SeafCachedFile cachedFile, File localFile, int version) { AutoUpdateInfo info = new AutoUpdateInfo(account, cachedFile.repoID, cachedFile.repoName, Utils.getParentPath(cachedFile.path), localFile.getPath(), version); synchronized (infos) { if (infos.contains(info)) { return; } infos.add(info); } db.saveAutoUpdateInfo(info); if (!Utils.isNetworkOn() || txService == null) { return; } ArrayList<AutoUpdateInfo> infosList = new ArrayList<AutoUpdateInfo>(1); infosList.add(info); addAllUploadTasks(infosList); } private void addAllUploadTasks(final List<AutoUpdateInfo> infos) { mHandler.post(new Runnable() { @Override public void run() { for (AutoUpdateInfo info : infos) { if (info.canLocalDecrypt()) { txService.addTaskToUploadQue(info.account, info.repoID, info.repoName, info.parentDir, info.localPath, true, true, info.version); } else { txService.addUploadTask(info.account, info.repoID, info.repoName, info.parentDir, info.localPath, true, true); } } } }); } /** * This callback in called in the main thread when the transfer service broadcast is received */ public void onFileUpdateSuccess(Account account, String repoID, String repoName, String parentDir, String localPath, int version) { // This file has already been updated on server, so we abort auto update task if (removeAutoUpdateInfo(account, repoID, repoName, parentDir, localPath, version)) { Log.d(DEBUG_TAG, "auto updated " + localPath); } } private static int MAX_UPLOAD_FAILURES = 3; private ConcurrentHashMultiset<AutoUpdateInfo> uploadFailuresByFile = ConcurrentHashMultiset.create(); private boolean maxFailureReached(Account account, String repoID, String repoName, String parentDir, String localPath, int version) { AutoUpdateInfo info = new AutoUpdateInfo(account, repoID, repoName, parentDir, localPath, version); int failures = uploadFailuresByFile.count(info) + 1; if (failures >= MAX_UPLOAD_FAILURES) { uploadFailuresByFile.remove(info); return true; } uploadFailuresByFile.setCount(info, failures); return false; } public void onFileUpdateFailure(Account account, String repoID, String repoName, String parentDir, String localPath, SeafException e, int version) { boolean shouldAbortUpload = false; if (e.getCode() / 100 == 4) { // This file has already been removed on server, so we abort the auto update task shouldAbortUpload = true; } if (!shouldAbortUpload && maxFailureReached(account, repoID, repoName, parentDir, localPath, version)) { Log.d(DEBUG_TAG, String.format("abort auto updating %s because failed for more than %s times", localPath, MAX_UPLOAD_FAILURES)); shouldAbortUpload = true; } if (!shouldAbortUpload) { return; } if (removeAutoUpdateInfo(account, repoID, repoName, parentDir, localPath, version)) { Log.d(DEBUG_TAG, String.format("failed to auto update %s, error %s", localPath, e)); } else { Log.d(DEBUG_TAG, String.format("failed to remove auto update task for %s", localPath)); } } private boolean removeAutoUpdateInfo(Account account, String repoID, String repoName, String parentDir, String localPath, int version) { final AutoUpdateInfo info = new AutoUpdateInfo(account, repoID, repoName, parentDir, localPath, version); boolean exist = false; synchronized (infos) { exist = infos.remove(info); } if (exist) { ConcurrentAsyncTask.submit(new Runnable() { @Override public void run() { db.removeAutoUpdateInfo(info); } }); } return exist; } /** * Periodically checks the upload tasks and schedule them to run **/ private void scheduleUpdateTasks() { int size = infos.size(); if (!Utils.isNetworkOn()) { Log.d(DEBUG_TAG, "network is not available, " + size + " in queue"); return; } if (txService == null) { return; } Log.v(DEBUG_TAG, String.format("check auto upload tasks, %d in queue", size)); List<AutoUpdateInfo> infosList; synchronized (infos) { if (infos.isEmpty()) { return; } infosList = ImmutableList.copyOf(infos); } addAllUploadTasks(infosList); } public void run() { synchronized (infos) { infos.addAll(db.getAutoUploadInfos()); } while (running) { scheduleUpdateTasks(); if (!running) { break; } try { Thread.sleep(CHECK_INTERVAL_MILLI); } catch (final InterruptedException ignored) { break; } } } }