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