1 package com
.owncloud
.android
.files
.services
;
4 import java
.util
.AbstractList
;
5 import java
.util
.Collections
;
6 import java
.util
.HashMap
;
7 import java
.util
.Iterator
;
9 import java
.util
.Vector
;
11 import com
.owncloud
.android
.authenticator
.AccountAuthenticator
;
12 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
;
13 import com
.owncloud
.android
.datamodel
.OCFile
;
14 import com
.owncloud
.android
.files
.PhotoTakenBroadcastReceiver
;
15 import com
.owncloud
.android
.operations
.ChunkedUploadFileOperation
;
16 import com
.owncloud
.android
.operations
.RemoteOperationResult
;
17 import com
.owncloud
.android
.operations
.UploadFileOperation
;
18 import com
.owncloud
.android
.utils
.OwnCloudVersion
;
20 import eu
.alefzero
.webdav
.ChunkFromFileChannelRequestEntity
;
21 import eu
.alefzero
.webdav
.OnDatatransferProgressListener
;
23 import com
.owncloud
.android
.network
.OwnCloudClientUtils
;
25 import android
.accounts
.Account
;
26 import android
.accounts
.AccountManager
;
27 import android
.app
.Notification
;
28 import android
.app
.NotificationManager
;
29 import android
.app
.PendingIntent
;
30 import android
.app
.Service
;
31 import android
.content
.Intent
;
32 import android
.os
.Handler
;
33 import android
.os
.HandlerThread
;
34 import android
.os
.IBinder
;
35 import android
.os
.Looper
;
36 import android
.os
.Message
;
37 import android
.os
.Process
;
38 import android
.util
.Log
;
39 import android
.widget
.RemoteViews
;
41 import com
.owncloud
.android
.AccountUtils
;
42 import com
.owncloud
.android
.R
;
43 import eu
.alefzero
.webdav
.WebdavClient
;
45 public class FileUploader
extends Service
implements OnDatatransferProgressListener
{
47 public static final String UPLOAD_FINISH_MESSAGE
= "UPLOAD_FINISH";
48 public static final String EXTRA_PARENT_DIR_ID
= "PARENT_DIR_ID";
49 public static final String EXTRA_UPLOAD_RESULT
= "RESULT";
50 public static final String EXTRA_REMOTE_PATH
= "REMOTE_PATH";
51 public static final String EXTRA_FILE_PATH
= "FILE_PATH";
53 public static final String KEY_LOCAL_FILE
= "LOCAL_FILE";
54 public static final String KEY_REMOTE_FILE
= "REMOTE_FILE";
55 public static final String KEY_ACCOUNT
= "ACCOUNT";
56 public static final String KEY_UPLOAD_TYPE
= "UPLOAD_TYPE";
57 public static final String KEY_FORCE_OVERWRITE
= "KEY_FORCE_OVERWRITE";
58 public static final String ACCOUNT_NAME
= "ACCOUNT_NAME";
59 public static final String KEY_MIME_TYPE
= "MIME_TYPE";
60 public static final String KEY_INSTANT_UPLOAD
= "INSTANT_UPLOAD";
62 public static final int UPLOAD_SINGLE_FILE
= 0;
63 public static final int UPLOAD_MULTIPLE_FILES
= 1;
65 private static final String TAG
= FileUploader
.class.getSimpleName();
67 private NotificationManager mNotificationManager
;
68 private Looper mServiceLooper
;
69 private ServiceHandler mServiceHandler
;
70 private AbstractList
<Account
> mAccounts
= new Vector
<Account
>();
71 private AbstractList
<UploadFileOperation
> mUploads
= new Vector
<UploadFileOperation
>();
72 private Notification mNotification
;
73 private long mTotalDataToSend
, mSendData
;
74 private int mTotalFilesToSend
;
75 private int mCurrentIndexUpload
, mPreviousPercent
;
76 private int mSuccessCounter
;
77 private RemoteViews mDefaultNotificationContentView
;
80 * Static map with the files being download and the path to the temporal file were are download
82 private static Map
<String
, String
> mUploadsInProgress
= Collections
.synchronizedMap(new HashMap
<String
, String
>());
85 * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
87 public static boolean isUploading(Account account
, String remotePath
) {
88 return (mUploadsInProgress
.get(buildRemoteName(account
.name
, remotePath
)) != null
);
92 * Builds a key for mUplaodsInProgress from the accountName and remotePath
94 private static String
buildRemoteName(String accountName
, String remotePath
) {
95 return accountName
+ remotePath
;
100 * Checks if an ownCloud server version should support chunked uploads.
102 * @param version OwnCloud version instance corresponding to an ownCloud server.
103 * @return 'True' if the ownCloud server with version supports chunked uploads.
105 private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) {
106 //return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0); // TODO uncomment when feature is full in server
113 public IBinder
onBind(Intent arg0
) {
117 private final class ServiceHandler
extends Handler
{
118 public ServiceHandler(Looper looper
) {
123 public void handleMessage(Message msg
) {
130 public void onCreate() {
132 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
133 HandlerThread thread
= new HandlerThread("FileUploaderThread",
134 Process
.THREAD_PRIORITY_BACKGROUND
);
136 mServiceLooper
= thread
.getLooper();
137 mServiceHandler
= new ServiceHandler(mServiceLooper
);
141 public int onStartCommand(Intent intent
, int flags
, int startId
) {
142 if (!intent
.hasExtra(KEY_ACCOUNT
) && !intent
.hasExtra(KEY_UPLOAD_TYPE
)) {
143 Log
.e(TAG
, "Not enough information provided in intent");
144 return Service
.START_NOT_STICKY
;
146 Account account
= intent
.getParcelableExtra(KEY_ACCOUNT
);
147 if (account
== null
) {
148 Log
.e(TAG
, "Bad account information provided in upload intent");
149 return Service
.START_NOT_STICKY
;
152 int uploadType
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1);
153 if (uploadType
== -1) {
154 Log
.e(TAG
, "Incorrect upload type provided");
155 return Service
.START_NOT_STICKY
;
157 String
[] localPaths
, remotePaths
, mimeTypes
;
158 if (uploadType
== UPLOAD_SINGLE_FILE
) {
159 localPaths
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) };
160 remotePaths
= new String
[] { intent
161 .getStringExtra(KEY_REMOTE_FILE
) };
162 mimeTypes
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) };
164 } else { // mUploadType == UPLOAD_MULTIPLE_FILES
165 localPaths
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
);
166 remotePaths
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
);
167 mimeTypes
= intent
.getStringArrayExtra(KEY_MIME_TYPE
);
170 if (localPaths
.length
!= remotePaths
.length
) {
171 Log
.e(TAG
, "Different number of remote paths and local paths!");
172 return Service
.START_NOT_STICKY
;
175 boolean isInstant
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
);
176 boolean forceOverwrite
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
);
178 for (int i
=0; i
< localPaths
.length
; i
++) {
179 OwnCloudVersion ocv
= new OwnCloudVersion(AccountManager
.get(this).getUserData(account
, AccountAuthenticator
.KEY_OC_VERSION
));
180 if (FileUploader
.chunkedUploadIsSupported(ocv
)) {
181 mUploads
.add(new ChunkedUploadFileOperation(localPaths
[i
], remotePaths
[i
], ((mimeTypes
!=null
)?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
183 mUploads
.add(new UploadFileOperation(localPaths
[i
], remotePaths
[i
], (mimeTypes
!=null?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
185 mAccounts
.add(account
);
188 Message msg
= mServiceHandler
.obtainMessage();
190 mServiceHandler
.sendMessage(msg
);
192 return Service
.START_NOT_STICKY
;
197 * Core upload method: sends the file(s) to upload
199 public void uploadFile() {
201 /// prepare upload statistics
202 mTotalDataToSend
= mSendData
= mPreviousPercent
= 0;
203 Iterator
<UploadFileOperation
> it
= mUploads
.iterator();
204 while (it
.hasNext()) {
205 mTotalDataToSend
+= new File(it
.next().getLocalPath()).length();
207 mTotalFilesToSend
= mUploads
.size();
208 Log
.d(TAG
, "Will upload " + mTotalDataToSend
+ " bytes, with " + mUploads
.size() + " files");
213 UploadFileOperation currentUpload
;
214 Account currentAccount
, lastAccount
= null
;
215 FileDataStorageManager storageManager
= null
;
216 WebdavClient wc
= null
;
218 boolean createdInstantDir
= false
;
220 for (mCurrentIndexUpload
= 0; mCurrentIndexUpload
< mUploads
.size(); mCurrentIndexUpload
++) {
221 currentUpload
= mUploads
.get(mCurrentIndexUpload
);
222 currentAccount
= mAccounts
.get(mCurrentIndexUpload
);
224 /// prepare client object to send request(s) to the ownCloud server
225 if (lastAccount
== null
|| !lastAccount
.equals(currentAccount
)) {
226 storageManager
= new FileDataStorageManager(currentAccount
, getContentResolver());
227 wc
= OwnCloudClientUtils
.createOwnCloudClient(currentAccount
, getApplicationContext());
228 wc
.setDataTransferProgressListener(this);
231 if (currentUpload
.isInstant() && !createdInstantDir
) {
232 createdInstantDir
= createRemoteFolderForInstantUploads(wc
, storageManager
);
235 /// perform the upload
236 long parentDirId
= -1;
237 RemoteOperationResult uploadResult
= null
;
238 boolean updateResult
= false
;
240 File remote
= new File(currentUpload
.getRemotePath());
241 parentDirId
= storageManager
.getFileByPath(remote
.getParent().endsWith("/")?remote
.getParent():remote
.getParent()+"/").getFileId();
242 File local
= new File(currentUpload
.getLocalPath());
243 long size
= local
.length();
244 mUploadsInProgress
.put(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()), currentUpload
.getLocalPath());
245 uploadResult
= currentUpload
.execute(wc
);
246 if (uploadResult
.isSuccess()) {
247 saveNewOCFile(currentUpload
, storageManager
, parentDirId
, size
);
253 mUploadsInProgress
.remove(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()));
254 broadcastUploadEnd(currentUpload
, currentAccount
, updateResult
, parentDirId
);
258 notifyUploadEndOverview();
263 * Create remote folder for instant uploads if necessary.
265 * @param client WebdavClient to the ownCloud server.
266 * @param storageManager Interface to the local database caching the data in the server.
267 * @return 'True' if the folder exists when the methods finishes.
269 private boolean createRemoteFolderForInstantUploads(WebdavClient client
, FileDataStorageManager storageManager
) {
270 boolean result
= true
;
271 OCFile instantUploadDir
= storageManager
.getFileByPath(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
272 if (instantUploadDir
== null
) {
273 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
274 OCFile newDir
= new OCFile(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
275 newDir
.setMimetype("DIR");
276 newDir
.setParentId(storageManager
.getFileByPath(OCFile
.PATH_SEPARATOR
).getFileId());
277 storageManager
.saveFile(newDir
);
283 * Saves a new OC File after a successful upload.
285 * @param upload Upload operation completed.
286 * @param storageManager Interface to the database where the new OCFile has to be stored.
287 * @param parentDirId Id of the parent OCFile.
288 * @param size Size of the file.
290 private void saveNewOCFile(UploadFileOperation upload
, FileDataStorageManager storageManager
, long parentDirId
, long size
) {
291 OCFile newFile
= new OCFile(upload
.getRemotePath());
292 newFile
.setMimetype(upload
.getMimeType());
293 newFile
.setFileLength(size
);
294 newFile
.setModificationTimestamp(System
.currentTimeMillis());
295 newFile
.setLastSyncDate(0);
296 newFile
.setStoragePath(upload
.getLocalPath());
297 newFile
.setParentId(parentDirId
);
298 if (upload
.getForceOverwrite())
299 newFile
.setKeepInSync(true
);
300 storageManager
.saveFile(newFile
);
304 * Creates a status notification to show the upload progress
306 private void notifyUploadStart() {
307 mNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), System
.currentTimeMillis());
308 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
309 mDefaultNotificationContentView
= mNotification
.contentView
;
310 mNotification
.contentView
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
);
311 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
);
312 mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
);
313 // dvelasco ; contentIntent MUST be assigned to avoid app crashes in versions previous to Android 4.x ;
314 // 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
315 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
316 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
321 * Notifies upload (or fail) of a file to activities interested
323 private void broadcastUploadEnd(UploadFileOperation upload
, Account account
, boolean success
, long parentDirId
) {
325 Intent end
= new Intent(UPLOAD_FINISH_MESSAGE
);
326 end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath());
327 end
.putExtra(EXTRA_FILE_PATH
, upload
.getLocalPath());
328 end
.putExtra(ACCOUNT_NAME
, account
.name
);
329 end
.putExtra(EXTRA_UPLOAD_RESULT
, success
);
330 end
.putExtra(EXTRA_PARENT_DIR_ID
, parentDirId
);
336 * Updates the status notification with the results of a batch of uploads.
338 private void notifyUploadEndOverview() {
339 /// notify final result
340 if (mSuccessCounter
== mTotalFilesToSend
) { // success
341 mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove the ongoing flag
342 mNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
343 mNotification
.contentView
= mDefaultNotificationContentView
;
344 // TODO put something smart in the contentIntent below
345 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
346 if (mTotalFilesToSend
== 1) {
347 mNotification
.setLatestEventInfo( getApplicationContext(),
348 getString(R
.string
.uploader_upload_succeeded_ticker
),
349 String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
350 mNotification
.contentIntent
);
352 mNotification
.setLatestEventInfo( getApplicationContext(),
353 getString(R
.string
.uploader_upload_succeeded_ticker
),
354 String
.format(getString(R
.string
.uploader_upload_succeeded_content_multiple
), mSuccessCounter
),
355 mNotification
.contentIntent
);
357 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
); // NOT AN ERROR; uploader_upload_in_progress_ticker is the target, not a new notification
360 mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
);
361 Notification finalNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis());
362 finalNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
363 // TODO put something smart in the contentIntent below
364 finalNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
365 if (mTotalFilesToSend
== 1) {
366 finalNotification
.setLatestEventInfo( getApplicationContext(),
367 getString(R
.string
.uploader_upload_failed_ticker
),
368 String
.format(getString(R
.string
.uploader_upload_failed_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
369 finalNotification
.contentIntent
);
371 finalNotification
.setLatestEventInfo( getApplicationContext(),
372 getString(R
.string
.uploader_upload_failed_ticker
),
373 String
.format(getString(R
.string
.uploader_upload_failed_content_multiple
), mSuccessCounter
, mTotalFilesToSend
),
374 finalNotification
.contentIntent
);
376 mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
);
383 * Callback method to update the progress bar in the status notification.
386 public void transferProgress(long progressRate
) {
387 mSendData
+= progressRate
;
388 int percent
= (int)(100*((double)mSendData
)/((double)mTotalDataToSend
));
389 if (percent
!= mPreviousPercent
) {
390 String text
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, new File(mUploads
.get(mCurrentIndexUpload
).getLocalPath()).getName());
391 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
);
392 mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
);
393 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
395 mPreviousPercent
= percent
;