Browse Source

Files received via content:// URIs are temporary copied BEFORE requesting their upload to FileUploader service [WIP]

David A. Velasco 9 years ago
parent
commit
6430ece72c

+ 80 - 112
src/com/owncloud/android/ui/activity/Uploader.java

@@ -28,6 +28,7 @@ import android.annotation.SuppressLint;
 import android.app.Dialog;
 import android.app.ProgressDialog;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.IntentFilter;
@@ -36,12 +37,10 @@ import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources.NotFoundException;
-import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.preference.PreferenceManager;
-import android.provider.MediaStore.Images;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentTransaction;
@@ -53,7 +52,6 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
-import android.webkit.MimeTypeMap;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.Button;
@@ -81,6 +79,7 @@ import com.owncloud.android.ui.dialog.LoadingDialog;
 import com.owncloud.android.utils.CopyTmpFileAsyncTask;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.ErrorMessageAdapter;
+import com.owncloud.android.utils.UriUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -103,7 +102,6 @@ public class Uploader extends FileActivity
     private AccountManager mAccountManager;
     private Stack<String> mParents;
     private ArrayList<Parcelable> mStreamsToUpload;
-    private boolean mCreateDir;
     private String mUploadPath;
     private OCFile mFile;
 
@@ -112,7 +110,6 @@ public class Uploader extends FileActivity
     private boolean mAccountSelected;
     private boolean mAccountSelectionShowing;
 
-    private ArrayList<String> mRemoteCacheData;
     private int mNumCacheFile;
 
     private final static int DIALOG_NO_ACCOUNT = 0;
@@ -142,15 +139,12 @@ public class Uploader extends FileActivity
             mAccountSelectionShowing = false;
             mNumCacheFile = 0;
 
-            // ArrayList for files with path in private storage
-            mRemoteCacheData = new ArrayList<String>();
         } else {
             mParents = (Stack<String>) savedInstanceState.getSerializable(KEY_PARENTS);
             mFile = savedInstanceState.getParcelable(KEY_FILE);
             mAccountSelected = savedInstanceState.getBoolean(KEY_ACCOUNT_SELECTED);
             mAccountSelectionShowing = savedInstanceState.getBoolean(KEY_ACCOUNT_SELECTION_SHOWING);
             mNumCacheFile = savedInstanceState.getInt(KEY_NUM_CACHE_FILE);
-            mRemoteCacheData = savedInstanceState.getStringArrayList(KEY_REMOTE_CACHE_DATA);
         }
 
         super.onCreate(savedInstanceState);
@@ -209,7 +203,6 @@ public class Uploader extends FileActivity
         outState.putBoolean(KEY_ACCOUNT_SELECTED, mAccountSelected);
         outState.putBoolean(KEY_ACCOUNT_SELECTION_SHOWING, mAccountSelectionShowing);
         outState.putInt(KEY_NUM_CACHE_FILE, mNumCacheFile);
-        outState.putStringArrayList(KEY_REMOTE_CACHE_DATA, mRemoteCacheData);
         outState.putParcelable(FileActivity.EXTRA_ACCOUNT, getAccount());
 
         Log_OC.d(TAG, "onSaveInstanceState() end");
@@ -364,7 +357,6 @@ public class Uploader extends FileActivity
         @Override
         public void onClick(DialogInterface dialog, int which) {
             Uploader.this.mUploadPath = mPath + mDirname.getText().toString();
-            Uploader.this.mCreateDir = true;
             uploadFiles();
         }
     }
@@ -411,19 +403,22 @@ public class Uploader extends FileActivity
             case R.id.uploader_choose_folder:
                 mUploadPath = "";   // first element in mParents is root dir, represented by "";
                 // init mUploadPath with "/" results in a "//" prefix
-                for (String p : mParents)
+                for (String p : mParents) {
                     mUploadPath += p + OCFile.PATH_SEPARATOR;
+                }
                 Log_OC.d(TAG, "Uploading file to dir " + mUploadPath);
 
-                uploadFiles();
-
+                try {
+                    uploadFiles();
+                } catch (SecurityException e) {
+                    showDialog(DIALOG_STREAM_UNKNOWN);  // TODO: _FORBIDDEN
+                }
                 break;
 
             case R.id.uploader_cancel:
                 finish();
                 break;
 
-
             default:
                 throw new IllegalArgumentException("Wrong element clicked");
         }
@@ -547,98 +542,90 @@ public class Uploader extends FileActivity
 
     @SuppressLint("NewApi")
     public void uploadFiles() {
-        try {
-            for (Parcelable mStream : mStreamsToUpload) {
-
-                Uri uri = (Uri) mStream;
-                String[] columnValues;
-                String displayName = null;
-                String filePath = "";
-
-                if (uri != null) {
-                    if (uri.getScheme().equals("content")) {
-                        String mimeType = getContentResolver().getType(uri);
-
-                        if (mimeType.contains("image")) {
-                            columnValues = getColumnsValues(uri, Images.Media.DISPLAY_NAME);
-                        } else if (mimeType.contains("video")) {
-                            columnValues = getColumnsValues(uri, Images.Media.DISPLAY_NAME);
-                        } else if (mimeType.contains("audio")) {
-                            columnValues = getColumnsValues(uri, Images.Media.DISPLAY_NAME);
-                        } else {
-                            columnValues = getColumnsValues(uri, Images.Media.DISPLAY_NAME);
-                        }
-
-                        displayName = (columnValues != null) ? columnValues[0] :
-                            uri.getLastPathSegment().replaceAll("\\s", "");
+        for (Parcelable sourceStream : mStreamsToUpload) {
+            Uri sourceUri = (Uri) sourceStream;
+            if (sourceUri == null) {
+                showDialog(DIALOG_NO_STREAM);
 
-                        // Add extension if it does not exists in the file name
-                        int index = displayName.lastIndexOf(".");
-                        if(index == -1 || MimeTypeMap.getSingleton().
-                                getMimeTypeFromExtension(displayName.substring(index + 1)) == null) {
-                            String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
-                            displayName += (extension != null) ? "." + extension : "";
-                        }
-
-                    } else if (uri.getScheme().equals("file")) {
-                        displayName = uri.getLastPathSegment();
-                    }
+            } else {
+                String displayName = UriUtils.getDisplayNameForUri(sourceUri, this);
+                if(displayName == null) {
+                    showDialog(DIALOG_NO_STREAM);   // TODO - different dialog?
 
-                }
+                } else {
+                    String remotePath = mUploadPath + displayName;
 
-                if(displayName != null) {
-                    filePath = mUploadPath + displayName;
+                    if (ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) {
+                        /// content: uris will be copied to temporary files before calling {@link FileUploader}
+                        copyThenUpload(sourceUri, remotePath);
 
-                    mRemoteCacheData.add(filePath);
-                    CopyTmpFileAsyncTask copyTask = new CopyTmpFileAsyncTask(this);
-                    Object[] params = {uri, filePath, mRemoteCacheData.size() - 1,
-                        getAccount().name, getContentResolver()};
-                    mNumCacheFile++;
-                    showWaitingCopyDialog();
-                    copyTask.execute(params);
+                    } else if (ContentResolver.SCHEME_FILE.equals(sourceUri.getScheme())) {
+                        /// file: uris should point to a local file, should be safe let FileUploader handle them
+                        requestUpload(sourceUri.getPath(), remotePath); // TODO - CHECK PATH EXTRACTION
 
-                } else {
-                    throw new SecurityException();
+                    } else {
+                        showDialog(DIALOG_STREAM_UNKNOWN);
+                    }
                 }
-
-                //Save the path to shared preferences
-                SharedPreferences.Editor appPrefs = PreferenceManager
-                        .getDefaultSharedPreferences(getApplicationContext()).edit();
-                appPrefs.putString("last_upload_path", mUploadPath);
-                appPrefs.apply();
-
-                finish();
             }
-
-        } catch (SecurityException e) {
-            showDialog(DIALOG_STREAM_UNKNOWN);
         }
-    }
 
-    private String[] getColumnsValues(Uri uri, String... columns) {
+        // Save the path to shared preferences; even if upload is not possible, user chose the folder
+        SharedPreferences.Editor appPrefs = PreferenceManager
+            .getDefaultSharedPreferences(getApplicationContext()).edit();
+        appPrefs.putString("last_upload_path", mUploadPath);
+        appPrefs.apply();
 
-        Cursor cursor = null;
-        String[] result = new String[columns.length];
-        try {
-            cursor = getContentResolver().query(uri, columns, null,
-                null, null);
-            cursor.moveToFirst();
+        finish();
+    }
 
-            for (int i = 0; i < columns.length; i++) {
-                result[i] = cursor.getString(cursor.getColumnIndex(columns[i]));
-            }
-            return result;
-        } catch(IllegalArgumentException | NullPointerException e) {
-            // When the URI is wrong, NullPointerException will be thrown, and when the Content Provider don't
-            // retrieve the columns requested, an IllegalArgumentException will be thrown.
-            return null;
-        } finally {
-            if(cursor != null) {
-                cursor.close();
-            }
-        }
+    /**
+     *
+     * @param sourceUri
+     * @param remotePath
+     */
+    private void copyThenUpload(Uri sourceUri, String remotePath) {
+        mNumCacheFile++;
+
+        showWaitingCopyDialog();
+
+        CopyTmpFileAsyncTask copyTask = new CopyTmpFileAsyncTask(this, this);
+        copyTask.execute(
+            CopyTmpFileAsyncTask.makeParamsToExecute(
+                getAccount(),
+                sourceUri,
+                remotePath,
+                mNumCacheFile
+            )
+        );
+    }
+
+    /**
+     * Requests the upload of a file in the local file system to {@link FileUploader} service.
+     *
+     * The original file will be left in its original location, and will not be duplicated.
+     * As a side effect, the user will see the file as not uploaded when accesses to the OC app.
+     * This is considered as acceptable, since when a file is shared from another app to OC,
+     * the usual workflow will go back to the original app.
+     *
+     * @param localPath     Absolute path in the local file system to the file to upload.
+     * @param remotePath    Absolute path in the current OC account to set to the uploaded file.
+     */
+    private void requestUpload(String localPath, String remotePath) {
+        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+        requester.uploadNewFile(
+            this,
+            getAccount(),
+            localPath,
+            remotePath,
+            FileUploader.LOCAL_BEHAVIOUR_FORGET,
+            null,       // MIME type will be detected from file name
+            false,      // do not create parent folder if not existent
+            UploadFileOperation.CREATED_BY_USER
+        );
     }
 
+
     @Override
     public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
         super.onRemoteOperationFinish(operation, result);
@@ -849,25 +836,6 @@ public class Uploader extends FileActivity
         if (mNumCacheFile-- == 0) {
             dismissWaitingCopyDialog();
         }
-        if (result != null) {
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.uploadNewFile(
-                    this, getAccount(),
-                    result,
-                    mRemoteCacheData.get(index),
-                    FileUploader.LOCAL_BEHAVIOUR_FORGET,
-                    null,       // MIME type will be detected from file name
-                    false,      // do not create parent folder if not existent
-                    UploadFileOperation.CREATED_BY_USER
-            );
-
-        } else {
-            String message = String.format(getString(R.string.uploader_error_forbidden_content),
-                    getString(R.string.app_name));
-            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
-            Log_OC.d(TAG, message);
-        }
-
     }
 
     /**

+ 114 - 30
src/com/owncloud/android/utils/CopyTmpFileAsyncTask.java

@@ -19,11 +19,17 @@
  */
 package com.owncloud.android.utils;
 
+import android.accounts.Account;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.widget.Toast;
 
+import com.owncloud.android.R;
+import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.operations.UploadFileOperation;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -36,15 +42,50 @@ import java.lang.ref.WeakReference;
 public class CopyTmpFileAsyncTask  extends AsyncTask<Object, Void, String> {
 
     private final String TAG = CopyTmpFileAsyncTask.class.getSimpleName();
-    private final WeakReference<OnCopyTmpFileTaskListener> mListener;
-    private int mIndex;
 
-    public int getIndex(){
-        return mIndex;
+    /**
+     * 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.
+     *
+     * @return  Correct array of parameters to be passed to {@link #execute(Object[])}
+     */
+    public final static Object[] makeParamsToExecute(
+        Account account,
+        Uri sourceUri,
+        String remotePath,
+        Integer numCacheFile
+    ) {
+
+        return new Object[] {
+            account,
+            sourceUri,
+            remotePath,
+            numCacheFile
+        };
     }
 
-    public CopyTmpFileAsyncTask(OnCopyTmpFileTaskListener listener) {
+
+    /**
+     * Listener in main thread to be notified when the task ends. Held in a WeakReference assuming that it's
+     * lifespan is associated with an Activity context, that could be finished by the user before the AsyncTask
+     * ends.
+     */
+    private final WeakReference<OnCopyTmpFileTaskListener> 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;
+
+
+    private int mIndex;
+
+    public CopyTmpFileAsyncTask(OnCopyTmpFileTaskListener listener, Context context) {
         mListener = new WeakReference<OnCopyTmpFileTaskListener>(listener);
+        mAppContext = context.getApplicationContext();
     }
 
     /**
@@ -57,20 +98,24 @@ public class CopyTmpFileAsyncTask  extends AsyncTask<Object, Void, String> {
      */
     @Override
     protected String doInBackground(Object[] params) {
-        String result = null;
+        String pathToCopiedFile = null;
 
-        if (params != null && params.length == 5) {
-            Uri uri = (Uri) params[0];
-            String filePath = (String) params[1];
-            mIndex = ((Integer) params[2]).intValue();
-            String accountName = (String) params[3];
-            ContentResolver contentResolver = (ContentResolver) params[4];
+        if (params != null && params.length == 4) {
+            Account account = (Account) params[0];
+            Uri uri = (Uri) params[1];
+            String remotePath = (String) params[2];
+            mIndex = ((Integer) params[3]);  // TODO really?
 
-            String fullTempPath = FileStorageUtils.getTemporalPath(accountName) + filePath;
             InputStream inputStream = null;
             FileOutputStream outputStream = null;
+            String fullTempPath = null;
+
+            ContentResolver contentResolver = mAppContext.getContentResolver();
+            // TODO: test that it's safe for URLs with temporary access;
+            //      alternative: receive InputStream in another parameter
 
             try {
+                fullTempPath = FileStorageUtils.getTemporalPath(account.name) + remotePath;
                 inputStream = contentResolver.openInputStream(uri);
                 File cacheFile = new File(fullTempPath);
                 File tempDir = cacheFile.getParentFile();
@@ -90,47 +135,86 @@ public class CopyTmpFileAsyncTask  extends AsyncTask<Object, Void, String> {
                 outputStream.close();
                 inputStream.close();
 
-                result = fullTempPath;
+                pathToCopiedFile = fullTempPath;
+
             } catch (Exception e) {
-                 Log_OC.e(TAG, "Exception ", e);
+                Log_OC.e(TAG, "Exception while copying " + uri.toString() + " to temporary file", e);
+
+                // clean
+                if (fullTempPath != null) {
+                    File f = new File(fullTempPath);
+                    if (f.exists()) {
+                        if (!f.delete()) {
+                            Log_OC.e(TAG, "Could not delete temporary file " + fullTempPath);
+                        }
+                    }
+                }
+
+            } finally {
                 if (inputStream != null) {
                     try {
                         inputStream.close();
-                    } catch (Exception e1) {
-                        Log_OC.e(TAG, "Input Stream Exception ", e1);
+                    } catch (Exception e) {
+                        Log_OC.w(TAG, "Ignoring exception of inputStream closure");
                     }
                 }
 
                 if (outputStream != null) {
                     try {
                         outputStream.close();
-                    } catch (Exception e1) {
-                        Log_OC.e(TAG, "Output Stream Exception ", e1);
+                    } catch (Exception e) {
+                        Log_OC.w(TAG, "Ignoring exception of outStream closure");
                     }
                 }
+            }
 
-                if (fullTempPath != null) {
-                    File f = new File(fullTempPath);
-                    f.delete();
-                }
-                result =  null;
+            if (pathToCopiedFile != null) {
+                requestUpload(
+                    account,
+                    pathToCopiedFile,
+                    remotePath,
+                    contentResolver.getType(uri)
+                );
+                // mRemoteCacheData.get(index),
+
+            } else {
+                String message = String.format(
+                    mAppContext.getString(R.string.uploader_error_forbidden_content),
+                    mAppContext.getString(R.string.app_name)
+                );
+                Toast.makeText(mAppContext, message, Toast.LENGTH_LONG).show();
+                Log_OC.d(TAG, message);
             }
+
         } else {
-             throw new IllegalArgumentException("Error in parameters number");
+            throw new IllegalArgumentException("Error in parameters number");
         }
 
-        return result;
+        return pathToCopiedFile;
+    }
+
+    private void requestUpload(Account account, String localPath, String remotePath, String mimeType) {
+        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+        requester.uploadNewFile(
+            mAppContext,
+            account,
+            localPath,
+            remotePath,
+            FileUploader.LOCAL_BEHAVIOUR_MOVE,  // the copy was already done, let's take advantage and move it
+            // into the OC folder so that the folder was not
+            mimeType,
+            false,      // do not create parent folder if not existent
+            UploadFileOperation.CREATED_BY_USER // TODO , different category?
+        );
     }
 
     @Override
     protected void onPostExecute(String result) {
-
         OnCopyTmpFileTaskListener listener = mListener.get();
-        if (listener!= null)
-        {
+        if (listener!= null) {
             listener.onTmpFileCopied(result, mIndex);
         } else {
-            Log_OC.e(TAG, "Lost upload because of lost context.");
+            Log_OC.i(TAG, "User left Uploader activity before the temporal copies were finished ");
         }
     }
 

+ 92 - 4
src/com/owncloud/android/utils/UriUtils.java

@@ -20,6 +20,7 @@
 package com.owncloud.android.utils;
 
 import android.annotation.TargetApi;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.Context;
 import android.database.Cursor;
@@ -28,13 +29,10 @@ import android.os.Build;
 import android.os.Environment;
 import android.provider.DocumentsContract;
 import android.provider.MediaStore;
-import android.provider.OpenableColumns;
+import android.webkit.MimeTypeMap;
 
-import com.owncloud.android.MainApp;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
-import java.io.File;
-
 
 /**
  * A helper class for some Uri operations.
@@ -190,4 +188,94 @@ public class UriUtils {
         return null;
     }
 
+
+
+    public static String getDisplayNameForUri(Uri uri, Context context) {
+
+        if (uri == null || context == null) {
+            throw new IllegalArgumentException("Received NULL!");
+        }
+
+        String displayName = null;
+
+        if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
+            displayName = uri.getLastPathSegment();     // ready to return
+
+        } else {
+            // content: URI
+
+            displayName = getDisplayNameFromContentResolver(uri, context);
+
+            try {
+                if (displayName == null) {
+                    // last chance to have a name
+                    displayName = uri.getLastPathSegment().replaceAll("\\s", "");
+                }
+
+                // Add best possible extension
+                int index = displayName.lastIndexOf(".");
+                if (index == -1 || MimeTypeMap.getSingleton().
+                    getMimeTypeFromExtension(displayName.substring(index + 1)) == null) {
+                    String mimeType = context.getContentResolver().getType(uri);
+                    String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+                    if (extension != null) {
+                        displayName += "." + extension;
+                    }
+                }
+
+            } catch (Exception e) {
+                Log_OC.e(TAG, "No way to get a display name for " + uri.toString());
+            }
+        }
+
+        return displayName;
+    }
+
+
+    private static String getDisplayNameFromContentResolver(Uri uri, Context context) {
+        String displayName = null;
+        String mimeType = context.getContentResolver().getType(uri);
+        if (mimeType != null) {
+            String displayNameColumn;
+            if (mimeType.toLowerCase().startsWith("image/")) {
+                displayNameColumn = MediaStore.Images.ImageColumns.DISPLAY_NAME;
+
+            } else if (mimeType.toLowerCase().startsWith("video/")) {
+                displayNameColumn = MediaStore.Video.VideoColumns.DISPLAY_NAME;
+
+            } else if (mimeType.toLowerCase().startsWith("audio/")) {
+                displayNameColumn = MediaStore.Audio.AudioColumns.DISPLAY_NAME;
+
+            } else {
+                displayNameColumn = MediaStore.Files.FileColumns.DISPLAY_NAME;
+            }
+
+            Cursor cursor = null;
+            try {
+                cursor = context.getContentResolver().query(
+                    uri,
+                    new String[]{displayNameColumn},
+                    null,
+                    null,
+                    null
+                );
+                if (cursor != null) {
+                    cursor.moveToFirst();
+                    displayName = cursor.getString(cursor.getColumnIndex(displayNameColumn));
+                }
+
+            } catch (Exception e) {
+                Log_OC.e(TAG, "Could not retrieve display name for " + uri.toString());
+                // nothing else, displayName keeps null
+
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+        return displayName;
+    }
+
+
 }