1 package com
.owncloud
.android
.files
.services
;
4 import java
.util
.Collections
;
5 import java
.util
.HashMap
;
8 import com
.owncloud
.android
.datamodel
.FileDataStorageManager
;
9 import com
.owncloud
.android
.datamodel
.OCFile
;
10 import com
.owncloud
.android
.files
.PhotoTakenBroadcastReceiver
;
12 import eu
.alefzero
.webdav
.OnDatatransferProgressListener
;
14 import com
.owncloud
.android
.network
.OwnCloudClientUtils
;
16 import android
.accounts
.Account
;
17 import android
.app
.Notification
;
18 import android
.app
.NotificationManager
;
19 import android
.app
.PendingIntent
;
20 import android
.app
.Service
;
21 import android
.content
.Intent
;
22 import android
.os
.Handler
;
23 import android
.os
.HandlerThread
;
24 import android
.os
.IBinder
;
25 import android
.os
.Looper
;
26 import android
.os
.Message
;
27 import android
.os
.Process
;
28 import android
.util
.Log
;
29 import android
.webkit
.MimeTypeMap
;
30 import android
.widget
.RemoteViews
;
31 import com
.owncloud
.android
.R
;
32 import eu
.alefzero
.webdav
.WebdavClient
;
34 public class FileUploader
extends Service
implements OnDatatransferProgressListener
{
36 public static final String UPLOAD_FINISH_MESSAGE
= "UPLOAD_FINISH";
37 public static final String EXTRA_PARENT_DIR_ID
= "PARENT_DIR_ID";
38 public static final String EXTRA_UPLOAD_RESULT
= "RESULT";
39 public static final String EXTRA_REMOTE_PATH
= "REMOTE_PATH";
40 public static final String EXTRA_FILE_PATH
= "FILE_PATH";
42 public static final String KEY_LOCAL_FILE
= "LOCAL_FILE";
43 public static final String KEY_REMOTE_FILE
= "REMOTE_FILE";
44 public static final String KEY_ACCOUNT
= "ACCOUNT";
45 public static final String KEY_UPLOAD_TYPE
= "UPLOAD_TYPE";
46 public static final String KEY_FORCE_OVERWRITE
= "KEY_FORCE_OVERWRITE";
47 public static final String ACCOUNT_NAME
= "ACCOUNT_NAME";
48 public static final String KEY_MIME_TYPE
= "MIME_TYPE";
49 public static final String KEY_INSTANT_UPLOAD
= "INSTANT_UPLOAD";
51 public static final int UPLOAD_SINGLE_FILE
= 0;
52 public static final int UPLOAD_MULTIPLE_FILES
= 1;
54 private static final String TAG
= "FileUploader";
56 private NotificationManager mNotificationManager
;
57 private Looper mServiceLooper
;
58 private ServiceHandler mServiceHandler
;
59 private Account mAccount
;
60 private String
[] mLocalPaths
, mRemotePaths
, mMimeTypes
;
61 private int mUploadType
;
62 private Notification mNotification
;
63 private long mTotalDataToSend
, mSendData
;
64 private int mCurrentIndexUpload
, mPreviousPercent
;
65 private int mSuccessCounter
;
66 private boolean mIsInstant
;
69 * Static map with the files being download and the path to the temporal file were are download
71 private static Map
<String
, String
> mUploadsInProgress
= Collections
.synchronizedMap(new HashMap
<String
, String
>());
74 * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
76 public static boolean isUploading(Account account
, String remotePath
) {
77 return (mUploadsInProgress
.get(buildRemoteName(account
.name
, remotePath
)) != null
);
81 * Builds a key for mUplaodsInProgress from the accountName and remotePath
83 private static String
buildRemoteName(String accountName
, String remotePath
) {
84 return accountName
+ remotePath
;
91 public IBinder
onBind(Intent arg0
) {
95 private final class ServiceHandler
extends Handler
{
96 public ServiceHandler(Looper looper
) {
101 public void handleMessage(Message msg
) {
102 uploadFile(msg
.arg2
==1?true
:false
);
108 public void onCreate() {
110 mNotificationManager
= (NotificationManager
) getSystemService(NOTIFICATION_SERVICE
);
111 HandlerThread thread
= new HandlerThread("FileUploaderThread",
112 Process
.THREAD_PRIORITY_BACKGROUND
);
114 mServiceLooper
= thread
.getLooper();
115 mServiceHandler
= new ServiceHandler(mServiceLooper
);
119 public int onStartCommand(Intent intent
, int flags
, int startId
) {
120 if (!intent
.hasExtra(KEY_ACCOUNT
) && !intent
.hasExtra(KEY_UPLOAD_TYPE
)) {
121 Log
.e(TAG
, "Not enough information provided in intent");
122 return Service
.START_NOT_STICKY
;
124 mAccount
= intent
.getParcelableExtra(KEY_ACCOUNT
);
125 mUploadType
= intent
.getIntExtra(KEY_UPLOAD_TYPE
, -1);
126 if (mUploadType
== -1) {
127 Log
.e(TAG
, "Incorrect upload type provided");
128 return Service
.START_NOT_STICKY
;
130 if (mUploadType
== UPLOAD_SINGLE_FILE
) {
131 mLocalPaths
= new String
[] { intent
.getStringExtra(KEY_LOCAL_FILE
) };
132 mRemotePaths
= new String
[] { intent
133 .getStringExtra(KEY_REMOTE_FILE
) };
134 mMimeTypes
= new String
[] { intent
.getStringExtra(KEY_MIME_TYPE
) };
136 } else { // mUploadType == UPLOAD_MULTIPLE_FILES
137 mLocalPaths
= intent
.getStringArrayExtra(KEY_LOCAL_FILE
);
138 mRemotePaths
= intent
.getStringArrayExtra(KEY_REMOTE_FILE
);
139 mMimeTypes
= intent
.getStringArrayExtra(KEY_MIME_TYPE
);
142 if (mLocalPaths
.length
!= mRemotePaths
.length
) {
143 Log
.e(TAG
, "Different number of remote paths and local paths!");
144 return Service
.START_NOT_STICKY
;
147 mIsInstant
= intent
.getBooleanExtra(KEY_INSTANT_UPLOAD
, false
);
149 Message msg
= mServiceHandler
.obtainMessage();
151 msg
.arg2
= intent
.getBooleanExtra(KEY_FORCE_OVERWRITE
, false
)?
1:0;
152 mServiceHandler
.sendMessage(msg
);
154 return Service
.START_NOT_STICKY
;
159 * Core upload method: sends the file(s) to upload
161 public void uploadFile(boolean force_override
) {
162 FileDataStorageManager storageManager
= new FileDataStorageManager(mAccount
, getContentResolver());
164 mTotalDataToSend
= mSendData
= mPreviousPercent
= 0;
166 /// prepare client object to send the request to the ownCloud server
167 WebdavClient wc
= OwnCloudClientUtils
.createOwnCloudClient(mAccount
, getApplicationContext());
168 wc
.setDataTransferProgressListener(this);
170 /// create status notification to show the upload progress
171 mNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_in_progress_ticker
), System
.currentTimeMillis());
172 mNotification
.flags
|= Notification
.FLAG_ONGOING_EVENT
;
173 RemoteViews oldContentView
= mNotification
.contentView
;
174 mNotification
.contentView
= new RemoteViews(getApplicationContext().getPackageName(), R
.layout
.progressbar_layout
);
175 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, 0, false
);
176 mNotification
.contentView
.setImageViewResource(R
.id
.status_icon
, R
.drawable
.icon
);
177 // dvelasco ; contentIntent MUST be assigned to avoid app crashes in versions previous to Android 4.x ;
178 // 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
179 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
180 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
182 /// create remote folder for instant uploads if necessary
184 OCFile instantUploadDir
= storageManager
.getFileByPath(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
185 if (instantUploadDir
== null
) {
186 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
187 OCFile newDir
= new OCFile(PhotoTakenBroadcastReceiver
.INSTANT_UPLOAD_DIR
);
188 newDir
.setMimetype("DIR");
189 newDir
.setParentId(storageManager
.getFileByPath(OCFile
.PATH_SEPARATOR
).getFileId());
190 storageManager
.saveFile(newDir
);
194 /// perform the upload
195 File
[] localFiles
= new File
[mLocalPaths
.length
];
196 for (int i
= 0; i
< mLocalPaths
.length
; ++i
) {
197 localFiles
[i
] = new File(mLocalPaths
[i
]);
198 mTotalDataToSend
+= localFiles
[i
].length();
200 Log
.d(TAG
, "Will upload " + mTotalDataToSend
+ " bytes, with " + mLocalPaths
.length
+ " files");
202 for (int i
= 0; i
< mLocalPaths
.length
; ++i
) {
203 String mimeType
= (mMimeTypes
!= null
) ? mMimeTypes
[i
] : null
;
204 if (mimeType
== null
) {
206 mimeType
= MimeTypeMap
.getSingleton()
207 .getMimeTypeFromExtension(
208 mLocalPaths
[i
].substring(mLocalPaths
[i
]
209 .lastIndexOf('.') + 1));
210 } catch (IndexOutOfBoundsException e
) {
211 Log
.e(TAG
, "Trying to find out MIME type of a file without extension: " + mLocalPaths
[i
]);
214 if (mimeType
== null
)
215 mimeType
= "application/octet-stream";
216 mCurrentIndexUpload
= i
;
217 long parentDirId
= -1;
218 boolean uploadResult
= false
;
219 String availablePath
= mRemotePaths
[i
];
221 availablePath
= getAvailableRemotePath(wc
, mRemotePaths
[i
]);
223 File f
= new File(mRemotePaths
[i
]);
224 long size
= localFiles
[i
].length();
225 parentDirId
= storageManager
.getFileByPath(f
.getParent().endsWith("/")?f
.getParent():f
.getParent()+"/").getFileId();
226 if(availablePath
!= null
) {
227 mRemotePaths
[i
] = availablePath
;
228 mUploadsInProgress
.put(buildRemoteName(mAccount
.name
, mRemotePaths
[i
]), mLocalPaths
[i
]);
229 if (wc
.putFile(mLocalPaths
[i
], mRemotePaths
[i
], mimeType
)) {
230 OCFile new_file
= new OCFile(mRemotePaths
[i
]);
231 new_file
.setMimetype(mimeType
);
232 new_file
.setFileLength(size
);
233 new_file
.setModificationTimestamp(System
.currentTimeMillis());
234 new_file
.setLastSyncDate(0);
235 new_file
.setStoragePath(mLocalPaths
[i
]);
236 new_file
.setParentId(parentDirId
);
238 new_file
.setKeepInSync(true
);
239 storageManager
.saveFile(new_file
);
245 mUploadsInProgress
.remove(buildRemoteName(mAccount
.name
, mRemotePaths
[i
]));
247 /// notify upload (or fail) of EACH file to activities interested
248 Intent end
= new Intent(UPLOAD_FINISH_MESSAGE
);
249 end
.putExtra(EXTRA_PARENT_DIR_ID
, parentDirId
);
250 end
.putExtra(EXTRA_UPLOAD_RESULT
, uploadResult
);
251 end
.putExtra(EXTRA_REMOTE_PATH
, mRemotePaths
[i
]);
252 end
.putExtra(EXTRA_FILE_PATH
, mLocalPaths
[i
]);
253 end
.putExtra(ACCOUNT_NAME
, mAccount
.name
);
259 /// notify final result
260 if (mSuccessCounter
== mLocalPaths
.length
) { // success
261 //Notification finalNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_succeeded_ticker), System.currentTimeMillis());
262 mNotification
.flags ^
= Notification
.FLAG_ONGOING_EVENT
; // remove the ongoing flag
263 mNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
264 mNotification
.contentView
= oldContentView
;
265 // TODO put something smart in the contentIntent below
266 mNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
267 if (mLocalPaths
.length
== 1) {
268 mNotification
.setLatestEventInfo( getApplicationContext(),
269 getString(R
.string
.uploader_upload_succeeded_ticker
),
270 String
.format(getString(R
.string
.uploader_upload_succeeded_content_single
), localFiles
[0].getName()),
271 mNotification
.contentIntent
);
273 mNotification
.setLatestEventInfo( getApplicationContext(),
274 getString(R
.string
.uploader_upload_succeeded_ticker
),
275 String
.format(getString(R
.string
.uploader_upload_succeeded_content_multiple
), mSuccessCounter
),
276 mNotification
.contentIntent
);
278 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
281 mNotificationManager
.cancel(R
.string
.uploader_upload_in_progress_ticker
);
282 Notification finalNotification
= new Notification(R
.drawable
.icon
, getString(R
.string
.uploader_upload_failed_ticker
), System
.currentTimeMillis());
283 finalNotification
.flags
|= Notification
.FLAG_AUTO_CANCEL
;
284 // TODO put something smart in the contentIntent below
285 finalNotification
.contentIntent
= PendingIntent
.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent
.FLAG_UPDATE_CURRENT
);
286 if (mLocalPaths
.length
== 1) {
287 finalNotification
.setLatestEventInfo( getApplicationContext(),
288 getString(R
.string
.uploader_upload_failed_ticker
),
289 String
.format(getString(R
.string
.uploader_upload_failed_content_single
), localFiles
[0].getName()),
290 finalNotification
.contentIntent
);
292 finalNotification
.setLatestEventInfo( getApplicationContext(),
293 getString(R
.string
.uploader_upload_failed_ticker
),
294 String
.format(getString(R
.string
.uploader_upload_failed_content_multiple
), mSuccessCounter
, mLocalPaths
.length
),
295 finalNotification
.contentIntent
);
297 mNotificationManager
.notify(R
.string
.uploader_upload_failed_ticker
, finalNotification
);
303 * Checks if remotePath does not exist in the server and returns it, or adds a suffix to it in order to avoid the server
304 * file is overwritten.
309 private String
getAvailableRemotePath(WebdavClient wc
, String remotePath
) {
310 Boolean check
= wc
.existsFile(remotePath
);
311 if (check
== null
) { // null means fail
317 int pos
= remotePath
.lastIndexOf(".");
319 String extension
= "";
321 extension
= remotePath
.substring(pos
+1);
322 remotePath
= remotePath
.substring(0, pos
);
325 while (check
!= null
&& check
) {
326 suffix
= " (" + count
+ ")";
328 check
= wc
.existsFile(remotePath
+ suffix
+ "." + extension
);
330 check
= wc
.existsFile(remotePath
+ suffix
);
335 } else if (pos
>=0) {
336 return remotePath
+ suffix
+ "." + extension
;
338 return remotePath
+ suffix
;
344 * Callback method to update the progress bar in the status notification.
347 public void transferProgress(long progressRate
) {
348 mSendData
+= progressRate
;
349 int percent
= (int)(100*((double)mSendData
)/((double)mTotalDataToSend
));
350 if (percent
!= mPreviousPercent
) {
351 String text
= String
.format(getString(R
.string
.uploader_upload_in_progress_content
), percent
, new File(mLocalPaths
[mCurrentIndexUpload
]).getName());
352 mNotification
.contentView
.setProgressBar(R
.id
.status_progress
, 100, percent
, false
);
353 mNotification
.contentView
.setTextViewText(R
.id
.status_text
, text
);
354 mNotificationManager
.notify(R
.string
.uploader_upload_in_progress_ticker
, mNotification
);
356 mPreviousPercent
= percent
;