From: David A. Velasco Date: Tue, 8 Jul 2014 10:51:01 +0000 (+0200) Subject: Fixed last minute bugs, and clean up X-Git-Tag: oc-android-1.7.0_signed~256^2~1 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/c880929ffd512c13fc7c7e7a085504b99ab5c09d?ds=inline Fixed last minute bugs, and clean up --- diff --git a/AndroidManifest.xml b/AndroidManifest.xml index acf695b4..8311d528 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -187,7 +187,7 @@ - + . - * - */ - -package com.owncloud.android.files; - -import java.io.File; - -import android.accounts.Account; -import android.content.Context; -import android.content.Intent; -import android.os.FileObserver; -import android.os.Handler; - -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.utils.Log_OC; - -public class OwnCloudFileObserver extends FileObserver { - - //private static int MASK = (FileObserver.MODIFY | FileObserver.CLOSE_WRITE); - public static int IN_IGNORE = 32768; - public static int ALL_EVENTS_AND_MORE = 0xffffffff; - - private static String TAG = OwnCloudFileObserver.class.getSimpleName(); - - private String mPath; - //private int mMask; - private Account mOCAccount; - private Context mContext; - private boolean mModified; - private long mFileLastModified; - //private boolean mRestartWatching; - //private Handler mHandler; - - public OwnCloudFileObserver(String path, Account account, Context context, Handler handler) { - super(path, ALL_EVENTS); - if (path == null) - throw new IllegalArgumentException("NULL path argument received"); - if (account == null) - throw new IllegalArgumentException("NULL account argument received"); - if (context == null) - throw new IllegalArgumentException("NULL context argument received"); - mPath = path; - mOCAccount = account; - mContext = context; - mModified = false; - mFileLastModified = new File(path).lastModified(); - //mHandler = handler; - Log_OC.d(TAG, "Create Observer - FileLastModified: " + mFileLastModified); - } - - @Override - public void onEvent(int event, String path) { - Log_OC.v(TAG, "Got event " + event + " on FILE " + mPath + " about " - + ((path != null) ? path : "")); - - /* - if ((event & MASK) == 0) { - Log_OC.wtf(TAG, "Incorrect event " + event + " sent for file " + mPath - + ((path != null) ? File.separator + path : "") + " with registered for " + mMask - + " and original path " + mPath); - - // in case need start watching again - if ((event & IN_IGNORE) != 0 && mRestartWatching) { - mRestartWatching = false; - - mHandler.postDelayed(new Runnable() { - public void run() { - startWatching(); - } - }, 5000); - - } - } else { - */ - if ((event & FileObserver.MODIFY) != 0) { - // file changed - mModified = true; - } - // not sure if it's possible, but let's assume that both kind of - // events can be received at the same time - if ((event & FileObserver.CLOSE_WRITE) != 0) { - // file closed - if (mModified) { - mModified = false; - //mRestartWatching = false; - startSyncOperation(); - - } else if (isFileUpdated()) { - // if file has been modified but Modify event type has not - // been launched - //mRestartWatching = true; - mFileLastModified = new File(mPath).lastModified(); - Log_OC.d(TAG, "CLOSE_WRITE - New FileLastModified: " + mFileLastModified); - startSyncOperation(); - } - } - //} - } - - private void startSyncOperation() { - FileDataStorageManager storageManager = new FileDataStorageManager(mOCAccount, mContext.getContentResolver()); - // a fresh object is needed; many things could have occurred to the file - // since it was registered to observe again, assuming that local files - // are linked to a remote file AT MOST, SOMETHING TO BE DONE; - OCFile file = storageManager.getFileByLocalPath(mPath); - SynchronizeFileOperation sfo = new SynchronizeFileOperation(file, null, mOCAccount, true, mContext); - RemoteOperationResult result = sfo.execute(storageManager, mContext); - if (result.getCode() == ResultCode.SYNC_CONFLICT) { - // ISSUE 5: if the user is not running the app (this is a service!), - // this can be very intrusive; a notification should be preferred - Intent i = new Intent(mContext, ConflictsResolveActivity.class); - i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - i.putExtra(ConflictsResolveActivity.EXTRA_FILE, file); - i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, mOCAccount); - mContext.startActivity(i); - } - // TODO save other errors in some point where the user can inspect them - // later; - // or maybe just toast them; - // or nothing, very strange fails - } - - /** - * Check if the timestamp of last file modification in local is more current - * that the timestamp when setting observer to the file - * - * @return boolean: True if file is updated, False if not - */ - private boolean isFileUpdated() { - Log_OC.d(TAG, "FileLastModified: " + mFileLastModified); - return (new File(mPath).lastModified() > mFileLastModified); - } -} diff --git a/src/com/owncloud/android/files/OwnCloudFolderObserver.java b/src/com/owncloud/android/files/OwnCloudFolderObserver.java deleted file mode 100644 index d958e2f5..00000000 --- a/src/com/owncloud/android/files/OwnCloudFolderObserver.java +++ /dev/null @@ -1,177 +0,0 @@ -/* ownCloud Android client application - * Copyright (C) 2012-2014 ownCloud Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.owncloud.android.files; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -import android.accounts.Account; -import android.content.Context; -import android.content.Intent; -import android.os.FileObserver; - -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.utils.Log_OC; - -/** - * Observer watching a folder to request the synchronization of kept-in-sync files - * inside it. - * - * Takes into account two possible update cases: - * - an editor directly updates the file; - * - an editor works on a temporal file, and later replaces the kept-in-sync file with the - * temporal. - * - * The second case requires to monitor the folder parent of the files, since a direct - * {@link FileObserver} on it will not receive more events after the file is deleted to - * be replaced later. - * - * @author David A. Velasco - */ -public class OwnCloudFolderObserver extends FileObserver { - - private static int UPDATE_MASK = ( - FileObserver.ATTRIB | FileObserver.MODIFY | - FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE - ); - /* - private static int ALL_EVENTS_EVEN_THOSE_NOT_DOCUMENTED = 0x7fffffff; // NEVER use 0xffffffff - */ - - private static String TAG = OwnCloudFolderObserver.class.getSimpleName(); - - private String mPath; - private Account mAccount; - private Context mContext; - private Map mObservedChildren; - - public OwnCloudFolderObserver(String path, Account account, Context context) { - super(path, UPDATE_MASK); - - if (path == null) - throw new IllegalArgumentException("NULL path argument received"); - if (account == null) - throw new IllegalArgumentException("NULL account argument received"); - if (context == null) - throw new IllegalArgumentException("NULL context argument received"); - - mPath = path; - mAccount = account; - mContext = context; - mObservedChildren = new HashMap(); - } - - - @Override - public void onEvent(int event, String path) { - Log_OC.d(TAG, "Got event " + event + " on FOLDER " + mPath + " about " - + ((path != null) ? path : "")); - - boolean shouldSynchronize = false; - synchronized(mObservedChildren) { - if (path != null && path.length() > 0 && mObservedChildren.containsKey(path)) { - - if ( ((event & FileObserver.MODIFY) != 0) || - ((event & FileObserver.ATTRIB) != 0) || - ((event & FileObserver.MOVED_TO) != 0) ) { - - if (mObservedChildren.get(path) != true) { - mObservedChildren.put(path, Boolean.valueOf(true)); - } - } - - if ((event & FileObserver.CLOSE_WRITE) != 0) { - mObservedChildren.put(path, Boolean.valueOf(false)); - shouldSynchronize = true; - } - } - } - if (shouldSynchronize) { - startSyncOperation(path); - } - - if ((event & OwnCloudFileObserver.IN_IGNORE) != 0 && - (path == null || path.length() == 0)) { - Log_OC.d(TAG, "Stopping the observance on " + mPath); - } - - } - - - public void startWatching(String localPath) { - synchronized (mObservedChildren) { - if (!mObservedChildren.containsKey(localPath)) { - mObservedChildren.put(localPath, Boolean.valueOf(false)); - } - } - - if (new File(mPath).exists()) { - startWatching(); - Log_OC.d(TAG, "Started watching parent folder " + mPath + "/"); - } - // else - the observance can't be started on a file not existing; - } - - public void stopWatching(String localPath) { - synchronized (mObservedChildren) { - mObservedChildren.remove(localPath); - if (mObservedChildren.isEmpty()) { - stopWatching(); - Log_OC.d(TAG, "Stopped watching parent folder " + mPath + "/"); - } - } - } - - public boolean isEmpty() { - synchronized (mObservedChildren) { - return mObservedChildren.isEmpty(); - } - } - - - private void startSyncOperation(String childName) { - FileDataStorageManager storageManager = - new FileDataStorageManager(mAccount, mContext.getContentResolver()); - // a fresh object is needed; many things could have occurred to the file - // since it was registered to observe again, assuming that local files - // are linked to a remote file AT MOST, SOMETHING TO BE DONE; - OCFile file = storageManager.getFileByLocalPath(mPath + File.separator + childName); - SynchronizeFileOperation sfo = - new SynchronizeFileOperation(file, null, mAccount, true, mContext); - RemoteOperationResult result = sfo.execute(storageManager, mContext); - if (result.getCode() == ResultCode.SYNC_CONFLICT) { - // ISSUE 5: if the user is not running the app (this is a service!), - // this can be very intrusive; a notification should be preferred - Intent i = new Intent(mContext, ConflictsResolveActivity.class); - i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - i.putExtra(ConflictsResolveActivity.EXTRA_FILE, file); - i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, mAccount); - mContext.startActivity(i); - } - // TODO save other errors in some point where the user can inspect them later; - // or maybe just toast them; - // or nothing, very strange fails - } - -} diff --git a/src/com/owncloud/android/files/services/FileObserverService.java b/src/com/owncloud/android/files/services/FileObserverService.java deleted file mode 100644 index a602f8cf..00000000 --- a/src/com/owncloud/android/files/services/FileObserverService.java +++ /dev/null @@ -1,368 +0,0 @@ -/* ownCloud Android client application - * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2012-2013 ownCloud Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package com.owncloud.android.files.services; - -import java.io.File; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import android.accounts.Account; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.database.Cursor; -import android.os.FileObserver; -import android.os.IBinder; - -import com.owncloud.android.MainApp; -import com.owncloud.android.authentication.AccountUtils; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; -import com.owncloud.android.files.OwnCloudFolderObserver; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.Log_OC; - - -/** - * Service keeping a list of {@link FileObserver} instances that watch for local - * changes in favorite files (formerly known as kept-in-sync files) and try to - * synchronize them with the OC server as soon as possible. - * - * Tries to be alive as long as possible; that is the reason why stopSelf() is - * never called. - * - * It is expected that the system eventually kills the service when runs low of - * memory. To minimize the impact of this, the service always returns - * Service.START_STICKY, and the later restart of the service is explicitly - * considered in {@link FileObserverService#onStartCommand(Intent, int, int)}. - * - * @author David A. Velasco - */ -public class FileObserverService extends Service { - - public final static String MY_NAME = FileObserverService.class.getCanonicalName(); - public final static String ACTION_START_OBSERVE = MY_NAME + ".action.START_OBSERVATION"; - public final static String ACTION_ADD_OBSERVED_FILE = MY_NAME + ".action.ADD_OBSERVED_FILE"; - public final static String ACTION_DEL_OBSERVED_FILE = MY_NAME + ".action.DEL_OBSERVED_FILE"; - - private final static String ARG_FILE = "ARG_FILE"; - private final static String ARG_ACCOUNT = "ARG_ACCOUNT"; - - private static String TAG = FileObserverService.class.getSimpleName(); - - private Map mFolderObserversMap; - private DownloadCompletedReceiver mDownloadReceiver; - - /** - * Factory method to create intents that allow to start an - * ACTION_START_OBSERVE command. - * - * @param context Android context of the caller component. - * @return Intent that starts a command ACTION_START_OBSERVE when - * {@link Context#startService(Intent)} is called. - */ - public static Intent makeInitIntent(Context context) { - Intent i = new Intent(context, FileObserverService.class); - i.setAction(ACTION_START_OBSERVE); - return i; - } - - /** - * Factory method to create intents that allow to start or stop the - * observance of a file. - * - * @param context Android context of the caller component. - * @param file OCFile to start or stop to watch. - * @param account OC account containing file. - * @param watchIt 'True' creates an intent to watch, 'false' an intent to - * stop watching. - * @return Intent to start or stop the observance of a file through a call - * to {@link Context#startService(Intent)}. - */ - public static Intent makeObservedFileIntent( - Context context, OCFile file, Account account, boolean watchIt) { - Intent intent = new Intent(context, FileObserverService.class); - intent.setAction(watchIt ? FileObserverService.ACTION_ADD_OBSERVED_FILE - : FileObserverService.ACTION_DEL_OBSERVED_FILE); - intent.putExtra(FileObserverService.ARG_FILE, file); - intent.putExtra(FileObserverService.ARG_ACCOUNT, account); - return intent; - } - - @Override - public void onCreate() { - Log_OC.d(TAG, "onCreate"); - super.onCreate(); - - mDownloadReceiver = new DownloadCompletedReceiver(); - IntentFilter filter = new IntentFilter(); - filter.addAction(FileDownloader.getDownloadAddedMessage()); - filter.addAction(FileDownloader.getDownloadFinishMessage()); - registerReceiver(mDownloadReceiver, filter); - - mFolderObserversMap = new HashMap(); - } - - @Override - public void onDestroy() { - Log_OC.d(TAG, "onDestroy - finishing observation of favourite files"); - - unregisterReceiver(mDownloadReceiver); - - Iterator itOCFolder = mFolderObserversMap.values().iterator(); - while (itOCFolder.hasNext()) { - itOCFolder.next().stopWatching(); - } - mFolderObserversMap.clear(); - mFolderObserversMap = null; - - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - // this service cannot be bound - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log_OC.d(TAG, "Starting command " + intent); - - if (intent == null || ACTION_START_OBSERVE.equals(intent.getAction())) { - // NULL occurs when system tries to restart the service after its - // process was killed - startObservation(); - return Service.START_STICKY; - - } else if (ACTION_ADD_OBSERVED_FILE.equals(intent.getAction())) { - OCFile file = (OCFile) intent.getParcelableExtra(ARG_FILE); - Account account = (Account) intent.getParcelableExtra(ARG_ACCOUNT); - addObservedFile(file, account); - - } else if (ACTION_DEL_OBSERVED_FILE.equals(intent.getAction())) { - removeObservedFile((OCFile) intent.getParcelableExtra(ARG_FILE), - (Account) intent.getParcelableExtra(ARG_ACCOUNT)); - - } else { - Log_OC.e(TAG, "Unknown action recieved; ignoring it: " + intent.getAction()); - } - - return Service.START_STICKY; - } - - - /** - * Read from the local database the list of files that must to be kept - * synchronized and starts observers to monitor local changes on them - */ - private void startObservation() { - Log_OC.d(TAG, "Loading all kept-in-sync files from database to start watching them"); - - // query for any favorite file in any OC account - Cursor cursorOnKeptInSync = getContentResolver().query( - ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?", - new String[] { String.valueOf(1) }, - null - ); - - if (cursorOnKeptInSync != null) { - - if (cursorOnKeptInSync.moveToFirst()) { - - String localPath = ""; - // String remotePath = ""; - String accountName = ""; - Account account = null; - do { - localPath = cursorOnKeptInSync.getString(cursorOnKeptInSync - .getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH)); - accountName = cursorOnKeptInSync.getString(cursorOnKeptInSync - .getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER)); - /* - * remotePath = cursorOnKeptInSync.getString( - * cursorOnKeptInSync - * .getColumnIndex(ProviderTableMeta.FILE_PATH) ); - */ - - account = new Account(accountName, MainApp.getAccountType()); - if (!AccountUtils.exists(account, this) || localPath == null || localPath.length() <= 0) { - continue; - } - - addObservedFile(localPath, account); - - } while (cursorOnKeptInSync.moveToNext()); - - } - cursorOnKeptInSync.close(); - } - - // service does not stopSelf() ; that way it tries to be alive forever - - } - - - /** - * Registers the local copy of a remote file to be observed for local - * changes, an automatically updated in the ownCloud server. - * - * This method does NOT perform a {@link SynchronizeFileOperation} over the - * file. - * - * @param file Object representing a remote file which local copy must be - * observed. - * @param account OwnCloud account containing file. - */ - private void addObservedFile(OCFile file, Account account) { - Log_OC.v(TAG, "Adding a file to be watched"); - - if (file == null) { - Log_OC.e(TAG, "Trying to add a NULL file to observer"); - return; - } - if (account == null) { - Log_OC.e(TAG, "Trying to add a file with a NULL account to observer"); - return; - } - - String localPath = file.getStoragePath(); - if (localPath == null || localPath.length() <= 0) { - // file downloading or to be downloaded for the first time - localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); - } - - addObservedFile(localPath, account); - - } - - - - - /** - * Registers the folder to be observed in which there are changes inside any - * file - * - * @param account OwnCloud account containing file. - */ - private void addObservedFile(String localPath, Account account) { - File file = new File(localPath); - String parentPath = file.getParent(); - OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath); - if (observer == null) { - observer = new OwnCloudFolderObserver(parentPath, account, getApplicationContext()); - mFolderObserversMap.put(parentPath, observer); - Log_OC.d(TAG, "Observer added for parent folder " + parentPath + "/"); - } - - observer.startWatching(file.getName()); - Log_OC.d(TAG, "Added " + localPath + " to list of observed children"); - } - - - /** - * Unregisters the local copy of a remote file to be observed for local - * changes. - * - * Starts to watch it, if the file has a local copy to watch. - * - * @param file Object representing a remote file which local copy must be - * not observed longer. - * @param account OwnCloud account containing file. - */ - private void removeObservedFile(OCFile file, Account account) { - Log_OC.v(TAG, "Removing a file from being watched"); - - if (file == null) { - Log_OC.e(TAG, "Trying to remove a NULL file"); - return; - } - if (account == null) { - Log_OC.e(TAG, "Trying to add a file with a NULL account to observer"); - return; - } - - String localPath = file.getStoragePath(); - if (localPath == null || localPath.length() <= 0) { - localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); - } - - removeObservedFile(localPath); - } - - - private void removeObservedFile(String localPath) { - File file = new File(localPath); - String parentPath = file.getParent(); - OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath); - if (observer != null) { - observer.stopWatching(file.getName()); - if (observer.isEmpty()) { - mFolderObserversMap.remove(parentPath); - Log_OC.d(TAG, "Observer removed for parent folder " + parentPath + "/"); - } - - } else { - Log_OC.d(TAG, "No observer to remove for path " + localPath); - } - } - - - /** - * Private receiver listening to events broadcasted by the FileDownloader service. - * - * Pauses and resumes the observance on registered files while being download, - * in order to avoid to unnecessary synchronizations. - */ - private class DownloadCompletedReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - Log_OC.d(TAG, "Received broadcast intent " + intent); - - File downloadedFile = new File(intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH)); - String parentPath = downloadedFile.getParent(); - OwnCloudFolderObserver observer = mFolderObserversMap.get(parentPath); - if (observer != null) { - if (intent.getAction().equals(FileDownloader.getDownloadFinishMessage()) - && downloadedFile.exists()) { - // no matter if the download was be successful or not; the - // file could be down anyway due to a former download or upload - observer.startWatching(downloadedFile.getName()); - Log_OC.d(TAG, "Resuming observance of " + downloadedFile.getAbsolutePath()); - - } else if (intent.getAction().equals(FileDownloader.getDownloadAddedMessage())) { - observer.stopWatching(downloadedFile.getName()); - Log_OC.d(TAG, "Pausing observance of " + downloadedFile.getAbsolutePath()); - } - - } else { - Log_OC.d(TAG, "No observer for path " + downloadedFile.getAbsolutePath()); - } - } - - } - -} diff --git a/src/com/owncloud/android/services/observer/FileObserverService.java b/src/com/owncloud/android/services/observer/FileObserverService.java new file mode 100644 index 00000000..95e19211 --- /dev/null +++ b/src/com/owncloud/android/services/observer/FileObserverService.java @@ -0,0 +1,376 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2014 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.services.observer; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import android.accounts.Account; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.os.IBinder; + +import com.owncloud.android.MainApp; +import com.owncloud.android.authentication.AccountUtils; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; +import com.owncloud.android.files.services.FileDownloader; +import com.owncloud.android.operations.SynchronizeFileOperation; +import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.Log_OC; + + +/** + * Service keeping a list of {@link FolderObserver} instances that watch for local + * changes in favorite files (formerly known as kept-in-sync files) and try to + * synchronize them with the OC server as soon as possible. + * + * Tries to be alive as long as possible; that is the reason why stopSelf() is + * never called. + * + * It is expected that the system eventually kills the service when runs low of + * memory. To minimize the impact of this, the service always returns + * Service.START_STICKY, and the later restart of the service is explicitly + * considered in {@link FileObserverService#onStartCommand(Intent, int, int)}. + * + * @author David A. Velasco + */ +public class FileObserverService extends Service { + + public final static String MY_NAME = FileObserverService.class.getCanonicalName(); + public final static String ACTION_START_OBSERVE = MY_NAME + ".action.START_OBSERVATION"; + public final static String ACTION_ADD_OBSERVED_FILE = MY_NAME + ".action.ADD_OBSERVED_FILE"; + public final static String ACTION_DEL_OBSERVED_FILE = MY_NAME + ".action.DEL_OBSERVED_FILE"; + + private final static String ARG_FILE = "ARG_FILE"; + private final static String ARG_ACCOUNT = "ARG_ACCOUNT"; + + private static String TAG = FileObserverService.class.getSimpleName(); + + private Map mFolderObserversMap; + private DownloadCompletedReceiver mDownloadReceiver; + + /** + * Factory method to create intents that allow to start an ACTION_START_OBSERVE command. + * + * @param context Android context of the caller component. + * @return Intent that starts a command ACTION_START_OBSERVE when + * {@link Context#startService(Intent)} is called. + */ + public static Intent makeInitIntent(Context context) { + Intent i = new Intent(context, FileObserverService.class); + i.setAction(ACTION_START_OBSERVE); + return i; + } + + /** + * Factory method to create intents that allow to start or stop the + * observance of a file. + * + * @param context Android context of the caller component. + * @param file OCFile to start or stop to watch. + * @param account OC account containing file. + * @param watchIt 'True' creates an intent to watch, 'false' an intent to stop watching. + * @return Intent to start or stop the observance of a file through a call + * to {@link Context#startService(Intent)}. + */ + public static Intent makeObservedFileIntent( + Context context, OCFile file, Account account, boolean watchIt) { + Intent intent = new Intent(context, FileObserverService.class); + intent.setAction(watchIt ? FileObserverService.ACTION_ADD_OBSERVED_FILE + : FileObserverService.ACTION_DEL_OBSERVED_FILE); + intent.putExtra(FileObserverService.ARG_FILE, file); + intent.putExtra(FileObserverService.ARG_ACCOUNT, account); + return intent; + } + + /** + * Initialize the service. + */ + @Override + public void onCreate() { + Log_OC.d(TAG, "onCreate"); + super.onCreate(); + + mDownloadReceiver = new DownloadCompletedReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(FileDownloader.getDownloadAddedMessage()); + filter.addAction(FileDownloader.getDownloadFinishMessage()); + registerReceiver(mDownloadReceiver, filter); + + mFolderObserversMap = new HashMap(); + } + + /** + * Release resources. + */ + @Override + public void onDestroy() { + Log_OC.d(TAG, "onDestroy - finishing observation of favorite files"); + + unregisterReceiver(mDownloadReceiver); + + Iterator itOCFolder = mFolderObserversMap.values().iterator(); + while (itOCFolder.hasNext()) { + itOCFolder.next().stopWatching(); + } + mFolderObserversMap.clear(); + mFolderObserversMap = null; + + super.onDestroy(); + } + + /** + * This service cannot be bound. + */ + @Override + public IBinder onBind(Intent intent) { + return null; + } + + /** + * Handles requests to: + * - (re)start watching (ACTION_START_OBSERVE) + * - add an {@link OCFile} to be watched (ATION_ADD_OBSERVED_FILE) + * - stop observing an {@link OCFile} (ACTION_DEL_OBSERVED_FILE) + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log_OC.d(TAG, "Starting command " + intent); + + if (intent == null || ACTION_START_OBSERVE.equals(intent.getAction())) { + // NULL occurs when system tries to restart the service after its + // process was killed + startObservation(); + return Service.START_STICKY; + + } else if (ACTION_ADD_OBSERVED_FILE.equals(intent.getAction())) { + OCFile file = (OCFile) intent.getParcelableExtra(ARG_FILE); + Account account = (Account) intent.getParcelableExtra(ARG_ACCOUNT); + addObservedFile(file, account); + + } else if (ACTION_DEL_OBSERVED_FILE.equals(intent.getAction())) { + removeObservedFile((OCFile) intent.getParcelableExtra(ARG_FILE), + (Account) intent.getParcelableExtra(ARG_ACCOUNT)); + + } else { + Log_OC.e(TAG, "Unknown action recieved; ignoring it: " + intent.getAction()); + } + + return Service.START_STICKY; + } + + + /** + * Read from the local database the list of files that must to be kept + * synchronized and starts observers to monitor local changes on them. + * + * Updates the list of currently observed files if called multiple times. + */ + private void startObservation() { + Log_OC.d(TAG, "Loading all kept-in-sync files from database to start watching them"); + + // query for any favorite file in any OC account + Cursor cursorOnKeptInSync = getContentResolver().query( + ProviderTableMeta.CONTENT_URI, + null, + ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?", + new String[] { String.valueOf(1) }, + null + ); + + if (cursorOnKeptInSync != null) { + + if (cursorOnKeptInSync.moveToFirst()) { + + String localPath = ""; + String accountName = ""; + Account account = null; + do { + localPath = cursorOnKeptInSync.getString(cursorOnKeptInSync + .getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH)); + accountName = cursorOnKeptInSync.getString(cursorOnKeptInSync + .getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER)); + + account = new Account(accountName, MainApp.getAccountType()); + if (!AccountUtils.exists(account, this) || localPath == null || localPath.length() <= 0) { + continue; + } + + addObservedFile(localPath, account); + + } while (cursorOnKeptInSync.moveToNext()); + + } + cursorOnKeptInSync.close(); + } + + // service does not stopSelf() ; that way it tries to be alive forever + + } + + + /** + * Registers the local copy of a remote file to be observed for local + * changes, an automatically updated in the ownCloud server. + * + * This method does NOT perform a {@link SynchronizeFileOperation} over the + * file. + * + * @param file Object representing a remote file which local copy must be observed. + * @param account OwnCloud account containing file. + */ + private void addObservedFile(OCFile file, Account account) { + Log_OC.v(TAG, "Adding a file to be watched"); + + if (file == null) { + Log_OC.e(TAG, "Trying to add a NULL file to observer"); + return; + } + if (account == null) { + Log_OC.e(TAG, "Trying to add a file with a NULL account to observer"); + return; + } + + String localPath = file.getStoragePath(); + if (localPath == null || localPath.length() <= 0) { + // file downloading or to be downloaded for the first time + localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); + } + + addObservedFile(localPath, account); + + } + + + + + /** + * Registers a local file to be observed for changes. + * + * @param localPath Absolute path in the local file system to the file to be observed. + * @param account OwnCloud account associated to the local file. + */ + private void addObservedFile(String localPath, Account account) { + File file = new File(localPath); + String parentPath = file.getParent(); + FolderObserver observer = mFolderObserversMap.get(parentPath); + if (observer == null) { + observer = new FolderObserver(parentPath, account, getApplicationContext()); + mFolderObserversMap.put(parentPath, observer); + Log_OC.d(TAG, "Observer added for parent folder " + parentPath + "/"); + } + + observer.startWatching(file.getName()); + Log_OC.d(TAG, "Added " + localPath + " to list of observed children"); + } + + + /** + * Unregisters the local copy of a remote file to be observed for local changes. + * + * @param file Object representing a remote file which local copy must be not + * observed longer. + * @param account OwnCloud account containing file. + */ + private void removeObservedFile(OCFile file, Account account) { + Log_OC.v(TAG, "Removing a file from being watched"); + + if (file == null) { + Log_OC.e(TAG, "Trying to remove a NULL file"); + return; + } + if (account == null) { + Log_OC.e(TAG, "Trying to add a file with a NULL account to observer"); + return; + } + + String localPath = file.getStoragePath(); + if (localPath == null || localPath.length() <= 0) { + localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); + } + + removeObservedFile(localPath); + } + + + /** + * Unregisters a local file from being observed for changes. + * + * @param localPath Absolute path in the local file system to the target file. + */ + private void removeObservedFile(String localPath) { + File file = new File(localPath); + String parentPath = file.getParent(); + FolderObserver observer = mFolderObserversMap.get(parentPath); + if (observer != null) { + observer.stopWatching(file.getName()); + if (observer.isEmpty()) { + mFolderObserversMap.remove(parentPath); + Log_OC.d(TAG, "Observer removed for parent folder " + parentPath + "/"); + } + + } else { + Log_OC.d(TAG, "No observer to remove for path " + localPath); + } + } + + + /** + * Private receiver listening to events broadcasted by the {@link FileDownloader} service. + * + * Pauses and resumes the observance on registered files while being download, + * in order to avoid to unnecessary synchronizations. + */ + private class DownloadCompletedReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Log_OC.d(TAG, "Received broadcast intent " + intent); + + File downloadedFile = new File(intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH)); + String parentPath = downloadedFile.getParent(); + FolderObserver observer = mFolderObserversMap.get(parentPath); + if (observer != null) { + if (intent.getAction().equals(FileDownloader.getDownloadFinishMessage()) + && downloadedFile.exists()) { + // no matter if the download was successful or not; the + // file could be down anyway due to a former download or upload + observer.startWatching(downloadedFile.getName()); + Log_OC.d(TAG, "Resuming observance of " + downloadedFile.getAbsolutePath()); + + } else if (intent.getAction().equals(FileDownloader.getDownloadAddedMessage())) { + observer.stopWatching(downloadedFile.getName()); + Log_OC.d(TAG, "Pausing observance of " + downloadedFile.getAbsolutePath()); + } + + } else { + Log_OC.d(TAG, "No observer for path " + downloadedFile.getAbsolutePath()); + } + } + + } + +} diff --git a/src/com/owncloud/android/services/observer/FolderObserver.java b/src/com/owncloud/android/services/observer/FolderObserver.java new file mode 100644 index 00000000..ea1a9ede --- /dev/null +++ b/src/com/owncloud/android/services/observer/FolderObserver.java @@ -0,0 +1,215 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2014 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.services.observer; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import android.accounts.Account; +import android.content.Context; +import android.content.Intent; +import android.os.FileObserver; + +import com.owncloud.android.datamodel.FileDataStorageManager; +import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; +import com.owncloud.android.operations.SynchronizeFileOperation; +import com.owncloud.android.ui.activity.ConflictsResolveActivity; +import com.owncloud.android.utils.Log_OC; + +/** + * Observer watching a folder to request the synchronization of kept-in-sync files + * inside it. + * + * Takes into account two possible update cases: + * - an editor directly updates the file; + * - an editor works on a temporary file, and later replaces the kept-in-sync file with the + * former. + * + * The second case requires to monitor the folder parent of the files, since a direct + * {@link FileObserver} on it will not receive more events after the file is deleted to + * be replaced. + * + * @author David A. Velasco + */ +public class FolderObserver extends FileObserver { + + private static String TAG = FolderObserver.class.getSimpleName(); + + private static int UPDATE_MASK = ( + FileObserver.ATTRIB | FileObserver.MODIFY | + FileObserver.MOVED_TO | FileObserver.CLOSE_WRITE + ); + + private static int IN_IGNORE = 32768; + /* + private static int ALL_EVENTS_EVEN_THOSE_NOT_DOCUMENTED = 0x7fffffff; // NEVER use 0xffffffff + */ + + private String mPath; + private Account mAccount; + private Context mContext; + private Map mObservedChildren; + + /** + * Constructor. + * + * Initializes the observer to receive events about the update of the passed folder, and + * its children files. + * + * @param path Absolute path to the local folder to watch. + * @param account OwnCloud account associated to the folder. + * @param context Used to start an operation to synchronize the file, when needed. + */ + public FolderObserver(String path, Account account, Context context) { + super(path, UPDATE_MASK); + + if (path == null) + throw new IllegalArgumentException("NULL path argument received"); + if (account == null) + throw new IllegalArgumentException("NULL account argument received"); + if (context == null) + throw new IllegalArgumentException("NULL context argument received"); + + mPath = path; + mAccount = account; + mContext = context; + mObservedChildren = new HashMap(); + } + + + /** + * Receives and processes events about updates of the monitor folder and its children files. + * + * @param event Kind of event occurred. + * @param path Relative path of the file referred by the event. + */ + @Override + public void onEvent(int event, String path) { + Log_OC.d(TAG, "Got event " + event + " on FOLDER " + mPath + " about " + + ((path != null) ? path : "")); + + boolean shouldSynchronize = false; + synchronized(mObservedChildren) { + if (path != null && path.length() > 0 && mObservedChildren.containsKey(path)) { + + if ( ((event & FileObserver.MODIFY) != 0) || + ((event & FileObserver.ATTRIB) != 0) || + ((event & FileObserver.MOVED_TO) != 0) ) { + + if (mObservedChildren.get(path) != true) { + mObservedChildren.put(path, Boolean.valueOf(true)); + } + } + + if ((event & FileObserver.CLOSE_WRITE) != 0 && mObservedChildren.get(path)) { + mObservedChildren.put(path, Boolean.valueOf(false)); + shouldSynchronize = true; + } + } + } + if (shouldSynchronize) { + startSyncOperation(path); + } + + if ((event & IN_IGNORE) != 0 && + (path == null || path.length() == 0)) { + Log_OC.d(TAG, "Stopping the observance on " + mPath); + } + + } + + + /** + * Adds a child file to the list of files observed by the folder observer. + * + * @param fileName Name of a file inside the observed folder. + */ + public void startWatching(String fileName) { + synchronized (mObservedChildren) { + if (!mObservedChildren.containsKey(fileName)) { + mObservedChildren.put(fileName, Boolean.valueOf(false)); + } + } + + if (new File(mPath).exists()) { + startWatching(); + Log_OC.d(TAG, "Started watching parent folder " + mPath + "/"); + } + // else - the observance can't be started on a file not existing; + } + + + /** + * Removes a child file from the list of files observed by the folder observer. + * + * @param fileName Name of a file inside the observed folder. + */ + public void stopWatching(String fileName) { + synchronized (mObservedChildren) { + mObservedChildren.remove(fileName); + if (mObservedChildren.isEmpty()) { + stopWatching(); + Log_OC.d(TAG, "Stopped watching parent folder " + mPath + "/"); + } + } + } + + /** + * @return 'True' when the folder is not watching any file inside. + */ + public boolean isEmpty() { + synchronized (mObservedChildren) { + return mObservedChildren.isEmpty(); + } + } + + + /** + * Triggers an operation to synchronize the contents of a file inside the observed folder with + * its remote counterpart in the associated ownCloud account. + * + * @param fileName Name of a file inside the watched folder. + */ + private void startSyncOperation(String fileName) { + FileDataStorageManager storageManager = + new FileDataStorageManager(mAccount, mContext.getContentResolver()); + // a fresh object is needed; many things could have occurred to the file + // since it was registered to observe again, assuming that local files + // are linked to a remote file AT MOST, SOMETHING TO BE DONE; + OCFile file = storageManager.getFileByLocalPath(mPath + File.separator + fileName); + SynchronizeFileOperation sfo = + new SynchronizeFileOperation(file, null, mAccount, true, mContext); + RemoteOperationResult result = sfo.execute(storageManager, mContext); + if (result.getCode() == ResultCode.SYNC_CONFLICT) { + // ISSUE 5: if the user is not running the app (this is a service!), + // this can be very intrusive; a notification should be preferred + Intent i = new Intent(mContext, ConflictsResolveActivity.class); + i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + i.putExtra(ConflictsResolveActivity.EXTRA_FILE, file); + i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, mAccount); + mContext.startActivity(i); + } + // TODO save other errors in some point where the user can inspect them later; + // or maybe just toast them; + // or nothing, very strange fails + } + +} diff --git a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java index c1065c4a..640f2b81 100644 --- a/src/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -66,7 +66,6 @@ import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.services.FileDownloader; -import com.owncloud.android.files.services.FileObserverService; import com.owncloud.android.files.services.FileUploader; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; @@ -87,6 +86,7 @@ import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareLinkOperation; +import com.owncloud.android.services.observer.FileObserverService; import com.owncloud.android.syncadapter.FileSyncAdapter; import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; @@ -165,7 +165,7 @@ FileFragment.ContainerActivity, OnNavigationListener, OnSslUntrustedCertListener } /// grant that FileObserverService is watching favourite files - if (savedInstanceState != null) { + if (savedInstanceState == null) { Intent initObserversIntent = FileObserverService.makeInitIntent(this); startService(initObserversIntent); } diff --git a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 15a8b365..b3d8567d 100644 --- a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -38,10 +38,10 @@ import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.files.FileMenuFilter; -import com.owncloud.android.files.services.FileObserverService; import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder; import com.owncloud.android.files.services.FileUploader.FileUploaderBinder; import com.owncloud.android.lib.common.network.OnDatatransferProgressListener; +import com.owncloud.android.services.observer.FileObserverService; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.dialog.RemoveFileDialogFragment;