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