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