FileUploader.java 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. /**
  2. * ownCloud Android client application
  3. *
  4. * Copyright (C) 2012 Bartek Przybylski
  5. * Copyright (C) 2012-2015 ownCloud Inc.
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. package com.owncloud.android.files.services;
  21. import java.io.File;
  22. import java.io.IOException;
  23. import java.util.AbstractList;
  24. import java.util.HashMap;
  25. import java.util.Iterator;
  26. import java.util.Map;
  27. import java.util.Vector;
  28. import java.util.concurrent.ConcurrentHashMap;
  29. import java.util.concurrent.ConcurrentMap;
  30. import android.accounts.Account;
  31. import android.accounts.AccountManager;
  32. import android.accounts.AccountsException;
  33. import android.accounts.OnAccountsUpdateListener;
  34. import android.app.NotificationManager;
  35. import android.app.PendingIntent;
  36. import android.app.Service;
  37. import android.content.Intent;
  38. import android.os.Binder;
  39. import android.os.Handler;
  40. import android.os.HandlerThread;
  41. import android.os.IBinder;
  42. import android.os.Looper;
  43. import android.os.Message;
  44. import android.os.Process;
  45. import android.support.v4.app.NotificationCompat;
  46. import android.webkit.MimeTypeMap;
  47. import com.owncloud.android.R;
  48. import com.owncloud.android.authentication.AccountUtils;
  49. import com.owncloud.android.authentication.AuthenticatorActivity;
  50. import com.owncloud.android.datamodel.FileDataStorageManager;
  51. import com.owncloud.android.datamodel.OCFile;
  52. import com.owncloud.android.db.DbHandler;
  53. import com.owncloud.android.lib.common.OwnCloudAccount;
  54. import com.owncloud.android.lib.common.OwnCloudClient;
  55. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  56. import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
  57. import com.owncloud.android.lib.common.operations.RemoteOperation;
  58. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  59. import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
  60. import com.owncloud.android.lib.common.utils.Log_OC;
  61. import com.owncloud.android.lib.resources.files.ExistenceCheckRemoteOperation;
  62. import com.owncloud.android.lib.resources.files.FileUtils;
  63. import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation;
  64. import com.owncloud.android.lib.resources.files.RemoteFile;
  65. import com.owncloud.android.lib.resources.status.OwnCloudVersion;
  66. import com.owncloud.android.notifications.NotificationBuilderWithProgressBar;
  67. import com.owncloud.android.notifications.NotificationDelayer;
  68. import com.owncloud.android.operations.CreateFolderOperation;
  69. import com.owncloud.android.operations.UploadFileOperation;
  70. import com.owncloud.android.operations.common.SyncOperation;
  71. import com.owncloud.android.ui.activity.FileActivity;
  72. import com.owncloud.android.ui.activity.FileDisplayActivity;
  73. import com.owncloud.android.utils.ErrorMessageAdapter;
  74. import com.owncloud.android.utils.UriUtils;
  75. public class FileUploader extends Service
  76. implements OnDatatransferProgressListener, OnAccountsUpdateListener {
  77. private static final String UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH";
  78. public static final String EXTRA_UPLOAD_RESULT = "RESULT";
  79. public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
  80. public static final String EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH";
  81. public static final String EXTRA_OLD_FILE_PATH = "OLD_FILE_PATH";
  82. public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
  83. public static final String KEY_FILE = "FILE";
  84. public static final String KEY_LOCAL_FILE = "LOCAL_FILE";
  85. public static final String KEY_REMOTE_FILE = "REMOTE_FILE";
  86. public static final String KEY_MIME_TYPE = "MIME_TYPE";
  87. public static final String KEY_ACCOUNT = "ACCOUNT";
  88. public static final String KEY_UPLOAD_TYPE = "UPLOAD_TYPE";
  89. public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE";
  90. public static final String KEY_INSTANT_UPLOAD = "INSTANT_UPLOAD";
  91. public static final String KEY_LOCAL_BEHAVIOUR = "BEHAVIOUR";
  92. public static final int LOCAL_BEHAVIOUR_COPY = 0;
  93. public static final int LOCAL_BEHAVIOUR_MOVE = 1;
  94. public static final int LOCAL_BEHAVIOUR_FORGET = 2;
  95. public static final int UPLOAD_SINGLE_FILE = 0;
  96. public static final int UPLOAD_MULTIPLE_FILES = 1;
  97. private static final String TAG = FileUploader.class.getSimpleName();
  98. private Looper mServiceLooper;
  99. private ServiceHandler mServiceHandler;
  100. private IBinder mBinder;
  101. private OwnCloudClient mUploadClient = null;
  102. private Account mLastAccount = null;
  103. private FileDataStorageManager mStorageManager;
  104. private ConcurrentMap<String, UploadFileOperation> mPendingUploads =
  105. new ConcurrentHashMap<String, UploadFileOperation>();
  106. private UploadFileOperation mCurrentUpload = null;
  107. private NotificationManager mNotificationManager;
  108. private NotificationCompat.Builder mNotificationBuilder;
  109. private int mLastPercent;
  110. private static final String MIME_TYPE_PDF = "application/pdf";
  111. private static final String FILE_EXTENSION_PDF = ".pdf";
  112. public static String getUploadFinishMessage() {
  113. return FileUploader.class.getName() + UPLOAD_FINISH_MESSAGE;
  114. }
  115. /**
  116. * Builds a key for mPendingUploads from the account and file to upload
  117. *
  118. * @param account Account where the file to upload is stored
  119. * @param file File to upload
  120. */
  121. private String buildRemoteName(Account account, OCFile file) {
  122. return account.name + file.getRemotePath();
  123. }
  124. private String buildRemoteName(Account account, String remotePath) {
  125. return account.name + remotePath;
  126. }
  127. /**
  128. * Checks if an ownCloud server version should support chunked uploads.
  129. *
  130. * @param version OwnCloud version instance corresponding to an ownCloud
  131. * server.
  132. * @return 'True' if the ownCloud server with version supports chunked
  133. * uploads.
  134. */
  135. private static boolean chunkedUploadIsSupported(OwnCloudVersion version) {
  136. return (version != null && version.compareTo(OwnCloudVersion.owncloud_v4_5) >= 0);
  137. }
  138. /**
  139. * Service initialization
  140. */
  141. @Override
  142. public void onCreate() {
  143. super.onCreate();
  144. Log_OC.d(TAG, "Creating service");
  145. mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  146. HandlerThread thread = new HandlerThread("FileUploaderThread",
  147. Process.THREAD_PRIORITY_BACKGROUND);
  148. thread.start();
  149. mServiceLooper = thread.getLooper();
  150. mServiceHandler = new ServiceHandler(mServiceLooper, this);
  151. mBinder = new FileUploaderBinder();
  152. // add AccountsUpdatedListener
  153. AccountManager am = AccountManager.get(getApplicationContext());
  154. am.addOnAccountsUpdatedListener(this, null, false);
  155. }
  156. /**
  157. * Service clean up
  158. */
  159. @Override
  160. public void onDestroy() {
  161. Log_OC.v(TAG, "Destroying service" );
  162. mBinder = null;
  163. mServiceHandler = null;
  164. mServiceLooper.quit();
  165. mServiceLooper = null;
  166. mNotificationManager = null;
  167. // remove AccountsUpdatedListener
  168. AccountManager am = AccountManager.get(getApplicationContext());
  169. am.removeOnAccountsUpdatedListener(this);
  170. super.onDestroy();
  171. }
  172. /**
  173. * Entry point to add one or several files to the queue of uploads.
  174. *
  175. * New uploads are added calling to startService(), resulting in a call to
  176. * this method. This ensures the service will keep on working although the
  177. * caller activity goes away.
  178. */
  179. @Override
  180. public int onStartCommand(Intent intent, int flags, int startId) {
  181. Log_OC.d(TAG, "Starting command with id " + startId);
  182. if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_UPLOAD_TYPE)
  183. || !(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) {
  184. Log_OC.e(TAG, "Not enough information provided in intent");
  185. return Service.START_NOT_STICKY;
  186. }
  187. int uploadType = intent.getIntExtra(KEY_UPLOAD_TYPE, -1);
  188. if (uploadType == -1) {
  189. Log_OC.e(TAG, "Incorrect upload type provided");
  190. return Service.START_NOT_STICKY;
  191. }
  192. Account account = intent.getParcelableExtra(KEY_ACCOUNT);
  193. if (!AccountUtils.exists(account, getApplicationContext())) {
  194. return Service.START_NOT_STICKY;
  195. }
  196. String[] localPaths = null, remotePaths = null, mimeTypes = null;
  197. OCFile[] files = null;
  198. if (uploadType == UPLOAD_SINGLE_FILE) {
  199. if (intent.hasExtra(KEY_FILE)) {
  200. files = new OCFile[] { intent.getParcelableExtra(KEY_FILE) };
  201. } else {
  202. localPaths = new String[] { intent.getStringExtra(KEY_LOCAL_FILE) };
  203. remotePaths = new String[] { intent.getStringExtra(KEY_REMOTE_FILE) };
  204. mimeTypes = new String[] { intent.getStringExtra(KEY_MIME_TYPE) };
  205. }
  206. } else { // mUploadType == UPLOAD_MULTIPLE_FILES
  207. if (intent.hasExtra(KEY_FILE)) {
  208. files = (OCFile[]) intent.getParcelableArrayExtra(KEY_FILE); // TODO
  209. // will
  210. // this
  211. // casting
  212. // work
  213. // fine?
  214. } else {
  215. localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE);
  216. remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE);
  217. mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE);
  218. }
  219. }
  220. FileDataStorageManager storageManager = new FileDataStorageManager(account,
  221. getContentResolver());
  222. boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false);
  223. boolean isInstant = intent.getBooleanExtra(KEY_INSTANT_UPLOAD, false);
  224. int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_COPY);
  225. if (intent.hasExtra(KEY_FILE) && files == null) {
  226. Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent");
  227. return Service.START_NOT_STICKY;
  228. } else if (!intent.hasExtra(KEY_FILE)) {
  229. if (localPaths == null) {
  230. Log_OC.e(TAG, "Incorrect array for local paths provided in upload intent");
  231. return Service.START_NOT_STICKY;
  232. }
  233. if (remotePaths == null) {
  234. Log_OC.e(TAG, "Incorrect array for remote paths provided in upload intent");
  235. return Service.START_NOT_STICKY;
  236. }
  237. if (localPaths.length != remotePaths.length) {
  238. Log_OC.e(TAG, "Different number of remote paths and local paths!");
  239. return Service.START_NOT_STICKY;
  240. }
  241. files = new OCFile[localPaths.length];
  242. for (int i = 0; i < localPaths.length; i++) {
  243. files[i] = obtainNewOCFileToUpload(remotePaths[i], localPaths[i],
  244. ((mimeTypes != null) ? mimeTypes[i] : null), storageManager);
  245. if (files[i] == null) {
  246. // TODO @andomaex add failure Notification
  247. return Service.START_NOT_STICKY;
  248. }
  249. }
  250. }
  251. OwnCloudVersion ocv = AccountUtils.getServerVersion(account);
  252. boolean chunked = FileUploader.chunkedUploadIsSupported(ocv);
  253. AbstractList<String> requestedUploads = new Vector<String>();
  254. String uploadKey = null;
  255. UploadFileOperation newUpload = null;
  256. try {
  257. for (int i = 0; i < files.length; i++) {
  258. uploadKey = buildRemoteName(account, files[i].getRemotePath());
  259. newUpload = new UploadFileOperation(account, files[i], chunked, isInstant,
  260. forceOverwrite, localAction,
  261. getApplicationContext());
  262. if (isInstant) {
  263. newUpload.setRemoteFolderToBeCreated();
  264. }
  265. // Grants that the file only upload once time
  266. mPendingUploads.putIfAbsent(uploadKey, newUpload);
  267. newUpload.addDatatransferProgressListener(this);
  268. newUpload.addDatatransferProgressListener((FileUploaderBinder)mBinder);
  269. requestedUploads.add(uploadKey);
  270. }
  271. } catch (IllegalArgumentException e) {
  272. Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
  273. return START_NOT_STICKY;
  274. } catch (IllegalStateException e) {
  275. Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage());
  276. return START_NOT_STICKY;
  277. } catch (Exception e) {
  278. Log_OC.e(TAG, "Unexpected exception while processing upload intent", e);
  279. return START_NOT_STICKY;
  280. }
  281. if (requestedUploads.size() > 0) {
  282. Message msg = mServiceHandler.obtainMessage();
  283. msg.arg1 = startId;
  284. msg.obj = requestedUploads;
  285. mServiceHandler.sendMessage(msg);
  286. }
  287. Log_OC.i(TAG, "mPendingUploads size:" + mPendingUploads.size());
  288. return Service.START_NOT_STICKY;
  289. }
  290. /**
  291. * Provides a binder object that clients can use to perform operations on
  292. * the queue of uploads, excepting the addition of new files.
  293. *
  294. * Implemented to perform cancellation, pause and resume of existing
  295. * uploads.
  296. */
  297. @Override
  298. public IBinder onBind(Intent arg0) {
  299. return mBinder;
  300. }
  301. /**
  302. * Called when ALL the bound clients were onbound.
  303. */
  304. @Override
  305. public boolean onUnbind(Intent intent) {
  306. ((FileUploaderBinder)mBinder).clearListeners();
  307. return false; // not accepting rebinding (default behaviour)
  308. }
  309. @Override
  310. public void onAccountsUpdated(Account[] accounts) {
  311. // Review current upload, and cancel it if its account doen't exist
  312. if (mCurrentUpload != null &&
  313. !AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) {
  314. mCurrentUpload.cancel();
  315. }
  316. // The rest of uploads are cancelled when they try to start
  317. }
  318. /**
  319. * Binder to let client components to perform operations on the queue of
  320. * uploads.
  321. *
  322. * It provides by itself the available operations.
  323. */
  324. public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener {
  325. /**
  326. * Map of listeners that will be reported about progress of uploads from a
  327. * {@link FileUploaderBinder} instance
  328. */
  329. private Map<String, OnDatatransferProgressListener> mBoundListeners =
  330. new HashMap<String, OnDatatransferProgressListener>();
  331. /**
  332. * Cancels a pending or current upload of a remote file.
  333. *
  334. * @param account Owncloud account where the remote file will be stored.
  335. * @param file A file in the queue of pending uploads
  336. */
  337. public void cancel(Account account, OCFile file) {
  338. UploadFileOperation upload;
  339. synchronized (mPendingUploads) {
  340. upload = mPendingUploads.remove(buildRemoteName(account, file));
  341. }
  342. if (upload != null) {
  343. upload.cancel();
  344. }
  345. }
  346. /**
  347. * Cancels a pending or current upload for an account
  348. *
  349. * @param account Owncloud accountName where the remote file will be stored.
  350. */
  351. public void cancel(Account account) {
  352. Log_OC.d(TAG, "Account= " + account.name);
  353. if (mCurrentUpload != null) {
  354. Log_OC.d(TAG, "Current Upload Account= " + mCurrentUpload.getAccount().name);
  355. if (mCurrentUpload.getAccount().name.equals(account.name)) {
  356. mCurrentUpload.cancel();
  357. }
  358. }
  359. // Cancel pending uploads
  360. cancelUploadForAccount(account.name);
  361. }
  362. public void clearListeners() {
  363. mBoundListeners.clear();
  364. }
  365. /**
  366. * Returns True when the file described by 'file' is being uploaded to
  367. * the ownCloud account 'account' or waiting for it
  368. *
  369. * If 'file' is a directory, returns 'true' if some of its descendant files
  370. * is uploading or waiting to upload.
  371. *
  372. * @param account ownCloud account where the remote file will be stored.
  373. * @param file A file that could be in the queue of pending uploads
  374. */
  375. public boolean isUploading(Account account, OCFile file) {
  376. if (account == null || file == null)
  377. return false;
  378. String targetKey = buildRemoteName(account, file);
  379. synchronized (mPendingUploads) {
  380. if (file.isFolder()) {
  381. // this can be slow if there are many uploads :(
  382. Iterator<String> it = mPendingUploads.keySet().iterator();
  383. boolean found = false;
  384. while (it.hasNext() && !found) {
  385. found = it.next().startsWith(targetKey);
  386. }
  387. return found;
  388. } else {
  389. return (mPendingUploads.containsKey(targetKey));
  390. }
  391. }
  392. }
  393. /**
  394. * Adds a listener interested in the progress of the upload for a concrete file.
  395. *
  396. * @param listener Object to notify about progress of transfer.
  397. * @param account ownCloud account holding the file of interest.
  398. * @param file {@link OCFile} of interest for listener.
  399. */
  400. public void addDatatransferProgressListener (OnDatatransferProgressListener listener,
  401. Account account, OCFile file) {
  402. if (account == null || file == null || listener == null) return;
  403. String targetKey = buildRemoteName(account, file);
  404. mBoundListeners.put(targetKey, listener);
  405. }
  406. /**
  407. * Removes a listener interested in the progress of the upload for a concrete file.
  408. *
  409. * @param listener Object to notify about progress of transfer.
  410. * @param account ownCloud account holding the file of interest.
  411. * @param file {@link OCFile} of interest for listener.
  412. */
  413. public void removeDatatransferProgressListener (OnDatatransferProgressListener listener,
  414. Account account, OCFile file) {
  415. if (account == null || file == null || listener == null) return;
  416. String targetKey = buildRemoteName(account, file);
  417. if (mBoundListeners.get(targetKey) == listener) {
  418. mBoundListeners.remove(targetKey);
  419. }
  420. }
  421. @Override
  422. public void onTransferProgress(long progressRate, long totalTransferredSoFar,
  423. long totalToTransfer, String fileName) {
  424. String key = buildRemoteName(mCurrentUpload.getAccount(), mCurrentUpload.getFile());
  425. OnDatatransferProgressListener boundListener = mBoundListeners.get(key);
  426. if (boundListener != null) {
  427. boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
  428. totalToTransfer, fileName);
  429. }
  430. }
  431. /**
  432. * Review uploads and cancel it if its account doesn't exist
  433. */
  434. public void checkAccountOfCurrentUpload() {
  435. if (mCurrentUpload != null &&
  436. !AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) {
  437. mCurrentUpload.cancel();
  438. }
  439. // The rest of uploads are cancelled when they try to start
  440. }
  441. }
  442. /**
  443. * Upload worker. Performs the pending uploads in the order they were
  444. * requested.
  445. *
  446. * Created with the Looper of a new thread, started in
  447. * {@link FileUploader#onCreate()}.
  448. */
  449. private static class ServiceHandler extends Handler {
  450. // don't make it a final class, and don't remove the static ; lint will
  451. // warn about a possible memory leak
  452. FileUploader mService;
  453. public ServiceHandler(Looper looper, FileUploader service) {
  454. super(looper);
  455. if (service == null)
  456. throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
  457. mService = service;
  458. }
  459. @Override
  460. public void handleMessage(Message msg) {
  461. @SuppressWarnings("unchecked")
  462. AbstractList<String> requestedUploads = (AbstractList<String>) msg.obj;
  463. if (msg.obj != null) {
  464. Iterator<String> it = requestedUploads.iterator();
  465. while (it.hasNext()) {
  466. mService.uploadFile(it.next());
  467. }
  468. }
  469. Log_OC.d(TAG, "Stopping command after id " + msg.arg1);
  470. mService.stopSelf(msg.arg1);
  471. }
  472. }
  473. /**
  474. * Core upload method: sends the file(s) to upload
  475. *
  476. * @param uploadKey Key to access the upload to perform, contained in
  477. * mPendingUploads
  478. */
  479. public void uploadFile(String uploadKey) {
  480. synchronized (mPendingUploads) {
  481. mCurrentUpload = mPendingUploads.get(uploadKey);
  482. }
  483. if (mCurrentUpload != null) {
  484. // Detect if the account exists
  485. if (AccountUtils.exists(mCurrentUpload.getAccount(), getApplicationContext())) {
  486. Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().name + " exists");
  487. notifyUploadStart(mCurrentUpload);
  488. RemoteOperationResult uploadResult = null, grantResult;
  489. try {
  490. /// prepare client object to send requests to the ownCloud server
  491. if (mUploadClient == null ||
  492. !mLastAccount.equals(mCurrentUpload.getAccount())) {
  493. mLastAccount = mCurrentUpload.getAccount();
  494. mStorageManager =
  495. new FileDataStorageManager(mLastAccount, getContentResolver());
  496. OwnCloudAccount ocAccount = new OwnCloudAccount(mLastAccount, this);
  497. mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
  498. getClientFor(ocAccount, this);
  499. }
  500. /// check the existence of the parent folder for the file to upload
  501. String remoteParentPath = new File(mCurrentUpload.getRemotePath()).getParent();
  502. remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ?
  503. remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR;
  504. grantResult = grantFolderExistence(remoteParentPath);
  505. /// perform the upload
  506. if (grantResult.isSuccess()) {
  507. OCFile parent = mStorageManager.getFileByPath(remoteParentPath);
  508. mCurrentUpload.getFile().setParentId(parent.getFileId());
  509. uploadResult = mCurrentUpload.execute(mUploadClient);
  510. if (uploadResult.isSuccess()) {
  511. saveUploadedFile();
  512. }
  513. } else {
  514. uploadResult = grantResult;
  515. }
  516. } catch (AccountsException e) {
  517. Log_OC.e(TAG, "Error while trying to get autorization for " +
  518. mLastAccount.name, e);
  519. uploadResult = new RemoteOperationResult(e);
  520. } catch (IOException e) {
  521. Log_OC.e(TAG, "Error while trying to get autorization for " +
  522. mLastAccount.name, e);
  523. uploadResult = new RemoteOperationResult(e);
  524. } finally {
  525. synchronized (mPendingUploads) {
  526. mPendingUploads.remove(uploadKey);
  527. Log_OC.i(TAG, "Remove CurrentUploadItem from pending upload Item Map.");
  528. }
  529. if (uploadResult != null && uploadResult.isException()) {
  530. // enforce the creation of a new client object for next uploads;
  531. // this grant that a new socket will be created in the future if
  532. // the current exception is due to an abrupt lose of network connection
  533. mUploadClient = null;
  534. }
  535. }
  536. /// notify result
  537. notifyUploadResult(uploadResult, mCurrentUpload);
  538. sendFinalBroadcast(mCurrentUpload, uploadResult);
  539. } else {
  540. // Cancel the transfer
  541. Log_OC.d(TAG, "Account " + mCurrentUpload.getAccount().toString() +
  542. " doesn't exist");
  543. cancelUploadForAccount(mCurrentUpload.getAccount().name);
  544. }
  545. }
  546. }
  547. /**
  548. * Checks the existence of the folder where the current file will be uploaded both
  549. * in the remote server and in the local database.
  550. *
  551. * If the upload is set to enforce the creation of the folder, the method tries to
  552. * create it both remote and locally.
  553. *
  554. * @param pathToGrant Full remote path whose existence will be granted.
  555. * @return An {@link OCFile} instance corresponding to the folder where the file
  556. * will be uploaded.
  557. */
  558. private RemoteOperationResult grantFolderExistence(String pathToGrant) {
  559. RemoteOperation operation = new ExistenceCheckRemoteOperation(pathToGrant, this, false);
  560. RemoteOperationResult result = operation.execute(mUploadClient);
  561. if (!result.isSuccess() && result.getCode() == ResultCode.FILE_NOT_FOUND &&
  562. mCurrentUpload.isRemoteFolderToBeCreated()) {
  563. SyncOperation syncOp = new CreateFolderOperation( pathToGrant, true);
  564. result = syncOp.execute(mUploadClient, mStorageManager);
  565. }
  566. if (result.isSuccess()) {
  567. OCFile parentDir = mStorageManager.getFileByPath(pathToGrant);
  568. if (parentDir == null) {
  569. parentDir = createLocalFolder(pathToGrant);
  570. }
  571. if (parentDir != null) {
  572. result = new RemoteOperationResult(ResultCode.OK);
  573. } else {
  574. result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR);
  575. }
  576. }
  577. return result;
  578. }
  579. private OCFile createLocalFolder(String remotePath) {
  580. String parentPath = new File(remotePath).getParent();
  581. parentPath = parentPath.endsWith(OCFile.PATH_SEPARATOR) ?
  582. parentPath : parentPath + OCFile.PATH_SEPARATOR;
  583. OCFile parent = mStorageManager.getFileByPath(parentPath);
  584. if (parent == null) {
  585. parent = createLocalFolder(parentPath);
  586. }
  587. if (parent != null) {
  588. OCFile createdFolder = new OCFile(remotePath);
  589. createdFolder.setMimetype("DIR");
  590. createdFolder.setParentId(parent.getFileId());
  591. mStorageManager.saveFile(createdFolder);
  592. return createdFolder;
  593. }
  594. return null;
  595. }
  596. /**
  597. * Saves a OC File after a successful upload.
  598. *
  599. * A PROPFIND is necessary to keep the props in the local database
  600. * synchronized with the server, specially the modification time and Etag
  601. * (where available)
  602. *
  603. * TODO refactor this ugly thing
  604. */
  605. private void saveUploadedFile() {
  606. OCFile file = mCurrentUpload.getFile();
  607. if (file.fileExists()) {
  608. file = mStorageManager.getFileById(file.getFileId());
  609. }
  610. long syncDate = System.currentTimeMillis();
  611. file.setLastSyncDateForData(syncDate);
  612. // new PROPFIND to keep data consistent with server
  613. // in theory, should return the same we already have
  614. ReadRemoteFileOperation operation =
  615. new ReadRemoteFileOperation(mCurrentUpload.getRemotePath());
  616. RemoteOperationResult result = operation.execute(mUploadClient);
  617. if (result.isSuccess()) {
  618. updateOCFile(file, (RemoteFile) result.getData().get(0));
  619. file.setLastSyncDateForProperties(syncDate);
  620. }
  621. // / maybe this would be better as part of UploadFileOperation... or
  622. // maybe all this method
  623. if (mCurrentUpload.wasRenamed()) {
  624. OCFile oldFile = mCurrentUpload.getOldFile();
  625. if (oldFile.fileExists()) {
  626. oldFile.setStoragePath(null);
  627. mStorageManager.saveFile(oldFile);
  628. } // else: it was just an automatic renaming due to a name
  629. // coincidence; nothing else is needed, the storagePath is right
  630. // in the instance returned by mCurrentUpload.getFile()
  631. }
  632. file.setNeedsUpdateThumbnail(true);
  633. mStorageManager.saveFile(file);
  634. }
  635. private void updateOCFile(OCFile file, RemoteFile remoteFile) {
  636. file.setCreationTimestamp(remoteFile.getCreationTimestamp());
  637. file.setFileLength(remoteFile.getLength());
  638. file.setMimetype(remoteFile.getMimeType());
  639. file.setModificationTimestamp(remoteFile.getModifiedTimestamp());
  640. file.setModificationTimestampAtLastSyncForData(remoteFile.getModifiedTimestamp());
  641. // file.setEtag(remoteFile.getEtag()); // TODO Etag, where available
  642. file.setRemoteId(remoteFile.getRemoteId());
  643. }
  644. private OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType,
  645. FileDataStorageManager storageManager) {
  646. // MIME type
  647. if (mimeType == null || mimeType.length() <= 0) {
  648. try {
  649. mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
  650. remotePath.substring(remotePath.lastIndexOf('.') + 1));
  651. } catch (IndexOutOfBoundsException e) {
  652. Log_OC.e(TAG, "Trying to find out MIME type of a file without extension: " +
  653. remotePath);
  654. }
  655. }
  656. if (mimeType == null) {
  657. mimeType = "application/octet-stream";
  658. }
  659. if (isPdfFileFromContentProviderWithoutExtension(localPath, mimeType)){
  660. remotePath += FILE_EXTENSION_PDF;
  661. }
  662. OCFile newFile = new OCFile(remotePath);
  663. newFile.setStoragePath(localPath);
  664. newFile.setLastSyncDateForProperties(0);
  665. newFile.setLastSyncDateForData(0);
  666. // size
  667. if (localPath != null && localPath.length() > 0) {
  668. File localFile = new File(localPath);
  669. newFile.setFileLength(localFile.length());
  670. newFile.setLastSyncDateForData(localFile.lastModified());
  671. } // don't worry about not assigning size, the problems with localPath
  672. // are checked when the UploadFileOperation instance is created
  673. newFile.setMimetype(mimeType);
  674. return newFile;
  675. }
  676. /**
  677. * Creates a status notification to show the upload progress
  678. *
  679. * @param upload Upload operation starting.
  680. */
  681. private void notifyUploadStart(UploadFileOperation upload) {
  682. // / create status notification with a progress bar
  683. mLastPercent = 0;
  684. mNotificationBuilder =
  685. NotificationBuilderWithProgressBar.newNotificationBuilderWithProgressBar(this);
  686. mNotificationBuilder
  687. .setOngoing(true)
  688. .setSmallIcon(R.drawable.notification_icon)
  689. .setTicker(getString(R.string.uploader_upload_in_progress_ticker))
  690. .setContentTitle(getString(R.string.uploader_upload_in_progress_ticker))
  691. .setProgress(100, 0, false)
  692. .setContentText(
  693. String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName()));
  694. /// includes a pending intent in the notification showing the details view of the file
  695. Intent showDetailsIntent = new Intent(this, FileDisplayActivity.class);
  696. showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, upload.getFile());
  697. showDetailsIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount());
  698. showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  699. mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
  700. this, (int) System.currentTimeMillis(), showDetailsIntent, 0
  701. ));
  702. mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build());
  703. }
  704. /**
  705. * Callback method to update the progress bar in the status notification
  706. */
  707. @Override
  708. public void onTransferProgress(long progressRate, long totalTransferredSoFar,
  709. long totalToTransfer, String filePath) {
  710. int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer));
  711. if (percent != mLastPercent) {
  712. mNotificationBuilder.setProgress(100, percent, false);
  713. String fileName = filePath.substring(
  714. filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
  715. String text = String.format(getString(R.string.uploader_upload_in_progress_content), percent, fileName);
  716. mNotificationBuilder.setContentText(text);
  717. mNotificationManager.notify(R.string.uploader_upload_in_progress_ticker, mNotificationBuilder.build());
  718. }
  719. mLastPercent = percent;
  720. }
  721. /**
  722. * Updates the status notification with the result of an upload operation.
  723. *
  724. * @param uploadResult Result of the upload operation.
  725. * @param upload Finished upload operation
  726. */
  727. private void notifyUploadResult(
  728. RemoteOperationResult uploadResult, UploadFileOperation upload) {
  729. Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.getCode());
  730. // / cancelled operation or success -> silent removal of progress notification
  731. mNotificationManager.cancel(R.string.uploader_upload_in_progress_ticker);
  732. // Show the result: success or fail notification
  733. if (!uploadResult.isCancelled()) {
  734. int tickerId = (uploadResult.isSuccess()) ? R.string.uploader_upload_succeeded_ticker :
  735. R.string.uploader_upload_failed_ticker;
  736. String content;
  737. // check credentials error
  738. boolean needsToUpdateCredentials = (
  739. uploadResult.getCode() == ResultCode.UNAUTHORIZED ||
  740. uploadResult.isIdPRedirection()
  741. );
  742. tickerId = (needsToUpdateCredentials) ?
  743. R.string.uploader_upload_failed_credentials_error : tickerId;
  744. mNotificationBuilder
  745. .setTicker(getString(tickerId))
  746. .setContentTitle(getString(tickerId))
  747. .setAutoCancel(true)
  748. .setOngoing(false)
  749. .setProgress(0, 0, false);
  750. content = ErrorMessageAdapter.getErrorCauseMessage(
  751. uploadResult, upload, getResources()
  752. );
  753. if (needsToUpdateCredentials) {
  754. // let the user update credentials with one click
  755. Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
  756. updateAccountCredentials.putExtra(
  757. AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount()
  758. );
  759. updateAccountCredentials.putExtra(
  760. AuthenticatorActivity.EXTRA_ACTION,
  761. AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
  762. );
  763. updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  764. updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
  765. updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
  766. mNotificationBuilder.setContentIntent(PendingIntent.getActivity(
  767. this,
  768. (int) System.currentTimeMillis(),
  769. updateAccountCredentials,
  770. PendingIntent.FLAG_ONE_SHOT
  771. ));
  772. mUploadClient = null;
  773. // grant that future retries on the same account will get the fresh credentials
  774. } else {
  775. mNotificationBuilder.setContentText(content);
  776. if (upload.isInstant()) {
  777. DbHandler db = null;
  778. try {
  779. db = new DbHandler(this.getBaseContext());
  780. String message = uploadResult.getLogMessage() + " errorCode: " +
  781. uploadResult.getCode();
  782. Log_OC.e(TAG, message + " Http-Code: " + uploadResult.getHttpCode());
  783. if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) {
  784. //message = getString(R.string.failed_upload_quota_exceeded_text);
  785. if (db.updateFileState(
  786. upload.getOriginalStoragePath(),
  787. DbHandler.UPLOAD_STATUS_UPLOAD_FAILED,
  788. message) == 0) {
  789. db.putFileForLater(
  790. upload.getOriginalStoragePath(),
  791. upload.getAccount().name,
  792. message
  793. );
  794. }
  795. }
  796. } finally {
  797. if (db != null) {
  798. db.close();
  799. }
  800. }
  801. }
  802. }
  803. mNotificationBuilder.setContentText(content);
  804. mNotificationManager.notify(tickerId, mNotificationBuilder.build());
  805. if (uploadResult.isSuccess()) {
  806. DbHandler db = new DbHandler(this.getBaseContext());
  807. db.removeIUPendingFile(mCurrentUpload.getOriginalStoragePath());
  808. db.close();
  809. // remove success notification, with a delay of 2 seconds
  810. NotificationDelayer.cancelWithDelay(
  811. mNotificationManager,
  812. R.string.uploader_upload_succeeded_ticker,
  813. 2000);
  814. }
  815. }
  816. }
  817. /**
  818. * Sends a broadcast in order to the interested activities can update their
  819. * view
  820. *
  821. * @param upload Finished upload operation
  822. * @param uploadResult Result of the upload operation
  823. */
  824. private void sendFinalBroadcast(UploadFileOperation upload, RemoteOperationResult uploadResult) {
  825. Intent end = new Intent(getUploadFinishMessage());
  826. end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote
  827. // path, after
  828. // possible
  829. // automatic
  830. // renaming
  831. if (upload.wasRenamed()) {
  832. end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath());
  833. }
  834. end.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath());
  835. end.putExtra(ACCOUNT_NAME, upload.getAccount().name);
  836. end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult.isSuccess());
  837. sendStickyBroadcast(end);
  838. }
  839. /**
  840. * Checks if content provider, using the content:// scheme, returns a file with mime-type
  841. * 'application/pdf' but file has not extension
  842. * @param localPath Full path to a file in the local file system.
  843. * @param mimeType MIME type of the file.
  844. * @return true if is needed to add the pdf file extension to the file
  845. */
  846. private boolean isPdfFileFromContentProviderWithoutExtension(String localPath,
  847. String mimeType) {
  848. return localPath.startsWith(UriUtils.URI_CONTENT_SCHEME) &&
  849. mimeType.equals(MIME_TYPE_PDF) &&
  850. !localPath.endsWith(FILE_EXTENSION_PDF);
  851. }
  852. /**
  853. * Remove uploads of an account
  854. * @param accountName Name of an OC account
  855. */
  856. private void cancelUploadForAccount(String accountName){
  857. // this can be slow if there are many uploads :(
  858. Iterator<String> it = mPendingUploads.keySet().iterator();
  859. Log_OC.d(TAG, "Number of pending updloads= " + mPendingUploads.size());
  860. while (it.hasNext()) {
  861. String key = it.next();
  862. Log_OC.d(TAG, "mPendingUploads CANCELLED " + key);
  863. if (key.startsWith(accountName)) {
  864. synchronized (mPendingUploads) {
  865. mPendingUploads.remove(key);
  866. }
  867. }
  868. }
  869. }
  870. }