Watch for replacements of favorite file besides than direct notifications
[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 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.os.FileObserver;
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.OCFile;
39 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
40 import com.owncloud.android.files.OwnCloudFolderObserver;
41 import com.owncloud.android.operations.SynchronizeFileOperation;
42 import com.owncloud.android.utils.FileStorageUtils;
43 import com.owncloud.android.utils.Log_OC;
44
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_START_OBSERVE = MY_NAME + ".action.START_OBSERVATION";
65 public final static String ACTION_ADD_OBSERVED_FILE = MY_NAME + ".action.ADD_OBSERVED_FILE";
66 public final static String ACTION_DEL_OBSERVED_FILE = MY_NAME + ".action.DEL_OBSERVED_FILE";
67
68 private final static String ARG_FILE = "ARG_FILE";
69 private final static String ARG_ACCOUNT = "ARG_ACCOUNT";
70
71 private static String TAG = FileObserverService.class.getSimpleName();
72
73 private Map<String, OwnCloudFolderObserver> mFolderObserversMap;
74 private DownloadCompletedReceiver mDownloadReceiver;
75
76 /**
77 * Factory method to create intents that allow to start an
78 * 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
98 * stop watching.
99 * @return Intent to start or stop the observance of a file through a call
100 * to {@link Context#startService(Intent)}.
101 */
102 public static Intent makeObservedFileIntent(
103 Context context, OCFile file, Account account, boolean watchIt) {
104 Intent intent = new Intent(context, FileObserverService.class);
105 intent.setAction(watchIt ? FileObserverService.ACTION_ADD_OBSERVED_FILE
106 : FileObserverService.ACTION_DEL_OBSERVED_FILE);
107 intent.putExtra(FileObserverService.ARG_FILE, file);
108 intent.putExtra(FileObserverService.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 mFolderObserversMap = new HashMap<String, OwnCloudFolderObserver>();
124 }
125
126 @Override
127 public void onDestroy() {
128 Log_OC.d(TAG, "onDestroy - finishing observation of favourite files");
129
130 unregisterReceiver(mDownloadReceiver);
131
132 Iterator<OwnCloudFolderObserver> itOCFolder = mFolderObserversMap.values().iterator();
133 while (itOCFolder.hasNext()) {
134 itOCFolder.next().stopWatching();
135 }
136 mFolderObserversMap.clear();
137 mFolderObserversMap = null;
138
139 super.onDestroy();
140 }
141
142 @Override
143 public IBinder onBind(Intent intent) {
144 // this service cannot be bound
145 return null;
146 }
147
148 @Override
149 public int onStartCommand(Intent intent, int flags, int startId) {
150 Log_OC.d(TAG, "Starting command " + intent);
151
152 if (intent == null || ACTION_START_OBSERVE.equals(intent.getAction())) {
153 // NULL occurs when system tries to restart the service after its
154 // process was killed
155 startObservation();
156 return Service.START_STICKY;
157
158 } else if (ACTION_ADD_OBSERVED_FILE.equals(intent.getAction())) {
159 OCFile file = (OCFile) intent.getParcelableExtra(ARG_FILE);
160 Account account = (Account) intent.getParcelableExtra(ARG_ACCOUNT);
161 addObservedFile(file, account);
162
163 } else if (ACTION_DEL_OBSERVED_FILE.equals(intent.getAction())) {
164 removeObservedFile((OCFile) intent.getParcelableExtra(ARG_FILE),
165 (Account) intent.getParcelableExtra(ARG_ACCOUNT));
166
167 } else {
168 Log_OC.e(TAG, "Unknown action recieved; ignoring it: " + intent.getAction());
169 }
170
171 return Service.START_STICKY;
172 }
173
174
175 /**
176 * Read from the local database the list of files that must to be kept
177 * synchronized and starts observers to monitor local changes on them
178 */
179 private void startObservation() {
180 Log_OC.d(TAG, "Loading all kept-in-sync files from database to start watching them");
181
182 // query for any favorite file in any OC account
183 Cursor cursorOnKeptInSync = getContentResolver().query(
184 ProviderTableMeta.CONTENT_URI,
185 null,
186 ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?",
187 new String[] { String.valueOf(1) },
188 null
189 );
190
191 if (cursorOnKeptInSync != null) {
192
193 if (cursorOnKeptInSync.moveToFirst()) {
194
195 String localPath = "";
196 // String remotePath = "";
197 String accountName = "";
198 Account account = null;
199 do {
200 localPath = cursorOnKeptInSync.getString(cursorOnKeptInSync
201 .getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH));
202 accountName = cursorOnKeptInSync.getString(cursorOnKeptInSync
203 .getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER));
204 /*
205 * remotePath = cursorOnKeptInSync.getString(
206 * cursorOnKeptInSync
207 * .getColumnIndex(ProviderTableMeta.FILE_PATH) );
208 */
209
210 account = new Account(accountName, MainApp.getAccountType());
211 if (!AccountUtils.exists(account, this) || localPath == null || localPath.length() <= 0) {
212 continue;
213 }
214
215 addObservedFile(localPath, account);
216
217 } while (cursorOnKeptInSync.moveToNext());
218
219 }
220 cursorOnKeptInSync.close();
221 }
222
223 // service does not stopSelf() ; that way it tries to be alive forever
224
225 }
226
227
228 /**
229 * Registers the local copy of a remote file to be observed for local
230 * changes, an automatically updated in the ownCloud server.
231 *
232 * This method does NOT perform a {@link SynchronizeFileOperation} over the
233 * file.
234 *
235 * @param file Object representing a remote file which local copy must be
236 * observed.
237 * @param account OwnCloud account containing file.
238 */
239 private void addObservedFile(OCFile file, Account account) {
240 Log_OC.v(TAG, "Adding a file to be watched");
241
242 if (file == null) {
243 Log_OC.e(TAG, "Trying to add a NULL file to observer");
244 return;
245 }
246 if (account == null) {
247 Log_OC.e(TAG, "Trying to add a file with a NULL account to observer");
248 return;
249 }
250
251 String localPath = file.getStoragePath();
252 if (localPath == null || localPath.length() <= 0) {
253 // file downloading or to be downloaded for the first time
254 localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file);
255 }
256
257 addObservedFile(localPath, account);
258
259 }
260
261
262
263
264 /**
265 * Registers the folder to be observed in which there are changes inside any
266 * file
267 *
268 * @param account OwnCloud account containing file.
269 */
270 private void addObservedFile(String localPath, Account account) {
271 File file = new File(localPath);
272 String parentPath = file.getParent();
273 OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath);
274 if (observer == null) {
275 observer = new OwnCloudFolderObserver(parentPath, account, getApplicationContext());
276 mFolderObserversMap.put(parentPath, observer);
277 Log_OC.d(TAG, "Observer added for parent folder " + parentPath + "/");
278 }
279
280 observer.startWatching(file.getName());
281 Log_OC.d(TAG, "Added " + localPath + " to list of observed children");
282 }
283
284
285 /**
286 * Unregisters the local copy of a remote file to be observed for local
287 * changes.
288 *
289 * Starts to watch it, if the file has a local copy to watch.
290 *
291 * @param file Object representing a remote file which local copy must be
292 * not observed longer.
293 * @param account OwnCloud account containing file.
294 */
295 private void removeObservedFile(OCFile file, Account account) {
296 Log_OC.v(TAG, "Removing a file from being watched");
297
298 if (file == null) {
299 Log_OC.e(TAG, "Trying to remove a NULL file");
300 return;
301 }
302 if (account == null) {
303 Log_OC.e(TAG, "Trying to add a file with a NULL account to observer");
304 return;
305 }
306
307 String localPath = file.getStoragePath();
308 if (localPath == null || localPath.length() <= 0) {
309 localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file);
310 }
311
312 removeObservedFile(localPath);
313 }
314
315
316 private void removeObservedFile(String localPath) {
317 File file = new File(localPath);
318 String parentPath = file.getParent();
319 OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath);
320 if (observer != null) {
321 observer.stopWatching(file.getName());
322 if (observer.isEmpty()) {
323 mFolderObserversMap.remove(parentPath);
324 Log_OC.d(TAG, "Observer removed for parent folder " + parentPath + "/");
325 }
326
327 } else {
328 Log_OC.d(TAG, "No observer to remove for path " + localPath);
329 }
330 }
331
332
333 /**
334 * Private receiver listening to events broadcasted by the FileDownloader service.
335 *
336 * Pauses and resumes the observance on registered files while being download,
337 * in order to avoid to unnecessary synchronizations.
338 */
339 private class DownloadCompletedReceiver extends BroadcastReceiver {
340
341 @Override
342 public void onReceive(Context context, Intent intent) {
343 Log_OC.d(TAG, "Received broadcast intent " + intent);
344
345 File downloadedFile = new File(intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH));
346 String parentPath = downloadedFile.getParent();
347 OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath);
348 if (observer != null) {
349 if (intent.getAction().equals(FileDownloader.getDownloadFinishMessage())
350 && downloadedFile.exists()) {
351 // no matter if the download was be successful or not; the
352 // file could be down anyway due to a former download or upload
353 observer.startWatching(downloadedFile.getName());
354 Log_OC.d(TAG, "Resuming observance of " + downloadedFile.getAbsolutePath());
355
356 } else if (intent.getAction().equals(FileDownloader.getDownloadAddedMessage())) {
357 observer.stopWatching(downloadedFile.getName());
358 Log_OC.d(TAG, "Pausing observance of " + downloadedFile.getAbsolutePath());
359 }
360
361 } else {
362 Log_OC.d(TAG, "No observer for path " + downloadedFile.getAbsolutePath());
363 }
364 }
365
366 }
367
368 }