From: David A. Velasco Date: Wed, 9 Jul 2014 10:27:35 +0000 (+0200) Subject: Merge branch 'develop' into release-1.5.8 X-Git-Tag: oc-android-1.7.0_signed~125^2~11 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/49c69609eaba304a740b1a8dbd37e619d0fb82cf?hp=df5fe2d6484b875b92336967ac2c947786d749e6 Merge branch 'develop' into release-1.5.8 --- diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8d5b6307..7a3b1fc3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -187,7 +187,7 @@ - + La descàrrega ha fallat, t\'has de tornar a acreditar Escolliu el compte La sincronització ha fallat + La sincronització ha fallat, us heu d\'acreditar de nou La sincronització de %1$s no s\'ha pogut acabar Contrasenya no vàlida per %1$s S\'han trobat conflictes diff --git a/res/values-ja-rJP/strings.xml b/res/values-ja-rJP/strings.xml index 90c4dcec..b57b6eaa 100644 --- a/res/values-ja-rJP/strings.xml +++ b/res/values-ja-rJP/strings.xml @@ -93,6 +93,7 @@ ダウンロードに失敗しました。再ログインする必要があります。 アカウントを選択 同期失敗 + 同期に失敗しました。再ログインする必要があります。 %1$s の同期が完了しませんでした。 1$sの無効なパスワード 競合が見つかりました @@ -108,7 +109,7 @@ 一部のファイルは移動できませんでした ローカル: %1$s リモート: %1$s - 十分なスペースが無いため、選択されたファイルを %1$s フォルダーにコピーすることができません。コピーする代わりに、それらを移動させますか? + 十分なスペースがないため、選択されたファイルを %1$s フォルダーにコピーすることができません。コピーする代わりに、それらを移動させますか? アプリのパスワードを入力してください アプリのパスワードを入力してください アプリ開始時に毎回PINが要求されます。 diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 6df723e9..109b19e6 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -20,6 +20,7 @@ 即时图片上传 即时上传相机拍摄的图片 立即上传视频 + 即时上传由相机拍摄的视频 开启日志 这过去是日志问题 日志历史 @@ -30,6 +31,7 @@ 反馈 版本说明 在您的智能手机上试用一下 %1$s! + “我邀请你使用在你的智能手机上使用 %1$s!\n立即下载:%2$s”\n\t 检查服务器 服务器地址 https://... 用户名 @@ -80,6 +82,7 @@ %1$s 成功上传 上传失败 1$上传未能完成 + 上传失败,您需要重新登录 下载中…… %1$d%% 下载中 %2$s 下载成功 @@ -87,8 +90,10 @@ 下载失败 下载1$s 未能完成 未下载完毕 + 下载失败,您需要重新登录 选择账户 同步失败 + 同步失败,您需要重新登录 %1$s同步未完成。 密码错误%1$s 发现冲突 @@ -104,6 +109,7 @@ 某些文件无法被移动 本地: %1$s 远程:%1$s + 没有足够的空间以复制选定的文件到 %1$s 文件夹,您想移动文件到此文件夹吗? 请输入您的App PIN码 输入 App PIN码 每次应用启动时都会请求PIN码 @@ -139,6 +145,8 @@ 连接已建立。 测试连接…… 服务器配置不正确。 + 此设备中已经存在同名同服务器的帐号 + 输入用户与此帐户的用户不符 发生未知错误! 无法找到服务器 未发现服务器实例 @@ -156,9 +164,11 @@ 你的授权已经过期。请重新授权。 请输入当前密码: 您的会话超时了,请重新连接 + 正在连接到认证服务器.... 服务器不支持这种验证方式 %1$s不支持多个账户 您的服务器没有返回一个正确的用户 id,请联系管理员\n\t + 无法通过此服务器认证 保证文件更新 重命名 删除 @@ -177,6 +187,7 @@ 文件内容已同步 文件夹无法创建 禁用字符: / \\ < > : \" | ? * + 文件名不能为空 请稍候 未知问题;请试试用其他程序选择此文件 未选择文件。 @@ -214,6 +225,7 @@ 2012/05/18 下午12:23 12:23:45 仅通过WIFI上传图片。 + 仅在 WIFI 下上传视频 /InstantUpload 上传冲突 远程文件 %s 未与本地文件同步。继续将替换服务器上的文件内容。 @@ -242,4 +254,9 @@ 发送 复制链接 复制到剪贴板 + 严重错误:无法执行操作 + 连接到服务器时发生了一个错误。 + 等待服务器响应时发生了一个错误,此操作无法完成 + 等待服务器响应时发生了一个错误,此操作无法完成 + 服务器不可用,此操作无法完成 diff --git a/src/com/owncloud/android/files/BootupBroadcastReceiver.java b/src/com/owncloud/android/files/BootupBroadcastReceiver.java index e90e8d4a..ea5a44a9 100644 --- a/src/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/src/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -18,17 +18,32 @@ package com.owncloud.android.files; -import com.owncloud.android.files.services.FileObserverService; +import com.owncloud.android.services.observer.FileObserverService; import com.owncloud.android.utils.Log_OC; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; + +/** + * App-registered receiver catching the broadcast intent reporting that the system was + * just boot up. + * + * @author David A. Velasco + */ public class BootupBroadcastReceiver extends BroadcastReceiver { - private static String TAG = "BootupBroadcastReceiver"; + private static String TAG = BootupBroadcastReceiver.class.getSimpleName(); + /** + * Receives broadcast intent reporting that the system was just boot up. + * + * Starts {@link FileObserverService} to enable observation of favourite files. + * + * @param context The context where the receiver is running. + * @param intent The intent received. + */ @Override public void onReceive(Context context, Intent intent) { if (!intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { @@ -36,11 +51,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { return; } Log_OC.d(TAG, "Starting file observer service..."); - Intent i = new Intent(context, FileObserverService.class); - i.putExtra(FileObserverService.KEY_FILE_CMD, - FileObserverService.CMD_INIT_OBSERVED_LIST); - context.startService(i); - Log_OC.d(TAG, "DONE"); + Intent initObservers = FileObserverService.makeInitIntent(context); + context.startService(initObservers); } } diff --git a/src/com/owncloud/android/files/OwnCloudFileObserver.java b/src/com/owncloud/android/files/OwnCloudFileObserver.java deleted file mode 100644 index 551bc25f..00000000 --- a/src/com/owncloud/android/files/OwnCloudFileObserver.java +++ /dev/null @@ -1,111 +0,0 @@ -/* 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.files; - -import java.io.File; - -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.utils.Log_OC; - -import android.accounts.Account; -import android.content.Context; -import android.content.Intent; -import android.os.FileObserver; - -public class OwnCloudFileObserver extends FileObserver { - - private static int MASK = (FileObserver.MODIFY | FileObserver.CLOSE_WRITE); - - private static String TAG = OwnCloudFileObserver.class.getSimpleName(); - - private String mPath; - private int mMask; - private Account mOCAccount; - private Context mContext; - private boolean mModified; - - - public OwnCloudFileObserver(String path, Account account, Context context) { - super(path, 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; - mOCAccount = account; - mContext = context; - mModified = false; - } - - - @Override - public void onEvent(int event, String path) { - Log_OC.d(TAG, "Got file modified with event " + event + " and path " + mPath + ((path != null) ? File.separator + 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); - } 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; - startSyncOperation(); - } - } - } - } - - - private void startSyncOperation() { - FileDataStorageManager storageManager = new FileDataStorageManager(mOCAccount, mContext.getContentResolver()); - OCFile file = storageManager.getFileByLocalPath(mPath); // 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; - 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 - } - -} 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 1bcd8d63..00000000 --- a/src/com/owncloud/android/files/services/FileObserverService.java +++ /dev/null @@ -1,277 +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.Map; - -import com.owncloud.android.MainApp; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; -import com.owncloud.android.files.OwnCloudFileObserver; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.Log_OC; - - -import android.accounts.Account; -import android.accounts.AccountManager; -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.Binder; -import android.os.IBinder; - -public class FileObserverService extends Service { - - public final static int CMD_INIT_OBSERVED_LIST = 1; - public final static int CMD_ADD_OBSERVED_FILE = 2; - public final static int CMD_DEL_OBSERVED_FILE = 3; - - public final static String KEY_FILE_CMD = "KEY_FILE_CMD"; - public final static String KEY_CMD_ARG_FILE = "KEY_CMD_ARG_FILE"; - public final static String KEY_CMD_ARG_ACCOUNT = "KEY_CMD_ARG_ACCOUNT"; - - private static String TAG = FileObserverService.class.getSimpleName(); - - private static Map mObserversMap; - private static DownloadCompletedReceiverBis mDownloadReceiver; - private IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - FileObserverService getService() { - return FileObserverService.this; - } - } - - @Override - public void onCreate() { - super.onCreate(); - mDownloadReceiver = new DownloadCompletedReceiverBis(); - - IntentFilter filter = new IntentFilter(); - filter.addAction(FileDownloader.getDownloadAddedMessage()); - filter.addAction(FileDownloader.getDownloadFinishMessage()); - registerReceiver(mDownloadReceiver, filter); - - mObserversMap = new HashMap(); - //initializeObservedList(); - } - - - @Override - public void onDestroy() { - unregisterReceiver(mDownloadReceiver); - mObserversMap = null; // TODO study carefully the life cycle of Services to grant the best possible observance - Log_OC.d(TAG, "Bye, bye"); - super.onDestroy(); - } - - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // this occurs when system tries to restart - // service, so we need to reinitialize observers - if (intent == null) { - initializeObservedList(); - return Service.START_STICKY; - } - - if (!intent.hasExtra(KEY_FILE_CMD)) { - Log_OC.e(TAG, "No KEY_FILE_CMD argument given"); - return Service.START_STICKY; - } - - switch (intent.getIntExtra(KEY_FILE_CMD, -1)) { - case CMD_INIT_OBSERVED_LIST: - initializeObservedList(); - break; - case CMD_ADD_OBSERVED_FILE: - addObservedFile( (OCFile)intent.getParcelableExtra(KEY_CMD_ARG_FILE), - (Account)intent.getParcelableExtra(KEY_CMD_ARG_ACCOUNT)); - break; - case CMD_DEL_OBSERVED_FILE: - removeObservedFile( (OCFile)intent.getParcelableExtra(KEY_CMD_ARG_FILE), - (Account)intent.getParcelableExtra(KEY_CMD_ARG_ACCOUNT)); - break; - default: - Log_OC.wtf(TAG, "Incorrect key given"); - } - - return Service.START_STICKY; - } - - - /** - * Read from the local database the list of files that must to be kept synchronized and - * starts file observers to monitor local changes on them - */ - private void initializeObservedList() { - mObserversMap.clear(); - Cursor c = getContentResolver().query( - ProviderTableMeta.CONTENT_URI, - null, - ProviderTableMeta.FILE_KEEP_IN_SYNC + " = ?", - new String[] {String.valueOf(1)}, - null); - if (c == null || !c.moveToFirst()) return; - AccountManager acm = AccountManager.get(this); - Account[] accounts = acm.getAccountsByType(MainApp.getAccountType()); - do { - Account account = null; - for (Account a : accounts) - if (a.name.equals(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_ACCOUNT_OWNER)))) { - account = a; - break; - } - - if (account == null) continue; - FileDataStorageManager storage = - new FileDataStorageManager(account, getContentResolver()); - if (!storage.fileExists(c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH)))) - continue; - - String path = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_STORAGE_PATH)); - if (path == null || path.length() <= 0) - continue; - OwnCloudFileObserver observer = - new OwnCloudFileObserver( path, - account, - getApplicationContext()); - mObserversMap.put(path, observer); - if (new File(path).exists()) { - observer.startWatching(); - Log_OC.d(TAG, "Started watching file " + path); - } - - } while (c.moveToNext()); - c.close(); - } - - - /** - * 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. - * - * TODO We are ignoring that, currently, a local file can be linked to different files - * in ownCloud if it's uploaded several times. That's something pending to update: we - * will avoid that the same local file is linked to different remote files. - * - * @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) { - if (file == null) { - Log_OC.e(TAG, "Trying to add a NULL file to observer"); - return; - } - String localPath = file.getStoragePath(); - if (localPath == null || localPath.length() <= 0) { // file downloading / to be download for the first time - localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); - } - OwnCloudFileObserver observer = mObserversMap.get(localPath); - if (observer == null) { - /// the local file was never registered to observe before - observer = new OwnCloudFileObserver( localPath, - account, - getApplicationContext()); - mObserversMap.put(localPath, observer); - Log_OC.d(TAG, "Observer added for path " + localPath); - - if (file.isDown()) { - observer.startWatching(); - Log_OC.d(TAG, "Started watching " + localPath); - } // else - the observance can't be started on a file not already down; mDownloadReceiver will get noticed when the download of the file finishes - } - - } - - - /** - * 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. - * - * TODO We are ignoring that, currently, a local file can be linked to different files - * in ownCloud if it's uploaded several times. That's something pending to update: we - * will avoid that the same local file is linked to different remote files. - * - * @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) { - if (file == null) { - Log_OC.e(TAG, "Trying to remove a NULL file"); - return; - } - String localPath = file.getStoragePath(); - if (localPath == null || localPath.length() <= 0) { - localPath = FileStorageUtils.getDefaultSavePathFor(account.name, file); - } - - OwnCloudFileObserver observer = mObserversMap.get(localPath); - if (observer != null) { - observer.stopWatching(); - mObserversMap.remove(observer); - Log_OC.d(TAG, "Stopped watching " + localPath); - } - - } - - - /** - * Private receiver listening to events broadcast by the FileDownloader service. - * - * Starts and stops the observance on registered files when they are being download, - * in order to avoid to start unnecessary synchronizations. - */ - private class DownloadCompletedReceiverBis extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - String downloadPath = intent.getStringExtra(FileDownloader.EXTRA_FILE_PATH); - OwnCloudFileObserver observer = mObserversMap.get(downloadPath); - if (observer != null) { - if (intent.getAction().equals(FileDownloader.getDownloadFinishMessage()) && - new File(downloadPath).exists()) { // the download could be successful. not; in both cases, the file could be down, due to a former download or upload - observer.startWatching(); - Log_OC.d(TAG, "Watching again " + downloadPath); - - } else if (intent.getAction().equals(FileDownloader.getDownloadAddedMessage())) { - observer.stopWatching(); - Log_OC.d(TAG, "Disabling observance of " + downloadPath); - } - } - } - - } - -} 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 cb771309..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; @@ -164,11 +164,12 @@ FileFragment.ContainerActivity, OnNavigationListener, OnSslUntrustedCertListener requestPinCode(); } - /// file observer - Intent observer_intent = new Intent(this, FileObserverService.class); - observer_intent.putExtra(FileObserverService.KEY_FILE_CMD, FileObserverService.CMD_INIT_OBSERVED_LIST); - startService(observer_intent); - + /// grant that FileObserverService is watching favourite files + if (savedInstanceState == null) { + Intent initObserversIntent = FileObserverService.makeInitIntent(this); + startService(initObserversIntent); + } + /// Load of saved instance state if(savedInstanceState != null) { mWaitingToPreview = (OCFile) savedInstanceState.getParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW); @@ -1003,53 +1004,58 @@ FileFragment.ContainerActivity, OnNavigationListener, OnSslUntrustedCertListener */ @Override public void onReceive(Context context, Intent intent) { - String uploadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH); - String accountName = intent.getStringExtra(FileUploader.ACCOUNT_NAME); - boolean sameAccount = getAccount() != null && accountName.equals(getAccount().name); - OCFile currentDir = getCurrentDir(); - boolean isDescendant = (currentDir != null) && (uploadedRemotePath != null) && - (uploadedRemotePath.startsWith(currentDir.getRemotePath())); - - if (sameAccount && isDescendant) { - refreshListOfFilesFragment(); - } - - boolean uploadWasFine = intent.getBooleanExtra(FileUploader.EXTRA_UPLOAD_RESULT, false); - boolean renamedInUpload = getFile().getRemotePath(). - equals(intent.getStringExtra(FileUploader.EXTRA_OLD_REMOTE_PATH)); - boolean sameFile = getFile().getRemotePath().equals(uploadedRemotePath) || - renamedInUpload; - FileFragment details = getSecondFragment(); - boolean detailFragmentIsShown = (details != null && - details instanceof FileDetailFragment); - - if (sameAccount && sameFile && detailFragmentIsShown) { - if (uploadWasFine) { - setFile(getStorageManager().getFileByPath(uploadedRemotePath)); - } - if (renamedInUpload) { - String newName = (new File(uploadedRemotePath)).getName(); - Toast msg = Toast.makeText( - context, - String.format( - getString(R.string.filedetails_renamed_in_upload_msg), - newName), - Toast.LENGTH_LONG); - msg.show(); + try { + String uploadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH); + String accountName = intent.getStringExtra(FileUploader.ACCOUNT_NAME); + boolean sameAccount = getAccount() != null && accountName.equals(getAccount().name); + OCFile currentDir = getCurrentDir(); + boolean isDescendant = (currentDir != null) && (uploadedRemotePath != null) && + (uploadedRemotePath.startsWith(currentDir.getRemotePath())); + + if (sameAccount && isDescendant) { + refreshListOfFilesFragment(); } - if (uploadWasFine || getFile().fileExists()) { - ((FileDetailFragment)details).updateFileDetails(false, true); - } else { - cleanSecondFragment(); + + boolean uploadWasFine = intent.getBooleanExtra(FileUploader.EXTRA_UPLOAD_RESULT, false); + boolean renamedInUpload = getFile().getRemotePath(). + equals(intent.getStringExtra(FileUploader.EXTRA_OLD_REMOTE_PATH)); + boolean sameFile = getFile().getRemotePath().equals(uploadedRemotePath) || + renamedInUpload; + FileFragment details = getSecondFragment(); + boolean detailFragmentIsShown = (details != null && + details instanceof FileDetailFragment); + + if (sameAccount && sameFile && detailFragmentIsShown) { + if (uploadWasFine) { + setFile(getStorageManager().getFileByPath(uploadedRemotePath)); + } + if (renamedInUpload) { + String newName = (new File(uploadedRemotePath)).getName(); + Toast msg = Toast.makeText( + context, + String.format( + getString(R.string.filedetails_renamed_in_upload_msg), + newName), + Toast.LENGTH_LONG); + msg.show(); + } + if (uploadWasFine || getFile().fileExists()) { + ((FileDetailFragment)details).updateFileDetails(false, true); + } else { + cleanSecondFragment(); + } + + // Force the preview if the file is an image + if (uploadWasFine && PreviewImageFragment.canBePreviewed(getFile())) { + startImagePreview(getFile()); + } // TODO what about other kind of previews? } - // Force the preview if the file is an image - if (uploadWasFine && PreviewImageFragment.canBePreviewed(getFile())) { - startImagePreview(getFile()); - } // TODO what about other kind of previews? + } finally { + if (intent != null) { + removeStickyBroadcast(intent); + } } - - removeStickyBroadcast(intent); } @@ -1065,23 +1071,28 @@ FileFragment.ContainerActivity, OnNavigationListener, OnSslUntrustedCertListener private class DownloadFinishReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - boolean sameAccount = isSameAccount(context, intent); - String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH); - boolean isDescendant = isDescendant(downloadedRemotePath); - - if (sameAccount && isDescendant) { - refreshListOfFilesFragment(); - refreshSecondFragment(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false)); - } - - if (mWaitingToSend != null) { - mWaitingToSend = getStorageManager().getFileByPath(mWaitingToSend.getRemotePath()); // Update the file to send - if (mWaitingToSend.isDown()) { - sendDownloadedFile(); + try { + boolean sameAccount = isSameAccount(context, intent); + String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH); + boolean isDescendant = isDescendant(downloadedRemotePath); + + if (sameAccount && isDescendant) { + refreshListOfFilesFragment(); + refreshSecondFragment(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false)); + } + + if (mWaitingToSend != null) { + mWaitingToSend = getStorageManager().getFileByPath(mWaitingToSend.getRemotePath()); // Update the file to send + if (mWaitingToSend.isDown()) { + sendDownloadedFile(); + } } - } - removeStickyBroadcast(intent); + } finally { + if (intent != null) { + removeStickyBroadcast(intent); + } + } } private boolean isDescendant(String downloadedRemotePath) { diff --git a/src/com/owncloud/android/ui/fragment/FileDetailFragment.java b/src/com/owncloud/android/ui/fragment/FileDetailFragment.java index 768275cd..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; @@ -271,18 +271,15 @@ public class FileDetailFragment extends FileFragment implements OnClickListener file.setKeepInSync(cb.isChecked()); mContainerActivity.getStorageManager().saveFile(file); - /// register the OCFile instance in the observer service to monitor local updates; - /// if necessary, the file is download - Intent intent = new Intent(getActivity().getApplicationContext(), - FileObserverService.class); - intent.putExtra(FileObserverService.KEY_FILE_CMD, - (cb.isChecked()? - FileObserverService.CMD_ADD_OBSERVED_FILE: - FileObserverService.CMD_DEL_OBSERVED_FILE)); - intent.putExtra(FileObserverService.KEY_CMD_ARG_FILE, file); - intent.putExtra(FileObserverService.KEY_CMD_ARG_ACCOUNT, mAccount); - getActivity().startService(intent); + /// register the OCFile instance in the observer service to monitor local updates + Intent observedFileIntent = FileObserverService.makeObservedFileIntent( + getActivity(), + file, + mAccount, + cb.isChecked()); + getActivity().startService(observedFileIntent); + /// immediate content synchronization if (file.keepInSync()) { mContainerActivity.getFileOperationsHelper().syncFile(getFile()); }