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
.InstantUploadBroadcastReceiver
;
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
.OnDatatransferProgressListener
;
22 import com
.owncloud
.android
.network
.OwnCloudClientUtils
;
24 import android
.accounts
.Account
;
25 import android
.accounts
.AccountManager
;
26 import android
.app
.Notification
;
27 import android
.app
.NotificationManager
;
28 import android
.app
.PendingIntent
;
29 import android
.app
.Service
;
30 import android
.content
.Intent
;
31 import android
.os
.Handler
;
32 import android
.os
.HandlerThread
;
33 import android
.os
.IBinder
;
34 import android
.os
.Looper
;
35 import android
.os
.Message
;
36 import android
.os
.Process
;
37 import android
.util
.Log
;
38 import android
.widget
.RemoteViews
;
40 import com
.owncloud
.android
.R
;
41 import eu
.alefzero
.webdav
.WebdavClient
;
43 public class FileUploader
extends Service
implements OnDatatransferProgressListener
{
45 public static final String UPLOAD_FINISH_MESSAGE
= "UPLOAD_FINISH";
46 public static final String EXTRA_PARENT_DIR_ID
= "PARENT_DIR_ID";
47 public static final String EXTRA_UPLOAD_RESULT
= "RESULT";
48 public static final String EXTRA_REMOTE_PATH
= "REMOTE_PATH";
49 public static final String EXTRA_FILE_PATH
= "FILE_PATH";
51 public static final String KEY_LOCAL_FILE
= "LOCAL_FILE";
52 public static final String KEY_REMOTE_FILE
= "REMOTE_FILE";
53 public static final String KEY_ACCOUNT
= "ACCOUNT";
54 public static final String KEY_UPLOAD_TYPE
= "UPLOAD_TYPE";
55 public static final String KEY_FORCE_OVERWRITE
= "KEY_FORCE_OVERWRITE";
56 public static final String ACCOUNT_NAME
= "ACCOUNT_NAME";
57 public static final String KEY_MIME_TYPE
= "MIME_TYPE";
58 public static final String KEY_INSTANT_UPLOAD
= "INSTANT_UPLOAD";
60 public static final int UPLOAD_SINGLE_FILE
= 0;
61 public static final int UPLOAD_MULTIPLE_FILES
= 1;
63 private static final String TAG
= FileUploader
.class.getSimpleName();
65 private Looper mServiceLooper
;
66 private ServiceHandler mServiceHandler
;
68 private AbstractList
<Account
> mAccounts
= new Vector
<Account
>();
69 private AbstractList
<UploadFileOperation
> mUploads
= new Vector
<UploadFileOperation
>();
70 private int mCurrentIndexUpload
;
72 private NotificationManager mNotificationManager
;
73 private Notification mNotification
;
74 private RemoteViews mDefaultNotificationContentView
;
75 private long mTotalDataToSend
, mSendData
;
76 private int mTotalFilesToSend
, mPreviousPercent
;
77 private int mSuccessCounter
;
81 * Static map with the files being download and the path to the temporal file were are download
83 private static Map
<String
, String
> mUploadsInProgress
= Collections
.synchronizedMap(new HashMap
<String
, String
>());
86 * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
88 public static boolean isUploading(Account account
, String remotePath
) {
89 return (mUploadsInProgress
.get(buildRemoteName(account
.name
, remotePath
)) != null
);
93 * Builds a key for mUplaodsInProgress from the accountName and remotePath
95 private static String
buildRemoteName(String accountName
, String remotePath
) {
96 return accountName
+ remotePath
;
101 * Checks if an ownCloud server version should support chunked uploads.
103 * @param version OwnCloud version instance corresponding to an ownCloud server.
104 * @return 'True' if the ownCloud server with version supports chunked uploads.
106 private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) {
107 return (version
!= null
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); // TODO uncomment when feature is full in server
114 * Service initialization
117 public void onCreate() {
119 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
120 HandlerThread thread
= new HandlerThread("FileUploaderThread",
121 Process
.THREAD_PRIORITY_BACKGROUND
);
123 mServiceLooper
= thread
.getLooper();
124 mServiceHandler
= new ServiceHandler(mServiceLooper
);
129 * Entry point to add one or several files to the queue of uploads.
131 * New uploads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
132 * although the caller activity goes away.
135 public int onStartCommand(Intent intent
, int flags
, int startId
) {
136 if (!intent
.hasExtra(KEY_ACCOUNT
) && !intent
.hasExtra(KEY_UPLOAD_TYPE
)) {
137 Log
.e(TAG
, "Not enough information provided in intent");
138 return Service
.START_NOT_STICKY
;
140 Account account
= intent
.getParcelableExtra(KEY_ACCOUNT
);
141 if (account
== null
) {
142 Log
.e(TAG
, "Bad account information provided in upload intent");
143 return Service
.START_NOT_STICKY
;
146 int uploadType
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1);
147 if (uploadType
== -1) {
148 Log
.e(TAG
, "Incorrect upload type provided");
149 return Service
.START_NOT_STICKY
;
151 String
[] localPaths
, remotePaths
, mimeTypes
;
152 if (uploadType
== UPLOAD_SINGLE_FILE
) {
153 localPaths
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) };
154 remotePaths
= new String
[] { intent
155 .getStringExtra(KEY_REMOTE_FILE
) };
156 mimeTypes
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) };
158 } else { // mUploadType == UPLOAD_MULTIPLE_FILES
159 localPaths
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
);
160 remotePaths
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
);
161 mimeTypes
= intent
.getStringArrayExtra(KEY_MIME_TYPE
);
164 if (localPaths
.length
!= remotePaths
.length
) {
165 Log
.e(TAG
, "Different number of remote paths and local paths!");
166 return Service
.START_NOT_STICKY
;
169 boolean isInstant
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
);
170 boolean forceOverwrite
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
);
172 for (int i
=0; i
< localPaths
.length
; i
++) {
173 OwnCloudVersion ocv
= new OwnCloudVersion(AccountManager
.get(this).getUserData(account
, AccountAuthenticator
.KEY_OC_VERSION
));
174 if (FileUploader
.chunkedUploadIsSupported(ocv
)) {
175 mUploads
.add(new ChunkedUploadFileOperation(localPaths
[i
], remotePaths
[i
], ((mimeTypes
!=null
)?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
177 mUploads
.add(new UploadFileOperation(localPaths
[i
], remotePaths
[i
], (mimeTypes
!=null?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
179 mAccounts
.add(account
);
182 Message msg
= mServiceHandler
.obtainMessage();
184 mServiceHandler
.sendMessage(msg
);
186 return Service
.START_NOT_STICKY
;
191 * Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the addition of new files.
193 * Implemented to perform cancellation, pause and resume of existing uploads.
196 public IBinder
onBind(Intent arg0
) {
202 * Upload worker. Performs the pending uploads in the order they were requested.
204 * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
206 private final class ServiceHandler
extends Handler
{
207 public ServiceHandler(Looper looper
) {
212 public void handleMessage(Message msg
) {
222 * Core upload method: sends the file(s) to upload
224 public void uploadFile() {
226 /// prepare upload statistics
227 mTotalDataToSend
= mSendData
= mPreviousPercent
= 0;
228 Iterator
<UploadFileOperation
> it
= mUploads
.iterator();
229 while (it
.hasNext()) {
230 mTotalDataToSend
+= new File(it
.next().getLocalPath()).length();
232 mTotalFilesToSend
= mUploads
.size();
233 Log
.d(TAG
, "Will upload " + mTotalDataToSend
+ " bytes, with " + mUploads
.size() + " files");
238 UploadFileOperation currentUpload
;
239 Account currentAccount
, lastAccount
= null
;
240 FileDataStorageManager storageManager
= null
;
241 WebdavClient wc
= null
;
243 boolean createdInstantDir
= false
;
245 for (mCurrentIndexUpload
= 0; mCurrentIndexUpload
< mUploads
.size(); mCurrentIndexUpload
++) {
246 currentUpload
= mUploads
.get(mCurrentIndexUpload
);
247 currentAccount
= mAccounts
.get(mCurrentIndexUpload
);
249 /// prepare client object to send request(s) to the ownCloud server
250 if (lastAccount
== null
|| !lastAccount
.equals(currentAccount
)) {
251 storageManager
= new FileDataStorageManager(currentAccount
, getContentResolver());
252 wc
= OwnCloudClientUtils
.createOwnCloudClient(currentAccount
, getApplicationContext());
253 wc
.setDataTransferProgressListener(this);
256 if (currentUpload
.isInstant() && !createdInstantDir
) {
257 createdInstantDir
= createRemoteFolderForInstantUploads(wc
, storageManager
);
260 /// perform the upload
261 long parentDirId
= -1;
262 RemoteOperationResult uploadResult
= null
;
263 boolean updateResult
= false
;
265 File remote
= new File(currentUpload
.getRemotePath());
266 parentDirId
= storageManager
.getFileByPath(remote
.getParent().endsWith("/")?remote
.getParent():remote
.getParent()+"/").getFileId();
267 File local
= new File(currentUpload
.getLocalPath());
268 long size
= local
.length();
269 mUploadsInProgress
.put(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()), currentUpload
.getLocalPath());
270 uploadResult
= currentUpload
.execute(wc
);
271 if (uploadResult
.isSuccess()) {
272 saveNewOCFile(currentUpload
, storageManager
, parentDirId
, size
);
278 mUploadsInProgress
.remove(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()));
279 broadcastUploadEnd(currentUpload
, currentAccount
, updateResult
, parentDirId
);
283 notifyUploadEndOverview();
288 * Create remote folder for instant uploads if necessary.
290 * @param client WebdavClient to the ownCloud server.
291 * @param storageManager Interface to the local database caching the data in the server.
292 * @return 'True' if the folder exists when the methods finishes.
294 private boolean createRemoteFolderForInstantUploads(WebdavClient client
, FileDataStorageManager storageManager
) {
295 boolean result
= true
;
296 OCFile instantUploadDir
= storageManager
.getFileByPath(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
297 if (instantUploadDir
== null
) {
298 result
= client
.createDirectory(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
); // fail could just mean that it already exists, but local database is not synchronized; the upload will be started anyway
299 OCFile newDir
= new OCFile(InstantUploadBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
300 newDir
.setMimetype("DIR");
301 newDir
.setParentId(storageManager
.getFileByPath(OCFile
.PATH_SEPARATOR
).getFileId());
302 storageManager
.saveFile(newDir
);
308 * Saves a new OC File after a successful upload.
310 * @param upload Upload operation completed.
311 * @param storageManager Interface to the database where the new OCFile has to be stored.
312 * @param parentDirId Id of the parent OCFile.
313 * @param size Size of the file.
315 private void saveNewOCFile(UploadFileOperation upload
, FileDataStorageManager storageManager
, long parentDirId
, long size
) {
316 OCFile newFile
= new OCFile(upload
.getRemotePath());
317 newFile
.setMimetype(upload
.getMimeType());
318 newFile
.setFileLength(size
);
319 newFile
.setModificationTimestamp(System
.currentTimeMillis());
320 newFile
.setLastSyncDate(0);
321 newFile
.setStoragePath(upload
.getLocalPath());
322 newFile
.setParentId(parentDirId
);
323 if (upload
.getForceOverwrite())
324 newFile
.setKeepInSync(true
);
325 storageManager
.saveFile(newFile
);
329 * Creates a status notification to show the upload progress
331 private void notifyUploadStart() {
332 mNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), System
.currentTimeMillis());
333 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
334 mDefaultNotificationContentView
= mNotification
.contentView
;
335 mNotification
.contentView
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
);
336 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
);
337 mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
);
338 // dvelasco ; contentIntent MUST be assigned to avoid app crashes in versions previous to Android 4.x ;
339 // 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
340 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
341 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
346 * Notifies upload (or fail) of a file to activities interested
348 private void broadcastUploadEnd(UploadFileOperation upload
, Account account
, boolean success
, long parentDirId
) {
350 Intent end
= new Intent(UPLOAD_FINISH_MESSAGE
);
351 end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath());
352 end
.putExtra(EXTRA_FILE_PATH
, upload
.getLocalPath());
353 end
.putExtra(ACCOUNT_NAME
, account
.name
);
354 end
.putExtra(EXTRA_UPLOAD_RESULT
, success
);
355 end
.putExtra(EXTRA_PARENT_DIR_ID
, parentDirId
);
361 * Updates the status notification with the results of a batch of uploads.
363 private void notifyUploadEndOverview() {
364 /// notify final result
365 if (mSuccessCounter
== mTotalFilesToSend
) { // success
366 mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove the ongoing flag
367 mNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
368 mNotification
.contentView
= mDefaultNotificationContentView
;
369 // TODO put something smart in the contentIntent below
370 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
371 if (mTotalFilesToSend
== 1) {
372 mNotification
.setLatestEventInfo( getApplicationContext(),
373 getString(R
.string
.uploader_upload_succeeded_ticker
),
374 String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
375 mNotification
.contentIntent
);
377 mNotification
.setLatestEventInfo( getApplicationContext(),
378 getString(R
.string
.uploader_upload_succeeded_ticker
),
379 String
.format(getString(R
.string
.uploader_upload_succeeded_content_multiple
), mSuccessCounter
),
380 mNotification
.contentIntent
);
382 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
385 mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
);
386 Notification finalNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis());
387 finalNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
388 // TODO put something smart in the contentIntent below
389 finalNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
390 if (mTotalFilesToSend
== 1) {
391 finalNotification
.setLatestEventInfo( getApplicationContext(),
392 getString(R
.string
.uploader_upload_failed_ticker
),
393 String
.format(getString(R
.string
.uploader_upload_failed_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
394 finalNotification
.contentIntent
);
396 finalNotification
.setLatestEventInfo( getApplicationContext(),
397 getString(R
.string
.uploader_upload_failed_ticker
),
398 String
.format(getString(R
.string
.uploader_upload_failed_content_multiple
), mSuccessCounter
, mTotalFilesToSend
),
399 finalNotification
.contentIntent
);
401 mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
);
408 * Callback method to update the progress bar in the status notification.
411 public void onTransferProgress(long progressRate
) {
412 mSendData
+= progressRate
;
413 int percent
= (int)(100*((double)mSendData
)/((double)mTotalDataToSend
));
414 if (percent
!= mPreviousPercent
) {
415 String text
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, new File(mUploads
.get(mCurrentIndexUpload
).getLocalPath()).getName());
416 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
);
417 mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
);
418 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
420 mPreviousPercent
= percent
;
424 public void onTransferProgress(long progressRate
, long totalTransferredSoFar
, long totalToTransfer
, String fileName
) {
425 // TODO Maybe replace the other transferProgress with this