- update to obey non-image files
[pub/Android/ownCloud.git] / src / com / owncloud / android / services / observer / FileObserverService.java
1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 * Copyright (C) 2012-2014 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 version 2,
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.services.observer;
20
21 import java.io.File;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.Map;
25
26 import android.accounts.Account;
27 import android.app.Service;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.database.Cursor;
33 import android.media.MediaScannerConnection;
34 import android.os.IBinder;
35
36 import com.owncloud.android.MainApp;
37 import com.owncloud.android.authentication.AccountUtils;
38 import com.owncloud.android.datamodel.FileDataStorageManager;
39 import com.owncloud.android.datamodel.OCFile;
40 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
41 import com.owncloud.android.files.services.FileDownloader;
42 import com.owncloud.android.lib.common.utils.Log_OC;
43 import com.owncloud.android.operations.SynchronizeFileOperation;
44 import com.owncloud.android.utils.FileStorageUtils;
45
46
47 /**
48 * Service keeping a list of {@link FolderObserver} instances that watch for local
49 * changes in favorite files (formerly known as kept-in-sync files) and try to
50 * synchronize them with the OC server as soon as possible.
51 *
52 * Tries to be alive as long as possible; that is the reason why stopSelf() is
53 * never called.
54 *
55 * It is expected that the system eventually kills the service when runs low of
56 * memory. To minimize the impact of this, the service always returns
57 * Service.START_STICKY, and the later restart of the service is explicitly
58 * considered in {@link FileObserverService#onStartCommand(Intent, int, int)}.
59 *
60 * @author David A. Velasco
61 */
62 public class FileObserverService extends Service {
63
64 public final static String MY_NAME = FileObserverService.class.getCanonicalName();
65 public final static String ACTION_START_OBSERVE = MY_NAME + ".action.START_OBSERVATION";
66 public final static String ACTION_ADD_OBSERVED_FILE = MY_NAME + ".action.ADD_OBSERVED_FILE";
67 public final static String ACTION_DEL_OBSERVED_FILE = MY_NAME + ".action.DEL_OBSERVED_FILE";
68
69 private final static String ARG_FILE = "ARG_FILE";
70 private final static String ARG_ACCOUNT = "ARG_ACCOUNT";
71
72 private static String TAG = FileObserverService.class.getSimpleName();
73
74 private Map<String, FolderObserver> mFolderObserversMap;
75 private DownloadCompletedReceiver mDownloadReceiver;
76
77 /**
78 * Factory method to create intents that allow to start an ACTION_START_OBSERVE command.
79 *
80 * @param context Android context of the caller component.
81 * @return Intent that starts a command ACTION_START_OBSERVE when
82 * {@link Context#startService(Intent)} is called.
83 */
84 public static Intent makeInitIntent(Context context) {
85 Intent i = new Intent(context, FileObserverService.class);
86 i.setAction(ACTION_START_OBSERVE);
87 return i;
88 }
89
90 /**
91 * Factory method to create intents that allow to start or stop the
92 * observance of a file.
93 *
94 * @param context Android context of the caller component.
95 * @param file OCFile to start or stop to watch.
96 * @param account OC account containing file.
97 * @param watchIt 'True' creates an intent to watch, 'false' an intent to stop watching.
98 * @return Intent to start or stop the observance of a file through a call
99 * to {@link Context#startService(Intent)}.
100 */
101 public static Intent makeObservedFileIntent(
102 Context context, OCFile file, Account account, boolean watchIt) {
103 Intent intent = new Intent(context, FileObserverService.class);
104 intent.setAction(watchIt ? FileObserverService.ACTION_ADD_OBSERVED_FILE
105 : FileObserverService.ACTION_DEL_OBSERVED_FILE);
106 intent.putExtra(FileObserverService.ARG_FILE, file);
107 intent.putExtra(FileObserverService.ARG_ACCOUNT, account);
108 return intent;
109 }
110
111 /**
112 * Initialize the service.
113 */
114 @Override
115 public void onCreate() {
116 Log_OC.d(TAG, "onCreate");
117 super.onCreate();
118
119 mDownloadReceiver = new DownloadCompletedReceiver();
120 IntentFilter filter = new IntentFilter();
121 filter.addAction(FileDownloader.getDownloadAddedMessage());
122 filter.addAction(FileDownloader.getDownloadFinishMessage());
123 registerReceiver(mDownloadReceiver, filter);
124
125 mFolderObserversMap = new HashMap<String, FolderObserver>();
126 }
127
128 /**
129 * Release resources.
130 */
131 @Override
132 public void onDestroy() {
133 Log_OC.d(TAG, "onDestroy - finishing observation of favorite files");
134
135 unregisterReceiver(mDownloadReceiver);
136
137 Iterator<FolderObserver> itOCFolder = mFolderObserversMap.values().iterator();
138 while (itOCFolder.hasNext()) {
139 itOCFolder.next().stopWatching();
140 }
141 mFolderObserversMap.clear();
142 mFolderObserversMap = null;
143
144 super.onDestroy();
145 }
146
147 /**
148 * This service cannot be bound.
149 */
150 @Override
151 public IBinder onBind(Intent intent) {
152 return null;
153 }
154
155 /**
156 * Handles requests to:
157 * - (re)start watching (ACTION_START_OBSERVE)
158 * - add an {@link OCFile} to be watched (ATION_ADD_OBSERVED_FILE)
159 * - stop observing an {@link OCFile} (ACTION_DEL_OBSERVED_FILE)
160 */
161 @Override
162 public int onStartCommand(Intent intent, int flags, int startId) {
163 Log_OC.d(TAG, "Starting command " + intent);
164
165 if (intent == null || ACTION_START_OBSERVE.equals(intent.getAction())) {
166 // NULL occurs when system tries to restart the service after its
167 // process was killed
168 startObservation();
169 return Service.START_STICKY;
170
171 } else if (ACTION_ADD_OBSERVED_FILE.equals(intent.getAction())) {
172 OCFile file = (OCFile) intent.getParcelableExtra(ARG_FILE);
173 Account account = (Account) intent.getParcelableExtra(ARG_ACCOUNT);
174 addObservedFile(file, account);
175
176 } else if (ACTION_DEL_OBSERVED_FILE.equals(intent.getAction())) {
177 removeObservedFile((OCFile) intent.getParcelableExtra(ARG_FILE),
178 (Account) intent.getParcelableExtra(ARG_ACCOUNT));
179
180 } else {
181 Log_OC.e(TAG, "Unknown action recieved; ignoring it: " + intent.getAction());
182 }
183
184 return Service.START_STICKY;
185 }
186
187
188 /**
189 * Read from the local database the list of files that must to be kept
190 * synchronized and starts observers to monitor local changes on them.
191 *
192 * Updates the list of currently observed files if called multiple times.
193 */
194 private void startObservation() {
195 Log_OC.d(TAG, "Loading all kept-in-sync files from database to start watching them");
196
197 // query for any favorite file in any OC account
198 Cursor cursorOnKeptInSync = getContentResolver().query(
199 ProviderTableMeta.CONTENT_URI,
200 null,
201 ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?",
202 new String[] { String.valueOf(1) },
203 null
204 );
205
206 if (cursorOnKeptInSync != null) {
207
208 if (cursorOnKeptInSync.moveToFirst()) {
209
210 String localPath = "";
211 String accountName = "";
212 Account account = null;
213 do {
214 localPath = cursorOnKeptInSync.getString(cursorOnKeptInSync
215 .getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH));
216 accountName = cursorOnKeptInSync.getString(cursorOnKeptInSync
217 .getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER));
218
219 account = new Account(accountName, MainApp.getAccountType());
220 if (!AccountUtils.exists(account, this) || localPath == null || localPath.length() <= 0) {
221 continue;
222 }
223
224 addObservedFile(localPath, account);
225
226 } while (cursorOnKeptInSync.moveToNext());
227
228 }
229 cursorOnKeptInSync.close();
230 }
231
232 // service does not stopSelf() ; that way it tries to be alive forever
233
234 }
235
236
237 /**
238 * Registers the local copy of a remote file to be observed for local
239 * changes, an automatically updated in the ownCloud server.
240 *
241 * This method does NOT perform a {@link SynchronizeFileOperation} over the
242 * file.
243 *
244 * @param file Object representing a remote file which local copy must be observed.
245 * @param account OwnCloud account containing file.
246 */
247 private void addObservedFile(OCFile file, Account account) {
248 Log_OC.v(TAG, "Adding a file to be watched");
249
250 if (file == null) {
251 Log_OC.e(TAG, "Trying to add a NULL file to observer");
252 return;
253 }
254 if (account == null) {
255 Log_OC.e(TAG, "Trying to add a file with a NULL account to observer");
256 return;
257 }
258
259 String localPath = file.getStoragePath();
260 if (localPath == null || localPath.length() <= 0) {
261 // file downloading or to be downloaded for the first time
262 localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file);
263 }
264
265 addObservedFile(localPath, account);
266
267 }
268
269
270
271
272 /**
273 * Registers a local file to be observed for changes.
274 *
275 * @param localPath Absolute path in the local file system to the file to be observed.
276 * @param account OwnCloud account associated to the local file.
277 */
278 private void addObservedFile(String localPath, Account account) {
279 File file = new File(localPath);
280 String parentPath = file.getParent();
281 FolderObserver observer = mFolderObserversMap.get(parentPath);
282 if (observer == null) {
283 observer = new FolderObserver(parentPath, account, getApplicationContext());
284 mFolderObserversMap.put(parentPath, observer);
285 Log_OC.d(TAG, "Observer added for parent folder " + parentPath + "/");
286 }
287
288 observer.startWatching(file.getName());
289 Log_OC.d(TAG, "Added " + localPath + " to list of observed children");
290 }
291
292
293 /**
294 * Unregisters the local copy of a remote file to be observed for local changes.
295 *
296 * @param file Object representing a remote file which local copy must be not
297 * observed longer.
298 * @param account OwnCloud account containing file.
299 */
300 private void removeObservedFile(OCFile file, Account account) {
301 Log_OC.v(TAG, "Removing a file from being watched");
302
303 if (file == null) {
304 Log_OC.e(TAG, "Trying to remove a NULL file");
305 return;
306 }
307 if (account == null) {
308 Log_OC.e(TAG, "Trying to add a file with a NULL account to observer");
309 return;
310 }
311
312 String localPath = file.getStoragePath();
313 if (localPath == null || localPath.length() <= 0) {
314 localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file);
315 }
316
317 removeObservedFile(localPath);
318 }
319
320
321 /**
322 * Unregisters a local file from being observed for changes.
323 *
324 * @param localPath Absolute path in the local file system to the target file.
325 */
326 private void removeObservedFile(String localPath) {
327 File file = new File(localPath);
328 String parentPath = file.getParent();
329 FolderObserver observer = mFolderObserversMap.get(parentPath);
330 if (observer != null) {
331 observer.stopWatching(file.getName());
332 if (observer.isEmpty()) {
333 mFolderObserversMap.remove(parentPath);
334 Log_OC.d(TAG, "Observer removed for parent folder " + parentPath + "/");
335 }
336
337 } else {
338 Log_OC.d(TAG, "No observer to remove for path " + localPath);
339 }
340 }
341
342
343 /**
344 * Private receiver listening to events broadcasted by the {@link FileDownloader} service.
345 *
346 * Pauses and resumes the observance on registered files while being download,
347 * in order to avoid to unnecessary synchronizations.
348 */
349 private class DownloadCompletedReceiver extends BroadcastReceiver {
350
351 @Override
352 public void onReceive(Context context, Intent intent) {
353 Log_OC.d(TAG, "Received broadcast intent " + intent);
354
355 File downloadedFile = new File(intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH));
356 String parentPath = downloadedFile.getParent();
357 FolderObserver observer = mFolderObserversMap.get(parentPath);
358 if (observer != null) {
359 if (intent.getAction().equals(FileDownloader.getDownloadFinishMessage())
360 && downloadedFile.exists()) {
361 // no matter if the download was successful or not; the
362 // file could be down anyway due to a former download or upload
363 observer.startWatching(downloadedFile.getName());
364 Log_OC.d(TAG, "Resuming observance of " + downloadedFile.getAbsolutePath());
365
366 } else if (intent.getAction().equals(FileDownloader.getDownloadAddedMessage())) {
367 observer.stopWatching(downloadedFile.getName());
368 Log_OC.d(TAG, "Pausing observance of " + downloadedFile.getAbsolutePath());
369 }
370
371 } else {
372
373 if (downloadedFile.exists()){
374 Log_OC.d("mediaScan", "mediaScan : " + downloadedFile.getAbsolutePath());
375 MediaScannerConnection.scanFile(getApplicationContext(),
376 new String[]{downloadedFile.getAbsolutePath()}, null, null);
377 }
378
379 Log_OC.d(TAG, "No observer for path " + downloadedFile.getAbsolutePath());
380 }
381 }
382
383 }
384
385 }