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