d813aaf31c767e7fd229369ee62b424f75c03d79
[pub/Android/ownCloud.git] / src / com / owncloud / android / files / services / FileDownloader.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 * Copyright (C) 2012-2013 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 package com.owncloud.android.files.services;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.AbstractList;
25 import java.util.Iterator;
26 import java.util.Vector;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ConcurrentMap;
29
30 import com.owncloud.android.datamodel.FileDataStorageManager;
31 import com.owncloud.android.datamodel.OCFile;
32 import eu.alefzero.webdav.OnDatatransferProgressListener;
33
34 import com.owncloud.android.network.OwnCloudClientUtils;
35 import com.owncloud.android.operations.DownloadFileOperation;
36 import com.owncloud.android.operations.RemoteOperationResult;
37 import com.owncloud.android.ui.activity.FileDetailActivity;
38 import com.owncloud.android.ui.fragment.FileDetailFragment;
39
40 import android.accounts.Account;
41 import android.accounts.AccountsException;
42 import android.app.Notification;
43 import android.app.NotificationManager;
44 import android.app.PendingIntent;
45 import android.app.Service;
46 import android.content.Intent;
47 import android.os.Binder;
48 import android.os.Handler;
49 import android.os.HandlerThread;
50 import android.os.IBinder;
51 import android.os.Looper;
52 import android.os.Message;
53 import android.os.Process;
54 import android.util.Log;
55 import android.widget.RemoteViews;
56
57 import com.owncloud.android.R;
58 import eu.alefzero.webdav.WebdavClient;
59
60 public class FileDownloader extends Service implements OnDatatransferProgressListener {
61
62 public static final String EXTRA_ACCOUNT = "ACCOUNT";
63 public static final String EXTRA_FILE = "FILE";
64
65 public static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
66 public static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
67 public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";
68 public static final String EXTRA_FILE_PATH = "FILE_PATH";
69 public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
70 public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
71
72 private static final String TAG = "FileDownloader";
73
74 private Looper mServiceLooper;
75 private ServiceHandler mServiceHandler;
76 private IBinder mBinder;
77 private WebdavClient mDownloadClient = null;
78 private Account mLastAccount = null;
79 private FileDataStorageManager mStorageManager;
80
81 private ConcurrentMap<String, DownloadFileOperation> mPendingDownloads = new ConcurrentHashMap<String, DownloadFileOperation>();
82 private DownloadFileOperation mCurrentDownload = null;
83
84 private NotificationManager mNotificationManager;
85 private Notification mNotification;
86 private int mLastPercent;
87
88
89 /**
90 * Builds a key for mPendingDownloads from the account and file to download
91 *
92 * @param account Account where the file to download is stored
93 * @param file File to download
94 */
95 private String buildRemoteName(Account account, OCFile file) {
96 return account.name + file.getRemotePath();
97 }
98
99
100 /**
101 * Service initialization
102 */
103 @Override
104 public void onCreate() {
105 super.onCreate();
106 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
107 HandlerThread thread = new HandlerThread("FileDownloaderThread",
108 Process.THREAD_PRIORITY_BACKGROUND);
109 thread.start();
110 mServiceLooper = thread.getLooper();
111 mServiceHandler = new ServiceHandler(mServiceLooper, this);
112 mBinder = new FileDownloaderBinder();
113 }
114
115
116 /**
117 * Entry point to add one or several files to the queue of downloads.
118 *
119 * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service will keep on working
120 * although the caller activity goes away.
121 */
122 @Override
123 public int onStartCommand(Intent intent, int flags, int startId) {
124 if ( !intent.hasExtra(EXTRA_ACCOUNT) ||
125 !intent.hasExtra(EXTRA_FILE)
126 /*!intent.hasExtra(EXTRA_FILE_PATH) ||
127 !intent.hasExtra(EXTRA_REMOTE_PATH)*/
128 ) {
129 Log.e(TAG, "Not enough information provided in intent");
130 return START_NOT_STICKY;
131 }
132 Account account = intent.getParcelableExtra(EXTRA_ACCOUNT);
133 OCFile file = intent.getParcelableExtra(EXTRA_FILE);
134
135 AbstractList<String> requestedDownloads = new Vector<String>(); // dvelasco: now this always contains just one element, but that can change in a near future (download of multiple selection)
136 String downloadKey = buildRemoteName(account, file);
137 try {
138 DownloadFileOperation newDownload = new DownloadFileOperation(account, file);
139 mPendingDownloads.putIfAbsent(downloadKey, newDownload);
140 newDownload.addDatatransferProgressListener(this);
141 requestedDownloads.add(downloadKey);
142 sendBroadcastNewDownload(newDownload);
143
144 } catch (IllegalArgumentException e) {
145 Log.e(TAG, "Not enough information provided in intent: " + e.getMessage());
146 return START_NOT_STICKY;
147 }
148
149 if (requestedDownloads.size() > 0) {
150 Message msg = mServiceHandler.obtainMessage();
151 msg.arg1 = startId;
152 msg.obj = requestedDownloads;
153 mServiceHandler.sendMessage(msg);
154 }
155
156 return START_NOT_STICKY;
157 }
158
159
160 /**
161 * Provides a binder object that clients can use to perform operations on the queue of downloads, excepting the addition of new files.
162 *
163 * Implemented to perform cancellation, pause and resume of existing downloads.
164 */
165 @Override
166 public IBinder onBind(Intent arg0) {
167 return mBinder;
168 }
169
170
171 /**
172 * Binder to let client components to perform operations on the queue of downloads.
173 *
174 * It provides by itself the available operations.
175 */
176 public class FileDownloaderBinder extends Binder {
177
178 /**
179 * Cancels a pending or current download of a remote file.
180 *
181 * @param account Owncloud account where the remote file is stored.
182 * @param file A file in the queue of pending downloads
183 */
184 public void cancel(Account account, OCFile file) {
185 DownloadFileOperation download = null;
186 synchronized (mPendingDownloads) {
187 download = mPendingDownloads.remove(buildRemoteName(account, file));
188 }
189 if (download != null) {
190 download.cancel();
191 }
192 }
193
194
195 /**
196 * Returns True when the file described by 'file' in the ownCloud account 'account' is downloading or waiting to download.
197 *
198 * If 'file' is a directory, returns 'true' if some of its descendant files is downloading or waiting to download.
199 *
200 * @param account Owncloud account where the remote file is stored.
201 * @param file A file that could be in the queue of downloads.
202 */
203 public boolean isDownloading(Account account, OCFile file) {
204 if (account == null || file == null) return false;
205 String targetKey = buildRemoteName(account, file);
206 synchronized (mPendingDownloads) {
207 if (file.isDirectory()) {
208 // this can be slow if there are many downloads :(
209 Iterator<String> it = mPendingDownloads.keySet().iterator();
210 boolean found = false;
211 while (it.hasNext() && !found) {
212 found = it.next().startsWith(targetKey);
213 }
214 return found;
215 } else {
216 return (mPendingDownloads.containsKey(targetKey));
217 }
218 }
219 }
220 }
221
222
223 /**
224 * Download worker. Performs the pending downloads in the order they were requested.
225 *
226 * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
227 */
228 private static class ServiceHandler extends Handler {
229 // don't make it a final class, and don't remove the static ; lint will warn about a possible memory leak
230 FileDownloader mService;
231 public ServiceHandler(Looper looper, FileDownloader service) {
232 super(looper);
233 if (service == null)
234 throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
235 mService = service;
236 }
237
238 @Override
239 public void handleMessage(Message msg) {
240 @SuppressWarnings("unchecked")
241 AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
242 if (msg.obj != null) {
243 Iterator<String> it = requestedDownloads.iterator();
244 while (it.hasNext()) {
245 mService.downloadFile(it.next());
246 }
247 }
248 mService.stopSelf(msg.arg1);
249 }
250 }
251
252
253
254 /**
255 * Core download method: requests a file to download and stores it.
256 *
257 * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
258 */
259 private void downloadFile(String downloadKey) {
260
261 synchronized(mPendingDownloads) {
262 mCurrentDownload = mPendingDownloads.get(downloadKey);
263 }
264
265 if (mCurrentDownload != null) {
266
267 notifyDownloadStart(mCurrentDownload);
268
269 RemoteOperationResult downloadResult = null;
270 try {
271 /// prepare client object to send the request to the ownCloud server
272 if (mDownloadClient == null || !mLastAccount.equals(mCurrentDownload.getAccount())) {
273 mLastAccount = mCurrentDownload.getAccount();
274 mStorageManager = new FileDataStorageManager(mLastAccount, getContentResolver());
275 mDownloadClient = OwnCloudClientUtils.createOwnCloudClient(mLastAccount, getApplicationContext());
276 }
277
278 /// perform the download
279 if (downloadResult == null) {
280 downloadResult = mCurrentDownload.execute(mDownloadClient);
281 }
282 if (downloadResult.isSuccess()) {
283 saveDownloadedFile();
284 }
285
286 } catch (AccountsException e) {
287 Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
288 downloadResult = new RemoteOperationResult(e);
289 } catch (IOException e) {
290 Log.e(TAG, "Error while trying to get autorization for " + mLastAccount.name, e);
291 downloadResult = new RemoteOperationResult(e);
292
293 } finally {
294 synchronized(mPendingDownloads) {
295 mPendingDownloads.remove(downloadKey);
296 }
297 }
298
299
300 /// notify result
301 notifyDownloadResult(mCurrentDownload, downloadResult);
302
303 sendBroadcastDownloadFinished(mCurrentDownload, downloadResult);
304 }
305 }
306
307
308 /**
309 * Updates the OC File after a successful download.
310 */
311 private void saveDownloadedFile() {
312 OCFile file = mCurrentDownload.getFile();
313 long syncDate = System.currentTimeMillis();
314 file.setLastSyncDateForProperties(syncDate);
315 file.setLastSyncDateForData(syncDate);
316 file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
317 file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
318 // file.setEtag(mCurrentDownload.getEtag()); // TODO Etag, where available
319 file.setMimetype(mCurrentDownload.getMimeType());
320 file.setStoragePath(mCurrentDownload.getSavePath());
321 file.setFileLength((new File(mCurrentDownload.getSavePath()).length()));
322 mStorageManager.saveFile(file);
323 }
324
325
326 /**
327 * Creates a status notification to show the download progress
328 *
329 * @param download Download operation starting.
330 */
331 private void notifyDownloadStart(DownloadFileOperation download) {
332 /// create status notification with a progress bar
333 mLastPercent = 0;
334 mNotification = new Notification(R.drawable.icon, getString(R.string.downloader_download_in_progress_ticker), System.currentTimeMillis());
335 mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
336 mNotification.contentView = new RemoteViews(getApplicationContext().getPackageName(), R.layout.progressbar_layout);
337 mNotification.contentView.setProgressBar(R.id.status_progress, 100, 0, download.getSize() < 0);
338 mNotification.contentView.setTextViewText(R.id.status_text, String.format(getString(R.string.downloader_download_in_progress_content), 0, new File(download.getSavePath()).getName()));
339 mNotification.contentView.setImageViewResource(R.id.status_icon, R.drawable.icon);
340
341 /// includes a pending intent in the notification showing the details view of the file
342 Intent showDetailsIntent = new Intent(this, FileDetailActivity.class);
343 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_FILE, download.getFile());
344 showDetailsIntent.putExtra(FileDetailFragment.EXTRA_ACCOUNT, download.getAccount());
345 showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
346 mNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), (int)System.currentTimeMillis(), showDetailsIntent, 0);
347
348 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
349 }
350
351
352 /**
353 * Callback method to update the progress bar in the status notification.
354 */
355 @Override
356 public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
357 int percent = (int)(100.0*((double)totalTransferredSoFar)/((double)totalToTransfer));
358 if (percent != mLastPercent) {
359 mNotification.contentView.setProgressBar(R.id.status_progress, 100, percent, totalToTransfer < 0);
360 String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
361 mNotification.contentView.setTextViewText(R.id.status_text, text);
362 mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotification);
363 }
364 mLastPercent = percent;
365 }
366
367
368 /**
369 * Callback method to update the progress bar in the status notification (old version)
370 */
371 @Override
372 public void onTransferProgress(long progressRate) {
373 // NOTHING TO DO HERE ANYMORE
374 }
375
376
377 /**
378 * Updates the status notification with the result of a download operation.
379 *
380 * @param downloadResult Result of the download operation.
381 * @param download Finished download operation
382 */
383 private void notifyDownloadResult(DownloadFileOperation download, RemoteOperationResult downloadResult) {
384 mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
385 if (!downloadResult.isCancelled()) {
386 int tickerId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
387 int contentId = (downloadResult.isSuccess()) ? R.string.downloader_download_succeeded_content : R.string.downloader_download_failed_content;
388 Notification finalNotification = new Notification(R.drawable.icon, getString(tickerId), System.currentTimeMillis());
389 finalNotification.flags |= Notification.FLAG_AUTO_CANCEL;
390 // TODO put something smart in the contentIntent below
391 finalNotification.contentIntent = PendingIntent.getActivity(getApplicationContext(), (int)System.currentTimeMillis(), new Intent(), 0);
392 finalNotification.setLatestEventInfo(getApplicationContext(), getString(tickerId), String.format(getString(contentId), new File(download.getSavePath()).getName()), finalNotification.contentIntent);
393 mNotificationManager.notify(tickerId, finalNotification);
394 }
395 }
396
397
398 /**
399 * Sends a broadcast when a download finishes in order to the interested activities can update their view
400 *
401 * @param download Finished download operation
402 * @param downloadResult Result of the download operation
403 */
404 private void sendBroadcastDownloadFinished(DownloadFileOperation download, RemoteOperationResult downloadResult) {
405 Intent end = new Intent(DOWNLOAD_FINISH_MESSAGE);
406 end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
407 end.putExtra(ACCOUNT_NAME, download.getAccount().name);
408 end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
409 end.putExtra(EXTRA_FILE_PATH, download.getSavePath());
410 sendStickyBroadcast(end);
411 }
412
413
414 /**
415 * Sends a broadcast when a new download is added to the queue.
416 *
417 * @param download Added download operation
418 */
419 private void sendBroadcastNewDownload(DownloadFileOperation download) {
420 Intent added = new Intent(DOWNLOAD_ADDED_MESSAGE);
421 /*added.putExtra(ACCOUNT_NAME, download.getAccount().name);
422 added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());*/
423 added.putExtra(EXTRA_FILE_PATH, download.getSavePath());
424 sendStickyBroadcast(added);
425 }
426
427 }