android/app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTas...

312 lines
12 KiB
Java

/**
* ownCloud Android client application
*
* @author María Asensio Valverde
* @author Juan Carlos González Cabrero
* @author David A. Velasco
* Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
*
*/
package com.owncloud.android.ui.asynctasks;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.DocumentsContract;
import android.widget.Toast;
import com.nextcloud.client.account.User;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.owncloud.android.R;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.operations.UploadFileOperation;
import com.owncloud.android.utils.FileStorageUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.ref.WeakReference;
/**
* AsyncTask to copy a file from a uri in a temporal file
*/
public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, ResultCode> {
private final String TAG = CopyAndUploadContentUrisTask.class.getSimpleName();
/**
* Listener in main thread to be notified when the task ends. Held in a WeakReference assuming that its
* lifespan is associated with an Activity context, that could be finished by the user before the AsyncTask
* ends.
*/
private WeakReference<OnCopyTmpFilesTaskListener> mListener;
/**
* Reference to application context, used to access app resources. Holding it should not be a problem, since it
* needs to exist until the end of the AsyncTask although the caller Activity were finished before.
*/
private final Context mAppContext;
/**
* Helper method building a correct array of parameters to be passed to {@link #execute(Object[])} )}
*
* Just packages the received parameters in correct order, doesn't check anything about them.
*
* @param user user uploading shared files
* @param sourceUris Array of "content://" URIs to the files to be uploaded.
* @param remotePaths Array of absolute paths in the OC account to set to the uploaded files.
* @param behaviour Indicates what to do with the local file once uploaded.
* @param contentResolver {@link ContentResolver} instance with appropriate permissions to open the
* URIs in 'sourceUris'.
*
* Handling this parameter in {@link #doInBackground(Object[])} keeps an indirect reference to the
* caller Activity, what is technically wrong, since it will be held in memory
* (with all its associated resources) until the task finishes even though the user leaves the Activity.
*
* But we really, really, really want that the files are copied to temporary files in the OC folder and then
* uploaded, even if the user gets bored of waiting while the copy finishes. And we can't forward the job to
* another {@link Context}, because if any of the content:// URIs is constrained by a TEMPORARY READ PERMISSION,
* trying to open it will fail with a {@link SecurityException} after the user leaves the ReceiveExternalFilesActivity Activity. We
* really tried it.
*
* So we are doomed to leak here for the best interest of the user. Please, don't do similar in other places.
*
* Any idea to prevent this while keeping the functionality will be welcome.
*
* @return Correct array of parameters to be passed to {@link #execute(Object[])}
*/
public static Object[] makeParamsToExecute(
User user,
Uri[] sourceUris,
String[] remotePaths,
int behaviour,
ContentResolver contentResolver
) {
return new Object[]{
user,
sourceUris,
remotePaths,
Integer.valueOf(behaviour),
contentResolver
};
}
public CopyAndUploadContentUrisTask(
OnCopyTmpFilesTaskListener listener,
Context context
) {
mListener = new WeakReference<>(listener);
mAppContext = context.getApplicationContext();
}
/**
* @param params Params to execute the task; see
* {@link #makeParamsToExecute(User, Uri[], String[], int, ContentResolver)}
* for further details.
*/
@Override
protected ResultCode doInBackground(Object[] params) {
ResultCode result = ResultCode.UNKNOWN_ERROR;
InputStream inputStream = null;
FileOutputStream outputStream = null;
String fullTempPath = null;
Uri currentUri = null;
try {
User user = (User) params[0];
Uri[] uris = (Uri[]) params[1];
String[] remotePaths = (String[]) params[2];
int behaviour = (Integer) params[3];
ContentResolver leakedContentResolver = (ContentResolver) params[4];
String currentRemotePath;
for (int i = 0; i < uris.length; i++) {
currentUri = uris[i];
currentRemotePath = remotePaths[i];
long lastModified = 0;
try (Cursor cursor = leakedContentResolver.query(currentUri,
null,
null,
null,
null)) {
if (cursor != null && cursor.moveToFirst()) {
// this check prevents a crash when last modification time is not available on certain phones
int columnIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED);
if (columnIndex >= 0) {
lastModified = cursor.getLong(columnIndex);
}
}
}
fullTempPath = FileStorageUtils.getTemporalPath(user.getAccountName()) + currentRemotePath;
inputStream = leakedContentResolver.openInputStream(currentUri);
File cacheFile = new File(fullTempPath);
File tempDir = cacheFile.getParentFile();
if (!tempDir.exists()) {
tempDir.mkdirs();
}
cacheFile.createNewFile();
outputStream = new FileOutputStream(fullTempPath);
byte[] buffer = new byte[4096];
int count;
while ((count = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, count);
}
if (lastModified != 0) {
try {
if (!cacheFile.setLastModified(lastModified)) {
Log_OC.w(TAG, "Could not change mtime of cacheFile");
}
} catch (SecurityException e) {
Log_OC.e(TAG, "Not enough permissions to change mtime of cacheFile", e);
} catch (IllegalArgumentException e) {
Log_OC.e(TAG, "Could not change mtime of cacheFile, mtime is negativ: "+lastModified, e);
}
}
requestUpload(
user,
fullTempPath,
currentRemotePath,
behaviour
);
fullTempPath = null;
}
result = ResultCode.OK;
} catch (ArrayIndexOutOfBoundsException e) {
Log_OC.e(TAG, "Wrong number of arguments received ", e);
} catch (ClassCastException e) {
Log_OC.e(TAG, "Wrong parameter received ", e);
} catch (FileNotFoundException e) {
Log_OC.e(TAG, "Could not find source file " + currentUri, e);
result = ResultCode.LOCAL_FILE_NOT_FOUND;
} catch (SecurityException e) {
Log_OC.e(TAG, "Not enough permissions to read source file " + currentUri, e);
result = ResultCode.FORBIDDEN;
} catch (Exception e) {
Log_OC.e(TAG, "Exception while copying " + currentUri + " to temporary file", e);
result = ResultCode.LOCAL_STORAGE_NOT_COPIED;
// clean
if (fullTempPath != null) {
File f = new File(fullTempPath);
if (f.exists() && !f.delete()) {
Log_OC.e(TAG, "Could not delete temporary file " + fullTempPath);
}
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
Log_OC.w(TAG, "Ignoring exception of inputStream closure");
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception e) {
Log_OC.w(TAG, "Ignoring exception of outStream closure");
}
}
}
return result;
}
private void requestUpload(User user, String localPath, String remotePath, int behaviour) {
FileUploadHelper.Companion.instance().uploadNewFiles(
user,
new String[]{ localPath },
new String[]{ remotePath },
behaviour,
false, // do not create parent folder if not existent
UploadFileOperation.CREATED_BY_USER,
false,
false,
NameCollisionPolicy.ASK_USER);
}
@Override
protected void onPostExecute(ResultCode result) {
OnCopyTmpFilesTaskListener listener = mListener.get();
if (listener!= null) {
listener.onTmpFilesCopied(result);
} else {
Log_OC.i(TAG, "User left the caller activity before the temporal copies were finished ");
if (result != ResultCode.OK) {
// if the user left the app, report background error in a Toast
int messageId;
switch (result) {
case LOCAL_FILE_NOT_FOUND:
messageId = R.string.uploader_error_message_source_file_not_found;
break;
case LOCAL_STORAGE_NOT_COPIED:
messageId = R.string.uploader_error_message_source_file_not_copied;
break;
case FORBIDDEN:
messageId = R.string.uploader_error_message_read_permission_not_granted;
break;
default:
messageId = R.string.common_error_unknown;
break;
}
String message = String.format(
mAppContext.getString(messageId),
mAppContext.getString(R.string.app_name)
);
Toast.makeText(mAppContext, message, Toast.LENGTH_LONG).show();
}
}
}
/**
* Sets the object waiting for progress report via callbacks.
*
* @param listener New object to report progress via callbacks
*/
public void setListener(OnCopyTmpFilesTaskListener listener) {
mListener = new WeakReference<>(listener);
}
/**
* Interface to retrieve data from recognition task
*/
public interface OnCopyTmpFilesTaskListener {
void onTmpFilesCopied(ResultCode result);
}
}