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
.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 NotificationManager mNotificationManager
;
66 private Looper mServiceLooper
;
67 private ServiceHandler mServiceHandler
;
68 private AbstractList
<Account
> mAccounts
= new Vector
<Account
>();
69 private AbstractList
<UploadFileOperation
> mUploads
= new Vector
<UploadFileOperation
>();
70 private Notification mNotification
;
71 private long mTotalDataToSend
, mSendData
;
72 private int mTotalFilesToSend
;
73 private int mCurrentIndexUpload
, mPreviousPercent
;
74 private int mSuccessCounter
;
75 private RemoteViews mDefaultNotificationContentView
;
78 * Static map with the files being download and the path to the temporal file were are download
80 private static Map
<String
, String
> mUploadsInProgress
= Collections
.synchronizedMap(new HashMap
<String
, String
>());
83 * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
85 public static boolean isUploading(Account account
, String remotePath
) {
86 return (mUploadsInProgress
.get(buildRemoteName(account
.name
, remotePath
)) != null
);
90 * Builds a key for mUplaodsInProgress from the accountName and remotePath
92 private static String
buildRemoteName(String accountName
, String remotePath
) {
93 return accountName
+ remotePath
;
98 * Checks if an ownCloud server version should support chunked uploads.
100 * @param version OwnCloud version instance corresponding to an ownCloud server.
101 * @return 'True' if the ownCloud server with version supports chunked uploads.
103 private static boolean chunkedUploadIsSupported(OwnCloudVersion version
) {
104 return (version
!= null
&& version
.compareTo(OwnCloudVersion
.owncloud_v4_5
) >= 0); // TODO uncomment when feature is full in server
111 public IBinder
onBind(Intent arg0
) {
115 private final class ServiceHandler
extends Handler
{
116 public ServiceHandler(Looper looper
) {
121 public void handleMessage(Message msg
) {
128 public void onCreate() {
130 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
131 HandlerThread thread
= new HandlerThread("FileUploaderThread",
132 Process
.THREAD_PRIORITY_BACKGROUND
);
134 mServiceLooper
= thread
.getLooper();
135 mServiceHandler
= new ServiceHandler(mServiceLooper
);
139 public int onStartCommand(Intent intent
, int flags
, int startId
) {
140 if (!intent
.hasExtra(KEY_ACCOUNT
) && !intent
.hasExtra(KEY_UPLOAD_TYPE
)) {
141 Log
.e(TAG
, "Not enough information provided in intent");
142 return Service
.START_NOT_STICKY
;
144 Account account
= intent
.getParcelableExtra(KEY_ACCOUNT
);
145 if (account
== null
) {
146 Log
.e(TAG
, "Bad account information provided in upload intent");
147 return Service
.START_NOT_STICKY
;
150 int uploadType
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1);
151 if (uploadType
== -1) {
152 Log
.e(TAG
, "Incorrect upload type provided");
153 return Service
.START_NOT_STICKY
;
155 String
[] localPaths
, remotePaths
, mimeTypes
;
156 if (uploadType
== UPLOAD_SINGLE_FILE
) {
157 localPaths
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) };
158 remotePaths
= new String
[] { intent
159 .getStringExtra(KEY_REMOTE_FILE
) };
160 mimeTypes
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) };
162 } else { // mUploadType == UPLOAD_MULTIPLE_FILES
163 localPaths
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
);
164 remotePaths
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
);
165 mimeTypes
= intent
.getStringArrayExtra(KEY_MIME_TYPE
);
168 if (localPaths
.length
!= remotePaths
.length
) {
169 Log
.e(TAG
, "Different number of remote paths and local paths!");
170 return Service
.START_NOT_STICKY
;
173 boolean isInstant
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
);
174 boolean forceOverwrite
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
);
176 for (int i
=0; i
< localPaths
.length
; i
++) {
177 OwnCloudVersion ocv
= new OwnCloudVersion(AccountManager
.get(this).getUserData(account
, AccountAuthenticator
.KEY_OC_VERSION
));
178 if (FileUploader
.chunkedUploadIsSupported(ocv
)) {
179 mUploads
.add(new ChunkedUploadFileOperation(localPaths
[i
], remotePaths
[i
], ((mimeTypes
!=null
)?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
181 mUploads
.add(new UploadFileOperation(localPaths
[i
], remotePaths
[i
], (mimeTypes
!=null?mimeTypes
[i
]:""), isInstant
, forceOverwrite
, this));
183 mAccounts
.add(account
);
186 Message msg
= mServiceHandler
.obtainMessage();
188 mServiceHandler
.sendMessage(msg
);
190 return Service
.START_NOT_STICKY
;
195 * Core upload method: sends the file(s) to upload
197 public void uploadFile() {
199 /// prepare upload statistics
200 mTotalDataToSend
= mSendData
= mPreviousPercent
= 0;
201 Iterator
<UploadFileOperation
> it
= mUploads
.iterator();
202 while (it
.hasNext()) {
203 mTotalDataToSend
+= new File(it
.next().getLocalPath()).length();
205 mTotalFilesToSend
= mUploads
.size();
206 Log
.d(TAG
, "Will upload " + mTotalDataToSend
+ " bytes, with " + mUploads
.size() + " files");
211 UploadFileOperation currentUpload
;
212 Account currentAccount
, lastAccount
= null
;
213 FileDataStorageManager storageManager
= null
;
214 WebdavClient wc
= null
;
216 boolean createdInstantDir
= false
;
218 for (mCurrentIndexUpload
= 0; mCurrentIndexUpload
< mUploads
.size(); mCurrentIndexUpload
++) {
219 currentUpload
= mUploads
.get(mCurrentIndexUpload
);
220 currentAccount
= mAccounts
.get(mCurrentIndexUpload
);
222 /// prepare client object to send request(s) to the ownCloud server
223 if (lastAccount
== null
|| !lastAccount
.equals(currentAccount
)) {
224 storageManager
= new FileDataStorageManager(currentAccount
, getContentResolver());
225 wc
= OwnCloudClientUtils
.createOwnCloudClient(currentAccount
, getApplicationContext());
226 wc
.setDataTransferProgressListener(this);
229 if (currentUpload
.isInstant() && !createdInstantDir
) {
230 createdInstantDir
= createRemoteFolderForInstantUploads(wc
, storageManager
);
233 /// perform the upload
234 long parentDirId
= -1;
235 RemoteOperationResult uploadResult
= null
;
236 boolean updateResult
= false
;
238 File remote
= new File(currentUpload
.getRemotePath());
239 parentDirId
= storageManager
.getFileByPath(remote
.getParent().endsWith("/")?remote
.getParent():remote
.getParent()+"/").getFileId();
240 File local
= new File(currentUpload
.getLocalPath());
241 long size
= local
.length();
242 mUploadsInProgress
.put(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()), currentUpload
.getLocalPath());
243 uploadResult
= currentUpload
.execute(wc
);
244 if (uploadResult
.isSuccess()) {
245 saveNewOCFile(currentUpload
, storageManager
, parentDirId
, size
);
251 mUploadsInProgress
.remove(buildRemoteName(currentAccount
.name
, currentUpload
.getRemotePath()));
252 broadcastUploadEnd(currentUpload
, currentAccount
, updateResult
, parentDirId
);
256 notifyUploadEndOverview();
261 * Create remote folder for instant uploads if necessary.
263 * @param client WebdavClient to the ownCloud server.
264 * @param storageManager Interface to the local database caching the data in the server.
265 * @return 'True' if the folder exists when the methods finishes.
267 private boolean createRemoteFolderForInstantUploads(WebdavClient client
, FileDataStorageManager storageManager
) {
268 boolean result
= true
;
269 OCFile instantUploadDir
= storageManager
.getFileByPath(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
270 if (instantUploadDir
== null
) {
271 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
272 OCFile newDir
= new OCFile(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
273 newDir
.setMimetype("DIR");
274 newDir
.setParentId(storageManager
.getFileByPath(OCFile
.PATH_SEPARATOR
).getFileId());
275 storageManager
.saveFile(newDir
);
281 * Saves a new OC File after a successful upload.
283 * @param upload Upload operation completed.
284 * @param storageManager Interface to the database where the new OCFile has to be stored.
285 * @param parentDirId Id of the parent OCFile.
286 * @param size Size of the file.
288 private void saveNewOCFile(UploadFileOperation upload
, FileDataStorageManager storageManager
, long parentDirId
, long size
) {
289 OCFile newFile
= new OCFile(upload
.getRemotePath());
290 newFile
.setMimetype(upload
.getMimeType());
291 newFile
.setFileLength(size
);
292 newFile
.setModificationTimestamp(System
.currentTimeMillis());
293 newFile
.setLastSyncDate(0);
294 newFile
.setStoragePath(upload
.getLocalPath());
295 newFile
.setParentId(parentDirId
);
296 if (upload
.getForceOverwrite())
297 newFile
.setKeepInSync(true
);
298 storageManager
.saveFile(newFile
);
302 * Creates a status notification to show the upload progress
304 private void notifyUploadStart() {
305 mNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), System
.currentTimeMillis());
306 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
307 mDefaultNotificationContentView
= mNotification
.contentView
;
308 mNotification
.contentView
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
);
309 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
);
310 mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
);
311 // dvelasco ; contentIntent MUST be assigned to avoid app crashes in versions previous to Android 4.x ;
312 // 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
313 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
314 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
319 * Notifies upload (or fail) of a file to activities interested
321 private void broadcastUploadEnd(UploadFileOperation upload
, Account account
, boolean success
, long parentDirId
) {
323 Intent end
= new Intent(UPLOAD_FINISH_MESSAGE
);
324 end
.putExtra(EXTRA_REMOTE_PATH
, upload
.getRemotePath());
325 end
.putExtra(EXTRA_FILE_PATH
, upload
.getLocalPath());
326 end
.putExtra(ACCOUNT_NAME
, account
.name
);
327 end
.putExtra(EXTRA_UPLOAD_RESULT
, success
);
328 end
.putExtra(EXTRA_PARENT_DIR_ID
, parentDirId
);
334 * Updates the status notification with the results of a batch of uploads.
336 private void notifyUploadEndOverview() {
337 /// notify final result
338 if (mSuccessCounter
== mTotalFilesToSend
) { // success
339 mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove the ongoing flag
340 mNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
341 mNotification
.contentView
= mDefaultNotificationContentView
;
342 // TODO put something smart in the contentIntent below
343 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
344 if (mTotalFilesToSend
== 1) {
345 mNotification
.setLatestEventInfo( getApplicationContext(),
346 getString(R
.string
.uploader_upload_succeeded_ticker
),
347 String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
348 mNotification
.contentIntent
);
350 mNotification
.setLatestEventInfo( getApplicationContext(),
351 getString(R
.string
.uploader_upload_succeeded_ticker
),
352 String
.format(getString(R
.string
.uploader_upload_succeeded_content_multiple
), mSuccessCounter
),
353 mNotification
.contentIntent
);
355 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
358 mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
);
359 Notification finalNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis());
360 finalNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
361 // TODO put something smart in the contentIntent below
362 finalNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
363 if (mTotalFilesToSend
== 1) {
364 finalNotification
.setLatestEventInfo( getApplicationContext(),
365 getString(R
.string
.uploader_upload_failed_ticker
),
366 String
.format(getString(R
.string
.uploader_upload_failed_content_single
), (new File(mUploads
.get(0).getLocalPath())).getName()),
367 finalNotification
.contentIntent
);
369 finalNotification
.setLatestEventInfo( getApplicationContext(),
370 getString(R
.string
.uploader_upload_failed_ticker
),
371 String
.format(getString(R
.string
.uploader_upload_failed_content_multiple
), mSuccessCounter
, mTotalFilesToSend
),
372 finalNotification
.contentIntent
);
374 mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
);
381 * Callback method to update the progress bar in the status notification.
384 public void transferProgress(long progressRate
) {
385 mSendData
+= progressRate
;
386 int percent
= (int)(100*((double)mSendData
)/((double)mTotalDataToSend
));
387 if (percent
!= mPreviousPercent
) {
388 String text
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, new File(mUploads
.get(mCurrentIndexUpload
).getLocalPath()).getName());
389 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
);
390 mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
);
391 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
393 mPreviousPercent
= percent
;