/** * ownCloud Android client application * * @author LukeOwncloud * Copyright (C) 2015 ownCloud Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ package com.owncloud.android.files.services; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.support.v4.app.NotificationCompat; import android.util.Pair; import android.webkit.MimeTypeMap; import com.owncloud.android.R; import com.owncloud.android.authentication.AccountUtils; import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus; import com.owncloud.android.db.OCUpload; import com.owncloud.android.db.UploadResult; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation; import com.owncloud.android.lib.resources.files.FileUtils; import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation; import com.owncloud.android.lib.resources.files.RemoteFile; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.notifications.NotificationBuilderWithProgressBar; import com.owncloud.android.notifications.NotificationDelayer; import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.UploadListActivity; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.UploadUtils; import com.owncloud.android.utils.UriUtils; import java.io.File; import java.util.AbstractList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; /** * Service for uploading files. Invoke using context.startService(...). Files to * be uploaded are stored persistently using {@link UploadsStorageManager}. * * On next invocation of {@link FileUploadService} uploaded files which * previously failed will be uploaded again until either upload succeeded or a * fatal error occured. * * Every file passed to this service is uploaded. No filtering is performed. * However, Intent keys (e.g., KEY_WIFI_ONLY) are obeyed. * */ public class FileUploadService extends Service implements OnDatatransferProgressListener, OnAccountsUpdateListener { private static final String TAG = FileUploadService.class.getSimpleName(); private static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH"; public static final String EXTRA_UPLOAD_RESULT = "RESULT"; public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; public static final String EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH"; public static final String EXTRA_OLD_FILE_PATH = "OLD_FILE_PATH"; public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO"; public static final String ACCOUNT_NAME = "ACCOUNT_NAME"; public static final String KEY_FILE = "FILE"; public static final String KEY_LOCAL_FILE = "LOCAL_FILE"; public static final String KEY_REMOTE_FILE = "REMOTE_FILE"; public static final String KEY_MIME_TYPE = "MIME_TYPE"; /** * Call this Service with only this Intent key if all pending uploads are to be retried. */ private static final String KEY_RETRY = "KEY_RETRY"; /** * Call this Service with KEY_RETRY and KEY_RETRY_REMOTE_PATH to retry * download of file identified by KEY_RETRY_REMOTE_PATH. */ private static final String KEY_RETRY_REMOTE_PATH = "KEY_RETRY_REMOTE_PATH"; /** * {@link Account} to which file is to be uploaded. */ public static final String KEY_ACCOUNT = "ACCOUNT"; /** * Set whether single file or multiple files are to be uploaded. Value must be of type {@link UploadQuantity}. */ public static final String KEY_UPLOAD_TYPE = "UPLOAD_TYPE"; /** * Set to true if remote file is to be overwritten. Default action is to upload with different name. */ public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE"; /** * Set to true if remote folder is to be created if it does not exist. */ public static final String KEY_CREATE_REMOTE_FOLDER = "CREATE_REMOTE_FOLDER"; /** * Set to true if upload is to performed only when connected via wifi. */ public static final String KEY_WIFI_ONLY = "WIFI_ONLY"; /** * Set to true if upload is to performed only when phone is being charged. */ public static final String KEY_WHILE_CHARGING_ONLY = "KEY_WHILE_CHARGING_ONLY"; /** * Set to future UNIX timestamp. Upload will not be performed before this timestamp. */ public static final String KEY_UPLOAD_TIMESTAMP= "KEY_UPLOAD_TIMESTAMP"; public static final String KEY_LOCAL_BEHAVIOUR = "BEHAVIOUR"; public static final String KEY_INSTANT_UPLOAD = "INSTANT_UPLOAD"; public static final int LOCAL_BEHAVIOUR_COPY = 0; public static final int LOCAL_BEHAVIOUR_MOVE = 1; public static final int LOCAL_BEHAVIOUR_FORGET = 2; public static final int UPLOAD_SINGLE_FILE = 0; public static final int UPLOAD_MULTIPLE_FILES = 1; /** * Describes local behavior for upload. */ public enum LocalBehaviour { /** * Creates a copy of file and stores it in tmp folder inside owncloud * folder on sd-card. After upload it is moved to local owncloud * storage. Original file stays untouched. */ LOCAL_BEHAVIOUR_COPY(0), /** * Upload file from current storage. Afterwards original file is move to * local owncloud storage. */ LOCAL_BEHAVIOUR_MOVE(1), /** * Just uploads file and leaves it where it is. Original file stays * untouched. */ LOCAL_BEHAVIOUR_FORGET(2); private final int value; LocalBehaviour(int value) { this.value = value; } public int getValue() { return value; } public static LocalBehaviour fromValue(int value){ switch (value) { case 0: return LOCAL_BEHAVIOUR_COPY; case 1: return LOCAL_BEHAVIOUR_MOVE; case 2: return LOCAL_BEHAVIOUR_FORGET; } return null; } } // public enum UploadQuantity { // UPLOAD_SINGLE_FILE(0), UPLOAD_MULTIPLE_FILES(1); // private final int value; // // UploadQuantity(int value) { // this.value = value; // } // // public int getValue() { // return value; // } // }; private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private ExecutorService mUploadExecutor; private IBinder mBinder; private OwnCloudClient mUploadClient = null; private Account mCurrentAccount = null; // LastAccount in FileUploadService private FileDataStorageManager mStorageManager; //since there can be only one instance of an Android service, there also just one db connection. private UploadsStorageManager mUploadsStorageManager = null; private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false); private final AtomicBoolean mCancellationPossible = new AtomicBoolean(false); /** * List of uploads that are currently pending. Maps from remotePath to where file * is being uploaded to {@link UploadFileOperation}. */ //private ConcurrentMap mPendingUploads = new ConcurrentHashMap(); // TODO: change kind of mPendingUploads: List to IndexedForest private IndexedForest mPendingUploads = new IndexedForest(); /** * {@link UploadFileOperation} object of ongoing upload. Can be null. Note: There can only be one concurrent upload! */ private UploadFileOperation mCurrentUpload = null; private NotificationManager mNotificationManager; private NotificationCompat.Builder mNotificationBuilder; private int mLastPercent; private static final String MIME_TYPE_PDF = "application/pdf"; private static final String FILE_EXTENSION_PDF = ".pdf"; public FileUploadService() { super(); } public static String getUploadFinishMessage() { return FileUploadService.class.getName().toString() + UPLOAD_FINISH_MESSAGE; } /** * Builds a key for mPendingUploads from the account and file to upload * * @param account Account where the file to upload is stored * @param file File to upload */ private String buildRemoteName(Account account, OCFile file) { return account.name + file.getRemotePath(); } private String buildRemoteName(Account account, String remotePath) { return account.name + remotePath; } private String buildRemoteName(OCUpload OCUpload) { return OCUpload.getAccountName() + OCUpload.getRemotePath(); } /** * Checks if an ownCloud server version should support chunked uploads. * * @param version OwnCloud version instance corresponding to an ownCloud * server. * @return 'True' if the ownCloud server with version supports chunked * uploads. * * TODO - move to OCClient */ private static boolean chunkedUploadIsSupported(OwnCloudVersion version) { return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0); } /** * Service initialization */ // TODO: Clean method: comments on extra code @Override public void onCreate() { super.onCreate(); Log_OC.d(TAG, "Creating service"); //Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.size() + " - onCreate"); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); // HandlerThread thread = new HandlerThread("FileUploadService-Requester"); --> From FileUploadService HandlerThread thread = new HandlerThread("FileUploaderThread", android.os.Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); //mServiceHandler = new ServiceHandler(mServiceLooper); // FileUploaderService mServiceHandler = new ServiceHandler(mServiceLooper, this); // FileUploader mBinder = new FileUploaderBinder(); // From FileUploaderService mUploadsStorageManager = new UploadsStorageManager(getContentResolver()); //when this service starts there is no upload in progress. if db says so, app probably crashed before. mUploadsStorageManager.setAllCurrentToUploadLater(); mUploadExecutor = Executors.newFixedThreadPool(1); Log_OC.d(TAG, "FileUploadService.retry() called by onCreate()"); FileUploadService.retry(getApplicationContext()); AccountManager am = AccountManager.get(getApplicationContext()); am.addOnAccountsUpdatedListener(this, null, false); } /** * Service clean up */ @Override public void onDestroy() { Log_OC.v(TAG, "Destroying service" ); mBinder = null; mServiceHandler = null; mServiceLooper.quit(); mServiceLooper = null; mNotificationManager = null; // From FileUploader //Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.size() + " - onDestroy"); Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.toString() + " - onDestroy"); //mServiceLooper.quit(); mUploadExecutor.shutdown(); AccountManager am = AccountManager.get(getApplicationContext()); am.removeOnAccountsUpdatedListener(this); super.onDestroy(); } // Deprecated: TODO - Remove, not in FileUploader // @Override // public void onStart(Intent intent, int intentStartId) { // Message msg = mServiceHandler.obtainMessage(); // msg.arg1 = intentStartId; // msg.obj = intent; // mServiceHandler.sendMessage(msg); // } /** * Entry point to add one or several files to the queue of uploads. * * New uploads are added calling to startService(), resulting in a call to * this method. This ensures the service will keep on working although the * caller activity goes away. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Log_OC.d(TAG, "Starting command with id " + startId); if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_UPLOAD_TYPE) || !(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) { Log_OC.e(TAG, "Not enough information provided in intent"); return Service.START_NOT_STICKY; } int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1); if (uploadType == -1) { Log_OC.e(TAG, "Incorrect upload type provided"); return Service.START_NOT_STICKY; } Account account = intent.getParcelableExtra(KEY_ACCOUNT); if (!AccountUtils.exists(account, getApplicationContext())) { return Service.START_NOT_STICKY; } String[] localPaths = null, remotePaths = null, mimeTypes = null; OCFile[] files = null; if (uploadType == UPLOAD_SINGLE_FILE) { if (intent.hasExtra(KEY_FILE)) { files = new OCFile[] { intent.getParcelableExtra(KEY_FILE) }; } else { localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) }; remotePaths = new String[] { intent.getStringExtra(KEY_REMOTE_FILE) }; mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) }; } } else { // mUploadType == UPLOAD_MULTIPLE_FILES if (intent.hasExtra(KEY_FILE)) { files = (OCFile[]) intent.getParcelableArrayExtra(KEY_FILE); // TODO // will // this // casting // work // fine? } else { localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); } } FileDataStorageManager storageManager = new FileDataStorageManager(account, getContentResolver()); boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_FORGET); if (intent.hasExtra(KEY_FILE) && files == null) { Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent"); return Service.START_NOT_STICKY; } else if (!intent.hasExtra(KEY_FILE)) { if (localPaths == null) { Log_OC.e(TAG, "Incorrect array for local paths provided in upload intent"); return Service.START_NOT_STICKY; } if (remotePaths == null) { Log_OC.e(TAG, "Incorrect array for remote paths provided in upload intent"); return Service.START_NOT_STICKY; } if (localPaths.length != remotePaths.length) { Log_OC.e(TAG, "Different number of remote paths and local paths!"); return Service.START_NOT_STICKY; } files = new OCFile[localPaths.length]; for (int i = 0; i < localPaths.length; i++) { files[i] = obtainNewOCFileToUpload(remotePaths[i], localPaths[i], ((mimeTypes != null) ? mimeTypes[i] : null)); if (files[i] == null) { // TODO @andomaex add failure Notification return Service.START_NOT_STICKY; } } } OwnCloudVersion ocv = AccountUtils.getServerVersion(account); boolean chunked = FileUploadService.chunkedUploadIsSupported(ocv); AbstractList requestedUploads = new Vector(); String uploadKey = null; UploadFileOperation newUpload = null; try { for (int i = 0; i < files.length; i++) { newUpload = new UploadFileOperation( account, files[i], chunked, //isInstant, forceOverwrite, FileUploadService.LocalBehaviour.values()[localAction], // Change for compilation getApplicationContext() ); if (isInstant) { newUpload.setRemoteFolderToBeCreated(); } newUpload.addDatatransferProgressListener(this); newUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder); Pair putResult = mPendingUploads.putIfAbsent( account, files[i].getRemotePath(), newUpload ); if (putResult != null) { uploadKey = putResult.first; requestedUploads.add(uploadKey); } // else, file already in the queue of uploads; don't repeat the request } } catch (IllegalArgumentException e) { Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); return START_NOT_STICKY; } catch (IllegalStateException e) { Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage()); return START_NOT_STICKY; } catch (Exception e) { Log_OC.e(TAG, "Unexpected exception while processing upload intent", e); return START_NOT_STICKY; } if (requestedUploads.size() > 0) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = requestedUploads; mServiceHandler.sendMessage(msg); } return Service.START_NOT_STICKY; } @Override public void onAccountsUpdated(Account[] accounts) { // Review current upload, and cancel it if its account doen't exist if (mCurrentUpload != null && !AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) { mCurrentUpload.cancel(); } // The rest of uploads are cancelled when they try to start } /** * Provides a binder object that clients can use to perform operations on * the queue of uploads, excepting the addition of new files. * * Implemented to perform cancellation, pause and resume of existing * uploads. */ @Override public IBinder onBind(Intent arg0) { return mBinder; } /** * Called when ALL the bound clients were onbound. */ @Override public boolean onUnbind(Intent intent) { ((FileUploaderBinder) mBinder).clearListeners(); return false; // not accepting rebinding (default behaviour) } /** * Binder to let client components to perform operations on the queue of * uploads. * * It provides by itself the available operations. */ public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { /** * Map of listeners that will be reported about progress of uploads from * a {@link FileUploaderBinder} instance */ private Map mBoundListeners = new HashMap(); /** * Returns ongoing upload operation. May be null. */ public UploadFileOperation getCurrentUploadOperation() { return mCurrentUpload; } /** * Cancels a pending or current upload of a remote file. * * @param account Owncloud account where the remote file will be stored. * @param file A file in the queue of pending uploads */ public void cancel(Account account, OCFile file) { // From FileUploader Pair removeResult = mPendingUploads.remove(account, file.getRemotePath()); UploadFileOperation upload = removeResult.first; if (upload != null) { upload.cancel(); } else { if (mCurrentUpload != null && mCurrentAccount != null && mCurrentUpload.getRemotePath().startsWith(file.getRemotePath()) && account.name.equals(mCurrentAccount.name)) { mCurrentUpload.cancel(); } } // From FileUploadService // updating current references (i.e., uploadStatus of current // upload) is handled by updateDataseUploadResult() which is called // after upload finishes. Following cancel call makes sure that is // does finish right now. if (mCurrentUpload != null && mCurrentUpload.isUploadInProgress()) { Log_OC.d(TAG, "Calling cancel for " + file.getRemotePath() + " during upload operation."); mCurrentUpload.cancel(); } else if(mCancellationPossible.get()){ Log_OC.d(TAG, "Calling cancel for " + file.getRemotePath() + " during preparing for upload."); mCancellationRequested.set(true); } else { Log_OC.d(TAG, "Calling cancel for " + file.getRemotePath() + " while upload is pending."); // upload not in progress, but pending. // in this case we have to update the db here. //OCUpload upload = mPendingUploads.remove(buildRemoteName(account, file)); OCUpload ocUpload = mUploadsStorageManager.getUploadByLocalPath(upload.getStoragePath())[0]; ocUpload.setUploadStatus(UploadStatus.UPLOAD_CANCELLED); ocUpload.setLastResult(UploadResult.CANCELLED); // storagePath inside upload is the temporary path. file // contains the correct path used as db reference. ocUpload.getOCFile().setStoragePath(file.getStoragePath()); mUploadsStorageManager.updateUploadStatus(ocUpload); } } /** * Cancels a pending or current upload for an account * * @param account Owncloud accountName where the remote file will be stored. */ public void cancel(Account account) { Log_OC.d(TAG, "Account= " + account.name); if (mCurrentUpload != null) { Log_OC.d(TAG, "Current Upload Account= " + mCurrentUpload.getAccount().name); if (mCurrentUpload.getAccount().name.equals(account.name)) { mCurrentUpload.cancel(); } } // Cancel pending uploads cancelUploadsForAccount(account); } // TODO: Review: Method from FileUploadService with some changes because the merge with FileUploader public void remove(Account account, OCFile file) { Pair removeResult = mPendingUploads.remove(account, file.getRemotePath()); UploadFileOperation upload = removeResult.first; //OCUpload upload = mPendingUploads.remove(buildRemoteName(account, file)); if(upload == null) { Log_OC.e(TAG, "Could not delete upload "+ file +" from mPendingUploads."); } int d = mUploadsStorageManager.removeUpload(upload.getStoragePath()); if(d == 0) { Log_OC.e(TAG, "Could not delete upload "+ file.getStoragePath() +" from database."); } } // TODO: Review: Method from FileUploadService with some changes because the merge with FileUploader // TODO Complete operation to retry the upload /** * Puts upload in upload list and tell FileUploadService to upload items in list. */ public void retry(Account account, OCUpload upload) { String uploadKey = buildRemoteName(upload); //mPendingUploads.put(uploadKey, upload); FileUploadService.retry(getApplicationContext(), uploadKey); } public void clearListeners() { mBoundListeners.clear(); } /** * Returns True when the file described by 'file' is being uploaded to * the ownCloud account 'account' or waiting for it * * If 'file' is a directory, returns 'true' if some of its descendant * files is uploading or waiting to upload. * * Warning: If remote file exists and !forceOverwrite the original file * is being returned here. That is, it seems as if the original file is * being updated when actually a new file is being uploaded. * * @param account Owncloud account where the remote file will be stored. * @param file A file that could be in the queue of pending uploads */ public boolean isUploading(Account account, OCFile file) { if (account == null || file == null) return false; return (mPendingUploads.contains(account, file.getRemotePath())); // Commented code from FileUploadService // String targetKey = buildRemoteName(account, file); // synchronized (mPendingUploads) { // if (file.isFolder()) { // // this can be slow if there are many uploads :( // Iterator it = mPendingUploads.keySet().iterator(); // boolean found = false; // while (it.hasNext() && !found) { // found = it.next().startsWith(targetKey); // } // return found; // } else { // return (mPendingUploads.containsKey(targetKey)); // } // } } /** * Adds a listener interested in the progress of the upload for a * concrete file. * * @param listener Object to notify about progress of transfer. * @param account ownCloud account holding the file of interest. * @param file {@link OCFile} of interest for listener. */ public void addDatatransferProgressListener(OnDatatransferProgressListener listener, Account account, OCFile file) { if (account == null || file == null || listener == null) return; String targetKey = buildRemoteName(account, file); mBoundListeners.put(targetKey, listener); } /** * Removes a listener interested in the progress of the upload for a * concrete file. * * @param listener Object to notify about progress of transfer. * @param account ownCloud account holding the file of interest. * @param file {@link OCFile} of interest for listener. */ public void removeDatatransferProgressListener(OnDatatransferProgressListener listener, Account account, OCFile file) { if (account == null || file == null || listener == null) return; String targetKey = buildRemoteName(account, file); if (mBoundListeners.get(targetKey) == listener) { mBoundListeners.remove(targetKey); } } // TODO: Review: Method from FileUploadService with some changes because the merge with FileUploader @Override public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) { String key = buildRemoteName(mCurrentUpload.getAccount(), mCurrentUpload.getFile()); OnDatatransferProgressListener boundListener = mBoundListeners.get(key); if (boundListener != null) { boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName); } // // Set> uploads = mPendingUploads.entrySet(); // UploadFileOperation currentUpload = mCurrentUpload; // if (currentUpload == null) { // Log_OC.e(TAG, "Found no current upload with remote path " + localFileName + ". Ignore."); // return; // } // String key = buildRemoteName(currentUpload.getAccount(), currentUpload.getFile()); // OnDatatransferProgressListener boundListener = mBoundListeners.get(key); // if (boundListener != null) { // boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, localFileName); // } } } // TODO: Remove this comment: Class totally replaced by FileUploader ServiceHandler // TODO: Review merge modifications /** * Upload worker. Performs the pending uploads in the order they were * requested. * * Created with the Looper of a new thread, started in * {@link FileUploadService#onCreate()}. */ private static class ServiceHandler extends Handler { // don't make it a final class, and don't remove the static ; lint will // warn about a possible memory leak FileUploadService mService; public ServiceHandler(Looper looper, FileUploadService service) { super(looper); if (service == null) throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); mService = service; } @Override public void handleMessage(Message msg) { @SuppressWarnings("unchecked") AbstractList requestedUploads = (AbstractList) msg.obj; if (msg.obj != null) { Iterator it = requestedUploads.iterator(); while (it.hasNext()) { mService.uploadFile(it.next()); } } Log_OC.d(TAG, "Stopping command after id " + msg.arg1); mService.stopSelf(msg.arg1); } } // TODO: Method from FileUploader /** * Core upload method: sends the file(s) to upload * * @param uploadKey Key to access the upload to perform, contained in mPendingUploads */ public void uploadFile(String uploadKey) { mCurrentUpload = mPendingUploads.get(uploadKey); if (mCurrentUpload != null) { // Detect if the account exists if (AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) { Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().name + " exists"); notifyUploadStart(mCurrentUpload); RemoteOperationResult uploadResult = null, grantResult; try { /// prepare client object to send the request to the ownCloud server if (mCurrentAccount == null || !mCurrentAccount.equals(mCurrentUpload.getAccount())) { mCurrentAccount = mCurrentUpload.getAccount(); mStorageManager = new FileDataStorageManager( mCurrentAccount, getContentResolver() ); } // else, reuse storage manager from previous operation // always get client from client manager, to get fresh credentials in case of update OwnCloudAccount ocAccount = new OwnCloudAccount(mCurrentAccount, this); mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton(). getClientFor(ocAccount, this); /// check the existence of the parent folder for the file to upload String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent(); remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; grantResult = grantFolderExistence(remoteParentPath); /// perform the upload if (grantResult.isSuccess()) { OCFile parent = mStorageManager.getFileByPath(remoteParentPath); mCurrentUpload.getFile().setParentId(parent.getFileId()); uploadResult = mCurrentUpload.execute(mUploadClient); if (uploadResult.isSuccess()) { saveUploadedFile(); } else if (uploadResult.getCode() == ResultCode.SYNC_CONFLICT) { mStorageManager.saveConflict(mCurrentUpload.getFile(), mCurrentUpload.getFile().getEtagInConflict()); } } else { uploadResult = grantResult; } } catch (Exception e) { Log_OC.e(TAG, "Error uploading", e); uploadResult = new RemoteOperationResult(e); } finally { Pair removeResult; if (mCurrentUpload.wasRenamed()) { removeResult = mPendingUploads.removePayload( mCurrentAccount, mCurrentUpload.getOldFile().getRemotePath() ); } else { removeResult = mPendingUploads.removePayload( mCurrentAccount, mCurrentUpload.getRemotePath() ); } /// notify result notifyUploadResult(mCurrentUpload, uploadResult); sendBroadcastUploadFinished(mCurrentUpload, uploadResult, removeResult.second); } } else { // Cancel the transfer Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().toString() + " doesn't exist"); cancelUploadsForAccount(mCurrentUpload.getAccount()); } } } // TODO: Replaced by uploadFile(String uploadKey) /** * Core upload method: sends the file(s) to upload. This function blocks * until upload succeeded or failed. * * Before uploading starts mCurrentUpload is set. * * @param OCUpload Key to access the upload to perform, contained in * mPendingUploads * @return RemoteOperationResult of upload operation. */ // private RemoteOperationResult uploadFile(OCUpload OCUpload) { // // if (mCurrentUpload != null) { // Log_OC.e(TAG, // "mCurrentUpload != null. Meaning there is another upload in progress, cannot start a new one. Fix that!"); // return new RemoteOperationResult(new IllegalStateException("mCurrentUpload != null when calling uploadFile()")); // } // // AccountManager aMgr = AccountManager.get(this); // Account account = OCUpload.getAccount(getApplicationContext()); // String version = aMgr.getUserData(account, Constants.KEY_OC_VERSION); // OwnCloudVersion ocv = new OwnCloudVersion(version); // // boolean chunked = FileUploadService.chunkedUploadIsSupported(ocv); // String uploadKey = null; // // uploadKey = buildRemoteName(account, OCUpload.getRemotePath()); // OCFile file = OCUpload.getOCFile(); // Log_OC.d(TAG, "mCurrentUpload = new UploadFileOperation"); // mCurrentUpload = new UploadFileOperation(account, file, chunked, OCUpload.isForceOverwrite(), // OCUpload.getLocalAction(), getApplicationContext()); // if (OCUpload.isCreateRemoteFolder()) { // mCurrentUpload.setRemoteFolderToBeCreated(); // } // mCurrentUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder); // mCurrentUpload.addDatatransferProgressListener(this); // // mCancellationRequested.set(false); // mCancellationPossible.set(true); // notifyUploadStart(mCurrentUpload); // // RemoteOperationResult uploadResult = null, grantResult = null; // try { // // prepare client object to send requests to the ownCloud // // server // if (mUploadClient == null || !mCurrentAccount.equals(mCurrentUpload.getAccount())) { // mCurrentAccount = mCurrentUpload.getAccount(); // mStorageManager = new FileDataStorageManager(mCurrentAccount, getContentResolver()); // OwnCloudAccount ocAccount = new OwnCloudAccount(mCurrentAccount, this); // mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this); // } // // // check the existence of the parent folder for the file to // // upload // String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent(); // remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath // + OCFile.PATH_SEPARATOR; // //TODO this might take a moment and should thus be cancelable, for now: cancel afterwards. // grantResult = grantFolderExistence(mCurrentUpload, remoteParentPath); // // if(mCancellationRequested.get()) { // throw new OperationCancelledException(); // } // mCancellationPossible.set(false); // // // perform the upload // if (grantResult.isSuccess()) { // OCFile parent = mStorageManager.getFileByPath(remoteParentPath); // mCurrentUpload.getFile().setParentId(parent.getFileId()); // // inside this call the remote path may be changed (if remote // // file exists and !forceOverwrite) in this case the map from // // mPendingUploads is wrong. This results in an upload // // indication in the GUI showing that the original file is being // // uploaded (instead that a new one is created) // uploadResult = mCurrentUpload.execute(mUploadClient); // if (uploadResult.isSuccess()) { // saveUploadedFile(mCurrentUpload); // } // } else { // uploadResult = grantResult; // } // // } catch (AccountsException e) { // Log_OC.e(TAG, "Error while trying to get autorization for " + mCurrentAccount.name, e); // uploadResult = new RemoteOperationResult(e); // // } catch (IOException e) { // Log_OC.e(TAG, "Error while trying to get autorization for " + mCurrentAccount.name, e); // uploadResult = new RemoteOperationResult(e); // // } catch (OperationCancelledException e) { // uploadResult = new RemoteOperationResult(e); // } finally { // Log_OC.d(TAG, "Remove CurrentUploadItem from pending upload Item Map."); // if (uploadResult.isException()) { // // enforce the creation of a new client object for next // // uploads; this grant that a new socket will // // be created in the future if the current exception is due // // to an abrupt lose of network connection // mUploadClient = null; // } // } // return uploadResult; // } /** * Checks the existence of the folder where the current file will be uploaded both * in the remote server and in the local database. * * If the upload is set to enforce the creation of the folder, the method tries to * create it both remote and locally. * * @param pathToGrant Full remote path whose existence will be granted. * @return An {@link OCFile} instance corresponding to the folder where the file * will be uploaded. */ private RemoteOperationResult grantFolderExistence(String pathToGrant) { RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, this, false); RemoteOperationResult result = operation.execute(mUploadClient); if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND && mCurrentUpload.isRemoteFolderToBeCreated()) { SyncOperation syncOp = new CreateFolderOperation( pathToGrant, true); result = syncOp.execute(mUploadClient, mStorageManager); } if (result.isSuccess()) { OCFile parentDir = mStorageManager.getFileByPath(pathToGrant); if (parentDir == null) { parentDir = createLocalFolder(pathToGrant); } if (parentDir != null) { result = new RemoteOperationResult(ResultCode.OK); } else { result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); } } return result; } // TODO: Replaced by grantFolderExistence(String pathToGrant // /** // * Checks the existence of the folder where the current file will be // * uploaded both in the remote server and in the local database. // * // * If the upload is set to enforce the creation of the folder, the method // * tries to create it both remote and locally. // * // * @param pathToGrant Full remote path whose existence will be granted. // * @return An {@link OCFile} instance corresponding to the folder where the // * file will be uploaded. // */ // private RemoteOperationResult grantFolderExistence(UploadFileOperation currentUpload, String pathToGrant) { // RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, this, false); // RemoteOperationResult result = operation.execute(mUploadClient); // if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND // && currentUpload.isRemoteFolderToBeCreated()) { // SyncOperation syncOp = new CreateFolderOperation(pathToGrant, true); // result = syncOp.execute(mUploadClient, mStorageManager); // } // if (result.isSuccess()) { // OCFile parentDir = mStorageManager.getFileByPath(pathToGrant); // if (parentDir == null) { // parentDir = createLocalFolder(pathToGrant); // } // if (parentDir != null) { // result = new RemoteOperationResult(ResultCode.OK); // } else { // result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); // } // } // return result; // } private OCFile createLocalFolder(String remotePath) { String parentPath = new File(remotePath).getParent(); parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ? parentPath : parentPath + OCFile.PATH_SEPARATOR; OCFile parent = mStorageManager.getFileByPath(parentPath); if (parent == null) { parent = createLocalFolder(parentPath); } if (parent != null) { OCFile createdFolder = new OCFile(remotePath); createdFolder.setMimetype("DIR"); createdFolder.setParentId(parent.getFileId()); mStorageManager.saveFile(createdFolder); return createdFolder; } return null; } /** * Saves a OC File after a successful upload. * * A PROPFIND is necessary to keep the props in the local database * synchronized with the server, specially the modification time and Etag * (where available) * * TODO move into UploadFileOperation */ private void saveUploadedFile() { OCFile file = mCurrentUpload.getFile(); if (file.fileExists()) { file = mStorageManager.getFileById(file.getFileId()); } long syncDate = System.currentTimeMillis(); file.setLastSyncDateForData(syncDate); // new PROPFIND to keep data consistent with server // in theory, should return the same we already have ReadRemoteFileOperation operation = new ReadRemoteFileOperation(mCurrentUpload.getRemotePath()); RemoteOperationResult result = operation.execute(mUploadClient); if (result.isSuccess()) { updateOCFile(file, (RemoteFile) result.getData().get(0)); file.setLastSyncDateForProperties(syncDate); } else { Log_OC.e(TAG, "Error reading properties of file after successful upload; this is gonna hurt..."); } // / maybe this would be better as part of UploadFileOperation... or // maybe all this method if (mCurrentUpload.wasRenamed()) { OCFile oldFile = mCurrentUpload.getOldFile(); if (oldFile.fileExists()) { oldFile.setStoragePath(null); mStorageManager.saveFile(oldFile); mStorageManager.saveConflict(oldFile, null); } // else: it was just an automatic renaming due to a name // coincidence; nothing else is needed, the storagePath is right // in the instance returned by mCurrentUpload.getFile() } file.setNeedsUpdateThumbnail(true); mStorageManager.saveFile(file); mStorageManager.saveConflict(file, null); mStorageManager.triggerMediaScan(file.getStoragePath()); } private void updateOCFile(OCFile file, RemoteFile remoteFile) { file.setCreationTimestamp(remoteFile.getCreationTimestamp()); file.setFileLength(remoteFile.getLength()); file.setMimetype(remoteFile.getMimeType()); file.setModificationTimestamp(remoteFile.getModifiedTimestamp()); file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp()); file.setEtag(remoteFile.getEtag()); file.setRemoteId(remoteFile.getRemoteId()); } private OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) { // MIME type if (mimeType == null || mimeType.length() <= 0) { try { mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( remotePath.substring(remotePath.lastIndexOf('.') + 1)); } catch (IndexOutOfBoundsException e) { Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " + remotePath); } } if (mimeType == null) { mimeType = "application/octet-stream"; } if (isPdfFileFromContentProviderWithoutExtension(localPath, mimeType)){ remotePath += FILE_EXTENSION_PDF; } OCFile newFile = new OCFile(remotePath); newFile.setStoragePath(localPath); newFile.setLastSyncDateForProperties(0); newFile.setLastSyncDateForData(0); // size if (localPath != null && localPath.length() > 0) { File localFile = new File(localPath); newFile.setFileLength(localFile.length()); newFile.setLastSyncDateForData(localFile.lastModified()); } // don't worry about not assigning size, the problems with localPath // are checked when the UploadFileOperation instance is created newFile.setMimetype(mimeType); return newFile; } /** * Creates a status notification to show the upload progress * * @param upload Upload operation starting. */ private void notifyUploadStart(UploadFileOperation upload) { // / create status notification with a progress bar mLastPercent = 0; mNotificationBuilder = NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this); mNotificationBuilder .setOngoing(true) .setSmallIcon(R.drawable.notification_icon) .setTicker(getString(R.string.uploader_upload_in_progress_ticker)) .setContentTitle(getString(R.string.uploader_upload_in_progress_ticker)) .setProgress(100, 0, false) .setContentText( String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName())); /// includes a pending intent in the notification showing the details Intent showUploadListIntent = new Intent(this, UploadListActivity.class); showUploadListIntent.putExtra(FileActivity.EXTRA_FILE, upload.getFile()); showUploadListIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); showUploadListIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); showUploadListIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), showUploadListIntent, 0)); // TODO: Review Old behaviour in FileUploader /// includes a pending intent in the notification showing the details view of the file // Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class); // showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, (Parcelable) upload.getFile()); // showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); // showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // mNotificationBuilder.setContentIntent(PendingIntent.getActivity( // this, (int) System.currentTimeMillis(), showDetailsIntent, 0 // )); mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build()); updateDatabaseUploadStart(mCurrentUpload); } /** * Updates the persistent upload database that upload is in progress. */ private void updateDatabaseUploadStart(UploadFileOperation upload) { mUploadsStorageManager.updateUploadStatus( upload.getOriginalStoragePath(), UploadStatus.UPLOAD_IN_PROGRESS, null ); } /** * Callback method to update the progress bar in the status notification */ @Override public void onTransferProgress( long progressRate, long totalTransferredSoFar, long totalToTransfer, String filePath ) { int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer)); if (percent != mLastPercent) { mNotificationBuilder.setProgress(100, percent, false); String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, fileName); mNotificationBuilder.setContentText(text); mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build()); } mLastPercent = percent; } /** * Updates the status notification with the result of an upload operation. * * @param uploadResult Result of the upload operation. * @param upload Finished upload operation */ private void notifyUploadResult(UploadFileOperation upload, RemoteOperationResult uploadResult) { Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.getCode()); // / cancelled operation or success -> silent removal of progress // notification mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker); // Show the result: success or fail notification if (!uploadResult.isCancelled()) { int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker : R.string.uploader_upload_failed_ticker; String content; // = null; // check credentials error boolean needsToUpdateCredentials = ( uploadResult.getCode() == ResultCode.UNAUTHORIZED || uploadResult.isIdPRedirection() ); tickerId = (needsToUpdateCredentials) ? R.string.uploader_upload_failed_credentials_error : tickerId; mNotificationBuilder .setTicker(getString(tickerId)) .setContentTitle(getString(tickerId)) .setAutoCancel(true) .setOngoing(false) .setProgress(0, 0, false); content = ErrorMessageAdapter.getErrorCauseMessage( uploadResult, upload, getResources() ); if (needsToUpdateCredentials) { // let the user update credentials with one click Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class); updateAccountCredentials.putExtra( AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount() ); updateAccountCredentials.putExtra( AuthenticatorActivity.EXTRA_ACTION, AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN); updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); mNotificationBuilder.setContentIntent(PendingIntent.getActivity( this, (int) System.currentTimeMillis(), updateAccountCredentials, PendingIntent.FLAG_ONE_SHOT)); mUploadClient = null; // grant that future retries on the same account will get the fresh credentials }else { mNotificationBuilder.setContentText(content); //Changes for compilation // if (upload.isInstant()) { // DbHandler db = null; // try { // db = new DbHandler(this.getBaseContext()); // String message = uploadResult.getLogMessage() + " errorCode: " + // uploadResult.getCode(); // Log_OC.e(TAG, message + " Http-Code: " + uploadResult.getHttpCode()); // if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) { // //message = getString(R.string.failed_upload_quota_exceeded_text); // if (db.updateFileState( // upload.getOriginalStoragePath(), // DbHandler.UPLOAD_STATUS_UPLOAD_FAILED, // message) == 0) { // db.putFileForLater( // upload.getOriginalStoragePath(), // upload.getAccount().name, // message // ); // } // } // } finally { // if (db != null) { // db.close(); // } // } // } } if(!uploadResult.isSuccess()){ //in case of failure, do not show details file view (because there is no file!) Intent showUploadListIntent = new Intent(this, UploadListActivity.class); showUploadListIntent.putExtra(FileActivity.EXTRA_FILE, upload.getFile()); showUploadListIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), showUploadListIntent, 0)); } mNotificationBuilder.setContentText(content); mNotificationManager.notify(tickerId, mNotificationBuilder.build()); if (uploadResult.isSuccess()) { //Changes for compilation // DbHandler db = new DbHandler(this.getBaseContext()); // db.removeIUPendingFile(mCurrentUpload.getOriginalStoragePath()); // db.close(); // remove success notification, with a delay of 2 seconds NotificationDelayer.cancelWithDelay(mNotificationManager, R.string.uploader_upload_succeeded_ticker, 2000); } } } /** * Updates the persistent upload database with upload result. */ private void updateDatabaseUploadResult(RemoteOperationResult uploadResult, UploadFileOperation upload) { // result: success or fail notification Log_OC.d(TAG, "updateDataseUploadResult uploadResult: " + uploadResult + " upload: " + upload); if (uploadResult.isCancelled()) { mUploadsStorageManager.updateUploadStatus( upload.getOriginalStoragePath(), UploadStatus.UPLOAD_CANCELLED, UploadResult.CANCELLED ); } else { if (uploadResult.isSuccess()) { mUploadsStorageManager.updateUploadStatus( upload.getOriginalStoragePath(), UploadStatus.UPLOAD_SUCCEEDED, UploadResult.UPLOADED ); } else { // TODO: Disable for testing of menu actions in uploads view // if (shouldRetryFailedUpload(uploadResult)) { // mUploadsStorageManager.updateUploadStatus( // upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED_RETRY, uploadResult // ); // } else { // mUploadsStorageManager.updateUploadStatus(upload.getOriginalStoragePath(), // UploadsStorageManager.UploadStatus.UPLOAD_FAILED_GIVE_UP, uploadResult); // } } } } // TODO: Disable for testing of menu actions in uploads view // // /** // * Determines whether with given uploadResult the upload should be retried later. // * @param uploadResult // * @return true if upload should be retried later, false if is should be abandoned. // */ // private boolean shouldRetryFailedUpload(RemoteOperationResult uploadResult) { // if (uploadResult.isSuccess()) { // return false; // } // switch (uploadResult.getCode()) { // case HOST_NOT_AVAILABLE: // case NO_NETWORK_CONNECTION: // case TIMEOUT: // case WRONG_CONNECTION: // SocketException // return true; // default: // return false; // } // } /** * Sends a broadcast in order to the interested activities can update their * view * * @param upload Finished upload operation * @param uploadResult Result of the upload operation * @param unlinkedFromRemotePath Path in the uploads tree where the upload was unlinked from */ private void sendBroadcastUploadFinished( UploadFileOperation upload, RemoteOperationResult uploadResult, String unlinkedFromRemotePath) { Intent end = new Intent(getUploadFinishMessage()); end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote // path, after // possible // automatic // renaming if (upload.wasRenamed()) { end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath()); } end.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath()); end.putExtra(ACCOUNT_NAME, upload.getAccount().name); end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult.isSuccess()); if (unlinkedFromRemotePath != null) { end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath); } sendStickyBroadcast(end); } // TODO: Remove. Replace by sendBroadcastUploadFinished(UploadFileOperation, RemoteOperationResult, String) // /** // * Sends a broadcast in order to the interested activities can update their // * view // * @param uploadResult Result of the upload operation // * @param upload Finished upload operation // */ // private void sendFinalBroadcast(RemoteOperationResult uploadResult, UploadFileOperation upload) { // Intent end = new Intent(getUploadFinishMessage()); // end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote // // path, after // // possible // // automatic // // renaming // if (upload.wasRenamed()) { // end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath()); // } // end.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath()); // end.putExtra(ACCOUNT_NAME, upload.getAccount().name); // end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult.isSuccess()); // sendStickyBroadcast(end); // } /** * Checks if content provider, using the content:// scheme, returns a file with mime-type * 'application/pdf' but file has not extension * @param localPath Full path to a file in the local file system. * @param mimeType MIME type of the file. * @return true if is needed to add the pdf file extension to the file */ private boolean isPdfFileFromContentProviderWithoutExtension(String localPath, String mimeType) { return localPath.startsWith(UriUtils.URI_CONTENT_SCHEME) && mimeType.equals(MIME_TYPE_PDF) && !localPath.endsWith(FILE_EXTENSION_PDF); } /** * Remove uploads of an account * * @param account Downloads account to remove */ private void cancelUploadsForAccount(Account account){ // Cancel pending uploads mPendingUploads.remove(account); } // TODO: Remove // /** // * Remove uploads of an account // * @param accountName Name of an OC account // */ // private void cancelUploadForAccount(String accountName){ // // this can be slow if there are many uploads :( // Iterator it = mPendingUploads.keySet().iterator(); // Log_OC.d(TAG, "Number of pending updloads= " + mPendingUploads.size()); // while (it.hasNext()) { // String key = it.next(); // Log_OC.d(TAG, "mPendingUploads CANCELLED " + key); // if (key.startsWith(accountName)) { // synchronized (mPendingUploads) { // mPendingUploads.remove(key); // } // } // } // } // TODO: onHandleIntent not used in the new FileUploadService // TODO: Remove when clean code /** * The IntentService calls this method from the default worker thread with * the intent that started the service. When this method returns, * IntentService stops the service, as appropriate. * * Note: We use an IntentService here. It does not provide simultaneous * requests, but instead internally queues them and gets them to this * onHandleIntent method one after another. This makes implementation less * error-prone but prevents files to be added to list while another upload * is active. If everything else works fine, fixing this should be a TODO. * * Entry point to add one or several files to the queue of uploads. * * New uploads are added calling to startService(), resulting in a call to * this method. This ensures the service will keep on working although the * caller activity goes away. * * First, onHandleIntent() stores all information associated with the upload * in a {@link OCUpload} which is stored persistently using * {@link UploadsStorageManager}. Then, the oldest, pending upload from * {@link UploadsStorageManager} is taken and upload is started. * @param intentStartId */ // // protected void onHandleIntent(Intent intent, int intentStartId) { // Log_OC.d(TAG, "onHandleIntent start"); // //Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.size() + " - before adding new uploads."); // Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.toString() + " - before adding new uploads."); // if (intent == null || intent.hasExtra(KEY_RETRY)) { // Log_OC.d(TAG, "Received null intent."); // // service was restarted by OS or connectivity change was detected or // // retry of pending upload was requested. // // ==> First check persistent uploads, then perform upload. // int countAddedEntries = 0; // OCUpload[] list = mUploadsStorageManager.getPendingUploads(); // for (OCUpload OCUpload : list) { // Log_OC.d(TAG, "Retrieved from DB: " + OCUpload.toFormattedString()); // // String uploadKey = buildRemoteName(OCUpload); // OCUpload previous = mPendingUploads.putIfAbsent(uploadKey, OCUpload); // if(previous == null) { // Log_OC.d(TAG, "mPendingUploads added: " + OCUpload.toFormattedString()); // countAddedEntries++; // } else { // //already pending. ignore. // } // } // Log_OC.d(TAG, "added " + countAddedEntries // + " entrie(s) to mPendingUploads (this should be 0 except for the first time)."); // // null intent is received when charging or wifi state changes. // // fake a mUploadsStorageManager change event, so that GUI can update the reason for // // LATER status of uploads. // mUploadsStorageManager.notifyObserversNow(); // } else { // Log_OC.d(TAG, "Receive upload intent."); // UploadQuantity uploadType = (UploadQuantity) intent.getSerializableExtra(KEY_UPLOAD_TYPE); // if (uploadType == null) { // Log_OC.e(TAG, "Incorrect or no upload type provided"); // return; // } // // Account account = intent.getParcelableExtra(KEY_ACCOUNT); // if (!AccountUtils.exists(account, getApplicationContext())) { // Log_OC.e(TAG, "KEY_ACCOUNT no set or provided KEY_ACCOUNT does not exist"); // return; // } // mStorageManager = new FileDataStorageManager(account, getContentResolver()); // // OCFile[] files = null; // // if KEY_FILE given, use it // if (intent.hasExtra(KEY_FILE)) { // if (uploadType == UploadQuantity.UPLOAD_SINGLE_FILE) { // files = new OCFile[] { intent.getParcelableExtra(KEY_FILE) }; // } else { // files = (OCFile[]) intent.getParcelableArrayExtra(KEY_FILE); // } // // } else { // else use KEY_LOCAL_FILE and KEY_REMOTE_FILE // // if (!intent.hasExtra(KEY_LOCAL_FILE) || !intent.hasExtra(KEY_REMOTE_FILE)) { // Log_OC.e(TAG, "Not enough information provided in intent"); // return; // } // // String[] localPaths; // String[] remotePaths; // String[] mimeTypes; // if (uploadType == UploadQuantity.UPLOAD_SINGLE_FILE) { // localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) }; // remotePaths = new String[] { intent.getStringExtra(KEY_REMOTE_FILE) }; // mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) }; // } else { // localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); // remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); // mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); // } // if (localPaths.length != remotePaths.length) { // Log_OC.e(TAG, "Different number of remote paths and local paths!"); // return; // } // // files = new OCFile[localPaths.length]; // // for (int i = 0; i < localPaths.length; i++) { // files[i] = obtainNewOCFileToUpload(remotePaths[i], localPaths[i], // ((mimeTypes != null) ? mimeTypes[i] : (String) null)); // if (files[i] == null) { // Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i] // + " and localPaths[i]:" + localPaths[i]); // return; // } // mStorageManager.saveFile(files[i]); // files[i] = mStorageManager.getFileByLocalPath(files[i].getStoragePath()); // } // } // // // at this point variable "OCFile[] files" is loaded correctly. // // boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); // boolean isCreateRemoteFolder = intent.getBooleanExtra(KEY_CREATE_REMOTE_FOLDER, false); // boolean isUseWifiOnly = intent.getBooleanExtra(KEY_WIFI_ONLY, true); // boolean isWhileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false); // long uploadTimestamp = intent.getLongExtra(KEY_UPLOAD_TIMESTAMP, -1); // // LocalBehaviour localAction = (LocalBehaviour) intent.getSerializableExtra(KEY_LOCAL_BEHAVIOUR); // if (localAction == null) // localAction = LocalBehaviour.LOCAL_BEHAVIOUR_COPY; // // // save always persistently path of upload, so it can be retried if // // failed. // for (int i = 0; i < files.length; i++) { // OCUpload upload = new OCUpload(files[i]); // upload.setAccountName(account.name); // upload.setForceOverwrite(forceOverwrite); // upload.setCreateRemoteFolder(isCreateRemoteFolder); // upload.setLocalAction(localAction); // upload.setUseWifiOnly(isUseWifiOnly); // upload.setWhileChargingOnly(isWhileChargingOnly); // upload.setUploadTimestamp(uploadTimestamp); // upload.setUploadStatus(UploadStatus.UPLOAD_LATER); // // String uploadKey = buildRemoteName(upload); // OCUpload previous = mPendingUploads.putIfAbsent(uploadKey, upload); // // if(previous == null) // { // Log_OC.d(TAG, "mPendingUploads added: " + upload.toFormattedString()); // // // if upload was not queued before, we can add it to // // database because the user wants the file to be uploaded. // // however, it can happened that the user uploaded the same // // file before in which case there is an old db entry. // // delete that to be sure we have the latest one. // if(mUploadsStorageManager.removeUpload(upload.getLocalPath())>0) { // Log_OC.w(TAG, "There was an old DB entry " + upload.getLocalPath() // + " which had to be removed in order to add new one."); // } // boolean success = mUploadsStorageManager.storeUpload(upload); // if(!success) { // Log_OC.e(TAG, "Could not add upload " + upload.getLocalPath() // + " to database. This should not happen."); // } // } else { // Log_OC.w(TAG, "FileUploadService got upload intent for file which is already queued: " // + upload.getRemotePath()); // // upload already pending. ignore. // } // } // } // // at this point mPendingUploads is filled. // // // // Iterator it; // if (intent != null && intent.getStringExtra(KEY_RETRY_REMOTE_PATH) != null) { // ArrayList list = new ArrayList(1); // String remotePath = intent.getStringExtra(KEY_RETRY_REMOTE_PATH); // list.add(remotePath); // it = list.iterator(); // // update db status for upload // OCUpload OCUpload = mPendingUploads.get(remotePath); // OCUpload.setUploadStatus(UploadStatus.UPLOAD_LATER); // OCUpload.setLastResult(null); // mUploadsStorageManager.updateUploadStatus(OCUpload); // // Log_OC.d(TAG, "Start uploading " + remotePath); // } else { // it = mPendingUploads.keySet().iterator(); // } // if (it.hasNext()) { // while (it.hasNext()) { // String upload = it.next(); // OCUpload OCUpload = mPendingUploads.get(upload); // // if (OCUpload == null) { // Log_OC.e(TAG, "Cannot upload null. Fix that!"); // continue; // } // // UploadTask uploadTask = new UploadTask(OCUpload); // mUploadExecutor.submit(uploadTask); // } // StopSelfTask stopSelfTask = new StopSelfTask(intentStartId); // mUploadExecutor.submit(stopSelfTask); // // } else { // stopSelf(intentStartId); // } // // Log_OC.d(TAG, "onHandleIntent end"); // } // TODO: StopSelfTask not used in the new FileUploadService // TODO: Remove when clean code /** * Stops this services if latest intent id is intentStartId. */ // public class StopSelfTask implements Runnable { // int intentStartId; // // public StopSelfTask(int intentStartId) { // this.intentStartId = intentStartId; // } // // @Override // public void run() { // stopSelf(intentStartId); // } // } // TODO: UploaTask not used in the new FileUploadService // TODO: Remove when clean code /** * Tries uploading OCUpload, creates notifications, and updates mUploadsStorageManager. */ // public class UploadTask implements Runnable { // OCUpload OCUpload; // // public UploadTask(OCUpload OCUpload) { // this.OCUpload = OCUpload; // } // // @Override // public void run() { // Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.size() + " - before uploading."); // switch (canUploadFileNow(OCUpload)) { // case NOW: // Log_OC.d(TAG, "Calling uploadFile for " + OCUpload.getRemotePath()); // RemoteOperationResult uploadResult = uploadFile(OCUpload); // //TODO store renamed upload path? // updateDatabaseUploadResult(uploadResult, mCurrentUpload); // notifyUploadResult(uploadResult, mCurrentUpload); // sendFinalBroadcast(uploadResult, mCurrentUpload); // // TODO: Disable for testing of menu actions in uploads view //// if (!shouldRetryFailedUpload(uploadResult)) { //// Log_OC.d(TAG, "Upload with result " + uploadResult.getCode() + ": " + uploadResult.getLogMessage() //// + " will be abandoned."); //// mPendingUploads.remove(buildRemoteName(OCUpload)); //// } // Log_OC.d(TAG, "mCurrentUpload = null"); // mCurrentUpload = null; // break; // case LATER: // // Schedule retry for later upload. Delay can be due to: // // KEY_WIFI_ONLY - done by ConnectivityActionReceiver // // KEY_WHILE_CHARGING_ONLY - TODO add PowerConnectionReceiver // // KEY_UPLOAD_TIMESTAMP - TODO use AlarmManager to wake up this service // break; // case FILE_GONE: // mUploadsStorageManager.updateUploadStatus( // OCUpload.getLocalPath(), // UploadStatus.UPLOAD_FAILED_GIVE_UP, // com.owncloud.android.db.UploadResult.FILE_ERROR // ); // if (mPendingUploads.remove(OCUpload.getRemotePath()) == null) { // Log_OC.w(TAG, "Could remove " + OCUpload.getRemotePath() // + " from mPendingUploads because it does not exist."); // } // // break; // case ERROR: // Log_OC.e(TAG, "canUploadFileNow() returned ERROR. Fix that!"); // break; // } // Log_OC.d(TAG, "mPendingUploads size:" + mPendingUploads.size() + " - after uploading."); // } // // } /** * Returns the reason as String why state of OCUpload is LATER. If * upload state != LATER return null. */ static public String getUploadLaterReason(Context context, OCUpload OCUpload) { StringBuilder reason = new StringBuilder(); Date now = new Date(); if (now.getTime() < OCUpload.getUploadTimestamp()) { reason.append("Waiting for " + DisplayUtils.unixTimeToHumanReadable(OCUpload.getUploadTimestamp())); } if (OCUpload.isUseWifiOnly() && !UploadUtils.isConnectedViaWiFi(context)) { if (reason.length() > 0) { reason.append(" and wifi connectivity"); } else { reason.append("Waiting for wifi connectivity"); } } if (OCUpload.isWhileChargingOnly() && !UploadUtils.isCharging(context)) { if (reason.length() > 0) { reason.append(" and charging"); } else { reason.append("Waiting for charging"); } } reason.append("."); if (reason.length() > 1) { return reason.toString(); } if (OCUpload.getUploadStatus() == UploadStatus.UPLOAD_LATER) { return "Upload is pending and will start shortly."; } return null; } enum CanUploadFileNowStatus {NOW, LATER, FILE_GONE, ERROR}; /** * Returns true when the file may be uploaded now. This methods checks all * restraints of the passed {@link OCUpload}, these include * isUseWifiOnly(), check if local file exists, check if file was already * uploaded... * * If return value is CanUploadFileNowStatus.NOW, uploadFile() may be * called. * * @return CanUploadFileNowStatus.NOW is upload may proceed,
* CanUploadFileNowStatus.LATER if upload should be performed at a * later time,
* CanUploadFileNowStatus.ERROR if a severe error happened, calling * entity should remove upload from queue. * */ private CanUploadFileNowStatus canUploadFileNow(OCUpload OCUpload) { if (OCUpload.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) { Log_OC.w(TAG, "Already succeeded uploadObject was again scheduled for upload. Fix that!"); return CanUploadFileNowStatus.ERROR; } if (OCUpload.isUseWifiOnly() && !UploadUtils.isConnectedViaWiFi(getApplicationContext())) { Log_OC.d(TAG, "Do not start upload because it is wifi-only."); return CanUploadFileNowStatus.LATER; } if(OCUpload.isWhileChargingOnly() && !UploadUtils.isCharging(getApplicationContext())) { Log_OC.d(TAG, "Do not start upload because it is while charging only."); return CanUploadFileNowStatus.LATER; } Date now = new Date(); if (now.getTime() < OCUpload.getUploadTimestamp()) { Log_OC.d( TAG, "Do not start upload because it is schedule for " + DisplayUtils.unixTimeToHumanReadable(OCUpload.getUploadTimestamp())); return CanUploadFileNowStatus.LATER; } if (!new File(OCUpload.getLocalPath()).exists()) { Log_OC.d(TAG, "Do not start upload because local file does not exist."); return CanUploadFileNowStatus.FILE_GONE; } return CanUploadFileNowStatus.NOW; } /** * Call if all pending uploads are to be retried. */ public static void retry(Context context) { retry(context, null); } /** * Call to retry upload identified by remotePath */ private static void retry(Context context, String remotePath) { Log_OC.d(TAG, "FileUploadService.retry()"); Intent i = new Intent(context, FileUploadService.class); i.putExtra(FileUploadService.KEY_RETRY, true); if(remotePath != null) { i.putExtra(FileUploadService.KEY_RETRY_REMOTE_PATH, remotePath); } context.startService(i); } }