/* ownCloud Android client application * Copyright (C) 2012 Bartek Przybylski * Copyright (C) 2012-2013 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 java.io.File; import java.io.IOException; import java.util.AbstractList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountsException; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; 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.os.Process; import android.support.v4.app.NotificationCompat; 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.db.UploadDbHandler; import com.owncloud.android.db.UploadDbHandler.UploadStatus; import com.owncloud.android.db.UploadDbObject; import com.owncloud.android.files.InstantUploadBroadcastReceiver; 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.accounts.AccountUtils.Constants; 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.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.FileDisplayActivity; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.UriUtils; /** * Service for uploading files. Invoke using context.startService(...). This * service retries until upload succeeded. Files to be uploaded are stored * persistent using {@link UploadDbHandler}. * * @author LukeOwncloud * */ @SuppressWarnings("unused") public class FileUploadService extends Service { 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 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"; public static final String KEY_ACCOUNT = "ACCOUNT"; public static final String KEY_UPLOAD_TYPE = "UPLOAD_TYPE"; public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE"; public static final String KEY_CREATE_REMOTE_FOLDER = "CREATE_REMOTE_FOLDER"; public static final String KEY_WIFI_ONLY = "WIFI_ONLY"; public static final String KEY_LOCAL_BEHAVIOUR = "BEHAVIOUR"; /** * 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; private LocalBehaviour(int value) { this.value = value; } public int getValue() { return value; } } public enum UploadSingleMulti { UPLOAD_SINGLE_FILE(0), UPLOAD_MULTIPLE_FILES(1); private final int value; private UploadSingleMulti(int value) { this.value = value; } public int getValue() { return value; } }; // public static final int UPLOAD_SINGLE_FILE = 0; // public static final int UPLOAD_MULTIPLE_FILES = 1; private static final String TAG = FileUploadService.class.getSimpleName(); private Looper mServiceLooper; private ServiceHandler mServiceHandler; private IBinder mBinder; private ConnectivityChangeReceiver mConnectivityChangeReceiver; private OwnCloudClient mUploadClient = null; private Account mLastAccount = null; private FileDataStorageManager mStorageManager; //since there can be only one instance of an Android service, there also just one db connection. private UploadDbHandler mDb = null; /** * List of uploads that currently in progress. Maps from remotePath to where file * is being uploaded to {@link UploadFileOperation}. */ private ConcurrentMap mActiveUploads = new ConcurrentHashMap(); private NotificationManager mNotificationManager; private NotificationCompat.Builder mNotificationBuilder; private static final String MIME_TYPE_PDF = "application/pdf"; private static final String FILE_EXTENSION_PDF = ".pdf"; 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; } /** * 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. */ private static boolean chunkedUploadIsSupported(OwnCloudVersion version) { return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0); } /** * Service initialization */ @Override public void onCreate() { super.onCreate(); Log_OC.i(TAG, "mPendingUploads size:" + mActiveUploads.size()); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); HandlerThread thread = new HandlerThread("FileUploaderThread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper, this); mBinder = new FileUploaderBinder(); mConnectivityChangeReceiver = new ConnectivityChangeReceiver(); registerReceiver(mConnectivityChangeReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); mDb = UploadDbHandler.getInstance(this.getBaseContext()); mDb.recreateDb(); //for testing only } public class ConnectivityChangeReceiver extends BroadcastReceiver { @Override public void onReceive(Context arg0, Intent arg1) { if(InstantUploadBroadcastReceiver.isOnline(getApplicationContext())) { // upload pending wifi only files. onStartCommand(null, 0, 0); } } } @Override public void onDestroy() { mDb.close(); unregisterReceiver(mConnectivityChangeReceiver); super.onDestroy(); } /** * 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, onStartCommand() stores all information associated with the upload * in a {@link UploadDbObject} which is stored persistently using * {@link UploadDbHandler}. Then, {@link ServiceHandler} is invoked which * performs the upload and updates the DB entry (upload success, failure, * retry, ...) * * TODO: correct return values. should not always be NOT_STICKY. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { AbstractList requestedUploads = new Vector(); if (intent == null) { // service was restarted by OS (after return START_STICKY and kill // service) or connectivity change was detected. ==> check persistent upload // list. // List list = mDb.getAllPendingUploads(); for (UploadDbObject uploadDbObject : list) { uploadDbObject.setUploadStatus(UploadStatus.UPLOAD_LATER); uploadDbObject.setLastResult(null); mDb.updateUpload(uploadDbObject); } requestedUploads.addAll(list); } else { UploadSingleMulti uploadType = (UploadSingleMulti) intent.getSerializableExtra(KEY_UPLOAD_TYPE); if (uploadType == null) { Log_OC.e(TAG, "Incorrect or no upload type provided"); return Service.START_NOT_STICKY; } 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 Service.START_NOT_STICKY; } OCFile[] files = null; // if KEY_FILE given, use it if (intent.hasExtra(KEY_FILE)) { if (uploadType == UploadSingleMulti.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 Service.START_NOT_STICKY; } String[] localPaths; String[] remotePaths; String[] mimeTypes; if (uploadType == UploadSingleMulti.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 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] : (String) null)); if (files[i] == null) { Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i] + " and localPaths[i]:" + localPaths[i]); return Service.START_NOT_STICKY; } } } // 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); 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++) { UploadDbObject uploadObject = new UploadDbObject(); uploadObject.setRemotePath(files[i].getRemotePath()); uploadObject.setLocalPath(files[i].getStoragePath()); uploadObject.setMimeType(files[i].getMimetype()); uploadObject.setAccountName(account.name); uploadObject.setForceOverwrite(forceOverwrite); uploadObject.setCreateRemoteFolder(isCreateRemoteFolder); uploadObject.setLocalAction(localAction); uploadObject.setUseWifiOnly(isUseWifiOnly); uploadObject.setUploadStatus(UploadStatus.UPLOAD_LATER); boolean success = mDb.storeUpload(uploadObject); if(!success) { Log_OC.e(TAG, "Could not add upload to database. It is probably a duplicate. Ignore."); } else { requestedUploads.add(uploadObject); } } // TODO check if would be clever to read entries from // UploadDbHandler and add to requestedUploads at this point } Log_OC.i(TAG, "mPendingUploads size:" + mActiveUploads.size()); if (requestedUploads.size() > 0) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = requestedUploads; mServiceHandler.sendMessage(msg); return Service.START_STICKY; // there is work to do. If killed this // service should be restarted // eventually. } return Service.START_NOT_STICKY; //nothing to do. do not restart. } /** * 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(); /** * 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) { UploadFileOperation upload = null; synchronized (mActiveUploads) { upload = mActiveUploads.remove(buildRemoteName(account, file)); } if (upload != null) { upload.cancel(); } } 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. * * @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; String targetKey = buildRemoteName(account, file); synchronized (mActiveUploads) { if (file.isFolder()) { // this can be slow if there are many uploads :( Iterator it = mActiveUploads.keySet().iterator(); boolean found = false; while (it.hasNext() && !found) { found = it.next().startsWith(targetKey); } return found; } else { return (mActiveUploads.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); } } @Override public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String localFileName) { Set> uploads = mActiveUploads.entrySet(); UploadFileOperation currentUpload = null; //unfortunately we do not have the remote upload path here, so search through all uploads. //however, this may lead to problems, if user uploads same file twice to different destinations. //this can only be fixed by replacing localFileName with remote path. for (Entry entry : uploads) { if(entry.getValue().getStoragePath().equals(localFileName)) { if(currentUpload != null) { Log_OC.e(TAG, "Found two current uploads with same remote path " + localFileName + ". Ignore."); return; } currentUpload = entry.getValue(); } } 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); } } } /** * 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()) { UploadDbObject uploadObject = it.next(); mService.uploadFile(uploadObject); } } mService.stopSelf(msg.arg1); } } /** * Core upload method: sends the file(s) to upload. This function blocks until upload succeeded or failed. * * @param uploadDbObject Key to access the upload to perform, contained in * mPendingUploads */ private void uploadFile(UploadDbObject uploadDbObject) { if(uploadDbObject.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) { Log_OC.w(TAG, "Already succeeded uploadObject was again scheduled for upload. Fix that!"); return; } UploadFileOperation currentUpload = null; synchronized (mActiveUploads) { //How does this work? Is it thread-safe to set mCurrentUpload here? //What happens if other mCurrentUpload is currently in progress? // //It seems that upload does work, however the upload state is not set //back of the first upload when a second upload starts while first is //in progress (yellow up-arrow does not disappear of first upload) currentUpload = mActiveUploads.get(uploadDbObject.getRemotePath()); //if upload not in progress, start it now if(currentUpload == null) { if (uploadDbObject.isUseWifiOnly() && !InstantUploadBroadcastReceiver.isConnectedViaWiFi(getApplicationContext())) { Log_OC.d(TAG, "Do not start upload because it is wifi-only."); return; } if (!new File(uploadDbObject.getLocalPath()).exists()) { mDb.updateUpload(uploadDbObject.getLocalPath(), UploadStatus.UPLOAD_FAILED_GIVE_UP, new RemoteOperationResult(ResultCode.FILE_NOT_FOUND)); Log_OC.d(TAG, "Do not start upload because local file does not exist."); return; } AccountManager aMgr = AccountManager.get(this); Account account = uploadDbObject.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, uploadDbObject.getRemotePath()); OCFile file = obtainNewOCFileToUpload(uploadDbObject.getRemotePath(), uploadDbObject.getLocalPath(), uploadDbObject.getMimeType()); currentUpload = new UploadFileOperation(account, file, chunked, uploadDbObject.isForceOverwrite(), uploadDbObject.getLocalAction(), getApplicationContext()); if (uploadDbObject.isCreateRemoteFolder()) { currentUpload.setRemoteFolderToBeCreated(); } mActiveUploads.putIfAbsent(uploadKey, currentUpload); // Grants that // the file only upload once time currentUpload.addDatatransferProgressListener((FileUploaderBinder) mBinder); } } if (currentUpload != null) { notifyUploadStart(currentUpload); RemoteOperationResult uploadResult = null, grantResult = null; try { // / prepare client object to send requests to the ownCloud // server if (mUploadClient == null || !mLastAccount.equals(currentUpload.getAccount())) { mLastAccount = currentUpload.getAccount(); mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver()); OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this); mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this); } // / check the existence of the parent folder for the file to // upload String remoteParentPath = new File(currentUpload.getRemotePath()).getParent(); remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; grantResult = grantFolderExistence(currentUpload, remoteParentPath); // / perform the upload if (grantResult.isSuccess()) { OCFile parent = mStorageManager.getFileByPath(remoteParentPath); currentUpload.getFile().setParentId(parent.getFileId()); uploadResult = currentUpload.execute(mUploadClient); if (uploadResult.isSuccess()) { saveUploadedFile(currentUpload); } } else { uploadResult = grantResult; } } catch (AccountsException e) { Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); uploadResult = new RemoteOperationResult(e); } catch (IOException e) { Log_OC.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e); uploadResult = new RemoteOperationResult(e); } finally { synchronized (mActiveUploads) { mActiveUploads.remove(uploadDbObject); Log_OC.i(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; } } // notify result notifyUploadResult(uploadResult, currentUpload); sendFinalBroadcast(currentUpload, 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(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 refactor this ugly thing */ private void saveUploadedFile(UploadFileOperation currentUpload) { OCFile file = currentUpload.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(currentUpload.getRemotePath()); RemoteOperationResult result = operation.execute(mUploadClient); if (result.isSuccess()) { updateOCFile(file, (RemoteFile) result.getData().get(0)); file.setLastSyncDateForProperties(syncDate); } // / maybe this would be better as part of UploadFileOperation... or // maybe all this method if (currentUpload.wasRenamed()) { OCFile oldFile = currentUpload.getOldFile(); if (oldFile.fileExists()) { oldFile.setStoragePath(null); mStorageManager.saveFile(oldFile); } // 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); } 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()); // TODO Etag, where available 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 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 // view of the file Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class); showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, 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()); mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_IN_PROGRESS, null); } /** * 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(RemoteOperationResult uploadResult, UploadFileOperation upload) { 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 } mNotificationBuilder.setContentText(content); mNotificationManager.notify(tickerId, mNotificationBuilder.build()); if (uploadResult.isSuccess()) { mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_SUCCEEDED, uploadResult); // remove success notification, with a delay of 2 seconds NotificationDelayer.cancelWithDelay(mNotificationManager, R.string.uploader_upload_succeeded_ticker, 2000); } else { // TODO: add other cases in which upload attempt is to be // abandoned. if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) { mDb.updateUpload(upload.getOriginalStoragePath(), UploadDbHandler.UploadStatus.UPLOAD_FAILED_GIVE_UP, uploadResult); } else { mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED, uploadResult); } } } else { mDb.updateUpload(upload.getOriginalStoragePath(), UploadStatus.UPLOAD_FAILED, uploadResult); } } /** * 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 */ private void sendFinalBroadcast(UploadFileOperation upload, RemoteOperationResult uploadResult) { 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 * @param mimeType * @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); } }