Эх сурвалжийг харах

Added support for OC-Chunking in uploads (disabled, but ready)

David A. Velasco 12 жил өмнө
parent
commit
53b67429ea

+ 2 - 2
AndroidManifest.xml

@@ -17,8 +17,8 @@
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
  -->
 <manifest package="com.owncloud.android"
-    android:versionCode="103009"
-    android:versionName="1.3.9" xmlns:android="http://schemas.android.com/apk/res/android">
+    android:versionCode="103010"
+    android:versionName="1.3.10" xmlns:android="http://schemas.android.com/apk/res/android">
 
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
     <uses-permission android:name="android.permission.USE_CREDENTIALS" />

+ 2 - 2
src/com/owncloud/android/authenticator/ConnectionCheckOperation.java

@@ -39,7 +39,7 @@ public class ConnectionCheckOperation extends RemoteOperation {
     /** Maximum time to wait for a response from the server when the connection is being tested, in MILLISECONDs.  */
     public static final int TRY_CONNECTION_TIMEOUT = 5000;
     
-    private static final String TAG = ConnectionCheckerRunnable.class.getCanonicalName();
+    private static final String TAG = ConnectionCheckOperation.class.getCanonicalName();
     
     private String mUrl;
     private RemoteOperationResult mLatestResult;
@@ -132,5 +132,5 @@ public class ConnectionCheckOperation extends RemoteOperation {
             return mLatestResult;
         }
 	}
-
+	
 }

+ 193 - 154
src/com/owncloud/android/files/services/FileUploader.java

@@ -1,19 +1,29 @@
 package com.owncloud.android.files.services;
 
 import java.io.File;
+import java.util.AbstractList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Vector;
 
+import com.owncloud.android.authenticator.AccountAuthenticator;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.PhotoTakenBroadcastReceiver;
+import com.owncloud.android.operations.ChunkedUploadFileOperation;
+import com.owncloud.android.operations.RemoteOperationResult;
+import com.owncloud.android.operations.UploadFileOperation;
+import com.owncloud.android.utils.OwnCloudVersion;
 
+import eu.alefzero.webdav.ChunkFromFileChannelRequestEntity;
 import eu.alefzero.webdav.OnDatatransferProgressListener;
 
 import com.owncloud.android.network.OwnCloudClientUtils;
 
 import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -26,8 +36,9 @@ import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
 import android.util.Log;
-import android.webkit.MimeTypeMap;
 import android.widget.RemoteViews;
+
+import com.owncloud.android.AccountUtils;
 import com.owncloud.android.R;
 import eu.alefzero.webdav.WebdavClient;
 
@@ -51,19 +62,19 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     public static final int UPLOAD_SINGLE_FILE = 0;
     public static final int UPLOAD_MULTIPLE_FILES = 1;
 
-    private static final String TAG = "FileUploader";
+    private static final String TAG = FileUploader.class.getSimpleName();
     
     private NotificationManager mNotificationManager;
     private Looper mServiceLooper;
     private ServiceHandler mServiceHandler;
-    private Account mAccount;
-    private String[] mLocalPaths, mRemotePaths, mMimeTypes;
-    private int mUploadType;
+    private AbstractList<Account> mAccounts = new Vector<Account>();
+    private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>(); 
     private Notification mNotification;
     private long mTotalDataToSend, mSendData;
+    private int mTotalFilesToSend;
     private int mCurrentIndexUpload, mPreviousPercent;
     private int mSuccessCounter;
-    private boolean mIsInstant;
+    private RemoteViews mDefaultNotificationContentView;
     
     /**
      * Static map with the files being download and the path to the temporal file were are download
@@ -85,6 +96,17 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     }
 
     
+    /**
+     * 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);    // TODO uncomment when feature is full in server
+        return false;   
+    }
+
     
 
     @Override
@@ -99,7 +121,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
 
         @Override
         public void handleMessage(Message msg) {
-            uploadFile(msg.arg2==1?true:false);
+            uploadFile();
             stopSelf(msg.arg1);
         }
     }
@@ -121,34 +143,50 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
             Log.e(TAG, "Not enough information provided in intent");
             return Service.START_NOT_STICKY;
         }
-        mAccount = intent.getParcelableExtra(KEY_ACCOUNT);
-        mUploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1);
-        if (mUploadType == -1) {
+        Account account = intent.getParcelableExtra(KEY_ACCOUNT);
+        if (account == null) {
+            Log.e(TAG, "Bad account information provided in upload intent");
+            return Service.START_NOT_STICKY;
+        }
+        
+        int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1);
+        if (uploadType == -1) {
             Log.e(TAG, "Incorrect upload type provided");
             return Service.START_NOT_STICKY;
         }
-        if (mUploadType == UPLOAD_SINGLE_FILE) {
-            mLocalPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
-            mRemotePaths = new String[] { intent
+        String[] localPaths, remotePaths, mimeTypes; 
+        if (uploadType == UPLOAD_SINGLE_FILE) {
+            localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
+            remotePaths = new String[] { intent
                     .getStringExtra(KEY_REMOTE_FILE) };
-            mMimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) };
+            mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) };
             
         } else { // mUploadType == UPLOAD_MULTIPLE_FILES
-            mLocalPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE);
-            mRemotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE);
-            mMimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
+            localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE);
+            remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE);
+            mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
         }
         
-        if (mLocalPaths.length != mRemotePaths.length) {
+        if (localPaths.length != remotePaths.length) {
             Log.e(TAG, "Different number of remote paths and local paths!");
             return Service.START_NOT_STICKY;
         }
+        
+        boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); 
+        boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false);
+        
+        for (int i=0; i < localPaths.length; i++) {
+            OwnCloudVersion ocv = new OwnCloudVersion(AccountManager.get(this).getUserData(account, AccountAuthenticator.KEY_OC_VERSION));
+            if (FileUploader.chunkedUploadIsSupported(ocv)) {
+                mUploads.add(new ChunkedUploadFileOperation(localPaths[i], remotePaths[i], ((mimeTypes!=null)?mimeTypes[i]:""), isInstant, forceOverwrite, this));
+            } else {
+                mUploads.add(new UploadFileOperation(localPaths[i], remotePaths[i], (mimeTypes!=null?mimeTypes[i]:""), isInstant, forceOverwrite, this));
+            }
+            mAccounts.add(account);
+        }
 
-        mIsInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false); 
-                
         Message msg = mServiceHandler.obtainMessage();
         msg.arg1 = startId;
-        msg.arg2 = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false)?1:0;
         mServiceHandler.sendMessage(msg);
 
         return Service.START_NOT_STICKY;
@@ -158,19 +196,117 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
     /**
      * Core upload method: sends the file(s) to upload
      */
-    public void uploadFile(boolean force_override) {
-        FileDataStorageManager storageManager = new FileDataStorageManager(mAccount, getContentResolver());
+    public void uploadFile() {
 
+        /// prepare upload statistics
         mTotalDataToSend = mSendData = mPreviousPercent = 0;
+        Iterator<UploadFileOperation> it = mUploads.iterator();
+        while (it.hasNext()) {
+            mTotalDataToSend += new File(it.next().getLocalPath()).length();
+        }
+        mTotalFilesToSend = mUploads.size();
+        Log.d(TAG, "Will upload " + mTotalDataToSend + " bytes, with " + mUploads.size() + " files");
+
+        
+        notifyUploadStart();
+
+        UploadFileOperation currentUpload;
+        Account currentAccount, lastAccount = null;
+        FileDataStorageManager storageManager = null;
+        WebdavClient wc = null;
+        mSuccessCounter = 0;
+        boolean createdInstantDir = false;
+        
+        for (mCurrentIndexUpload = 0; mCurrentIndexUpload < mUploads.size(); mCurrentIndexUpload++) {
+            currentUpload = mUploads.get(mCurrentIndexUpload);
+            currentAccount =  mAccounts.get(mCurrentIndexUpload);
+            
+            /// prepare client object to send request(s) to the ownCloud server
+            if (lastAccount == null || !lastAccount.equals(currentAccount)) {
+                storageManager = new FileDataStorageManager(currentAccount, getContentResolver());
+                wc = OwnCloudClientUtils.createOwnCloudClient(currentAccount, getApplicationContext());
+                wc.setDataTransferProgressListener(this);
+            }
+            
+            if (currentUpload.isInstant() && !createdInstantDir) {
+                createdInstantDir = createRemoteFolderForInstantUploads(wc, storageManager);
+            }
         
-        /// prepare client object to send the request to the ownCloud server
-        WebdavClient wc = OwnCloudClientUtils.createOwnCloudClient(mAccount, getApplicationContext());
-        wc.setDataTransferProgressListener(this);
+            /// perform the upload
+            long parentDirId = -1;
+            RemoteOperationResult uploadResult = null;
+            boolean updateResult = false;
+            try {
+                File remote = new File(currentUpload.getRemotePath());
+                parentDirId = storageManager.getFileByPath(remote.getParent().endsWith("/")?remote.getParent():remote.getParent()+"/").getFileId();
+                File local = new File(currentUpload.getLocalPath());
+                long size = local.length();
+                mUploadsInProgress.put(buildRemoteName(currentAccount.name, currentUpload.getRemotePath()), currentUpload.getLocalPath());
+                uploadResult = currentUpload.execute(wc);
+                if (uploadResult.isSuccess()) {
+                    saveNewOCFile(currentUpload, storageManager, parentDirId, size);
+                    mSuccessCounter++;
+                    updateResult = true;
+                }
+                
+            } finally {
+                mUploadsInProgress.remove(buildRemoteName(currentAccount.name, currentUpload.getRemotePath()));
+                broadcastUploadEnd(currentUpload, currentAccount, updateResult, parentDirId);
+            }
+        }
+        
+        notifyUploadEndOverview();
+        
+    }
+
+    /**
+     * Create remote folder for instant uploads if necessary.
+     * 
+     * @param client            WebdavClient to the ownCloud server.
+     * @param storageManager    Interface to the local database caching the data in the server.
+     * @return                  'True' if the folder exists when the methods finishes.
+     */
+    private boolean createRemoteFolderForInstantUploads(WebdavClient client, FileDataStorageManager storageManager) {
+        boolean result = true;
+        OCFile instantUploadDir = storageManager.getFileByPath(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);
+        if (instantUploadDir == null) {
+            result = client.createDirectory(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);    // fail could just mean that it already exists, but local database is not synchronized; the upload will be started anyway
+            OCFile newDir = new OCFile(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);
+            newDir.setMimetype("DIR");
+            newDir.setParentId(storageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId());
+            storageManager.saveFile(newDir);
+        }
+        return result;
+    }
+
+    /**
+     * Saves a new OC File after a successful upload.
+     * 
+     * @param upload            Upload operation completed.
+     * @param storageManager    Interface to the database where the new OCFile has to be stored.
+     * @param parentDirId       Id of the parent OCFile.
+     * @param size              Size of the file.
+     */
+    private void saveNewOCFile(UploadFileOperation upload, FileDataStorageManager storageManager, long parentDirId, long size) {
+        OCFile newFile = new OCFile(upload.getRemotePath());
+        newFile.setMimetype(upload.getMimeType());
+        newFile.setFileLength(size);
+        newFile.setModificationTimestamp(System.currentTimeMillis());
+        newFile.setLastSyncDate(0);
+        newFile.setStoragePath(upload.getLocalPath());         
+        newFile.setParentId(parentDirId);
+        if (upload.getForceOverwrite())
+            newFile.setKeepInSync(true);
+        storageManager.saveFile(newFile);
+    }
 
-        /// create status notification to show the upload progress
+    /**
+     * Creates a status notification to show the upload progress
+     */
+    private void notifyUploadStart() {
         mNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_in_progress_ticker), System.currentTimeMillis());
         mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
-        RemoteViews oldContentView = mNotification.contentView;
+        mDefaultNotificationContentView = mNotification.contentView;
         mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
         mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, false);
         mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
@@ -178,96 +314,39 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
         //              BUT an empty Intent is not a very elegant solution; something smart should happen when a user 'clicks' on an upload in the notification bar
         mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
         mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification);
+    }
 
-        /// create remote folder for instant uploads if necessary
-        if (mIsInstant) {
-            OCFile instantUploadDir = storageManager.getFileByPath(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);
-            if (instantUploadDir == null) {
-                wc.createDirectory(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);    // fail could just mean that it already exists, but local database is not synchronized; the upload will be started anyway
-                OCFile newDir = new OCFile(PhotoTakenBroadcastReceiver.INSTANT_UPLOAD_DIR);
-                newDir.setMimetype("DIR");
-                newDir.setParentId(storageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId());
-                storageManager.saveFile(newDir);
-            }
-        }
-        
-        /// perform the upload
-        File [] localFiles = new File[mLocalPaths.length];
-        for (int i = 0; i < mLocalPaths.length; ++i) {
-            localFiles[i] = new File(mLocalPaths[i]);
-            mTotalDataToSend += localFiles[i].length();
-        }
-        Log.d(TAG, "Will upload " + mTotalDataToSend + " bytes, with " + mLocalPaths.length + " files");
-        mSuccessCounter = 0;
-        for (int i = 0; i < mLocalPaths.length; ++i) {
-            String mimeType = (mMimeTypes != null) ? mMimeTypes[i] : null;
-            if (mimeType == null) {
-                try {
-                    mimeType = MimeTypeMap.getSingleton()
-                        .getMimeTypeFromExtension(
-                                mLocalPaths[i].substring(mLocalPaths[i]
-                                    .lastIndexOf('.') + 1));
-                } catch (IndexOutOfBoundsException e) {
-                    Log.e(TAG, "Trying to find out MIME type of a file without extension: " + mLocalPaths[i]);
-                }
-            }
-            if (mimeType == null)
-                mimeType = "application/octet-stream";
-            mCurrentIndexUpload = i;
-            long parentDirId = -1;
-            boolean uploadResult = false;
-            String availablePath = mRemotePaths[i];
-            if (!force_override)
-                availablePath = getAvailableRemotePath(wc, mRemotePaths[i]);
-            try {
-                File f = new File(mRemotePaths[i]);
-                long size = localFiles[i].length();
-                parentDirId = storageManager.getFileByPath(f.getParent().endsWith("/")?f.getParent():f.getParent()+"/").getFileId();
-                if(availablePath != null) {
-                    mRemotePaths[i] = availablePath;
-                    mUploadsInProgress.put(buildRemoteName(mAccount.name, mRemotePaths[i]), mLocalPaths[i]);
-                    if (wc.putFile(mLocalPaths[i], mRemotePaths[i], mimeType)) {
-                        OCFile new_file = new OCFile(mRemotePaths[i]);
-                        new_file.setMimetype(mimeType);
-                        new_file.setFileLength(size);
-                        new_file.setModificationTimestamp(System.currentTimeMillis());
-                        new_file.setLastSyncDate(0);
-                        new_file.setStoragePath(mLocalPaths[i]);         
-                        new_file.setParentId(parentDirId);
-                        if (force_override)
-                            new_file.setKeepInSync(true);
-                        storageManager.saveFile(new_file);
-                        mSuccessCounter++;
-                        uploadResult = true;
-                    }
-                }
-            } finally {
-                mUploadsInProgress.remove(buildRemoteName(mAccount.name, mRemotePaths[i]));
-                
-                /// notify upload (or fail) of EACH file to activities interested
-                Intent end = new Intent(UPLOAD_FINISH_MESSAGE);
-                end.putExtra(EXTRA_PARENT_DIR_ID, parentDirId);
-                end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult);
-                end.putExtra(EXTRA_REMOTE_PATH, mRemotePaths[i]);
-                end.putExtra(EXTRA_FILE_PATH, mLocalPaths[i]);
-                end.putExtra(ACCOUNT_NAME, mAccount.name);
-                sendBroadcast(end);
-            }
-            
-        }
-        
+    
+    /**
+     * Notifies upload (or fail) of a file to activities interested
+     */
+    private void broadcastUploadEnd(UploadFileOperation upload, Account account, boolean success, long parentDirId) {
+        /// 
+        Intent end = new Intent(UPLOAD_FINISH_MESSAGE);
+        end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath());
+        end.putExtra(EXTRA_FILE_PATH, upload.getLocalPath());
+        end.putExtra(ACCOUNT_NAME, account.name);
+        end.putExtra(EXTRA_UPLOAD_RESULT, success);
+        end.putExtra(EXTRA_PARENT_DIR_ID, parentDirId);
+        sendBroadcast(end);
+    }
+
+
+    /**
+     * Updates the status notification with the results of a batch of uploads.
+     */
+    private void notifyUploadEndOverview() {
         /// notify final result
-        if (mSuccessCounter == mLocalPaths.length) {    // success
-            //Notification finalNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_succeeded_ticker), System.currentTimeMillis());
+        if (mSuccessCounter == mTotalFilesToSend) {    // success
             mNotification.flags ^= Notification.FLAG_ONGOING_EVENT; // remove the ongoing flag
             mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
-            mNotification.contentView = oldContentView;
+            mNotification.contentView = mDefaultNotificationContentView;
             // TODO put something smart in the contentIntent below
             mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
-            if (mLocalPaths.length == 1) {
+            if (mTotalFilesToSend == 1) {
                 mNotification.setLatestEventInfo(   getApplicationContext(), 
                                                     getString(R.string.uploader_upload_succeeded_ticker), 
-                                                    String.format(getString(R.string.uploader_upload_succeeded_content_single), localFiles[0].getName()), 
+                                                    String.format(getString(R.string.uploader_upload_succeeded_content_single), (new File(mUploads.get(0).getLocalPath())).getName()), 
                                                     mNotification.contentIntent);
             } else {
                 mNotification.setLatestEventInfo(   getApplicationContext(), 
@@ -283,61 +362,21 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
             finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
             // TODO put something smart in the contentIntent below
             finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
-            if (mLocalPaths.length == 1) {
+            if (mTotalFilesToSend == 1) {
                 finalNotification.setLatestEventInfo(   getApplicationContext(), 
                                                         getString(R.string.uploader_upload_failed_ticker), 
-                                                        String.format(getString(R.string.uploader_upload_failed_content_single), localFiles[0].getName()), 
+                                                        String.format(getString(R.string.uploader_upload_failed_content_single), (new File(mUploads.get(0).getLocalPath())).getName()), 
                                                         finalNotification.contentIntent);
             } else {
                 finalNotification.setLatestEventInfo(   getApplicationContext(), 
                                                         getString(R.string.uploader_upload_failed_ticker), 
-                                                        String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mLocalPaths.length), 
+                                                        String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mTotalFilesToSend), 
                                                         finalNotification.contentIntent);
             }                
             mNotificationManager.notify(R.string.uploader_upload_failed_ticker, finalNotification);
         }
         
     }
-
-    /**
-     * Checks if remotePath does not exist in the server and returns it, or adds a suffix to it in order to avoid the server
-     * file is overwritten.
-     * 
-     * @param string
-     * @return
-     */
-    private String getAvailableRemotePath(WebdavClient wc, String remotePath) {
-        Boolean check = wc.existsFile(remotePath);
-        if (check == null) {    // null means fail
-            return null;
-        } else if (!check) {
-            return remotePath;
-        }
-    
-        int pos = remotePath.lastIndexOf(".");
-        String suffix = "";
-        String extension = "";
-        if (pos >= 0) {
-            extension = remotePath.substring(pos+1);
-            remotePath = remotePath.substring(0, pos);
-        }
-        int count = 2;
-        while (check != null && check) {
-            suffix = " (" + count + ")";
-            if (pos >= 0)
-                check = wc.existsFile(remotePath + suffix + "." + extension);
-            else
-                check = wc.existsFile(remotePath + suffix);
-            count++;
-        }
-        if (check == null) {
-            return null;
-        } else if (pos >=0) {
-            return remotePath + suffix + "." + extension;
-        } else {
-            return remotePath + suffix;
-        }
-    }
     
     
     /**
@@ -348,7 +387,7 @@ public class FileUploader extends Service implements OnDatatransferProgressListe
         mSendData += progressRate;
         int percent = (int)(100*((double)mSendData)/((double)mTotalDataToSend));
         if (percent != mPreviousPercent) {
-            String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, new File(mLocalPaths[mCurrentIndexUpload]).getName());
+            String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, new File(mUploads.get(mCurrentIndexUpload).getLocalPath()).getName());
             mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, false);
             mNotification.contentView.setTextViewText(R.id.status_text, text);
             mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification);

+ 5 - 1
src/com/owncloud/android/files/services/InstantUploadService.java

@@ -128,7 +128,11 @@ public class InstantUploadService extends Service {
                 WebdavClient wdc = OwnCloudClientUtils.createOwnCloudClient(account, getApplicationContext());
                 
                 wdc.createDirectory(INSTANT_UPLOAD_DIR);    // fail could just mean that it already exists; put will be tried anyway
-                wdc.putFile(filepath, INSTANT_UPLOAD_DIR + "/" + filename, mimetype);
+                try {
+                    wdc.putFile(filepath, INSTANT_UPLOAD_DIR + "/" + filename, mimetype);
+                } catch (Exception e) {
+                    // nothing to do; this service is deprecated, indeed
+                }
             }
         }
     }

+ 89 - 0
src/com/owncloud/android/operations/ChunkedUploadFileOperation.java

@@ -0,0 +1,89 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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.operations;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.util.Random;
+
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.PutMethod;
+
+import eu.alefzero.webdav.ChunkFromFileChannelRequestEntity;
+import eu.alefzero.webdav.OnDatatransferProgressListener;
+import eu.alefzero.webdav.WebdavClient;
+import eu.alefzero.webdav.WebdavUtils;
+
+public class ChunkedUploadFileOperation extends UploadFileOperation {
+    
+    private static final long CHUNK_SIZE = 8192;
+    private static final String OC_CHUNKED_HEADER = "OC-Chunked";
+
+    public ChunkedUploadFileOperation(  String localPath, 
+                                        String remotePath, 
+                                        String mimeType, 
+                                        boolean isInstant, 
+                                        boolean forceOverwrite, 
+                                        OnDatatransferProgressListener dataTransferProgressListener) {
+        
+        super(localPath, remotePath, mimeType, isInstant, forceOverwrite, dataTransferProgressListener);
+    }
+
+    @Override
+    protected int uploadFile(WebdavClient client) throws HttpException, IOException {
+        int status = -1;
+
+        PutMethod put = null;
+        FileChannel channel = null;
+        FileLock lock = null;
+        try {
+            File file = new File(getLocalPath());
+            channel = new RandomAccessFile(file, "rw").getChannel();
+            lock = channel.tryLock();
+            ChunkFromFileChannelRequestEntity entity = new ChunkFromFileChannelRequestEntity(channel, getMimeType(), CHUNK_SIZE);
+            entity.setOnDatatransferProgressListener(getDataTransferListener());
+            long offset = 0;
+            String uriPrefix = client.getBaseUri() + WebdavUtils.encodePath(getRemotePath()) + "-chunking-" + Math.abs((new Random()).nextInt()) + "-" ;
+            long chunkCount = (long) Math.ceil((double)file.length() / CHUNK_SIZE);
+            for (int chunkIndex = 0; chunkIndex < chunkCount ; chunkIndex++, offset += CHUNK_SIZE) {
+                put = new PutMethod(uriPrefix + chunkIndex + "-" + chunkCount);
+                put.addRequestHeader(OC_CHUNKED_HEADER, OC_CHUNKED_HEADER);
+                entity.setOffset(offset);
+                put.setRequestEntity(entity);
+                status = client.executeMethod(put);
+                client.exhaustResponse(put.getResponseBodyAsStream());
+                if (!isSuccess(status))
+                    break;
+            }
+            
+        } finally {
+            if (lock != null)
+                lock.release();
+            if (channel != null)
+                channel.close();
+            if (put != null)
+                put.releaseConnection();    // let the connection available for other methods
+        }
+        return status;
+    }
+
+}

+ 1 - 1
src/com/owncloud/android/operations/OnRemoteOperationListener.java

@@ -3,5 +3,5 @@ package com.owncloud.android.operations;
 public interface OnRemoteOperationListener {
 
 	void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result);
-
+	
 }

+ 66 - 16
src/com/owncloud/android/operations/RemoteOperationResult.java

@@ -1,5 +1,24 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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.operations;
 
+import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.SocketException;
 import java.net.SocketTimeoutException;
@@ -8,6 +27,7 @@ import java.net.UnknownHostException;
 import javax.net.ssl.SSLException;
 
 import org.apache.commons.httpclient.ConnectTimeoutException;
+import org.apache.commons.httpclient.HttpException;
 import org.apache.commons.httpclient.HttpStatus;
 
 import android.util.Log;
@@ -15,9 +35,16 @@ import android.util.Log;
 import com.owncloud.android.network.CertificateCombinedException;
 
 
+/**
+ * The result of a remote operation required to an ownCloud server.
+ * 
+ * Provides a common classification of resulst for all the application. 
+ * 
+ * @author David A. Velasco
+ */
 public class RemoteOperationResult {
     
-    public enum ResultCode {    // TODO leave alone our own errors
+    public enum ResultCode { 
         OK,
         OK_SSL,
         OK_NO_SSL,
@@ -72,40 +99,26 @@ public class RemoteOperationResult {
         
         if (e instanceof SocketException) {  
             mCode = ResultCode.WRONG_CONNECTION;
-            Log.e(TAG, "Socket exception", e);
         
         } else if (e instanceof SocketTimeoutException) {
             mCode = ResultCode.TIMEOUT;
-            Log.e(TAG, "Socket timeout exception", e);
         
         } else if (e instanceof ConnectTimeoutException) {
             mCode = ResultCode.TIMEOUT;
-            Log.e(TAG, "Connect timeout exception", e);
             
         } else if (e instanceof MalformedURLException) {
             mCode = ResultCode.INCORRECT_ADDRESS;
-            Log.e(TAG, "Malformed URL exception", e);
         
         } else if (e instanceof UnknownHostException) {
             mCode = ResultCode.HOST_NOT_AVAILABLE;
-            Log.e(TAG, "Unknown host exception", e);
         
         } else if (e instanceof SSLException) {
             mCode = ResultCode.SSL_ERROR;
-            Log.e(TAG, "SSL exception", e);
             
         } else {
             mCode = ResultCode.UNKNOWN_ERROR;
-            Log.e(TAG, "Unknown exception", e);
         }
-            
-        /*  }   catch (HttpException e) { // other specific exceptions from org.apache.commons.httpclient
-                Log.e(TAG, "HTTP exception while trying connection", e);
-            }   catch (IOException e) {   // UnkownsServiceException, and any other transport exceptions that could occur
-                Log.e(TAG, "I/O exception while trying connection", e);
-            }   catch (Exception e) {
-                Log.e(TAG, "Unexpected exception while trying connection", e);
-        */
+        
     }
     
     
@@ -148,5 +161,42 @@ public class RemoteOperationResult {
         else
             return null;
     }
+    
+    
+    public String getLogMessage() {
+        
+        if (mException != null) {
+            if (mException instanceof SocketException) {  
+                return "Socket exception";
+        
+            } else if (mException instanceof SocketTimeoutException) {
+                return "Socket timeout exception";
+        
+            } else if (mException instanceof ConnectTimeoutException) {
+                return "Connect timeout exception";
+            
+            } else if (mException instanceof MalformedURLException) {
+                return "Malformed URL exception";
+        
+            } else if (mException instanceof UnknownHostException) {
+                return "Unknown host exception";
+        
+            } else if (mException instanceof SSLException) {
+                return "SSL exception";
+
+            } else if (mException instanceof HttpException) {
+                return "HTTP violation";
+
+            } else if (mException instanceof IOException) {
+                return "Unrecovered transport exception";
+
+            } else {
+                return "Unexpected exception";
+            }
+        }
+        
+        return "Operation finished with HTTP status code " + mHttpCode + " (" + (isSuccess()?"success":"fail") + ")";
+
+    }
 
 }

+ 195 - 0
src/com/owncloud/android/operations/UploadFileOperation.java

@@ -0,0 +1,195 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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.operations;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.http.HttpStatus;
+
+import com.owncloud.android.operations.RemoteOperation;
+import com.owncloud.android.operations.RemoteOperationResult;
+
+import eu.alefzero.webdav.FileRequestEntity;
+import eu.alefzero.webdav.OnDatatransferProgressListener;
+import eu.alefzero.webdav.WebdavClient;
+import eu.alefzero.webdav.WebdavUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+/**
+ * Remote operation performing the upload of a file to an ownCloud server
+ * 
+ * @author David A. Velasco
+ */
+public class UploadFileOperation extends RemoteOperation {
+    
+    private static final String TAG = UploadFileOperation.class.getCanonicalName();
+
+    private String mLocalPath = null;
+    private String mRemotePath = null;
+    private String mMimeType = null;
+    private boolean mIsInstant = false;
+    private boolean mForceOverwrite = false;
+    private OnDatatransferProgressListener mDataTransferListener = null;
+
+    
+    public String getLocalPath() {
+        return mLocalPath;
+    }
+
+    public String getRemotePath() {
+        return mRemotePath;
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+
+    
+    public boolean isInstant() {
+        return mIsInstant;
+    }
+    
+    
+    public boolean getForceOverwrite() {
+        return mForceOverwrite;
+    }
+
+    
+    public OnDatatransferProgressListener getDataTransferListener() {
+        return mDataTransferListener ;
+    }
+
+    
+    public UploadFileOperation( String localPath, 
+                                String remotePath, 
+                                String mimeType, 
+                                boolean isInstant, 
+                                boolean forceOverwrite, 
+                                OnDatatransferProgressListener dataTransferProgressListener) {
+        mLocalPath = localPath;
+        mRemotePath = remotePath;
+        mMimeType = mimeType;
+        if (mMimeType == null) {
+            try {
+                mMimeType = MimeTypeMap.getSingleton()
+                    .getMimeTypeFromExtension(
+                            localPath.substring(localPath.lastIndexOf('.') + 1));
+            } catch (IndexOutOfBoundsException e) {
+                Log.e(TAG, "Trying to find out MIME type of a file without extension: " + localPath);
+            }
+        }
+        if (mMimeType == null) {
+            mMimeType = "application/octet-stream";
+        }
+        mIsInstant = isInstant;
+        mForceOverwrite = forceOverwrite;
+        mDataTransferListener = dataTransferProgressListener;
+    }
+    
+    @Override
+    protected RemoteOperationResult run(WebdavClient client) {
+        RemoteOperationResult result = null;
+        boolean nameCheckPassed = false;
+        try {
+            /// rename the file to upload, if necessary
+            if (!mForceOverwrite) {
+                mRemotePath = getAvailableRemotePath(client, mRemotePath);
+            }
+        
+            /// perform the upload
+            nameCheckPassed = true;
+            int status = uploadFile(client);
+            result = new RemoteOperationResult(isSuccess(status), status);
+            Log.i(TAG, "Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
+            
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log.e(TAG, "Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage() + (nameCheckPassed?"":" (while checking file existence in server)"), e);
+            
+        }
+        
+        return result;
+    }
+
+    
+    public boolean isSuccess(int status) {
+        return ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT));
+    }
+    
+    
+    protected int uploadFile(WebdavClient client) throws HttpException, IOException {
+        int status = -1;
+        PutMethod put = new PutMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath));
+        
+        try {
+            File f = new File(mLocalPath);
+            FileRequestEntity entity = new FileRequestEntity(f, mMimeType);
+            entity.setOnDatatransferProgressListener(mDataTransferListener);
+            put.setRequestEntity(entity);
+            status = client.executeMethod(put);
+            client.exhaustResponse(put.getResponseBodyAsStream());
+            
+        } finally {
+            put.releaseConnection();    // let the connection available for other methods
+        }
+        return status;
+    }
+    
+    /**
+     * Checks if remotePath does not exist in the server and returns it, or adds a suffix to it in order to avoid the server
+     * file is overwritten.
+     * 
+     * @param string
+     * @return
+     */
+    private String getAvailableRemotePath(WebdavClient wc, String remotePath) throws Exception {
+        boolean check = wc.existsFile(remotePath);
+        if (!check) {
+            return remotePath;
+        }
+    
+        int pos = remotePath.lastIndexOf(".");
+        String suffix = "";
+        String extension = "";
+        if (pos >= 0) {
+            extension = remotePath.substring(pos+1);
+            remotePath = remotePath.substring(0, pos);
+        }
+        int count = 2;
+        do {
+            suffix = " (" + count + ")";
+            if (pos >= 0)
+                check = wc.existsFile(remotePath + suffix + "." + extension);
+            else
+                check = wc.existsFile(remotePath + suffix);
+            count++;
+        } while (check);
+
+        if (pos >=0) {
+            return remotePath + suffix + "." + extension;
+        } else {
+            return remotePath + suffix;
+        }
+    }
+
+}

+ 2 - 0
src/com/owncloud/android/utils/OwnCloudVersion.java

@@ -27,6 +27,8 @@ public class OwnCloudVersion implements Comparable<OwnCloudVersion> {
             0x030000);
     public static final OwnCloudVersion owncloud_v4 = new OwnCloudVersion(
             0x040000);
+    public static final OwnCloudVersion owncloud_v4_5 = new OwnCloudVersion(
+            0x040500);
 
     // format is in version
     // 0xAABBCC

+ 109 - 0
src/eu/alefzero/webdav/ChunkFromFileChannelRequestEntity.java

@@ -0,0 +1,109 @@
+/* ownCloud Android client application
+ *   Copyright (C) 2012 Bartek Przybylski
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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 eu.alefzero.webdav;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+import org.apache.commons.httpclient.methods.RequestEntity;
+
+import eu.alefzero.webdav.OnDatatransferProgressListener;
+
+import android.util.Log;
+
+
+/**
+ * A RequestEntity that represents a PIECE of a file.
+ * 
+ * @author David A. Velasco
+ */
+public class ChunkFromFileChannelRequestEntity implements RequestEntity {
+
+    private static final String TAG = ChunkFromFileChannelRequestEntity.class.getSimpleName();
+    
+    //private final File mFile;
+    private final FileChannel mChannel;
+    private final String mContentType;
+    private final long mSize;
+    private long mOffset;
+    private OnDatatransferProgressListener mListener;
+    private ByteBuffer mBuffer = ByteBuffer.allocate(4096);
+
+    public ChunkFromFileChannelRequestEntity(final FileChannel channel, final String contentType, long size) {
+        super();
+        if (channel == null) {
+            throw new IllegalArgumentException("File may not be null");
+        }
+        if (size <= 0) {
+            throw new IllegalArgumentException("Size must be greater than zero");
+        }
+        mChannel = channel;
+        mContentType = contentType;
+        mSize = size;
+        mOffset = 0;
+    }
+    
+    public void setOffset(long offset) {
+        mOffset = offset;
+    }
+    
+    public long getContentLength() {
+        try {
+            return Math.min(mSize, mChannel.size() - mChannel.position());
+        } catch (IOException e) {
+            return mSize;
+        }
+    }
+
+    public String getContentType() {
+        return mContentType;
+    }
+
+    public boolean isRepeatable() {
+        return true;
+    }
+    
+    public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
+        mListener = listener;
+    }
+    
+    public void writeRequest(final OutputStream out) throws IOException {
+        int readCount = 0;
+        
+       try {
+            //while ((i = instream.read(tmp)) >= 0) {
+            mChannel.position(mOffset);
+            while (mChannel.position() < mOffset + mSize) {
+                readCount = mChannel.read(mBuffer);
+                out.write(mBuffer.array(), 0, readCount);
+                mBuffer.clear();
+                if (mListener != null) 
+                    mListener.transferProgress(readCount);
+            }
+            
+        } catch (IOException io) {
+            Log.e(TAG, io.getMessage());
+            throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io);   
+            
+        }
+    }
+
+}

+ 1 - 4
src/eu/alefzero/webdav/FileRequestEntity.java

@@ -1,15 +1,12 @@
 package eu.alefzero.webdav;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
-import java.nio.channels.OverlappingFileLockException;
 
 import org.apache.commons.httpclient.methods.RequestEntity;
 
@@ -52,7 +49,7 @@ public class FileRequestEntity implements RequestEntity {
     public void setOnDatatransferProgressListener(OnDatatransferProgressListener listener) {
         this.listener = listener;
     }
-
+    
     public void writeRequest(final OutputStream out) throws IOException {
         //byte[] tmp = new byte[4096];
         ByteBuffer tmp = ByteBuffer.allocate(4096);

+ 15 - 20
src/eu/alefzero/webdav/WebdavClient.java

@@ -22,6 +22,7 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Random;
 
 import org.apache.commons.httpclient.Credentials;
 import org.apache.commons.httpclient.HttpClient;
@@ -151,14 +152,14 @@ public class WebdavClient extends HttpClient {
     /**
      * Creates or update a file in the remote server with the contents of a local file.
      * 
-     * 
      * @param localFile         Path to the local file to upload.
      * @param remoteTarget      Remote path to the file to create or update, URL DECODED
      * @param contentType       MIME type of the file.
-     * @return                  'True' then the upload was successfully completed
+     * @return                  Status HTTP code returned by the server.
+     * @throws IOException      When a transport error that could not be recovered occurred while uploading the file to the server.
+     * @throws HttpException    When a violation of the HTTP protocol occurred. 
      */
-    public boolean putFile(String localFile, String remoteTarget, String contentType) {
-        boolean result = false;
+    public int putFile(String localFile, String remoteTarget, String contentType) throws HttpException, IOException {
         int status = -1;
         PutMethod put = new PutMethod(mUri.toString() + WebdavUtils.encodePath(remoteTarget));
         
@@ -169,21 +170,14 @@ public class WebdavClient extends HttpClient {
             put.setRequestEntity(entity);
             status = executeMethod(put);
             
-            result = (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT);
-            
-            Log.d(TAG, "PUT to " + remoteTarget + " finished with HTTP status " + status + (!result?"(FAIL)":""));
-
             exhaustResponse(put.getResponseBodyAsStream());
             
-        } catch (Exception e) {
-            logException(e, "uploading " + localFile + " to " + remoteTarget);
-            
         } finally {
             put.releaseConnection();    // let the connection available for other methods
         }
-        return result;
+        return status;
     }
-
+    
     /**
      * Tries to log in to the current URI, with the current credentials
      * 
@@ -239,9 +233,10 @@ public class WebdavClient extends HttpClient {
     /**
      * Check if a file exists in the OC server
      * 
-     * @return      'Boolean.TRUE' if the file exists; 'Boolean.FALSE' it doesn't exist; NULL if couldn't be checked
+     * @return              'true' if the file exists; 'false' it doesn't exist
+     * @throws  Exception   When the existence could not be determined
      */
-    public Boolean existsFile(String path) {
+    public boolean existsFile(String path) throws IOException, HttpException {
         HeadMethod head = new HeadMethod(mUri.toString() + WebdavUtils.encodePath(path));
         try {
             int status = executeMethod(head);
@@ -249,10 +244,6 @@ public class WebdavClient extends HttpClient {
             exhaustResponse(head.getResponseBodyAsStream());
             return (status == HttpStatus.SC_OK);
             
-        } catch (Exception e) {
-            logException(e, "checking existence of " + path);
-            return null;
-            
         } finally {
             head.releaseConnection();    // let the connection available for other methods
         }
@@ -295,7 +286,7 @@ public class WebdavClient extends HttpClient {
      * 
      * @param responseBodyAsStream      InputStream with the HTTP response to exhaust.
      */
-    private static void exhaustResponse(InputStream responseBodyAsStream) {
+    public void exhaustResponse(InputStream responseBodyAsStream) {
         if (responseBodyAsStream != null) {
             try {
                 while (responseBodyAsStream.read(sExhaustBuffer) >= 0);
@@ -342,5 +333,9 @@ public class WebdavClient extends HttpClient {
     public void setBaseUri(Uri uri) {
         mUri = uri;
     }
+
+    public Uri getBaseUri() {
+        return mUri;
+    }
     
 }