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