FileUploader.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package com.owncloud.android.files.services;
  2. import java.io.File;
  3. import java.util.AbstractList;
  4. import java.util.Collections;
  5. import java.util.HashMap;
  6. import java.util.Iterator;
  7. import java.util.Map;
  8. import java.util.Vector;
  9. import com.owncloud.android.authenticator.AccountAuthenticator;
  10. import com.owncloud.android.datamodel.FileDataStorageManager;
  11. import com.owncloud.android.datamodel.OCFile;
  12. import com.owncloud.android.files.InstantUploadBroadcastReceiver;
  13. import com.owncloud.android.operations.ChunkedUploadFileOperation;
  14. import com.owncloud.android.operations.RemoteOperationResult;
  15. import com.owncloud.android.operations.UploadFileOperation;
  16. import com.owncloud.android.utils.OwnCloudVersion;
  17. import eu.alefzero.webdav.OnDatatransferProgressListener;
  18. import com.owncloud.android.network.OwnCloudClientUtils;
  19. import android.accounts.Account;
  20. import android.accounts.AccountManager;
  21. import android.app.Notification;
  22. import android.app.NotificationManager;
  23. import android.app.PendingIntent;
  24. import android.app.Service;
  25. import android.content.Intent;
  26. import android.os.Handler;
  27. import android.os.HandlerThread;
  28. import android.os.IBinder;
  29. import android.os.Looper;
  30. import android.os.Message;
  31. import android.os.Process;
  32. import android.util.Log;
  33. import android.widget.RemoteViews;
  34. import com.owncloud.android.R;
  35. import eu.alefzero.webdav.WebdavClient;
  36. public class FileUploader extends Service implements OnDatatransferProgressListener {
  37. public static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH";
  38. public static final String EXTRA_PARENT_DIR_ID = "PARENT_DIR_ID";
  39. public static final String EXTRA_UPLOAD_RESULT = "RESULT";
  40. public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
  41. 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";
  50. public static final int UPLOAD_SINGLE_FILE = 0;
  51. public static final int UPLOAD_MULTIPLE_FILES = 1;
  52. private static final String TAG = FileUploader.class.getSimpleName();
  53. private Looper mServiceLooper;
  54. private ServiceHandler mServiceHandler;
  55. private AbstractList<Account> mAccounts = new Vector<Account>();
  56. private AbstractList<UploadFileOperation> mUploads = new Vector<UploadFileOperation>();
  57. private int mCurrentIndexUpload;
  58. private NotificationManager mNotificationManager;
  59. private Notification mNotification;
  60. private RemoteViews mDefaultNotificationContentView;
  61. private long mTotalDataToSend, mSendData;
  62. private int mTotalFilesToSend, mPreviousPercent;
  63. private int mSuccessCounter;
  64. /**
  65. * Static map with the files being download and the path to the temporal file were are download
  66. */
  67. private static Map<String, String> mUploadsInProgress = Collections.synchronizedMap(new HashMap<String, String>());
  68. /**
  69. * Returns True when the file referred by 'remotePath' in the ownCloud account 'account' is downloading
  70. */
  71. public static boolean isUploading(Account account, String remotePath) {
  72. return (mUploadsInProgress.get(buildRemoteName(account.name, remotePath)) != null);
  73. }
  74. /**
  75. * Builds a key for mUplaodsInProgress from the accountName and remotePath
  76. */
  77. private static String buildRemoteName(String accountName, String remotePath) {
  78. return accountName + remotePath;
  79. }
  80. /**
  81. * Checks if an ownCloud server version should support chunked uploads.
  82. *
  83. * @param version OwnCloud version instance corresponding to an ownCloud server.
  84. * @return 'True' if the ownCloud server with version supports chunked uploads.
  85. */
  86. private static boolean chunkedUploadIsSupported(OwnCloudVersion version) {
  87. return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0); // TODO uncomment when feature is full in server
  88. //return false;
  89. }
  90. /**
  91. * Service initialization
  92. */
  93. @Override
  94. public void onCreate() {
  95. super.onCreate();
  96. mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  97. HandlerThread thread = new HandlerThread("FileUploaderThread",
  98. Process.THREAD_PRIORITY_BACKGROUND);
  99. thread.start();
  100. mServiceLooper = thread.getLooper();
  101. mServiceHandler = new ServiceHandler(mServiceLooper);
  102. }
  103. /**
  104. * Entry point to add one or several files to the queue of uploads.
  105. *
  106. * New uploads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
  107. * although the caller activity goes away.
  108. */
  109. @Override
  110. public int onStartCommand(Intent intent, int flags, int startId) {
  111. if (!intent.hasExtra(KEY_ACCOUNT) && !intent.hasExtra(KEY_UPLOAD_TYPE)) {
  112. Log.e(TAG, "Not enough information provided in intent");
  113. return Service.START_NOT_STICKY;
  114. }
  115. Account account = intent.getParcelableExtra(KEY_ACCOUNT);
  116. if (account == null) {
  117. Log.e(TAG, "Bad account information provided in upload intent");
  118. return Service.START_NOT_STICKY;
  119. }
  120. int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1);
  121. if (uploadType == -1) {
  122. Log.e(TAG, "Incorrect upload type provided");
  123. return Service.START_NOT_STICKY;
  124. }
  125. String[] localPaths, remotePaths, mimeTypes;
  126. if (uploadType == UPLOAD_SINGLE_FILE) {
  127. localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
  128. remotePaths = new String[] { intent
  129. .getStringExtra(KEY_REMOTE_FILE) };
  130. mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) };
  131. } else { // mUploadType == UPLOAD_MULTIPLE_FILES
  132. localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE);
  133. remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE);
  134. mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
  135. }
  136. if (localPaths.length != remotePaths.length) {
  137. Log.e(TAG, "Different number of remote paths and local paths!");
  138. return Service.START_NOT_STICKY;
  139. }
  140. boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false);
  141. boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false);
  142. for (int i=0; i < localPaths.length; i++) {
  143. OwnCloudVersion ocv = new OwnCloudVersion(AccountManager.get(this).getUserData(account, AccountAuthenticator.KEY_OC_VERSION));
  144. if (FileUploader.chunkedUploadIsSupported(ocv)) {
  145. mUploads.add(new ChunkedUploadFileOperation(localPaths[i], remotePaths[i], ((mimeTypes!=null)?mimeTypes[i]:""), isInstant, forceOverwrite, this));
  146. } else {
  147. mUploads.add(new UploadFileOperation(localPaths[i], remotePaths[i], (mimeTypes!=null?mimeTypes[i]:""), isInstant, forceOverwrite, this));
  148. }
  149. mAccounts.add(account);
  150. }
  151. Message msg = mServiceHandler.obtainMessage();
  152. msg.arg1 = startId;
  153. mServiceHandler.sendMessage(msg);
  154. return Service.START_NOT_STICKY;
  155. }
  156. /**
  157. * Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the addition of new files.
  158. *
  159. * Implemented to perform cancellation, pause and resume of existing uploads.
  160. */
  161. @Override
  162. public IBinder onBind(Intent arg0) {
  163. return null;
  164. }
  165. /**
  166. * Upload worker. Performs the pending uploads in the order they were requested.
  167. *
  168. * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
  169. */
  170. private final class ServiceHandler extends Handler {
  171. public ServiceHandler(Looper looper) {
  172. super(looper);
  173. }
  174. @Override
  175. public void handleMessage(Message msg) {
  176. uploadFile();
  177. stopSelf(msg.arg1);
  178. }
  179. }
  180. /**
  181. * Core upload method: sends the file(s) to upload
  182. */
  183. public void uploadFile() {
  184. /// prepare upload statistics
  185. mTotalDataToSend = mSendData = mPreviousPercent = 0;
  186. Iterator<UploadFileOperation> it = mUploads.iterator();
  187. while (it.hasNext()) {
  188. mTotalDataToSend += new File(it.next().getLocalPath()).length();
  189. }
  190. mTotalFilesToSend = mUploads.size();
  191. Log.d(TAG, "Will upload " + mTotalDataToSend + " bytes, with " + mUploads.size() + " files");
  192. notifyUploadStart();
  193. UploadFileOperation currentUpload;
  194. Account currentAccount, lastAccount = null;
  195. FileDataStorageManager storageManager = null;
  196. WebdavClient wc = null;
  197. mSuccessCounter = 0;
  198. boolean createdInstantDir = false;
  199. for (mCurrentIndexUpload = 0; mCurrentIndexUpload < mUploads.size(); mCurrentIndexUpload++) {
  200. currentUpload = mUploads.get(mCurrentIndexUpload);
  201. currentAccount = mAccounts.get(mCurrentIndexUpload);
  202. /// prepare client object to send request(s) to the ownCloud server
  203. if (lastAccount == null || !lastAccount.equals(currentAccount)) {
  204. storageManager = new FileDataStorageManager(currentAccount, getContentResolver());
  205. wc = OwnCloudClientUtils.createOwnCloudClient(currentAccount, getApplicationContext());
  206. wc.setDataTransferProgressListener(this);
  207. }
  208. if (currentUpload.isInstant() && !createdInstantDir) {
  209. createdInstantDir = createRemoteFolderForInstantUploads(wc, storageManager);
  210. }
  211. /// perform the upload
  212. long parentDirId = -1;
  213. RemoteOperationResult uploadResult = null;
  214. boolean updateResult = false;
  215. try {
  216. File remote = new File(currentUpload.getRemotePath());
  217. parentDirId = storageManager.getFileByPath(remote.getParent().endsWith("/")?remote.getParent():remote.getParent()+"/").getFileId();
  218. File local = new File(currentUpload.getLocalPath());
  219. long size = local.length();
  220. mUploadsInProgress.put(buildRemoteName(currentAccount.name, currentUpload.getRemotePath()), currentUpload.getLocalPath());
  221. uploadResult = currentUpload.execute(wc);
  222. if (uploadResult.isSuccess()) {
  223. saveNewOCFile(currentUpload, storageManager, parentDirId, size);
  224. mSuccessCounter++;
  225. updateResult = true;
  226. }
  227. } finally {
  228. mUploadsInProgress.remove(buildRemoteName(currentAccount.name, currentUpload.getRemotePath()));
  229. broadcastUploadEnd(currentUpload, currentAccount, updateResult, parentDirId);
  230. }
  231. }
  232. notifyUploadEndOverview();
  233. }
  234. /**
  235. * Create remote folder for instant uploads if necessary.
  236. *
  237. * @param client WebdavClient to the ownCloud server.
  238. * @param storageManager Interface to the local database caching the data in the server.
  239. * @return 'True' if the folder exists when the methods finishes.
  240. */
  241. private boolean createRemoteFolderForInstantUploads(WebdavClient client, FileDataStorageManager storageManager) {
  242. boolean result = true;
  243. OCFile instantUploadDir = storageManager.getFileByPath(InstantUploadBroadcastReceiver.INSTANT_UPLOAD_DIR);
  244. if (instantUploadDir == null) {
  245. 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
  246. OCFile newDir = new OCFile(InstantUploadBroadcastReceiver.INSTANT_UPLOAD_DIR);
  247. newDir.setMimetype("DIR");
  248. newDir.setParentId(storageManager.getFileByPath(OCFile.PATH_SEPARATOR).getFileId());
  249. storageManager.saveFile(newDir);
  250. }
  251. return result;
  252. }
  253. /**
  254. * Saves a new OC File after a successful upload.
  255. *
  256. * @param upload Upload operation completed.
  257. * @param storageManager Interface to the database where the new OCFile has to be stored.
  258. * @param parentDirId Id of the parent OCFile.
  259. * @param size Size of the file.
  260. */
  261. private void saveNewOCFile(UploadFileOperation upload, FileDataStorageManager storageManager, long parentDirId, long size) {
  262. OCFile newFile = new OCFile(upload.getRemotePath());
  263. newFile.setMimetype(upload.getMimeType());
  264. newFile.setFileLength(size);
  265. newFile.setModificationTimestamp(System.currentTimeMillis());
  266. newFile.setLastSyncDate(0);
  267. newFile.setStoragePath(upload.getLocalPath());
  268. newFile.setParentId(parentDirId);
  269. if (upload.getForceOverwrite())
  270. newFile.setKeepInSync(true);
  271. storageManager.saveFile(newFile);
  272. }
  273. /**
  274. * Creates a status notification to show the upload progress
  275. */
  276. private void notifyUploadStart() {
  277. mNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_in_progress_ticker), System.currentTimeMillis());
  278. mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
  279. mDefaultNotificationContentView = mNotification.contentView;
  280. mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
  281. mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, false);
  282. mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
  283. // dvelasco ; contentIntent MUST be assigned to avoid app crashes in versions previous to Android 4.x ;
  284. // 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
  285. mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
  286. mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification);
  287. }
  288. /**
  289. * Notifies upload (or fail) of a file to activities interested
  290. */
  291. private void broadcastUploadEnd(UploadFileOperation upload, Account account, boolean success, long parentDirId) {
  292. ///
  293. Intent end = new Intent(UPLOAD_FINISH_MESSAGE);
  294. end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath());
  295. end.putExtra(EXTRA_FILE_PATH, upload.getLocalPath());
  296. end.putExtra(ACCOUNT_NAME, account.name);
  297. end.putExtra(EXTRA_UPLOAD_RESULT, success);
  298. end.putExtra(EXTRA_PARENT_DIR_ID, parentDirId);
  299. sendBroadcast(end);
  300. }
  301. /**
  302. * Updates the status notification with the results of a batch of uploads.
  303. */
  304. private void notifyUploadEndOverview() {
  305. /// notify final result
  306. if (mSuccessCounter == mTotalFilesToSend) { // success
  307. mNotification.flags ^= Notification.FLAG_ONGOING_EVENT; // remove the ongoing flag
  308. mNotification.flags |= Notification.FLAG_AUTO_CANCEL;
  309. mNotification.contentView = mDefaultNotificationContentView;
  310. // TODO put something smart in the contentIntent below
  311. mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
  312. if (mTotalFilesToSend == 1) {
  313. mNotification.setLatestEventInfo( getApplicationContext(),
  314. getString(R.string.uploader_upload_succeeded_ticker),
  315. String.format(getString(R.string.uploader_upload_succeeded_content_single), (new File(mUploads.get(0).getLocalPath())).getName()),
  316. mNotification.contentIntent);
  317. } else {
  318. mNotification.setLatestEventInfo( getApplicationContext(),
  319. getString(R.string.uploader_upload_succeeded_ticker),
  320. String.format(getString(R.string.uploader_upload_succeeded_content_multiple), mSuccessCounter),
  321. mNotification.contentIntent);
  322. }
  323. 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
  324. } else {
  325. mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker);
  326. Notification finalNotification = new Notification(R.drawable.icon, getString(R.string.uploader_upload_failed_ticker), System.currentTimeMillis());
  327. finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
  328. // TODO put something smart in the contentIntent below
  329. finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
  330. if (mTotalFilesToSend == 1) {
  331. finalNotification.setLatestEventInfo( getApplicationContext(),
  332. getString(R.string.uploader_upload_failed_ticker),
  333. String.format(getString(R.string.uploader_upload_failed_content_single), (new File(mUploads.get(0).getLocalPath())).getName()),
  334. finalNotification.contentIntent);
  335. } else {
  336. finalNotification.setLatestEventInfo( getApplicationContext(),
  337. getString(R.string.uploader_upload_failed_ticker),
  338. String.format(getString(R.string.uploader_upload_failed_content_multiple), mSuccessCounter, mTotalFilesToSend),
  339. finalNotification.contentIntent);
  340. }
  341. mNotificationManager.notify(R.string.uploader_upload_failed_ticker, finalNotification);
  342. }
  343. }
  344. /**
  345. * Callback method to update the progress bar in the status notification.
  346. */
  347. @Override
  348. public void onTransferProgress(long progressRate) {
  349. mSendData += progressRate;
  350. int percent = (int)(100*((double)mSendData)/((double)mTotalDataToSend));
  351. if (percent != mPreviousPercent) {
  352. String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, new File(mUploads.get(mCurrentIndexUpload).getLocalPath()).getName());
  353. mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, false);
  354. mNotification.contentView.setTextViewText(R.id.status_text, text);
  355. mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotification);
  356. }
  357. mPreviousPercent = percent;
  358. }
  359. @Override
  360. public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
  361. // TODO Maybe replace the other transferProgress with this
  362. }
  363. }