diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d475c3151a9d7a4ce904130ba6d0ba5c5f1caa8d..30859edd891a332cd88abebce535f0d27c95421d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "registry.gitlab.e.foundation:5000/e/apps/docker-android-apps-cicd:latest" +image: "registry.gitlab.e.foundation/e/os/docker-android-apps-cicd:latest" stages: - test diff --git a/app/build.gradle b/app/build.gradle index c65b01d763b0d28230f03af8e2f0b82c8c54df46..e1ec7cf1f328351a27a6b23fbe9a249dc46bdaa9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ def getTestProp(String propName) { android { - compileSdkVersion 28 + compileSdkVersion 31 defaultConfig { applicationId "foundation.e.drive" minSdkVersion 26 @@ -53,32 +53,28 @@ android { //includeAndroidResources = true } } - } dependencies { + api project(':NextcloudLib') implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:26.1.0' - - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test:rules:1.0.2' - androidTestImplementation 'androidx.annotation:annotation:1.3.0' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' api 'androidx.annotation:annotation:1.3.0' - api project(':NextcloudLib') + implementation 'androidx.core:core:1.6.0' - //start to add lib for test - 1/4/21 - //@TODO: add junit runner as lib for testImplementation + def work_version = "2.7.1" + // (Java only) + implementation "androidx.work:work-runtime:$work_version" - testImplementation 'com.android.support.test:runner:1.0.2' - testImplementation 'com.android.support.test:rules:1.0.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.annotation:annotation:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation 'junit:junit:4.12' + + testImplementation 'androidx.test:runner:1.4.0' + testImplementation 'androidx.test:rules:1.4.0' testImplementation 'junit:junit:4.12' - //testImplementation 'org.robolectric:robolectric:4.4' //need AndroidX - testImplementation "org.robolectric:robolectric:3.8" + testImplementation 'org.robolectric:robolectric:4.4' testImplementation('org.mockito:mockito-inline:3.4.0') - - //testImplementation Libs.AndroidX.Test.archCoreTesting //TODO: replace by not android X version - //implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' - androidTestImplementation 'junit:junit:4.12' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be871995566a3c5de7fc4bbb8df3365b36fc9856..d2d8002ad1efd6a93e64efcdbd7259c904ef9e8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ http://www.gnu.org/licenses/gpl.html + + - - + - - - - - - diff --git a/app/src/main/java/foundation/e/drive/EdriveApplication.java b/app/src/main/java/foundation/e/drive/EdriveApplication.java index de7bdcf7f1a21e588e0d84f882d62c419f3d3e62..a8f39f238e6594391b2f74f5f0d58481f18c222b 100644 --- a/app/src/main/java/foundation/e/drive/EdriveApplication.java +++ b/app/src/main/java/foundation/e/drive/EdriveApplication.java @@ -11,13 +11,20 @@ package foundation.e.drive; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.os.Build; +import android.os.Environment; import android.util.Log; +import foundation.e.drive.FileObservers.FileEventListener; +import foundation.e.drive.FileObservers.RecursiveFileObserver; +import foundation.e.drive.services.SynchronizationService; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.utils.JobUtils; /** * Class representing the eDrive application. @@ -26,42 +33,59 @@ import foundation.e.drive.utils.JobUtils; public class EdriveApplication extends Application { private static final String TAG = "EdriveApplication"; + private RecursiveFileObserver mFileObserver = null; + private FileEventListener fileEventListener; @Override public void onCreate() { super.onCreate(); - + fileEventListener = new FileEventListener(getApplicationContext()); Log.i(TAG, "Starting"); - resetOperationManagerSetting(); + + final String pathForObserver = Environment.getExternalStorageDirectory().getAbsolutePath(); + mFileObserver = new RecursiveFileObserver(getApplicationContext(), pathForObserver, fileEventListener); SharedPreferences prefs = getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + CommonUtils.createNotificationChannel(getApplicationContext()); + if (prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null) { - scheduleScannerJob(); + Log.d(TAG, "Account already registered"); + startRecursiveFileObserver(); + + Intent SynchronizationServiceIntent = new Intent(getApplicationContext(), SynchronizationService.class); + startService(SynchronizationServiceIntent); + } else { Account mAccount = CommonUtils.getAccount(getString(R.string.eelo_account_type), AccountManager.get(this)); - if (mAccount != null) { - String accountName = mAccount.name; - String accountType = mAccount.type; + if (mAccount == null) {return ;} - prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName) - .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) - .apply(); + String accountName = mAccount.name; + String accountType = mAccount.type; - scheduleScannerJob(); - } + prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName) + .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) + .apply(); } } - private void scheduleScannerJob() { - if (!JobUtils.isScannerJobRegistered(this)) { - JobUtils.scheduleScannerJob(this); + /** + * Start Recursive FileObserver if not already watching + */ + public void startRecursiveFileObserver(){ + if (!mFileObserver.isWatching()) { + fileEventListener.bindToSynchronizationService(); + mFileObserver.startWatching(); + Log.d(TAG, "Starting RecursiveFileObserver on root folder"); + } + else { + Log.w(TAG, "RecursiveFileObserver (for media's root folder) was already running"); } } - private void resetOperationManagerSetting() { - getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE).edit() - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false) - .apply(); + public void stopRecursiveFileObserver(){ + mFileObserver.stopWatching(); + fileEventListener.unbindFromSynchronizationService(); + Log.d(TAG, "RecursiveFileObserver on root folder stops watching "); } @Override diff --git a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java new file mode 100644 index 0000000000000000000000000000000000000000..a4a0b634c9acea0ce2ed8c56312a43394a179591 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java @@ -0,0 +1,178 @@ +/* + * Copyright © Narinder Rana (/e/ foundation). + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.FileObservers; + +import static foundation.e.drive.models.SyncRequest.Type.UPLOAD; + +import android.content.Context; +import android.content.Intent; +import android.os.FileObserver; +import android.util.Log; + +import com.owncloud.android.lib.resources.files.FileUtils; + +import java.io.File; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.services.SynchronizationService; +import foundation.e.drive.utils.CommonUtils; +import foundation.e.drive.utils.SynchronizationServiceConnection; + +/** + * @author Narinder Rana + * @author vincent Bourgmayer + */ +public class FileEventListener { + private static final String TAG = FileEventListener.class.getSimpleName(); + + private final Context appContext; + private final SynchronizationServiceConnection serviceConnection = new SynchronizationServiceConnection(); + + public FileEventListener(Context applicationContext) { + this.appContext = applicationContext; + } + + public void onEvent(int event, File file) { + if (event == FileObserver.DELETE) { + if (file.isDirectory()) { + handleDirectoryDelete(file); + } else { + handleFileDelete(file); + } + } else if (event == FileObserver.CLOSE_WRITE) { + + if (file.isDirectory()) { + handleDirectoryCloseWrite(file); + } else { + handleFileCloseWrite(file); + } + } else if (event == FileObserver.MOVE_SELF){ + Log.d(TAG, file.getAbsolutePath() + " has been moved. Not handled yet"); + } + } + + private void sendSyncRequestToSynchronizationService(SyncRequest request) { + Log.d(TAG, "Sending a SyncRequest for " + request.getSyncedFileState().getName()); + if (serviceConnection.isBoundToSynchronizationService()) { + serviceConnection.getSynchronizationService().queueOperation(request); + serviceConnection.getSynchronizationService().startSynchronization(); + }else{ + Log.w(TAG, "Impossible to send SyncRequest. FileEventListener is not bound to SynchronizationService"); + } + } + + private void handleDirectoryCloseWrite(File directory) { + final String fileLocalPath = CommonUtils.getLocalPath(directory); + Log.d(TAG, "handleDirectoryCloseWrite(" + fileLocalPath + ")"); + SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext); + if (folder == null) { //it's a directory creation + final String parentPath = CommonUtils.getLocalPath(directory.getParentFile()); + SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext); + if (parentFolder != null) { //if parent is in the DB + folder = new SyncedFolder(parentFolder, directory.getName() + FileUtils.PATH_SEPARATOR, directory.lastModified(), ""); + DbHelper.insertSyncedFolder(folder, appContext); + } + } else { //It's a directory update + folder.setLastModified(directory.lastModified()); + DbHelper.updateSyncedFolder(folder, appContext); + } + } + + private void handleDirectoryDelete(File directory) { + final String fileLocalPath = CommonUtils.getLocalPath(directory); + Log.d(TAG, "handleDirectoryDelete("+fileLocalPath+")"); + SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext); + if (folder == null) { + //look for parent + final String parentPath = CommonUtils.getLocalPath(directory.getParentFile()); + SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext); + if (parentFolder != null ) { //if parent is in the DB + folder = new SyncedFolder(parentFolder, directory.getName()+ FileUtils.PATH_SEPARATOR, directory.lastModified(), ""); + folder.setEnabled(false); + DbHelper.insertSyncedFolder(folder, appContext); + } + } else { //If already in DB + if (folder.isEnabled()) { + folder.setEnabled(false); + DbHelper.updateSyncedFolder(folder, appContext); + } + } + } + + private void handleFileCloseWrite(File file) { + final String fileLocalPath = CommonUtils.getLocalPath(file); + Log.d(TAG, "handleFileCloseWrite("+fileLocalPath+")"); + SyncRequest request = null; + SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true); + + if (fileState == null) { //New file discovered + final String parentPath = CommonUtils.getLocalPath(file.getParentFile()); + SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext); + if (parentFolder == null || !parentFolder.isEnabled()) { + Log.w(TAG, "Won't send sync request: no parent are known for new file: "+file.getName()); + return; + } + int scannableValue = 0; + if (parentFolder.isEnabled()) { + if (parentFolder.isScanRemote()) scannableValue++; + if (parentFolder.isScanLocal()) scannableValue += 2; + } + + final String remotePath = parentFolder.getRemoteFolder()+file.getName(); + fileState = new SyncedFileState(-1, file.getName(), CommonUtils.getLocalPath(file), remotePath, "", 0L, parentFolder.getId(), parentFolder.isMediaType(), scannableValue); + int storedId = DbHelper.manageSyncedFileStateDB(fileState, "INSERT", appContext); + if (storedId > 0) { + fileState.setId(storedId); + request = new SyncRequest(fileState, UPLOAD); + } else { + Log.w(TAG, "New File " + file.getName() + " observed but impossible to insert it in DB"); + } + } else { //File update + if (fileState.getScannable() > 1) { + request = new SyncRequest(fileState, UPLOAD); + } + } + if (request != null) { + sendSyncRequestToSynchronizationService(request); + } + } + + private void handleFileDelete(File file) { + final String fileLocalPath = CommonUtils.getLocalPath(file); + Log.d(TAG, "handleFileDelete("+fileLocalPath+")"); + SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true); + if (fileState == null) { + Log.d(TAG, "Ignore event because file is not already in database"); + return; + } + + //If already in DB + if (fileState.getScannable() > 0) { + fileState.setScannable(0); + DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", appContext); + } + } + + public void unbindFromSynchronizationService(){ + if(serviceConnection.isBoundToSynchronizationService()) + appContext.unbindService(serviceConnection); + else + Log.w(TAG, "Not bound to SynchronizationService: can't unbind."); + } + + public void bindToSynchronizationService(){ + Log.d(TAG, "bindToSynchronizationService()"); + final Intent SynchronizationServiceIntent = new Intent(appContext, SynchronizationService.class); + appContext.bindService(SynchronizationServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + } +} diff --git a/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java b/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java new file mode 100644 index 0000000000000000000000000000000000000000..1e4a98741522f50a92de2de4b61db631cf237a66 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java @@ -0,0 +1,174 @@ +/* + * Copyright © Narinder Rana (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.FileObservers; + +import android.content.Context; +import android.os.FileObserver; + +import java.io.File; +import java.io.FileFilter; +import java.util.HashMap; +import java.util.List; +import java.util.Stack; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; + +/** + * @author Narinder Rana + * @author Vincent Bourgmayer + */ +public class RecursiveFileObserver extends FileObserver { + private final HashMap observers = new HashMap<>(); + private static final FileFilter WATCHABLE_DIRECTORIES_FILTER = new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory() && !file.getName().startsWith("."); + } + }; + + private boolean watching = false; + private final Context applicationContext; + private String path; + private int mask; + private FileEventListener listener; + + public RecursiveFileObserver(Context applicationContext, String path, FileEventListener listener) { + this(applicationContext, path, ALL_EVENTS, listener); + } + + public RecursiveFileObserver(Context applicationContext, String path, int mask, FileEventListener listener) { + super(path, mask); + this.path = path; + this.mask = mask | FileObserver.CREATE | FileObserver.DELETE_SELF; + this.listener = listener; + this.applicationContext = applicationContext; + } + + + @Override + public void onEvent(int event, String path) { + File file; + if (path == null) { + file = new File(this.path); + } else { + file = new File(this.path, path); + } + + notify(event, file); + } + + private void notify(int event, File file) { + if (listener != null) { + listener.onEvent(event & FileObserver.ALL_EVENTS, file); + } + } + + @Override + public void startWatching() { + Stack stack = new Stack<>(); + + List mSyncedFolders = DbHelper.getAllSyncedFolders(applicationContext); + if (!mSyncedFolders.isEmpty()){ + for (SyncedFolder syncedFolder:mSyncedFolders){ + stack.push(syncedFolder.getLocalFolder()); + stack.push(syncedFolder.getRemoteFolder()); + } + watching = true; + } + + // Recursively watch all child directories + while (!stack.empty()) { + String parent = stack.pop(); + startWatching(parent); + + File path = new File(parent); + File[] files = path.listFiles(WATCHABLE_DIRECTORIES_FILTER); + if (files != null) { + for (File file : files) { + stack.push(file.getAbsolutePath()); + } + } + } + } + + /** + * Start watching a single file + * @param path + */ + private void startWatching(String path) { + synchronized (observers) { + FileObserver observer = observers.remove(path); + if (observer != null) { + observer.stopWatching(); + } + observer = new SingleFileObserver(path, mask); + observer.startWatching(); + observers.put(path, observer); + } + } + + @Override + public void stopWatching() { + for (FileObserver observer : observers.values()) { + observer.stopWatching(); + } + observers.clear(); + watching = false; + } + + /** + * Stop watching a single file + * @param path + */ + private void stopWatching(String path) { + synchronized (observers) { + FileObserver observer = observers.remove(path); + if (observer != null) { + observer.stopWatching(); + } + } + } + + public boolean isWatching(){ + return watching; + } + + private class SingleFileObserver extends FileObserver { + private String filePath; + + public SingleFileObserver(String path, int mask) { + super(path, mask); + filePath = path; + } + + @Override + public void onEvent(int event, String path) { + File file; + if (path == null) { + file = new File(filePath); + } else { + file = new File(filePath, path); + } + + switch (event & FileObserver.ALL_EVENTS) { + case DELETE_SELF: + RecursiveFileObserver.this.stopWatching(filePath); + break; + case CREATE: + if (WATCHABLE_DIRECTORIES_FILTER.accept(file)) { + RecursiveFileObserver.this.startWatching(file.getAbsolutePath()); + } + break; + } + + RecursiveFileObserver.this.notify(event, file); + } + } +} diff --git a/app/src/main/java/foundation/e/drive/database/DbHelper.java b/app/src/main/java/foundation/e/drive/database/DbHelper.java index d1f94ee781423297c4ba95af632a4a9e9d625876..383c5fdb4c26f75bf149943d5aac80d5df431d40 100644 --- a/app/src/main/java/foundation/e/drive/database/DbHelper.java +++ b/app/src/main/java/foundation/e/drive/database/DbHelper.java @@ -24,7 +24,7 @@ import foundation.e.drive.models.SyncedFileState; */ public final class DbHelper extends SQLiteOpenHelper { final private static String TAG = DbHelper.class.getSimpleName(); //Tag for log - private static final int DATABASE_VERSION = 19; //20/09/2018 + private static final int DATABASE_VERSION = 20; //16/03/2022 public static final String DATABASE_NAME = "eelo_drive.db"; /** @@ -53,18 +53,22 @@ public final class DbHelper extends SQLiteOpenHelper { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.i(TAG, "onUpgrade(db, "+oldVersion+", "+newVersion+")"); - if(oldVersion < 19){ - try { + try { + if (oldVersion < 19) { db.execSQL(SyncedFolderContract.UPDATE_TABLE_TO_VERSION_19); db.execSQL(SyncedFileStateContract.UPDATE_TABLE_TO_VERSION_19); - db.execSQL(SyncedFileStateContract.UPDATE_MEDIA_DATA_TO_VERSION_19); db.execSQL(SyncedFileStateContract.UPDATE_SETTINGS_DATA_TO_VERSION_19); db.execSQL(SyncedFolderContract.UPDATE_MEDIA_DATA_TO_VERSION_19); db.execSQL(SyncedFolderContract.UPDATE_SETTINGS_DATA_TO_VERSION_19); - }catch(Exception e){ - Log.e(TAG, toString()); } + if (oldVersion < 20) { + db.execSQL(SyncedFileStateContract.UPDATE_TABLE_TO_VERSION_20); + db.execSQL(SyncedFileStateContract.UPDATE_MEDIA_DATA_TO_VERSION_20); + db.execSQL(SyncedFileStateContract.UPDATE_SETTINGS_DATA_TO_VERSION_20); + } + } catch(Exception e) { + Log.e(TAG, toString()); } } @@ -116,7 +120,6 @@ public final class DbHelper extends SQLiteOpenHelper { } dao.close(); } - return result; } @@ -178,6 +181,19 @@ public final class DbHelper extends SQLiteOpenHelper { return result; } + + public static int updateSyncedFolder(SyncedFolder syncedFolder, Context context) { + int result = -1; + //Connect to DB + SyncedFolderDAO dao = openSyncedFolderDAO(context, true); + if (dao == null){ + return result; + } + result = dao.update( syncedFolder ); + dao.close(); + return result; + } + /** * Load SyncedFolder's from DB * @param context app or service activity @@ -243,6 +259,16 @@ public final class DbHelper extends SQLiteOpenHelper { return result; } + public static SyncedFolder getSyncedFolderByLocalPath(String localPath, Context context){ + SyncedFolderDAO dao = openSyncedFolderDAO(context, true); + if (dao == null) { + return null; + } + SyncedFolder syncedFolder = dao.getSyncedFolderByLocalPath(localPath); + dao.close(); + return syncedFolder; + } + /** * Set the lastModified value of SyncedFolder to 1. * The goal is to force to rescan it next time. diff --git a/app/src/main/java/foundation/e/drive/database/SyncedFileStateContract.java b/app/src/main/java/foundation/e/drive/database/SyncedFileStateContract.java index 3d0f69cff904c80d6e663b79febce3189955e6ed..2b91c8c30470cc554f11bab09ce8c847b9e0800d 100644 --- a/app/src/main/java/foundation/e/drive/database/SyncedFileStateContract.java +++ b/app/src/main/java/foundation/e/drive/database/SyncedFileStateContract.java @@ -25,6 +25,7 @@ class SyncedFileStateContract implements BaseColumns{ static final String LOCAL_LAST_MODIFIED = "local_last_modified"; static final String SYNCEDFOLDER_ID = "synced_folder_id"; static final String IS_MEDIA_TYPE = "is_media_type"; + static final String SCANNABLE = "scannable"; static final String SQL_CREATE_TABLE_SYNCEDFILESTATE = "CREATE TABLE "+TABLE_NAME+" ( " @@ -36,6 +37,7 @@ class SyncedFileStateContract implements BaseColumns{ +LOCAL_LAST_MODIFIED+" INTEGER, " + SYNCEDFOLDER_ID +" INTEGER, " +IS_MEDIA_TYPE+" BOOLEAN," + +SCANNABLE+" INTEGER, " +"CONSTRAINT synced_unicity_constraint UNIQUE (" +FILE_NAME+", " +LOCAL_PATH+", " @@ -45,8 +47,8 @@ class SyncedFileStateContract implements BaseColumns{ static final String SQL_DELETE_TABLE_SYNCEDFILESTATE = " DROP TABLE IF EXISTS " + TABLE_NAME; + //Update for version 18 and lower static final String UPDATE_TABLE_TO_VERSION_19 = "ALTER TABLE "+TABLE_NAME+" ADD COLUMN "+IS_MEDIA_TYPE+" BOOLEAN;"; - static final String UPDATE_MEDIA_DATA_TO_VERSION_19 = "UPDATE "+TABLE_NAME+ " SET "+IS_MEDIA_TYPE+" = 1 WHERE "+ REMOTE_PATH+" LIKE \"/Photos/%\" OR "+ @@ -60,4 +62,11 @@ class SyncedFileStateContract implements BaseColumns{ static final String UPDATE_SETTINGS_DATA_TO_VERSION_19 = "UPDATE "+TABLE_NAME+ " SET "+IS_MEDIA_TYPE+" = 0 WHERE "+REMOTE_PATH+" LIKE \"/Devices/%\";"; + //update for version 19 and lower + static final String UPDATE_TABLE_TO_VERSION_20 = "ALTER TABLE "+TABLE_NAME+" ADD COLUMN "+SCANNABLE+" INTEGER;"; + static final String UPDATE_MEDIA_DATA_TO_VERSION_20 = "UPDATE "+TABLE_NAME+" SET "+SCANNABLE+" = 3 WHERE" + + IS_MEDIA_TYPE+" = 1 ;" ; + static final String UPDATE_SETTINGS_DATA_TO_VERSION_20 = "UPDATE "+TABLE_NAME+" SET "+SCANNABLE+" = 2 WHERE" + + IS_MEDIA_TYPE+" = 0 ;" ; + } diff --git a/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java b/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java index 9186581314def460b0ee5fae97ac75a0f1476953..9864b9de22b6628c2285305d1b2a24157acb6608 100644 --- a/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java +++ b/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; import foundation.e.drive.models.SyncedFileState; + +import static foundation.e.drive.database.SyncedFileStateContract.SCANNABLE; import static foundation.e.drive.database.SyncedFileStateContract.TABLE_NAME; import static foundation.e.drive.database.SyncedFileStateContract.FILE_NAME; import static foundation.e.drive.database.SyncedFileStateContract.IS_MEDIA_TYPE; @@ -39,16 +41,6 @@ class SyncedFileStateDAO { private SQLiteDatabase mDB; private final DbHelper mHelper; - /*private final String[] allColumns = { SyncedFileStateContract._ID, - SyncedFileStateContract.FILE_NAME, - SyncedFileStateContract.LOCAL_PATH, - SyncedFileStateContract.REMOTE_PATH, - SyncedFileStateContract.LAST_ETAG, - - SyncedFileStateContract.LOCAL_LAST_MODIFIED, - SyncedFileStateContract.SYNCEDFOLDER_ID, - SyncedFileStateContract.IS_MEDIA_TYPE - };*/ SyncedFileStateDAO(Context context){ this.mHelper = new DbHelper(context); @@ -75,7 +67,7 @@ class SyncedFileStateDAO { * @param syncedFileState syncedFileState to convert * @return a ContentValues object */ - private ContentValues toContentValues(SyncedFileState syncedFileState){ + private ContentValues toContentValues(SyncedFileState syncedFileState) { ContentValues values = new ContentValues(); values.put( FILE_NAME, syncedFileState.getName() ); values.put( LOCAL_PATH, syncedFileState.getLocalPath() ); @@ -84,6 +76,7 @@ class SyncedFileStateDAO { values.put( LOCAL_LAST_MODIFIED, syncedFileState.getLocalLastModified() ); values.put( SYNCEDFOLDER_ID, syncedFileState.getSyncedFolderId() ); values.put( IS_MEDIA_TYPE, syncedFileState.isMediaType() ? 1 : 0 ); + values.put( SCANNABLE, syncedFileState.getScannable()); return values; } @@ -142,7 +135,7 @@ class SyncedFileStateDAO { * @param syncedFolderids List of path to filter. Need to be directory path * @return List List of SyncedFileState filtered on syncedFolder ID. */ - List getBySyncedFolderID(List syncedFolderids){ + List getBySyncedFolderID(List syncedFolderids) { String query = "Select " +SyncedFileStateContract._ID+", " +FILE_NAME+", " @@ -151,10 +144,11 @@ class SyncedFileStateDAO { +LAST_ETAG+", " +LOCAL_LAST_MODIFIED+", " + SYNCEDFOLDER_ID+", " - + IS_MEDIA_TYPE + + IS_MEDIA_TYPE+", " + + SCANNABLE +" FROM " +TABLE_NAME; - if(syncedFolderids.size() > 0) { + if (syncedFolderids.size() > 0) { query+=" WHERE "; for (int i = -1, idsSize = syncedFolderids.size(); ++i < idsSize; ) { @@ -181,8 +175,8 @@ class SyncedFileStateDAO { * @param path local path or remote path * @return SyncedFileState obtain by the query or null if none has been found */ - SyncedFileState getByPath(String path, boolean isLocalPath){ - String query = "Select " + SyncedFileState getByPath(String path, boolean isLocalPath) { + String query = "Select " +SyncedFileStateContract._ID+", " +FILE_NAME+", " +LOCAL_PATH+", " @@ -190,10 +184,11 @@ class SyncedFileStateDAO { +LAST_ETAG+", " +LOCAL_LAST_MODIFIED+", " + SYNCEDFOLDER_ID+", " - + IS_MEDIA_TYPE+ + + IS_MEDIA_TYPE+", " + + SCANNABLE+ " FROM " +TABLE_NAME+" WHERE "; - if(isLocalPath) + if (isLocalPath) query+=LOCAL_PATH ; else query+=REMOTE_PATH ; @@ -203,7 +198,7 @@ class SyncedFileStateDAO { Cursor cursor = mDB.rawQuery(query, null); cursor.moveToFirst(); SyncedFileState syncedFileState = null; - if( !cursor.isAfterLast()) { + if ( !cursor.isAfterLast()) { syncedFileState = cursorToSyncedFileState(cursor); } cursor.close(); @@ -224,7 +219,8 @@ class SyncedFileStateDAO { cursor.getString(4 ),// last Etag cursor.getLong(5 ),//Local last modified cursor.getLong(6 ), //SyncedFolderID - (cursor.getInt(7) == 1 ) //is Media Type + (cursor.getInt(7) == 1), //is Media Type + cursor.getInt(8) //scannable ); } } diff --git a/app/src/main/java/foundation/e/drive/database/SyncedFolderDAO.java b/app/src/main/java/foundation/e/drive/database/SyncedFolderDAO.java index 8af54a1f40873e8dfb914799ef3a71968da6242a..c70be5e51f9177c62b64b83e2275caf8e5c27cb1 100644 --- a/app/src/main/java/foundation/e/drive/database/SyncedFolderDAO.java +++ b/app/src/main/java/foundation/e/drive/database/SyncedFolderDAO.java @@ -29,6 +29,8 @@ import static foundation.e.drive.database.SyncedFolderContract.SCANREMOTE; import static foundation.e.drive.database.SyncedFolderContract.IS_MEDIA_TYPE; import static foundation.e.drive.database.SyncedFolderContract.TABLE_NAME; +import com.owncloud.android.lib.resources.files.FileUtils; + /** * @author Vincent Bourgmayer * Source: https://vogella.developpez.com/tutoriels/android/utilisation-base-donnees-sqlite/ @@ -174,6 +176,17 @@ class SyncedFolderDAO { return result; } + SyncedFolder getSyncedFolderByLocalPath(String localPath){ + Cursor cursor = mDB.query(TABLE_NAME, allColumns, LOCAL_PATH+" like \""+localPath+ FileUtils.PATH_SEPARATOR+"\"", new String[0], null, null, null); + cursor.moveToFirst(); + SyncedFolder result = null; + if ( !cursor.isAfterLast() ) { + result = cursorToSyncedFolder(cursor); + } + cursor.close(); + return result; + } + /** * Create a syncedFolder object from data in cursor. * @param cursor cursor containing data to build syncedFolder diff --git a/app/src/main/java/foundation/e/drive/jobs/ScannerJob.java b/app/src/main/java/foundation/e/drive/jobs/ScannerJob.java deleted file mode 100644 index 878f554b882119cefd4795e8131d2660ddcba33a..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/jobs/ScannerJob.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.jobs; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.Intent; -import android.content.IntentFilter; -import android.util.Log; -import foundation.e.drive.receivers.ScreenOffReceiver; -import foundation.e.drive.services.ObserverService; -import foundation.e.drive.utils.CommonUtils; - -/** - * @author Vincent Bourgmayer - */ -public class ScannerJob extends JobService { - final private String TAG = ScannerJob.class.getSimpleName(); //Tag for log - - @Override - public boolean onStartJob(JobParameters params) { - Log.i(TAG, "onStartJob()"); - - Log.d(TAG, "RegisterReceiver: screenOffReceiver"); - IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_SCREEN_OFF); - getApplicationContext().registerReceiver(ScreenOffReceiver.getInstance(), filter); - - Intent observerServiceIntent = new Intent(this, ObserverService.class); - this.startService(observerServiceIntent); - jobFinished(params, false); - return true; - } - - /** - * - * @param params - * @return default return... ? - */ - @Override - public boolean onStopJob(JobParameters params) { - Log.i(TAG, "onStopJob"); - boolean unregisteredReceiver = CommonUtils.unregisterScreenOff(getApplicationContext()); - Intent observerServiceIntent = new Intent(this, ObserverService.class); - this.stopService(observerServiceIntent); - return false; - } -} diff --git a/app/src/main/java/foundation/e/drive/models/DownloadRequest.java b/app/src/main/java/foundation/e/drive/models/DownloadRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..7403eea5b8fa714963682429bdcef7a8a4754de9 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/models/DownloadRequest.java @@ -0,0 +1,26 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.models; + +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +/** + * @author vincent Bourgmayer + */ +public class DownloadRequest extends SyncRequest { + private final RemoteFile remoteFile; + + public DownloadRequest (RemoteFile remoteFile, SyncedFileState syncedFileState) { + super(syncedFileState, Type.DOWNLOAD); + this.remoteFile = remoteFile; + } + + public RemoteFile getRemoteFile() { + return remoteFile; + } +} diff --git a/app/src/main/java/foundation/e/drive/models/FileObserver.java b/app/src/main/java/foundation/e/drive/models/FileObserver.java new file mode 100644 index 0000000000000000000000000000000000000000..6407b8a66b1345c6b00ec03cf833dde3f97c5ee1 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/models/FileObserver.java @@ -0,0 +1,33 @@ +/* + * Copyright © Narinder Rana (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.models; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +/** + * @author Narinder Rana + */ +public class FileObserver implements Serializable { + + private List files; + public FileObserver(List files) { + this.files = files; + + } + + public List getFiles() { + return files; + } + + public void setFiles(List files) { + this.files = files; + } +} diff --git a/app/src/main/java/foundation/e/drive/models/SyncRequest.java b/app/src/main/java/foundation/e/drive/models/SyncRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..e2412cdf2fa5d9fb550cd08364174c85a2ae205f --- /dev/null +++ b/app/src/main/java/foundation/e/drive/models/SyncRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.models; + +import androidx.annotation.Nullable; + +public class SyncRequest { + public enum Type { UPLOAD, DOWNLOAD, REMOTE_DELETE}; + + private final SyncedFileState syncedFileState; + + private final Type operationType; + + public SyncRequest(SyncedFileState syncedFileState, Type operationType) { + this.syncedFileState = syncedFileState; + this.operationType = operationType; + } + + public Type getOperationType() { + return operationType; + } + + public SyncedFileState getSyncedFileState() { + return syncedFileState; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof SyncRequest) { + return (syncedFileState.getId() == ((SyncRequest) obj).syncedFileState.getId() ); + } + return super.equals(obj); + } +} diff --git a/app/src/main/java/foundation/e/drive/models/SyncedFileState.java b/app/src/main/java/foundation/e/drive/models/SyncedFileState.java index 42c76a102482a4681845082e752f1edf57211f3d..f3b842b33d8cca29b3232913882330906aa4f8e2 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncedFileState.java +++ b/app/src/main/java/foundation/e/drive/models/SyncedFileState.java @@ -17,7 +17,10 @@ import android.os.Parcelable; */ public class SyncedFileState implements Parcelable { - + public static final int NOT_SCANNABLE=0; + public static final int ECLOUD_SCANNABLE=1; + public static final int DEVICE_SCANNABLE=2; + public static final int ALL_SCANNABLE=3; protected SyncedFileState(){}; //@ToRemove. Test Only. It's to allow to make a mock SyncedFileState Class in test. private int id; @@ -27,7 +30,8 @@ public class SyncedFileState implements Parcelable { private String lastETAG; //Last got Etag for the file private long localLastModified; private long syncedFolderId; - private boolean isMediaType; //if true this is a media synchronisable else it it is a settings synchronisable. + private boolean isMediaType; // true if this is a media synchronisable else it is a settings synchronisable. + private int scannable; /** * Full constructor @@ -39,7 +43,7 @@ public class SyncedFileState implements Parcelable { * @param lastModified Last modified time where local file has changed * @param isMediaType true if its sync as medias or as device/app' settings. */ - public SyncedFileState(int id, String name, String localPath, String remotePath, String etag, long lastModified, long syncedFolderId, boolean isMediaType){ + public SyncedFileState(int id, String name, String localPath, String remotePath, String etag, long lastModified, long syncedFolderId, boolean isMediaType, int scannable){ this.id = id; this.name = name; this.localPath = localPath; @@ -48,6 +52,7 @@ public class SyncedFileState implements Parcelable { this.localLastModified = lastModified; this.syncedFolderId = syncedFolderId; this.isMediaType = isMediaType; + this.scannable = scannable; } protected SyncedFileState(Parcel in) { @@ -59,6 +64,7 @@ public class SyncedFileState implements Parcelable { localLastModified = in.readLong(); syncedFolderId = in.readLong(); isMediaType = in.readByte() != 0; + scannable = in.readInt(); } @Override @@ -71,6 +77,7 @@ public class SyncedFileState implements Parcelable { dest.writeLong(localLastModified); dest.writeLong(syncedFolderId); dest.writeByte((byte) (isMediaType ? 1 : 0)); + dest.writeInt(scannable); } public static final Creator CREATOR = new Creator() { @@ -152,6 +159,24 @@ public class SyncedFileState implements Parcelable { return isMediaType; } + + /** + * Return in which context the file can scan + * @return 0: not scannable. 1: scannable on ecloud. 2: scannable on device. 3: scannable everywhere + */ + public int getScannable() { + return scannable; + } + + /** + * Define in which context the file can be scan + * @param scannable 0: never. 1: on cloud only. 2: on device only. 3: in every context. + */ + public void setScannable(int scannable) { + this.scannable = scannable; + } + + @Override public String toString(){ return "SyncedFileState :" @@ -162,10 +187,10 @@ public class SyncedFileState implements Parcelable { +"\nLast Etag: "+this.lastETAG +"\nLocal last modified: "+this.localLastModified +"\nSyncedFolderId: "+this.syncedFolderId - +"\nisMediaType: "+this.isMediaType; + +"\nisMediaType: "+this.isMediaType + +"\nscannable: "+this.scannable; } - @Override public int describeContents() { return 0; diff --git a/app/src/main/java/foundation/e/drive/operations/ComparableOperation.java b/app/src/main/java/foundation/e/drive/operations/ComparableOperation.java deleted file mode 100644 index 02b8308c0419428754e02e8920fbcd7f3111759f..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/operations/ComparableOperation.java +++ /dev/null @@ -1,18 +0,0 @@ -package foundation.e.drive.operations; - -import com.owncloud.android.lib.common.operations.RemoteOperation; - -public interface ComparableOperation { - - /** - * Say if File affected by operation is a media or a settings - * @return - */ - public boolean isMediaType(); - - /** - * Cast instance to generic remoteOperation Type - * @return RemoteOperation this instance - */ - public RemoteOperation toRemoteOperation(); -} diff --git a/app/src/main/java/foundation/e/drive/operations/CreateInitialFolderRemoteOperation.java b/app/src/main/java/foundation/e/drive/operations/CreateInitialFolderRemoteOperation.java deleted file mode 100644 index 72b837a343873c06cbdf1af74186c1979b947eb2..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/operations/CreateInitialFolderRemoteOperation.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.operations; - -import android.content.Context; -import android.util.Log; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; -import java.io.File; -import foundation.e.drive.database.DbHelper; -import foundation.e.drive.models.SyncedFolder; - -/** - * @author Vincent Bourgmayer - * Perform initial folder creation - */ -public class CreateInitialFolderRemoteOperation extends RemoteOperation { - private static final String TAG = CreateInitialFolderRemoteOperation.class.getSimpleName(); - - private SyncedFolder mSyncedFolder; - private String mRemotePath; - private boolean mCreateFullPath;//should recreate parent path if not exist or not - private final Context mContext; - - public CreateInitialFolderRemoteOperation(SyncedFolder root, boolean createFullPath, Context context) { - super(); - this.mSyncedFolder = root; - this.mRemotePath = root.getRemoteFolder(); - this.mCreateFullPath = createFullPath; - this.mContext = context; - } - - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - File folder = new File(mSyncedFolder.getLocalFolder() ); - if( !folder.exists() ){ - Log.e(TAG, "Local folder doesn't exist, so create it"); - folder.mkdirs(); - } - - //Perfom request - CreateFolderRemoteOperation createFolderOperation = new CreateFolderRemoteOperation(mRemotePath, mCreateFullPath); - RemoteOperationResult createOperationResult = createFolderOperation.execute(client, true); - - if(createOperationResult.isSuccess() || createOperationResult.getCode() == RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS ){ - if(DbHelper.insertSyncedFolder(mSyncedFolder, mContext) >= 0 ) { - Log.d(TAG, "Insertion in DB succeed"); - return new RemoteOperationResult(RemoteOperationResult.ResultCode.OK); - }else { - Log.d(TAG, "insertion of folder in DB failed"); - return new RemoteOperationResult(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS); - } - } - return createOperationResult; - } -} diff --git a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java index cfcb8a79dc06d4e676900e3a3ccaf85a6e49c724..4445239c721e7a0ead86a1264ed77327d9b88cde 100644 --- a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java @@ -9,8 +9,6 @@ package foundation.e.drive.operations; import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; @@ -26,167 +24,115 @@ import foundation.e.drive.utils.CommonUtils; * @author Vincent Bourgmayer * Encapsulate a global download process for a file */ -public class DownloadFileOperation extends RemoteOperation implements ComparableOperation, Parcelable { +public class DownloadFileOperation extends RemoteOperation { private final static String TAG = DownloadFileOperation.class.getSimpleName(); - private final RemoteFile mRFile; - private Context mContext; - private String mTargetPath; + private final RemoteFile remoteFile; + private Context context; + private String targetPath; private int restartCounter =0; - private SyncedFileState mSyncedState; + private SyncedFileState syncedFileState; private String previousEtag; - /** * COnstructor of download operation where syncedFileState is already known * @param remoteFile remote file to Download * @param syncedFileState SyncedFileState corresponding to remote file */ - public DownloadFileOperation(RemoteFile remoteFile, SyncedFileState syncedFileState){ this.mRFile = remoteFile; - this.mSyncedState = syncedFileState; - this.previousEtag = mSyncedState.getLastETAG(); - this.mTargetPath = this.mSyncedState.getLocalPath(); - } - - protected DownloadFileOperation(Parcel in) { - mRFile = in.readParcelable(RemoteFile.class.getClassLoader()); - mTargetPath = in.readString(); - restartCounter = in.readInt(); - mSyncedState = in.readParcelable(SyncedFileState.class.getClassLoader()); - previousEtag = in.readString(); + public DownloadFileOperation(RemoteFile remoteFile, SyncedFileState syncedFileState, Context context) { + this.remoteFile = remoteFile; + this.syncedFileState = syncedFileState; + this.previousEtag = syncedFileState.getLastETAG(); + this.targetPath = syncedFileState.getLocalPath(); + this.context = context; } - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(mRFile, flags); - dest.writeString(mTargetPath); - dest.writeInt(restartCounter); - dest.writeParcelable(mSyncedState, flags); - dest.writeString(previousEtag); - } - - - public static final Creator CREATOR = new Creator() { - @Override - public DownloadFileOperation createFromParcel(Parcel in) { - return new DownloadFileOperation(in); - } - - @Override - public DownloadFileOperation[] newArray(int size) { - return new DownloadFileOperation[size]; - } - }; - - public void setContext(Context context){ - this.mContext = context; - } - - @Override protected RemoteOperationResult run(OwnCloudClient ownCloudClient) { Log.i(TAG, "run(ownCloudClient)"); //get or build synced file equivalent of this.mFile - if(mSyncedState == null || mTargetPath == null || mTargetPath.isEmpty()) { - Log.e(TAG, "mSyncedState or mTargetPath is empty or null. Can't Download in those conditions"); + if (syncedFileState == null || targetPath == null || targetPath.isEmpty()) { + Log.e(TAG, "syncedFileState or targetPath is empty or null. Can't Download in those conditions"); return new RemoteOperationResult(RemoteOperationResult.ResultCode.FORBIDDEN); - }else if(mSyncedState.getId() == -1){ - this.mSyncedState.setId( DbHelper.manageSyncedFileStateDB(this.mSyncedState, "INSERT", mContext) ); + } else if (syncedFileState.getId() == -1) { + this.syncedFileState.setId(DbHelper.manageSyncedFileStateDB(this.syncedFileState, "INSERT", context)); } - if(mSyncedState.getLastETAG().equals( mRFile.getEtag() ) && mSyncedState.getLocalLastModified() > 0L){ + if (syncedFileState.getLastETAG().equals(remoteFile.getEtag()) && syncedFileState.getLocalLastModified() > 0L) { //Same etag and localLastModified not null mean the file is up to date Log.w(TAG, "File already up-to-date"); - return new RemoteOperationResult(RemoteOperationResult.ResultCode.ETAG_UNCHANGED ); + return new RemoteOperationResult(RemoteOperationResult.ResultCode.ETAG_UNCHANGED); } - String tmpTargetPath = mContext.getExternalCacheDir()+ FileUtils.PATH_SEPARATOR+mSyncedState.getName(); - DownloadFileRemoteOperation downloadOperation = new DownloadFileRemoteOperation(mRFile.getRemotePath(), + + final String tmpTargetPath = context.getExternalCacheDir()+ FileUtils.PATH_SEPARATOR+ syncedFileState.getName(); + final DownloadFileRemoteOperation downloadOperation = new DownloadFileRemoteOperation(remoteFile.getRemotePath(), tmpTargetPath); - RemoteOperationResult downloadResult = downloadOperation.execute( ownCloudClient ); - RemoteOperationResult.ResultCode mResultCode; + final RemoteOperationResult downloadResult = downloadOperation.execute(ownCloudClient); + RemoteOperationResult.ResultCode resultCode; boolean mustRestart = true; - if( downloadResult.isSuccess() ){ - File tmpLocalFile = new File(tmpTargetPath); - if( !tmpLocalFile.exists() ){ + if (downloadResult.isSuccess()) { + final File tmpLocalFile = new File(tmpTargetPath); + if (!tmpLocalFile.exists()) { Log.e(TAG, "Downloaded file doesn't exist or is null"); - mResultCode = RemoteOperationResult.ResultCode.FILE_NOT_FOUND; + resultCode = RemoteOperationResult.ResultCode.FILE_NOT_FOUND; - }else if(tmpLocalFile.length() != mRFile.getLength() ){ + } else if (tmpLocalFile.length() != remoteFile.getLength()) { Log.e(TAG, "Local and remote file doesn't have the same size."); - mResultCode = RemoteOperationResult.ResultCode.INVALID_OVERWRITE; + resultCode = RemoteOperationResult.ResultCode.INVALID_OVERWRITE; tmpLocalFile.delete(); - }else{ + } else { //file has been correctly download. - File localFile = new File(mTargetPath); - if( localFile.exists() ){ + final File localFile = new File(targetPath); + if (localFile.exists()) { localFile.delete(); } //Check parentFolder existence and create if needed - String parentFoldersPath = localFile.getParent(); - File localParentFile = new File(parentFoldersPath); - if( !localParentFile.exists() ){ - if( localParentFile.mkdirs() ) - Log.d(TAG, "Created folders: "+parentFoldersPath ); + final String parentFoldersPath = localFile.getParent(); + final File localParentFile = new File(parentFoldersPath); + if (!localParentFile.exists()) { + if (localParentFile.mkdirs()) + Log.d(TAG, "Created folders: "+parentFoldersPath); else - Log.d(TAG, "Can't create folders: "+parentFoldersPath ); + Log.d(TAG, "Can't create folders: "+parentFoldersPath); } boolean renameResult = tmpLocalFile.renameTo(localFile); - if(!renameResult) + if (!renameResult) Log.d(TAG, "File hasn't been successfully moved at its place"); - mSyncedState.setLocalLastModified( localFile.lastModified() ) - .setLastETAG( mRFile.getEtag() ); + syncedFileState.setLocalLastModified(localFile.lastModified()) + .setLastETAG(remoteFile.getEtag()); mustRestart = false; - mResultCode = RemoteOperationResult.ResultCode.OK; + resultCode = RemoteOperationResult.ResultCode.OK; //needed to make Gallery show new image - CommonUtils.doActionMediaScannerConnexionScanFile(mContext, mSyncedState.getLocalPath() ); + CommonUtils.doActionMediaScannerConnexionScanFile(context, syncedFileState.getLocalPath()); } - }else{ + } else { //If download failed - Log.e(TAG, "Download failed: "+downloadResult.getLogMessage() ); - mResultCode = RemoteOperationResult.ResultCode.UNKNOWN_ERROR; + Log.e(TAG, "Download failed: "+downloadResult.getLogMessage()); + resultCode = RemoteOperationResult.ResultCode.UNKNOWN_ERROR; } - if(mustRestart){ + if (mustRestart) { Log.w(TAG, restartCounter+" unsuccessfull trial.s of downloading file " - +mRFile.getRemotePath() ); - mSyncedState.setLastETAG(this.previousEtag); - if(this.restartCounter < 3){ + + remoteFile.getRemotePath()); + syncedFileState.setLastETAG(this.previousEtag); + if (this.restartCounter < 3) { this.restartCounter += 1; return this.run(ownCloudClient); - }else{ - mResultCode = RemoteOperationResult.ResultCode.INVALID_OVERWRITE; + } else { + resultCode = RemoteOperationResult.ResultCode.INVALID_OVERWRITE; } } //So now, we can update instance of SyncedState and save it to DB - if( DbHelper.manageSyncedFileStateDB( mSyncedState, "UPDATE", mContext ) <= 0 ){ + if (DbHelper.manageSyncedFileStateDB(syncedFileState, "UPDATE", context) <= 0) { Log.e(TAG, "DB update failed: 0 affected row"); //@TODO : do smtg } - return new RemoteOperationResult( mResultCode ); - } - - /** - * tell what type of operation is it - * @return true if it is an operation for media's element, then false mean its a settings operation - */ - @Override - public boolean isMediaType(){ - return this.mSyncedState.isMediaType(); - } - - @Override - public RemoteOperation toRemoteOperation() { - return this; - } - - @Override - public int describeContents() { - return 0; + return new RemoteOperationResult(resultCode); } } diff --git a/app/src/main/java/foundation/e/drive/operations/RemoveFileOperation.java b/app/src/main/java/foundation/e/drive/operations/RemoveFileOperation.java index 6b2132b2b6daefbed03e00b05abd5135cc95ec62..d1f4570b13065ac99d5c495109667c6caff9e005 100644 --- a/app/src/main/java/foundation/e/drive/operations/RemoveFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/RemoveFileOperation.java @@ -8,10 +8,6 @@ package foundation.e.drive.operations; -import android.os.Parcel; -import android.os.Parcelable; - -import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation; import foundation.e.drive.models.SyncedFileState; @@ -21,58 +17,16 @@ import foundation.e.drive.models.SyncedFileState; * Created by Vincent on 19/06/2018. * Class to be able to wrap concerned SyncedFileState in operation, so it can be retrieve at the end */ -public class RemoveFileOperation extends RemoveFileRemoteOperation implements ComparableOperation, Parcelable { +public class RemoveFileOperation extends RemoveFileRemoteOperation { - private SyncedFileState mSyncedFileState; + private SyncedFileState syncedFileState; public RemoveFileOperation(SyncedFileState syncedFileState) { - super( syncedFileState.getRemotePath() ); - this.mSyncedFileState = syncedFileState; - } - - protected RemoveFileOperation(Parcel in) { - super(in.readString()); - mSyncedFileState = in.readParcelable(SyncedFileState.class.getClassLoader()); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mSyncedFileState.getRemotePath()); - dest.writeParcelable(mSyncedFileState, flags); - } - - @Override - public int describeContents() { - return 0; + super(syncedFileState.getRemotePath()); + this.syncedFileState = syncedFileState; } - public static final Creator CREATOR = new Creator() { - @Override - public RemoveFileOperation createFromParcel(Parcel in) { - return new RemoveFileOperation(in); - } - - @Override - public RemoveFileOperation[] newArray(int size) { - return new RemoveFileOperation[size]; - } - }; - public SyncedFileState getSyncedFileState() { - return mSyncedFileState; - } - - /** - * tell what type of element to sync it is - * @return true if it is an operation for media's element, then false mean its a settings operation - */ - @Override - public boolean isMediaType(){ - return this.mSyncedFileState.isMediaType(); - } - - @Override - public RemoteOperation toRemoteOperation() { - return this; + return syncedFileState; } } diff --git a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java index d4d685d08f401af3f8e3af4d7569dbf64e3b6fb3..5d972a0b2b30dae77900bf53d9a66b2e31fec27a 100644 --- a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java @@ -9,8 +9,6 @@ package foundation.e.drive.operations; import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; import android.util.Log; import androidx.annotation.VisibleForTesting; @@ -34,61 +32,25 @@ import foundation.e.drive.utils.CommonUtils; * @author Vincent Bourgmayer * High level Operation which upload a local file to a remote cloud storage */ -public class UploadFileOperation extends RemoteOperation implements ComparableOperation, Parcelable { +public class UploadFileOperation extends RemoteOperation { private final static String TAG = UploadFileOperation.class.getSimpleName(); - private int restartCounter =0; private long previousLastModified; //get to restore real value if all trials fails - private boolean checkEtag; - private Context mContext; - private SyncedFileState mSyncedState; - + private Context context; + private SyncedFileState syncedState; private long availableQuota = -1; - protected UploadFileOperation(Parcel in) { - restartCounter = in.readInt(); - previousLastModified = in.readLong(); - checkEtag = in.readByte() != 0; - mSyncedState = in.readParcelable(SyncedFileState.class.getClassLoader()); - availableQuota = in.readLong(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public UploadFileOperation createFromParcel(Parcel in) { - return new UploadFileOperation(in); - } - - @Override - public UploadFileOperation[] newArray(int size) { - return new UploadFileOperation[size]; - } - }; - - @Override - public RemoteOperation toRemoteOperation() { - return this; - } - - /** * Construct an upload operation with an already known syncedFileState * @param syncedFileState syncedFileState corresponding to file. - * @param checkEtag if we should use IF MATCH header with etag */ - public UploadFileOperation (SyncedFileState syncedFileState, boolean checkEtag){ - this.mSyncedState = syncedFileState; - this.previousLastModified = mSyncedState.getLocalLastModified(); - this.checkEtag = checkEtag; + public UploadFileOperation (SyncedFileState syncedFileState, Context context) { + this.syncedState = syncedFileState; + this.previousLastModified = syncedState.getLocalLastModified(); + this.context = context; } - public void setContext(Context context){ - this.mContext = context; - } - - - /** * Execute the operation: * @@ -101,99 +63,103 @@ public class UploadFileOperation extends RemoteOperation implements ComparableOp * hasn't change since last update or "forbidden" if no remote path can be fetch. */ @Override - protected RemoteOperationResult run( OwnCloudClient client ) { + protected RemoteOperationResult run(OwnCloudClient client ) { //as operation isn't executed immediatly, file might have been deleted since creation of operation - if(mSyncedState == null ){ + if (syncedState == null ) { Log.e(TAG, "run(client): no syncedFileState or target path, can't perform upload operation"); return new RemoteOperationResult(ResultCode.FORBIDDEN); } - File file = new File(mSyncedState.getLocalPath()); - if(file == null || !file.exists()){ + File file = new File(syncedState.getLocalPath()); + if (file == null || !file.exists()) { Log.w(TAG, "Can't get the file. It might have been deleted"); return new RemoteOperationResult(ResultCode.FORBIDDEN); } - String targetPath = mSyncedState.getRemotePath(); + final String targetPath = syncedState.getRemotePath(); //If an Etag is already Stored and LastModified from DB is the same as real file - if (mSyncedState.isLastEtagStored() - && mSyncedState.getLocalLastModified() == file.lastModified()) { - Log.d(TAG, "mySyncedState last modified: "+mSyncedState.getLocalLastModified()+" <=> mFile last modified: "+file.lastModified() +": So return sync_conflict"); + if (syncedState.isLastEtagStored() + && syncedState.getLocalLastModified() == file.lastModified()) { + Log.d(TAG, "syncedState last modified: "+ syncedState.getLocalLastModified()+" <=> file last modified: "+file.lastModified() +": So return sync_conflict"); return new RemoteOperationResult(ResultCode.SYNC_CONFLICT); } - if(this.availableQuota == -1){ + Float relativeQuotaBeforeFileUpload = 0.0f; + if (this.availableQuota == -1) { RemoteOperationResult checkQuotaResult = checkAvailableSpace(client, file.length()); - if( checkQuotaResult.getCode() != ResultCode.OK ){ + if (checkQuotaResult.getCode() != ResultCode.OK) { return new RemoteOperationResult(checkQuotaResult.getCode()); + } else { + relativeQuotaBeforeFileUpload = ((Double) checkQuotaResult.getSingleData()).floatValue(); } } - UploadFileRemoteOperation uploadOperation = buildUploadOperation(file, targetPath); + final UploadFileRemoteOperation uploadOperation = buildUploadOperation(file, targetPath); // Execute UploadFileOperation - RemoteOperationResult uploadResult = uploadOperation.execute( client ); - ResultCode mResultCode; + RemoteOperationResult uploadResult = uploadOperation.execute(client ); + ResultCode resultCode; boolean mustRestart = true; //if upload is a success - if( uploadResult.isSuccess() ){ + if (uploadResult.isSuccess()) { Object data = uploadResult.getSingleData(); - if(data != null){ - mSyncedState.setLastETAG((String) data); + if (data != null) { + syncedState.setLastETAG((String) data); } - mSyncedState.setLocalLastModified(file.lastModified()); - mResultCode = uploadResult.getCode(); + syncedState.setLocalLastModified(file.lastModified()); + resultCode = uploadResult.getCode(); mustRestart = false; - }else{ + } else { //Si les répértoires ou mettre le fichier n'existe pas, on les ajoutes. - if( uploadResult.getCode() == ResultCode.FILE_NOT_FOUND ){ - Log.d(TAG, "Catched a File not found result for : "+file.getName()+", create missing remote path then retry"); - String remoteFoldersPath = targetPath.substring( 0, targetPath.lastIndexOf(FileUtils.PATH_SEPARATOR)+1 ); - mResultCode = ResultCode.FILE_NOT_FOUND; - CreateFolderRemoteOperation createFolderOperation = new CreateFolderRemoteOperation( remoteFoldersPath, true ); + if (uploadResult.getCode() == ResultCode.CONFLICT ) { + resultCode = ResultCode.CONFLICT; + Log.d(TAG, "Catched a conflict result for : "+file.getName()+", create missing remote path then retry"); + final String remoteFolderPath = targetPath.substring(0, targetPath.lastIndexOf(FileUtils.PATH_SEPARATOR)+1 ); + final CreateFolderRemoteOperation createFolderOperation = new CreateFolderRemoteOperation(remoteFolderPath, true ); try{ - RemoteOperationResult createFolderResult = createFolderOperation.execute( client ); + RemoteOperationResult createFolderResult = createFolderOperation.execute(client ); - if(!createFolderResult.isSuccess() && createFolderResult.getCode() != ResultCode.FOLDER_ALREADY_EXISTS){ - mResultCode = createFolderResult.getCode(); + if (!createFolderResult.isSuccess() && createFolderResult.getCode() != ResultCode.FOLDER_ALREADY_EXISTS) { + resultCode = createFolderResult.getCode(); Log.e(TAG, createFolderResult.getLogMessage()); mustRestart = false; - mSyncedState.setLocalLastModified( this.previousLastModified); + syncedState.setLocalLastModified(this.previousLastModified); } - }catch(Exception e){ + }catch(Exception e) { Log.e(TAG, e.toString() ); - mSyncedState.setLocalLastModified( this.previousLastModified); + syncedState.setLocalLastModified(this.previousLastModified); mustRestart = false; } - }else if(uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED){ - mResultCode = ResultCode.QUOTA_EXCEEDED; + } else if (uploadResult.getCode() == ResultCode.QUOTA_EXCEEDED) { + resultCode = ResultCode.QUOTA_EXCEEDED; mustRestart = false; - }else{ + } else { //Upload failed Log.e(TAG, "UploadFileRemoteOperation for : " + file.getName() + " failed => code: " + uploadResult.getCode()); - mResultCode = ResultCode.UNKNOWN_ERROR; + resultCode = ResultCode.UNKNOWN_ERROR; mustRestart = false; } } - if(mustRestart) { + if (mustRestart) { if (this.restartCounter < 1) { this.restartCounter += 1; //if we encounter more than three times same error, stop trying to download. return this.run(client); } else { - mSyncedState.setLocalLastModified( this.previousLastModified); //Revert syncFileState to its previous state + syncedState.setLocalLastModified(this.previousLastModified); //Revert syncFileState to its previous state } } // updated syncedFile in DB - DbHelper.manageSyncedFileStateDB(mSyncedState, "UPDATE", mContext); + DbHelper.manageSyncedFileStateDB(syncedState, "UPDATE", context); ArrayList datas = new ArrayList<>(); - datas.add(mSyncedState.getSyncedFolderId()); - final RemoteOperationResult finalResult = new RemoteOperationResult(mResultCode); + datas.add(syncedState.getSyncedFolderId()); + datas.add(relativeQuotaBeforeFileUpload); + final RemoteOperationResult finalResult = new RemoteOperationResult(resultCode); finalResult.setData(datas); return finalResult; } @@ -202,14 +168,14 @@ public class UploadFileOperation extends RemoteOperation implements ComparableOp * Build the operation to put the file on server * @return the operation to execute */ - private UploadFileRemoteOperation buildUploadOperation(File file, String targetPath){ - String timeStamp = ( (Long) ( file.lastModified() / 1000) ).toString() ; + private UploadFileRemoteOperation buildUploadOperation(File file, String targetPath) { + String timeStamp = ((Long) (file.lastModified() / 1000) ).toString() ; //create UploadFileOperation - UploadFileRemoteOperation uploadRemoteFileOperation = new UploadFileRemoteOperation( mSyncedState.getLocalPath(), - ( targetPath != null ) ? targetPath : mSyncedState.getRemotePath(), - CommonUtils.getMimeType( file ), - ( !checkEtag || mSyncedState.getLastETAG().isEmpty() )? null : mSyncedState.getLastETAG(), //If not null, This can cause error 412; that means remote file has change + UploadFileRemoteOperation uploadRemoteFileOperation = new UploadFileRemoteOperation(syncedState.getLocalPath(), + (targetPath != null ) ? targetPath : syncedState.getRemotePath(), + CommonUtils.getMimeType(file ), + (!this.syncedState.isMediaType() || syncedState.getLastETAG().isEmpty() )? null : syncedState.getLastETAG(), //If not null, This can cause error 412; that means remote file has change timeStamp ); uploadRemoteFileOperation.askResultEtag(true); return uploadRemoteFileOperation; @@ -221,45 +187,24 @@ public class UploadFileOperation extends RemoteOperation implements ComparableOp * @return RemoteOperationResult */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - public RemoteOperationResult checkAvailableSpace(OwnCloudClient client, long fileSize){ + public RemoteOperationResult checkAvailableSpace(OwnCloudClient client, long fileSize) { GetRemoteUserInfoOperation getRemoteUserInfoOperation = new GetRemoteUserInfoOperation(); RemoteOperationResult ocsResult = getRemoteUserInfoOperation.execute(client); - if(ocsResult.isSuccess() && ocsResult.getData() != null){ + if (ocsResult.isSuccess() && ocsResult.getData() != null) { UserInfo userInfo = (UserInfo) ocsResult.getData().get(0); this.availableQuota = userInfo.getQuota().getFree(); - if( ((UserInfo) ocsResult.getData().get(0)).getQuota().getFree() < fileSize ) { + if (((UserInfo) ocsResult.getData().get(0)).getQuota().getFree() < fileSize ) { Log.w(TAG, "quota exceeded!"); return new RemoteOperationResult(ResultCode.QUOTA_EXCEEDED); - }else{ + } else { Log.d(TAG, "Quota Okay"); - return new RemoteOperationResult(ResultCode.OK); + RemoteOperationResult result = new RemoteOperationResult(ResultCode.OK); + result.setSingleData((Double) userInfo.getQuota().getRelative()); + return result; } - }else{ + } else { Log.w(TAG, "getRemoteUserInfoOperation failed: "+ocsResult.getHttpCode() ); return new RemoteOperationResult(ocsResult.getCode()); } } - - /** - * tell what type of operation is it - * @return true if it is an operation for media's element, then false mean its a settings operation - */ - @Override - public boolean isMediaType(){ - return this.mSyncedState.isMediaType(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(restartCounter); - dest.writeLong(previousLastModified); - dest.writeByte((byte) (checkEtag ? 1: 0) ); //No method to write boolean - dest.writeParcelable(mSyncedState, flags); - dest.writeLong(availableQuota); - } } diff --git a/app/src/main/java/foundation/e/drive/receivers/BatteryStateReceiver.java b/app/src/main/java/foundation/e/drive/receivers/BatteryStateReceiver.java deleted file mode 100644 index 2fd661d1f98da129baffb84b77b94dfff9ecfcbc..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/receivers/BatteryStateReceiver.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.receivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import foundation.e.drive.utils.JobUtils; - -/** - * @author Vincent Bourgmayer - */ -public class BatteryStateReceiver extends BroadcastReceiver { - private final static String TAG = BatteryStateReceiver.class.getSimpleName(); - - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "onReceive"); - - String intentAction = intent.getAction(); - if(intentAction == null) { - Log.e(TAG, "intent Action is null"); - } else if ( intentAction.equals(Intent.ACTION_BATTERY_OKAY) ) { - JobUtils.scheduleScannerJob(context); - }else if(intentAction.equals(Intent.ACTION_BATTERY_LOW)){ - JobUtils.stopScheduledJob(context, JobUtils.ScannerJobId); - try { - context.unregisterReceiver(ScreenOffReceiver.getInstance()); - }catch(Exception e){ - Log.e(TAG, e.toString() ); - } - } - } -} diff --git a/app/src/main/java/foundation/e/drive/receivers/ScreenOffReceiver.java b/app/src/main/java/foundation/e/drive/receivers/ScreenOffReceiver.java deleted file mode 100644 index 27d489598d6fb2b579ddd671612ac7a87c96541f..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/receivers/ScreenOffReceiver.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.receivers; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import foundation.e.drive.services.ObserverService; -import foundation.e.drive.utils.CommonUtils; - -/** - * @author Vincent Bourgmayer - * This is a broadcast receiver which catch "ACTION_SCREEN_OFF" to start scanning at a moment - * where the user won't need battery or network. - */ -public class ScreenOffReceiver extends BroadcastReceiver { - private final String TAG = ScreenOffReceiver.class.getSimpleName(); - private static ScreenOffReceiver instance; - - public static ScreenOffReceiver getInstance(){ - if(instance == null) - instance = new ScreenOffReceiver(); - return instance; - } - - /** - * Private constructor - */ - private ScreenOffReceiver(){} - - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "onReceive"); - String intentAction = intent.getAction(); - if(intentAction == null){ - Log.e(TAG, "intent Action is null"); - } else if ( intent.getAction().equals(Intent.ACTION_SCREEN_OFF) - && CommonUtils.haveNetworkConnexion( context ) ) { - Log.d(TAG, "onReceive: ACTION_SCREEN_OFF"); - Intent cloudIntent = new Intent(context, ObserverService.class); - context.startService(cloudIntent); - } - } -} diff --git a/app/src/main/java/foundation/e/drive/services/InitializerService.java b/app/src/main/java/foundation/e/drive/services/InitializerService.java index 289229b2aa8a50abea151c6b047ec39aa1a4a24b..d493028d2be1d477932bdac694748307b467eb0f 100644 --- a/app/src/main/java/foundation/e/drive/services/InitializerService.java +++ b/app/src/main/java/foundation/e/drive/services/InitializerService.java @@ -13,96 +13,78 @@ import android.accounts.AccountManager; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Build; import android.os.Environment; -import android.os.Handler; import android.os.IBinder; import android.util.Log; import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import foundation.e.drive.models.SyncedFolder; -import foundation.e.drive.operations.CreateInitialFolderRemoteOperation; -import foundation.e.drive.receivers.ScreenOffReceiver; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.utils.JobUtils; -import foundation.e.drive.utils.ServiceExceptionHandler; import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; -import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; import static foundation.e.drive.utils.AppConstants.MEDIA_SYNCABLE_CATEGORIES; import static foundation.e.drive.utils.AppConstants.SETTINGS_SYNCABLE_CATEGORIES; import androidx.annotation.Nullable; +import androidx.work.WorkManager; /** * @author Vincent Bourgmayer */ -public class InitializerService extends Service implements OnRemoteOperationListener { - final private String TAG = InitializerService.class.getSimpleName(); - //Complex properties - private int existingRemoteFolderCounter; //@dev-only; Temporarily used to know if all remotePath exist - private List mSyncedFolders; //syncedFolders - private OwnCloudClient mCloudClient; - private Handler mHandler; - private Account mAccount; - private int restartFolderCreationCounter =0; +public class InitializerService extends Service { + private final String TAG = InitializerService.class.getSimpleName(); + private List syncedFolders; + private OwnCloudClient cloudClient; + private Account account; @Override public void onCreate() { Log.i(TAG, "onCreate()"); super.onCreate(); - this.existingRemoteFolderCounter = 0; - //JobUtils.scheduleInitializerJob(getApplicationContext()); } @Override - public int onStartCommand( Intent intent, int flags, int startId ) { + public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand(...)"); CommonUtils.setServiceUnCaughtExceptionHandler(this); //Get account - SharedPreferences prefs = this.getSharedPreferences( AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE ); + SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - if( prefs.getBoolean( AppConstants.INITIALIZATION_HAS_BEEN_DONE, false ) ) { - JobUtils.scheduleScannerJob(this); + if (prefs.getBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, false)) { Log.w(TAG, "Initializer has already been run"); - }else{ - String accountName = prefs.getString( AccountManager.KEY_ACCOUNT_NAME, "" ); - String accountType = prefs.getString( AccountManager.KEY_ACCOUNT_TYPE, "" ); + } else { + String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - if ( accountName.isEmpty() && accountType.isEmpty() && intent.getExtras() != null ) { + if (accountName.isEmpty() && accountType.isEmpty() && intent.getExtras() != null) { - accountName = intent.getExtras().getString( AccountManager.KEY_ACCOUNT_NAME, "" ); - accountType = intent.getExtras().getString( AccountManager.KEY_ACCOUNT_TYPE, "" ); + accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); + accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - //If data come from intent, store them into pref because there aren't stored - prefs.edit().putString( AccountManager.KEY_ACCOUNT_NAME, accountName ) - .putString( AccountManager.KEY_ACCOUNT_TYPE, accountType ) + prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, accountName) + .putString(AccountManager.KEY_ACCOUNT_TYPE, accountType) .apply(); } - if(accountName.isEmpty() ) { + if (accountName.isEmpty()) { Log.w(TAG, "Account's name not found. Neither in shared prefs nor in intent's extras"); - //JobUtils.stopScheduledJob(getApplicationContext(), JobUtils.InitializerJobId); stopSelf(); - }else{ - this.mAccount = CommonUtils.getAccount( accountName, accountType, AccountManager.get(this) ); + } else { + this.account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); //Get OwnCloudlient - if (this.mAccount != null) { - this.mCloudClient = CommonUtils.getOwnCloudClient( this.mAccount, getApplicationContext()); + if (this.account != null) { + this.cloudClient = CommonUtils.getOwnCloudClient(this.account, getApplicationContext()); start(); - }else { + } else { Log.w(TAG, "Got account is invalid."); stopSelf(); } @@ -111,205 +93,91 @@ public class InitializerService extends Service implements OnRemoteOperationList return super.onStartCommand(intent, flags, startId); } - - /** - * start to do its job - */ - public void start() { Log.i(TAG, "start()"); - if (mCloudClient == null){ + if (cloudClient == null) { stopSelf(); return; } - //Get categories of element to sync - List syncCategories = new ArrayList<>(); + final List syncCategories = new ArrayList<>(); - if (CommonUtils.isMediaSyncEnabled(mAccount)) { - syncCategories.addAll(Arrays.asList(MEDIA_SYNCABLE_CATEGORIES)); - } - - if (CommonUtils.isSettingsSyncEnabled(mAccount)) { - syncCategories.addAll(Arrays.asList(SETTINGS_SYNCABLE_CATEGORIES)); - } + syncCategories.addAll(Arrays.asList(MEDIA_SYNCABLE_CATEGORIES)); + syncCategories.addAll(Arrays.asList(SETTINGS_SYNCABLE_CATEGORIES)); - //Get SyncedFolders getInitialSyncedFolders(syncCategories); - - this.existingRemoteFolderCounter = 0; - - CreateNextRemoteFolder(); - } + CommonUtils.registerInitializationWorkers(syncedFolders, WorkManager.getInstance(getApplicationContext()) ); + } /** * Return a list of SyncedFolder * @param categories categories indicating which syncedFolder to create */ - private void getInitialSyncedFolders( List categories){ + private void getInitialSyncedFolders(List categories) { Log.i(TAG, "getInitialSyncedFolders"); - this.mSyncedFolders = new ArrayList<>(); + this.syncedFolders = new ArrayList<>(); - for(int i=-1, size = categories.size(); ++i < size;){ + for(int i=-1, size = categories.size(); ++i < size;) { final String DEVICE_SPECIFIC_PATH = PATH_SEPARATOR+"Devices"+PATH_SEPARATOR+ Build.BRAND+"_"+ Build.MODEL+"_" + Build.SERIAL; - switch ( categories.get(i) ){ - case "Medias": - break; + switch (categories.get(i)) { case "Images": - mSyncedFolders.add( new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DCIM), - "/Photos/", true) ); - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PICTURES), - "/Pictures/", true ) ); + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DCIM), + "/Photos/", true)); + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PICTURES), + "/Pictures/", true)); break; case "Movies": - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MOVIES), + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MOVIES), "/Movies/", true)); break; case "Music": - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MUSIC), + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_MUSIC), "/Music/", true)); break; case "Ringtones": - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_RINGTONES), + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_RINGTONES), "/Ringtones/", true)); break; case "Documents": - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DOCUMENTS), + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_DOCUMENTS), "/Documents/", true)); break; case "Podcasts": - mSyncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PODCASTS), + syncedFolders.add(new SyncedFolder(categories.get(i), getExternalFolder(Environment.DIRECTORY_PODCASTS), "/Podcasts/", true)); break; case "Rom settings": + String remoteFolderPath = DEVICE_SPECIFIC_PATH+"/rom_settings/"; - mSyncedFolders.add( new SyncedFolder(categories.get(i), "/data/system/users/0/", remoteFolderPath, true, false, true, false) ); + syncedFolders.add(new SyncedFolder(categories.get(i), "/data/system/users/0/", remoteFolderPath, true, false, true, false)); try{ - mSyncedFolders.add( new SyncedFolder( + syncedFolders.add(new SyncedFolder( categories.get(i), getFilesDir().getCanonicalPath()+PATH_SEPARATOR, remoteFolderPath+"app_list/", true, false, - true, - false) ); - }catch (Exception e){ Log.e(TAG, e.toString()); } - break; + CommonUtils.isSettingsSyncEnabled(account), + false)); + } catch (Exception e) { Log.e(TAG, e.toString()); } + break; } } } - private String getExternalFolder(String directory){ + private String getExternalFolder(String directory) { return CommonUtils.getLocalPath(Environment.getExternalStoragePublicDirectory(directory))+ PATH_SEPARATOR; } - /** - * Start to createSyncedFolder in the cloud - */ - private void CreateNextRemoteFolder(){ - Log.i(TAG, "createNextRemoteFolder()"); - this.restartFolderCreationCounter = 0; - - if( this.mSyncedFolders == null || this.mSyncedFolders.isEmpty() ){ - //JobUtils.stopScheduledJob(getApplicationContext(), JobUtils.InitializerJobId); - this.stopSelf(); - } - - //It means that there are still folders to create - if( this.existingRemoteFolderCounter < this.mSyncedFolders.size() ){ - - if( this.mHandler == null ) this.mHandler = new Handler(); - - CreateInitialFolderRemoteOperation createFolderOperation = - new CreateInitialFolderRemoteOperation( - this.mSyncedFolders.get( this.existingRemoteFolderCounter ), - true, - this); - - createFolderOperation.execute(this.mCloudClient, this, this.mHandler); - - }else if(this.existingRemoteFolderCounter == this.mSyncedFolders.size() ){ - - doLastStep(); - - }else{ - Log.e(TAG, "this.existingRemoteFolderCounter : "+this.existingRemoteFolderCounter+" > this.mSyncedFolders.size() : "+this.mSyncedFolders.size() ); - this.stopSelf(); - } - } - - @Override - public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - Log.i(TAG, "onRemoteOperationFinish()"); - if(operation instanceof CreateInitialFolderRemoteOperation){ - - if(result.isSuccess() || result.getCode() == RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS){ - this.existingRemoteFolderCounter+=1; - CreateNextRemoteFolder(); - - }else if( result.getHttpCode() == 423 || result.getHttpCode() == 409){//file locked or conflict in result - Log.e( TAG, result.getLogMessage() ); - - if( this.restartFolderCreationCounter < 3) { - Log.w( TAG, " restart operation" ); - operation.execute( this.mCloudClient, this, this.mHandler ); - this.restartFolderCreationCounter+=1; - - }else{ - Log.e(TAG, "Remote folder's creation failed due to conflict with server"); - stopSelf(); - } - - }else{ - Log.e(TAG, result.getLogMessage()+" "+result.getHttpCode() ); - stopSelf(); - } - } - } - - - /** - * Function to check if all remote folder have been created - **/ - private void doLastStep(){ - Log.i(TAG, "doLastStep()"); - - Context appContext = getApplicationContext(); - appContext.getSharedPreferences( AppConstants.SHARED_PREFERENCE_NAME, - Context.MODE_PRIVATE ) - .edit() - .putBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, true) - .putInt( INITIALFOLDERS_NUMBER, mSyncedFolders.size() ) - .apply(); - - //all folder have been created - //JobUtils.stopScheduledJob(appContext, JobUtils.InitializerJobId); - JobUtils.scheduleScannerJob(appContext); - - Log.d(TAG, "RegisterReceiver: screenOffReceiver"); - IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_SCREEN_OFF); - getApplicationContext().registerReceiver(ScreenOffReceiver.getInstance(), filter); - - //Immediatly start ObserverService to not have to wait 30 minutes. - Intent observersServiceIntent = new Intent(getApplicationContext(), foundation.e.drive.services.ObserverService.class); - startService(observersServiceIntent); - - - stopSelf(); - - } - @Override public void onDestroy() { super.onDestroy(); - this.mHandler = null; - this.mAccount = null; - this.mCloudClient = null; - if(this.mSyncedFolders != null) this.mSyncedFolders.clear(); - this.mSyncedFolders = null; + this.account = null; + this.cloudClient = null; + if (this.syncedFolders != null) this.syncedFolders.clear(); + this.syncedFolders = null; } @Nullable diff --git a/app/src/main/java/foundation/e/drive/services/ObserverService.java b/app/src/main/java/foundation/e/drive/services/ObserverService.java index c90b4167adcfebb0632a9d8c751f40ff51ce44e9..cdcbe8d5cfe031ae976a6001157fa5a20ef1174e 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -17,7 +17,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.os.Handler; import android.os.IBinder; -import android.os.Parcelable; import android.provider.MediaStore; import android.util.Log; import com.owncloud.android.lib.common.OwnCloudClient; @@ -33,24 +32,22 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.ListIterator; -import java.util.Map; import foundation.e.drive.database.DbHelper; import foundation.e.drive.fileFilters.CrashlogsFileFilter; import foundation.e.drive.fileFilters.FileFilterFactory; import foundation.e.drive.fileFilters.OnlyFileFilter; +import foundation.e.drive.models.DownloadRequest; +import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.models.SyncedFileState; -import foundation.e.drive.operations.DownloadFileOperation; import foundation.e.drive.operations.ListFileRemoteOperation; -import foundation.e.drive.operations.RemoveFileOperation; -import foundation.e.drive.operations.UploadFileOperation; import foundation.e.drive.receivers.ForceSyncReceiver; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; -import foundation.e.drive.utils.JobUtils; import foundation.e.drive.utils.ServiceExceptionHandler; +import foundation.e.drive.utils.SynchronizationServiceConnection; import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; @@ -70,19 +67,25 @@ public class ObserverService extends Service implements OnRemoteOperationListene private boolean isWorking = false; private int initialFolderCounter; private Account mAccount; - private HashMap operationsForIntent; - /* Lifecycle Methods */ + private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation + private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection(); + + /* Lifecycle Methods */ @Override public void onDestroy(){ Log.i(TAG, "onDestroy()"); + unbindService(synchronizationServiceConnection); super.onDestroy(); this.mSyncedFolders = null; } + @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand("+startId+")"); + final Intent SynchronizationServiceIntent = new Intent(this, SynchronizationService.class); + bindService(SynchronizationServiceIntent, synchronizationServiceConnection, Context.BIND_AUTO_CREATE); CommonUtils.setServiceUnCaughtExceptionHandler(this); @@ -93,14 +96,13 @@ public class ObserverService extends Service implements OnRemoteOperationListene initialFolderCounter = prefs.getInt(AppConstants.INITIALFOLDERS_NUMBER, 0); // Check if account is invalid - if(this.mAccount == null){ + if (this.mAccount == null){ Log.w(TAG, "No account registered"); - JobUtils.stopScheduledJob(this, JobUtils.ScannerJobId); //If no account return super.onStartCommand(intent, flags, startId); } //check if user have disable eDrive's sync in account's settings - if(!CommonUtils.isMediaSyncEnabled(mAccount) && !CommonUtils.isSettingsSyncEnabled(mAccount) ){ + if (!CommonUtils.isMediaSyncEnabled(mAccount) && !CommonUtils.isSettingsSyncEnabled(mAccount) ){ Log.w(TAG, "eDrive syncing has been disabled in /e/ account's settings"); return super.onStartCommand(intent, flags, startId); } @@ -114,17 +116,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene } //Check this service isn't already working - if(isWorking){ + if (isWorking){ Log.w(TAG, "ObserverService is already working"); return super.onStartCommand(intent,flags,startId); } - //check OperationManagerService isn't working - if(prefs.getBoolean(AppConstants.KEY_OMS_IS_WORKING, false)){ - Log.w(TAG, "OperationManagerService is still performing some operation"); - return super.onStartCommand(intent,flags, startId); - } - //Check a minimum delay has been respected between two start. long lastSyncTime = prefs.getLong(AppConstants.KEY_LAST_SYNC_TIME, 0L); long currentTime = System.currentTimeMillis(); @@ -140,13 +136,14 @@ public class ObserverService extends Service implements OnRemoteOperationListene } //check for the case where intent has been launched by initializerService - if (!CommonUtils.haveNetworkConnexion(this)) { + if (!CommonUtils.haveNetworkConnection(this)) { Log.w(TAG, "There is no Internet connexion."); return super.onStartCommand( intent, flags, startId ); } - this.operationsForIntent = new HashMap<>(); + this.syncRequests = new HashMap<>(); + begin(); - return super.onStartCommand( intent, flags, startId ); + return START_NOT_STICKY; } /* Common methods */ @@ -193,7 +190,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.i(TAG, "clearCachedFile()"); //Load subfiles into external cache file File[] fileArray = this.getApplicationContext().getExternalCacheDir().listFiles(new OnlyFileFilter() ); - if(fileArray != null) { + if (fileArray != null) { boolean toRemove; for (int i = -1, size = fileArray.length; ++i < size; ) { toRemove = true; @@ -202,7 +199,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.v(TAG+"_handleCachedFile()", "Deletion of cached file: " + deleteResult); } } - }else{ + } else { Log.e(TAG+"_handleCachedFile()", "Array of cached file is null"); } } @@ -217,7 +214,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.i(TAG, "startScan("+remote+")"); this.mSyncedFolders = loadSyncedFolders(); - if(mSyncedFolders.isEmpty() ){ + if (mSyncedFolders.isEmpty() ){ Log.w(TAG, "List of synced folders is empty"); this.stopSelf(); return; @@ -240,7 +237,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } catch (IllegalArgumentException e){ Log.e(TAG, e.toString() ); } - }else{ + } else { Log.w(TAG, "OwnCloudClient is null"); return; } @@ -258,22 +255,17 @@ public class ObserverService extends Service implements OnRemoteOperationListene boolean mediaSyncEnabled = CommonUtils.isMediaSyncEnabled(mAccount); boolean settingsSyncedEnabled = CommonUtils.isSettingsSyncEnabled(mAccount); - if(mediaSyncEnabled && settingsSyncedEnabled){ + if (mediaSyncEnabled && settingsSyncedEnabled){ return DbHelper.getAllSyncedFolders(this); - }else if(mediaSyncEnabled){ + } else if (mediaSyncEnabled){ return DbHelper.getSyncedFolderList(this, true); - }else if(settingsSyncedEnabled){ + } else if (settingsSyncedEnabled){ return DbHelper.getSyncedFolderList(this, false); - }else{ + } else { return new ArrayList(); } } - - - - - /** * Handle end of remote Operation * @param operation The RemoteOperation which ends and call this methods @@ -282,7 +274,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene @Override public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result ) { Log.i( TAG, "onRemoteOperationFinish()" ); - if( operation instanceof ListFileRemoteOperation) { + if ( operation instanceof ListFileRemoteOperation) { if (result.isSuccess()) { List resultDatas = result.getData(); if (resultDatas != null) { @@ -302,31 +294,34 @@ public class ObserverService extends Service implements OnRemoteOperationListene } this.startScan(false); - Log.v(TAG, "operationsForIntent contains "+ operationsForIntent.size() ); + Log.v(TAG, "operationsForIntent contains " + syncRequests.size()); //After everything has been scanned. Send Intent to OperationmanagerService with data in bundle - if(operationsForIntent != null && !operationsForIntent.isEmpty()) { - Intent OMSIntent = new Intent(this, OperationManagerService.class); - for(Map.Entry entry: operationsForIntent.entrySet()){ - OMSIntent.putExtra(entry.getKey()+"", entry.getValue()); - } - - OMSIntent.putExtra("account", mAccount); - startService(OMSIntent); - }else{ + if (syncRequests != null && !syncRequests.isEmpty()) { + passSyncRequestsToSynchronizationService(); + } else { Log.w(TAG, "There is no file to sync."); getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) .edit() .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis()) - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false) .apply(); } - this.isWorking = false; this.stopSelf(); + } + } + + private void passSyncRequestsToSynchronizationService() { + if (synchronizationServiceConnection.isBoundToSynchronizationService()) { + synchronizationServiceConnection.getSynchronizationService().queueOperations(syncRequests.values()); + synchronizationServiceConnection.getSynchronizationService().startSynchronization(); + } else { + Log.e(TAG, "ERROR: impossible to bind ObserverService to SynchronizationService"); } } + + /** * Method to get Id of SyncedFolder to scan * @return List id of SyncedFolder to scan @@ -335,7 +330,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene List result = new ArrayList<>(); for(int i = -1, size = this.mSyncedFolders.size(); ++i < size;){ SyncedFolder syncedFolder = this.mSyncedFolders.get(i); - if(syncedFolder.isToSync() ){ + if (syncedFolder.isToSync() ){ result.add( (long) syncedFolder.getId() ); } } @@ -369,7 +364,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene SyncedFileState syncedFileState = syncedFileListIterator.next(); //ignore hidden file from db - if(syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")){ + if (syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")){ syncedFileListIterator.remove(); continue; } @@ -394,10 +389,10 @@ public class ObserverService extends Service implements OnRemoteOperationListene syncedFileState.setLastETAG(remoteFile.getEtag()); int affectedRows = DbHelper.manageSyncedFileStateDB(syncedFileState, "UPDATE", this); Log.v(TAG, affectedRows + " syncedFileState.s row in DB has been updated."); - }else { + } else { Log.i(TAG, "Add download operation for file "+syncedFileState.getId()); - DownloadFileOperation downloadFileOperation = new DownloadFileOperation(remoteFile, syncedFileState); - this.operationsForIntent.put(syncedFileState.getId(), downloadFileOperation); + + this.syncRequests.put(syncedFileState.getId(), new DownloadRequest(remoteFile, syncedFileState)); } } syncedFileListIterator.remove(); //we can delete syncedFile from list because its correspondant has already been found and handled @@ -405,7 +400,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - if( correspondant_found )continue; + if ( correspondant_found )continue; //If we get here, RemoteFile is a new file to download Log.v(TAG, "SyncedFileState corresponding to remoteFile not found."); @@ -421,8 +416,13 @@ public class ObserverService extends Service implements OnRemoteOperationListene String fileName = CommonUtils.getFileNameFromPath(remoteFilePath); //get remote file's name if (fileName != null) { + int scannableValue = 0; + if (parentFolder.isEnabled()) { + if (parentFolder.isScanRemote()) scannableValue++; + if (parentFolder.isScanLocal()) scannableValue += 2; + } //create syncedFileState - SyncedFileState newRemoteFile = new SyncedFileState(-1, fileName, parentFolder.getLocalFolder() + fileName, remoteFilePath, remoteFile.getEtag(), 0, parentFolder.getId(), parentFolder.isMediaType()); + SyncedFileState newRemoteFile = new SyncedFileState(-1, fileName, parentFolder.getLocalFolder() + fileName, remoteFilePath, remoteFile.getEtag(), 0, parentFolder.getId(), parentFolder.isMediaType(), scannableValue); //Store it in DB int storedId = DbHelper.manageSyncedFileStateDB(newRemoteFile, "INSERT", this); @@ -430,7 +430,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene newRemoteFile.setId(storedId); Log.i(TAG, "Add download operation for new file "+storedId); //Create Download operation and add it into Bundle - this.operationsForIntent.put(storedId, new DownloadFileOperation(remoteFile, newRemoteFile)); + this.syncRequests.put(storedId, new DownloadRequest(remoteFile, newRemoteFile)); } else { Log.w(TAG, "Can't save new remote File in DB. Ignore file."); @@ -460,20 +460,20 @@ public class ObserverService extends Service implements OnRemoteOperationListene //Loop through remaining file state for(int i = -1, size = syncedFileStates.size(); ++i < size; ){ SyncedFileState syncedFileState = syncedFileStates.get(i); - if( !CommonUtils.isThisSyncAllowed( mAccount, syncedFileState.isMediaType() ) ){ + if ( !CommonUtils.isThisSyncAllowed( mAccount, syncedFileState.isMediaType() ) ){ Log.d(TAG, "Sync of current file: "+syncedFileState.getName()+" isn't allowed"); continue; } //Check that file has already been synced fully - if( syncedFileState.isLastEtagStored() && syncedFileState.getLocalLastModified() > 0L) { + if ( syncedFileState.isLastEtagStored() && syncedFileState.getLocalLastModified() > 0L) { //Get local file File file = new File( syncedFileStates.get(i).getLocalPath() ); //Try to remove local file boolean fileExists = file.exists(); - if( fileExists) { + if ( fileExists) { Log.d(TAG, file.getName()+" exists *1"); //delete file int rowAffected = getContentResolver().delete(MediaStore.Files.getContentUri("external"), @@ -485,11 +485,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene } //if it succeed, remove syncedFileState in DB - if(! fileExists ) { + if (! fileExists ) { //It means that file has been correctly deleted from device. So update DB. if (DbHelper.manageSyncedFileStateDB(syncedFileState, "DELETE", this) <= 0) Log.e(TAG, "SyncedFileState row hasn't been deleted from DB"); - }else + } else Log.w(TAG, "local file:"+file.getName()+" still exist and can't be remove"); } } @@ -540,7 +540,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene List folderIdList= new ArrayList<>(); boolean contentToSyncFound = false; //Regenere list of application's package - if(CommonUtils.isSettingsSyncEnabled(mAccount)) generateAppListFile(); + if (CommonUtils.isSettingsSyncEnabled(mAccount)) generateAppListFile(); ListIterator iterator = mSyncedFolders.listIterator() ; @@ -550,13 +550,13 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.d(TAG, "SyncedFolder :"+syncedFolder.getLibelle()+", "+syncedFolder.getLocalFolder()+", "+syncedFolder.getLastModified()+", "+syncedFolder.isScanLocal()+", "+syncedFolder.getId() ); //Check it's not a hidden file - if(syncedFolder.isMediaType() && CommonUtils.getFileNameFromPath(syncedFolder.getLocalFolder()).startsWith(".")){ + if (syncedFolder.isMediaType() && CommonUtils.getFileNameFromPath(syncedFolder.getLocalFolder()).startsWith(".")){ iterator.remove(); continue; } //Check it can be scann from local - if(!syncedFolder.isScanLocal()){ + if (!syncedFolder.isScanLocal()){ iterator.remove(); continue; } @@ -569,7 +569,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene if (syncedFolder_id > 0) { Log.v(TAG, "Folder has been registered in DB"); syncedFolder.setId(syncedFolder_id); - }else { + } else { Log.w(TAG, "syncedFolder " + syncedFolder.getLocalFolder() + " remove iterator because it hasn't been registered in DB or already stored"); iterator.remove(); continue; @@ -580,7 +580,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.d(TAG, "Local Folder (last modified / exists): "+localFolder.lastModified()+", "+localFolder.exists() ); //Check if local folder exists - if(!localFolder.exists()){ + if (!localFolder.exists()){ Log.v(TAG, "local folder doesn't exist anymore . So content has change"); contentToSyncFound = true; folderIdList.add( (long) syncedFolder.getId() ); @@ -590,7 +590,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene boolean folderHasChange = false; //consider by default that file hadn't change //Check if folder had change - if(localFolder.lastModified() > syncedFolder.getLastModified() ) { //compare last modified date + if (localFolder.lastModified() > syncedFolder.getLastModified() ) { //compare last modified date Log.v(TAG, "local folder has changed"); syncedFolder.setLastModified( localFolder.lastModified() ); //@Todo: it would be better to set it after all it's content has been synced contentToSyncFound = true; //at least one dir has changed @@ -605,7 +605,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.v(TAG, "loop through subfiles"); for (int i = -1, subEltSize = (subElements != null)? subElements.length: 0; ++i < subEltSize; ) { File subElt = subElements[i]; - if(subElt == null) continue; + if (subElt == null) continue; if (subElt.isDirectory()) { //if its a subfolder add it to syncedFolder list //if a subfolder is found, add it to syncedFolder list Log.v(TAG, "subfile "+subElt.getAbsolutePath()+" is a directory."); @@ -621,14 +621,12 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } } //end of iterator loop - - - if(contentToSyncFound) { + if (contentToSyncFound) { DbHelper.updateSyncedFolders(mSyncedFolders, this); //@ToDo: maybe do this when all contents will be synced. List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, folderIdList); - if(!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { + if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { handleLocalFiles(fileList, syncedFileStates); } } @@ -664,7 +662,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene SyncedFileState syncedFileState = syncedFileListIterator.next(); //Ignore hidden media file store in DB - if(syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")){ + if (syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")){ syncedFileListIterator.remove(); continue; } @@ -672,34 +670,20 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.v(TAG, syncedFileState.getLocalPath()+", "+syncedFileState.getId()+", "+syncedFileState.getLocalLastModified()); //if syncedFileState correspond to local file - if( syncedFileState.getLocalPath().equals( filePath ) ){ + if ( syncedFileState.getLocalPath().equals( filePath ) ){ correspondant_found = true; //If no etag is stored in sfs, the file hasn't been sync up to server. then do upload - if( syncedFileState.getLocalLastModified() < localFile.lastModified() || !syncedFileState.isLastEtagStored()){ - Log.d(TAG+"_handleLocalFiles()", syncedFileState.getName()+" file has been modified or never sync" ); - boolean checkEtag = false; - - //Look for folder to know if the folder can be scan remotly. - for(int folderIndex =-1, size = mSyncedFolders.size();++folderIndex < size;) { - - final SyncedFolder syncedFolder = mSyncedFolders.get(folderIndex); - if (syncedFolder.getId() == syncedFileState.getSyncedFolderId()) { - //Parent folder has been found - checkEtag = syncedFolder.isScanRemote(); - break; - } - } - Log.i(TAG, "Add upload operation for file "+syncedFileState.getId()); - UploadFileOperation uploadFileOperation = new UploadFileOperation(syncedFileState, checkEtag ); - this.operationsForIntent.put(syncedFileState.getId(), uploadFileOperation); + if ( syncedFileState.getLocalLastModified() < localFile.lastModified() || !syncedFileState.isLastEtagStored()){ + Log.i(TAG, "Add upload request for file "+syncedFileState.getId()); + this.syncRequests.put(syncedFileState.getId(), new SyncRequest(syncedFileState, SyncRequest.Type.UPLOAD)); } // No need to reloop on it. syncedFileListIterator.remove(); break; } } - if( correspondant_found ) continue; + if ( correspondant_found ) continue; //if no correspondance, then it is a new file Log.v(TAG, "this is a new file to sync"); @@ -709,19 +693,24 @@ public class ObserverService extends Service implements OnRemoteOperationListene //look into synced folders if folder path exist for(SyncedFolder syncedFolder : mSyncedFolders){ - if(syncedFolder.getLocalFolder().equals(parentPath)){ + if (syncedFolder.getLocalFolder().equals(parentPath)){ + int scannableValue = 0; + if (syncedFolder.isEnabled()) { + if (syncedFolder.isScanRemote()) scannableValue++; + if (syncedFolder.isScanLocal()) scannableValue += 2; + } + //create the syncedFile State - SyncedFileState newSyncedFileState = new SyncedFileState(-1, localFile.getName(), filePath, syncedFolder.getRemoteFolder() + localFile.getName(), "", 0, syncedFolder.getId(), syncedFolder.isMediaType()); + SyncedFileState newSyncedFileState = new SyncedFileState(-1, localFile.getName(), filePath, syncedFolder.getRemoteFolder() + localFile.getName(), "", 0, syncedFolder.getId(), syncedFolder.isMediaType(),scannableValue); //Store it in DB int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", this); - if(storedId > 0){ + if (storedId > 0){ newSyncedFileState.setId( storedId ); Log.i(TAG, "Add upload operation for new file "+storedId); - //create UploadOperation and add it into bundle - UploadFileOperation uploadOperation = new UploadFileOperation(newSyncedFileState, syncedFolder.isScanRemote()); - this.operationsForIntent.put(storedId, uploadOperation); - }else{ + + this.syncRequests.put(storedId, new SyncRequest(newSyncedFileState, SyncRequest.Type.UPLOAD)); + } else { Log.w(TAG, "The new file to synced cannot be store in DB. Ignore it"); } break; @@ -740,16 +729,15 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.i(TAG, "handleLocalRemainingSyncedFileState(...)"); //Loop through remaining SyncedFileState for(SyncedFileState fileState : syncedFileStates){ - if(fileState.isLastEtagStored() && fileState.getLocalLastModified() > 0L){ + if (fileState.isLastEtagStored() && fileState.getLocalLastModified() > 0L){ //try to get File File file = new File(fileState.getLocalPath()); Log.v(TAG, "File : "+file.getAbsolutePath()+","+file.exists()); - if(file.exists()){ + if (file.exists()){ Log.w(TAG, "The file still exist. There is a problem!"); - }else{ - Log.i(TAG, "Add remove operation for file "+fileState.getId()); - RemoveFileOperation removeOperation = new RemoveFileOperation(fileState); - this.operationsForIntent.put(fileState.getId(), removeOperation); + } else { + Log.i(TAG, "Add remote remove request for file "+fileState.getId()); + this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.REMOTE_DELETE)); } } } diff --git a/app/src/main/java/foundation/e/drive/services/OperationManagerService.java b/app/src/main/java/foundation/e/drive/services/OperationManagerService.java deleted file mode 100644 index c523f54c61328d9a37a655acad1f50e558dd6c4b..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/services/OperationManagerService.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.services; - -import android.accounts.Account; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Parcelable; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; - -import java.lang.ref.WeakReference; -import java.util.Hashtable; -import java.util.concurrent.ConcurrentLinkedDeque; - -import foundation.e.drive.database.DbHelper; -import foundation.e.drive.operations.ComparableOperation; -import foundation.e.drive.operations.DownloadFileOperation; -import foundation.e.drive.operations.RemoveFileOperation; -import foundation.e.drive.operations.UploadFileOperation; -import foundation.e.drive.utils.AppConstants; -import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.utils.DavClientProvider; -import foundation.e.drive.utils.ServiceExceptionHandler; - -/** - * @author Vincent Bourgmayer - * Service to do upload, remove and download operation. - */ -public class OperationManagerService extends Service implements OnRemoteOperationListener{ - private final static String TAG = OperationManagerService.class.getSimpleName(); - - private int workerAmount = 0; //Number of thread available to execute RemoteOperation - private boolean[] mThreadWorkingState; //State of the threads; true mean the thread is working - private Thread[] mThreadPool; //The threads to use - - private ConcurrentLinkedDeque mOperationsQueue; // Queue of Operation - private Hashtable mStartedOperations; //Operations which are running - - private OperationManagerHandler mHandler; - private OwnCloudClient mClient; //ClientObject - private Account mAccount; //Will be used soon - - @Override - public void onDestroy() { - Log.i(TAG, "onDestroy()"); - super.onDestroy(); - } - - - /** - * Start to run all threads - */ - private void startAllThreads(){ - Log.i(TAG, "startAllThreads"); - for(int i =-1; ++i < workerAmount;){ - this.startWork(i); - } - } - - /** - * retrieve an operation from queue and execute it if a thread is available. - * @param threadIndex index of thread which execute job. - */ - private synchronized void startWork( int threadIndex ){ - Log.i(TAG, "startWork("+threadIndex+")" ); - - //Check if operation queue is empty - if(mOperationsQueue.isEmpty()){ - boolean stillWorking = false; - //check that all other thread has finished. - for(boolean a : this.mThreadWorkingState){ - if(a){ - stillWorking = a; - break; - } - } - - if(!stillWorking) { - Log.i(TAG, "Operation queue is empty. all jobs Done. The End"); - //Register timestamp to allow to calculate min delay between two sync - getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) - .edit() - .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis()) - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false) - .apply(); - - stopSelf(); - return; - } - } - //start the new Job - if( !mThreadWorkingState[threadIndex] && CommonUtils.haveNetworkConnexion( getApplicationContext() ) ) { //check if the thread corresponding to threadIndex isn't already working - - ComparableOperation operation = this.mOperationsQueue.poll(); //return null if deque is empty - if (operation != null) { - Log.v(TAG, " an operation has been poll from queue"); - - if( CommonUtils.isThisSyncAllowed(mAccount, operation.isMediaType() ) ) { - mStartedOperations.put(operation.toRemoteOperation(), threadIndex); - this.mThreadPool[threadIndex] = operation.toRemoteOperation().execute(mClient, this, this.mHandler); - this.mThreadWorkingState[threadIndex] = true; - } - } - } //else : thread is already running or no network connexion - } - - /** - * Called when a remoteOperation finish - * @param operation the operation which finished - * @param result the result of the operation - */ - @Override - public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - Log.i(TAG, "onRemoteOperationFinish()"); - - // Start an another operation with the thread which has run this one - Integer threadIndex = this.mStartedOperations.remove(operation); - if(threadIndex != null) { - this.mThreadWorkingState[threadIndex] = false; - this.startWork(threadIndex); - } - - if(operation instanceof RemoveFileOperation){ - if( result.isSuccess() ) { - DbHelper.manageSyncedFileStateDB( ( ( RemoveFileOperation ) operation ).getSyncedFileState(), - "DELETE", this); - } - }else { - String operationClassName = operation.getClass().getSimpleName(); - switch (result.getCode()) { - case OK: - Log.d(TAG, operationClassName + " Succeed"); - break; - case SYNC_CONFLICT: - //Case specific to UploadFileOperation - Log.e(TAG, operationClassName+" : Sync_conflict : File is already up to date"); - break; - case INVALID_OVERWRITE: - Log.e(TAG, operationClassName + " => invalid_overwrite :\n remote file and local file doesn't have the same size"); - break; - case UNKNOWN_ERROR: - if (operation instanceof UploadFileOperation) { - if(result.getData() != null) { - int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext()); - Log.e(TAG, " Upload failed for unknown reason.\n Force folder to be rescan next time (row affected) :" + rowAffected); - }else{ - Log.w(TAG, "result.getData() for UploadFileOperation returned null"); - } - } else if (operation instanceof DownloadFileOperation) { - Log.e(TAG, " Download: Unknown_error : failed"); - } - break; - case FORBIDDEN: - if (operation instanceof UploadFileOperation) { - - if(result.getData() != null) { - int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext()); - Log.e(TAG, " Upload: Forbidden : Can't get syncedFileState, no remote path defined. Force folder to be rescan next time (row affected) :" + rowAffected); - }else{ - Log.w(TAG, "result.getData() for UploadFileOperation returned null"); - } - } else if (operation instanceof DownloadFileOperation) { - Log.e(TAG, "Download : Forbidden: Can't get syncedFileState, no local path defined"); - } - break; - case QUOTA_EXCEEDED: - //Case specific to UploadFileOperation - Log.w(TAG, "Quota_EXCEEDED"); - - NotificationManager nM = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - - Notification notif = new Notification.Builder(this, AppConstants.notificationChannelID) - .setContentIntent(PendingIntent.getActivity(getApplicationContext(), - 0, - new Intent(Intent.ACTION_VIEW, mClient.getBaseUri()), - 0)) - .setContentText("Your drive lacks of space. Tap to check " + mClient.getBaseUri()) - .setSmallIcon(android.R.drawable.stat_sys_warning) - .build(); - - nM.notify(1,notif ); - break; - case FILE_NOT_FOUND: - //Case specific to DownloadFileOperation - Log.e(TAG, operationClassName+" : File_not_found: File not found after download"); - break; - case ETAG_UNCHANGED: - //Case specific to DownloadFileOperation - Log.e(TAG, operationClassName+" : Sync_conflict: File is already up to date"); - break; - - } //Close switch - } //Close else - } - - /** - * Handler for the class - */ - static class OperationManagerHandler extends Handler { - private final String TAG = OperationManagerHandler.class.getSimpleName(); - - private final WeakReference mOperationServiceWeakRef; - - OperationManagerHandler(OperationManagerService mOperationService){ - this.mOperationServiceWeakRef = new WeakReference<>(mOperationService); - } - - @Override - public void handleMessage(Message msg) { - Log.i(TAG, "handler.handleMessage()"); - Bundle data = msg.getData(); - - mOperationServiceWeakRef.get() - .mThreadWorkingState[data.getInt("thread index")] = data.getBoolean("mThreadWorkingState"); - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.i(TAG, "onStartCommand()"); - - try{ - CommonUtils.setServiceUnCaughtExceptionHandler(this); - - if (null == intent || null == intent.getAction ()) { - String source = null == intent ? "intent" : "action"; - Log.e (TAG, source + " was null, flags=" + flags + " bits=" + Integer.toBinaryString (flags)); - //return START_STICKY; - } - - Bundle extras = intent.getExtras(); - Log.d(TAG, "OperationManagerService recieved "+(extras == null ? "null extras": extras.size()+" operations to perform") ); - - if(extras != null) { - //Load operation from intent - this.mOperationsQueue = new ConcurrentLinkedDeque<>(); - for (String key : extras.keySet()) { - - Parcelable parcelableObject = extras.getParcelable(key); - if (key.equals("account")) { - this.mAccount = (Account) parcelableObject; - } else if (parcelableObject.getClass().getName().equals(DownloadFileOperation.class.getName())) { - DownloadFileOperation download = (DownloadFileOperation) parcelableObject; - download.setContext(getApplicationContext()); - mOperationsQueue.add(download); - } else if (parcelableObject.getClass().getName().equals(UploadFileOperation.class.getName())) { - UploadFileOperation upload = (UploadFileOperation) parcelableObject; - upload.setContext(getApplicationContext()); - mOperationsQueue.add(upload); - } else if (parcelableObject.getClass().getName().equals(RemoveFileOperation.class.getName())) { - mOperationsQueue.add((RemoveFileOperation) parcelableObject); - } - } - - if(mAccount == null || mOperationsQueue.isEmpty()){ - Log.w(TAG, "No account or Operation queue is empty"); - return super.onStartCommand(intent, flags, startId); - } - - //Initialize class's field - this.workerAmount = 4; //This variable could be replace later by an option in settings - - this.mThreadPool = new Thread[workerAmount]; - this.mThreadWorkingState = new boolean[workerAmount]; - this.mHandler = new OperationManagerHandler(this); - this.mStartedOperations = new Hashtable(); - - mClient = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext()); - if (mClient != null) { - getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, true) - .apply(); - - startAllThreads(); - } else { - Log.w(TAG, "No Client, Can't Work!"); - stopSelf(); - } - }else{ - Log.w(TAG, "Intent's extras is null."); - } - }catch (Exception ex){ - Log.e("Exception", ex.getMessage()); - ex.printStackTrace(); - } - - return super.onStartCommand(intent, flags, startId); - } - - - @Nullable - @Override - public IBinder onBind(Intent intent) { - throw new UnsupportedOperationException(); - } - - @Override - public void onLowMemory() { - Log.w(TAG, "System is low on memory. Service might get killed. Setting KEY_OMS_IS_WORKING to false"); - getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE).edit() - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false) - .apply(); - } -} diff --git a/app/src/main/java/foundation/e/drive/services/ResetService.java b/app/src/main/java/foundation/e/drive/services/ResetService.java index dad3cb20a6b024884a57bb35bd0e62e24ee7e55a..a9eb6e3ff6fb8576616b08cdb4bd83c226019077 100644 --- a/app/src/main/java/foundation/e/drive/services/ResetService.java +++ b/app/src/main/java/foundation/e/drive/services/ResetService.java @@ -18,15 +18,16 @@ import android.util.Log; import java.io.File; +import foundation.e.drive.EdriveApplication; import foundation.e.drive.database.DbHelper; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.utils.JobUtils; import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; import androidx.annotation.Nullable; +import androidx.work.WorkManager; /** * @author Vincent Bourgmayer @@ -38,63 +39,25 @@ public class ResetService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand()"); - if( intent.getExtras() != null ) { - String intent_accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); - String intent_accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + if ( intent.getExtras() != null ) { + final String intent_accountName = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME, ""); + final String intent_accountType = intent.getExtras().getString(AccountManager.KEY_ACCOUNT_TYPE, ""); - if(!intent_accountName.isEmpty() && !intent_accountType.isEmpty()){ - SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + if (!intent_accountName.isEmpty() && !intent_accountType.isEmpty()) { + final SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - if(intent_accountName.equals(prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "") ) - && intent_accountType.equals(prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, "") ) ){ + if (intent_accountName.equals(prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "")) + && intent_accountType.equals(prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""))) { + WorkManager.getInstance(this).cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG); + ( (EdriveApplication) this.getApplication() ).stopRecursiveFileObserver(); - //1. Stop sync services + stopAllServices(); - Intent stopperIntent = new Intent(getApplicationContext(), ObserverService.class); - boolean result = getApplicationContext().stopService( stopperIntent ); - Log.d(TAG, "stop ObserverService: "+result); - - stopperIntent = new Intent(getApplicationContext(), InitializerService.class); - result = getApplicationContext().stopService( stopperIntent ); - Log.d(TAG, "stop InitializerService: "+result); - - stopperIntent = new Intent(getApplicationContext(), OperationManagerService.class); //@todo try to replace it by stopperIntent.setClassName(getApplicationContext, OperationManagerService.class) - - - result = getApplicationContext().stopService( stopperIntent ); - Log.d(TAG, "stop OperationManagerService: "+result); - - //2. Stop the scheduledJob - JobUtils.stopScheduledJob(this, JobUtils.ScannerJobId); - //JobUtils.stopScheduledJob(this, JobUtils.InitializerJobId); - //3. delete DB - result = this.deleteDatabase(DbHelper.DATABASE_NAME); + boolean result = deleteDatabase(DbHelper.DATABASE_NAME); Log.d(TAG, "Remove Database: "+result); - //4. preferences cleaning - - //Remove the prefs file. - if(!this.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)){ - - //If removal failed, clear all data inside - prefs.edit().remove(AccountManager.KEY_ACCOUNT_NAME) - .remove(AccountManager.KEY_ACCOUNT_TYPE) - .putBoolean(INITIALIZATION_HAS_BEEN_DONE, false) - .remove(INITIALFOLDERS_NUMBER) - .remove(AppConstants.KEY_OMS_IS_WORKING) - .remove(AppConstants.KEY_LAST_SYNC_TIME) - .apply(); - } - - //5. Unregister screenOffReceiver - result = CommonUtils.unregisterScreenOff(getApplicationContext()); - Log.d(TAG, "Unregistered ScreenOffReceiver: "+result); - - //6. Remove Cached File - File[] cachedFiles = this.getApplicationContext().getExternalCacheDir().listFiles(); - for(File f : cachedFiles){ - f.delete(); - } + cleanSharedPreferences(prefs); + removeCachedFiles(); } } } @@ -102,9 +65,45 @@ public class ResetService extends Service { return super.onStartCommand(intent, flags, startId); } + + private void stopAllServices() { + Intent stopperIntent = new Intent(getApplicationContext(), ObserverService.class); + boolean result = getApplicationContext().stopService( stopperIntent ); + Log.d(TAG, "stop ObserverService: "+result); + + stopperIntent = new Intent(getApplicationContext(), InitializerService.class); + result = getApplicationContext().stopService( stopperIntent ); + Log.d(TAG, "stop InitializerService: "+result); + + stopperIntent = new Intent(getApplicationContext(), SynchronizationService.class); + result = getApplicationContext().stopService( stopperIntent ); + + Log.d(TAG, "stop SynchronizationService: "+result); + } + + private void cleanSharedPreferences(SharedPreferences prefs) { + if (!this.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) { + + //If removal failed, clear all data inside + prefs.edit().remove(AccountManager.KEY_ACCOUNT_NAME) + .remove(AccountManager.KEY_ACCOUNT_TYPE) + .putBoolean(INITIALIZATION_HAS_BEEN_DONE, false) + .remove(INITIALFOLDERS_NUMBER) + .remove(AppConstants.KEY_LAST_SYNC_TIME) + .apply(); + } + } + + private void removeCachedFiles() { + File[] cachedFiles = this.getApplicationContext().getExternalCacheDir().listFiles(); + for (File f : cachedFiles) { + f.delete(); + } + } + @Nullable @Override public IBinder onBind(Intent intent) { return null; } -} +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java new file mode 100644 index 0000000000000000000000000000000000000000..afc77d70aec182aa708f95a36d7d8770011295c2 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -0,0 +1,284 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.services; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Hashtable; +import java.util.concurrent.ConcurrentLinkedDeque; + +import foundation.e.drive.R; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.DownloadRequest; +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.operations.DownloadFileOperation; +import foundation.e.drive.operations.RemoveFileOperation; +import foundation.e.drive.operations.UploadFileOperation; +import foundation.e.drive.utils.AppConstants; +import foundation.e.drive.utils.CommonUtils; +import foundation.e.drive.utils.DavClientProvider; + +/** + * @author Vincent Bourgmayer + */ +public class SynchronizationService extends Service implements OnRemoteOperationListener { + private final static String TAG = SynchronizationService.class.getSimpleName(); + private final SynchronizationBinder binder = new SynchronizationBinder(); + + private ConcurrentLinkedDeque syncedRequestQueue; + private Hashtable startedOperations; //Operations which are running + private Account account; + private final int workerAmount = 4; + private boolean[] threadWorkingState; //State of the threads; true mean the thread is working + private Thread[] threadPool; + private OwnCloudClient client; + private OperationHandler handler; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand()"); + + SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + final String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + final String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); + + if (account == null) { + Log.w(TAG, "No account available. Stop SynchronizationService"); + stopSelf(); + return START_NOT_STICKY; + } + + syncedRequestQueue = new ConcurrentLinkedDeque<>(); + startedOperations = new Hashtable<>(); + threadPool = new Thread[workerAmount]; + threadWorkingState = new boolean[workerAmount]; + client = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext()); + handler = new OperationHandler(this); + + return START_REDELIVER_INTENT; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onLowMemory() { + Log.w(TAG, "System is low on memory. Service might get killed."); + } + + public boolean queueOperation(SyncRequest request){ + return syncedRequestQueue.add(request); + } + + public boolean queueOperations(Collection requests){ + return syncedRequestQueue.addAll(requests); + } + + public void startSynchronization(){ + Log.d(TAG, "startAllThreads"); + for(int i =-1; ++i < workerAmount;){ + this.startWorker(i); + } + } + + private void startWorker(int threadIndex){ + if (syncedRequestQueue.isEmpty()) return; + if (!threadWorkingState[threadIndex] && CommonUtils.haveNetworkConnection(getApplicationContext())) { //check if the thread corresponding to threadIndex isn't already working + + final SyncRequest request = this.syncedRequestQueue.poll(); //return null if deque is empty + + final RemoteOperation operation = this.createRemoteOperation(request); + + if (operation != null) { + Log.v(TAG, " an operation has been poll from queue"); + + if (CommonUtils.isThisSyncAllowed(account, request.getSyncedFileState().isMediaType())) { + CommonUtils.createNotificationChannel(this); + startedOperations.put(operation, threadIndex); + threadPool[threadIndex] = operation.execute(client, this, handler); + threadWorkingState[threadIndex] = true; + } + } + } + } + + @Override + public void onRemoteOperationFinish(RemoteOperation callerOperation, RemoteOperationResult result) { + Log.d(TAG, "onRemoteOperationFinish()"); + Integer threadIndex = this.startedOperations.remove(callerOperation); + if (threadIndex != null) { + this.threadWorkingState[threadIndex] = false; + this.startWorker(threadIndex); + } + + if (callerOperation instanceof RemoveFileOperation){ + if ( result.isSuccess() ) { + DbHelper.manageSyncedFileStateDB( ( ( RemoveFileOperation ) callerOperation ).getSyncedFileState(), + "DELETE", this); + } + } else { + String operationClassName = callerOperation.getClass().getSimpleName(); + if (callerOperation instanceof UploadFileOperation) { + final Float relativeQuota = (Float) result.getData().get(1); + if (relativeQuota >= 99.0) { + addNotification(getString(R.string.notif_quota_nearlyReached_title), getString(R.string.notif_quota_99Plus_text)); + } else if (relativeQuota >= 90.0) { + addNotification(getString(R.string.notif_quota_nearlyReached_title), getString(R.string.notif_quota_90Plus_text)); + } else if (relativeQuota >= 80.0) { + addNotification(getString(R.string.notif_quota_nearlyReached_title), getString(R.string.notif_quota_80Plus_text)); + } + } + switch (result.getCode()) { + case OK: + Log.d(TAG, operationClassName + " Succeed"); + break; + case SYNC_CONFLICT: + //Case specific to UploadFileOperation + Log.e(TAG, operationClassName+" : Sync_conflict : File is already up to date"); + break; + case INVALID_OVERWRITE: + Log.e(TAG, operationClassName + " => invalid_overwrite :\n remote file and local file doesn't have the same size"); + break; + case UNKNOWN_ERROR: + if (callerOperation instanceof UploadFileOperation) { + if(result.getData() != null) { + int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext()); + Log.e(TAG, " Upload failed for unknown reason.\n Force folder to be rescan next time (row affected) :" + rowAffected); + } else { + Log.w(TAG, "result.getData() for UploadFileOperation returned null"); + } + } else if (callerOperation instanceof DownloadFileOperation) { + Log.e(TAG, " Download: Unknown_error : failed"); + } + break; + case FORBIDDEN: + if (callerOperation instanceof UploadFileOperation) { + if (result.getData() != null) { + int rowAffected = DbHelper.forceFoldertoBeRescan(((Long) result.getData().get(0)).intValue(), getApplicationContext()); + Log.e(TAG, " Upload: Forbidden : Can't get syncedFileState, no remote path defined. Force folder to be rescan next time (row affected) :" + rowAffected); + } else { + Log.w(TAG, "result.getData() for UploadFileOperation returned null"); + } + } else if (callerOperation instanceof DownloadFileOperation) { + Log.e(TAG, "Download : Forbidden: Can't get syncedFileState, no local path defined"); + } + break; + case QUOTA_EXCEEDED: + //Case specific to UploadFileOperation + Log.w(TAG, "Quota_EXCEEDED"); + addNotification(getString(R.string.notif_quota_execeeded_title), getString(R.string.notif_quota_execeeded_text)); + break; + case FILE_NOT_FOUND: + //Case specific to DownloadFileOperation + Log.e(TAG, operationClassName+" : File_not_found: File not found after download"); + break; + case ETAG_UNCHANGED: + //Case specific to DownloadFileOperation + Log.e(TAG, operationClassName+" : Sync_conflict: File is already up to date"); + break; + + } + } + } + + private RemoteOperation createRemoteOperation(SyncRequest request){ + RemoteOperation operation; + switch (request.getOperationType()){ + case UPLOAD: + final SyncedFileState sfs = request.getSyncedFileState(); + operation = new UploadFileOperation(sfs, getApplicationContext()); + break; + case DOWNLOAD: + final DownloadRequest downloadRequest = (DownloadRequest) request; + operation = new DownloadFileOperation(downloadRequest.getRemoteFile(), downloadRequest.getSyncedFileState(), getApplicationContext()); + break; + case REMOTE_DELETE: + default: + operation = null; + break; + } + return operation; + } + + /** + * send notification to inform user that he lacks space on ecloud + * Improvement idea: + * - add translatable message & title + * - make message & title to be a parameter of the method, so we could reuse the function somewhere + * - else with different notification. File conflict for example. + **/ + private void addNotification(String title, String text){ + final NotificationCompat.Builder builder = + new NotificationCompat.Builder(this, AppConstants.notificationChannelID) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setContentTitle(title) + .setContentText(text+ client.getBaseUri()); + // Add as notification + final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + manager.notify(0, builder.build()); + } + + + + /** + * Handler for the class + */ + static class OperationHandler extends Handler { + private final String TAG = SynchronizationService.OperationHandler.class.getSimpleName(); + + private final WeakReference serviceWeakRef; + + OperationHandler(SynchronizationService mOperationService){ + serviceWeakRef = new WeakReference<>(mOperationService); + } + + @Override + public void handleMessage(Message msg) { + Log.i(TAG, "handler.handleMessage()"); + Bundle data = msg.getData(); + + serviceWeakRef.get() + .threadWorkingState[data.getInt("thread index")] = data.getBoolean("mThreadWorkingState"); + } + } + + public class SynchronizationBinder extends Binder{ + public SynchronizationService getService(){ + return SynchronizationService.this; + } + } +} diff --git a/app/src/main/java/foundation/e/drive/utils/AppConstants.java b/app/src/main/java/foundation/e/drive/utils/AppConstants.java index 1e180424b8cb05744a5e37cb6f1d72a6f92d46a6..63f00e488e3fc1c0d56650891354aac62757571f 100644 --- a/app/src/main/java/foundation/e/drive/utils/AppConstants.java +++ b/app/src/main/java/foundation/e/drive/utils/AppConstants.java @@ -8,7 +8,6 @@ package foundation.e.drive.utils; - import android.os.Build; import foundation.e.drive.BuildConfig; @@ -17,11 +16,9 @@ import java.util.Locale; import foundation.e.drive.BuildConfig; - /** * @author Vincent Bourgmayer */ -//Contains some constant value public abstract class AppConstants { public static final String MEDIASYNC_PROVIDER_AUTHORITY ="foundation.e.drive.providers.MediasSyncProvider"; @@ -33,16 +30,14 @@ public abstract class AppConstants { public static final String APPLICATIONS_LIST_FILE_NAME_TMP = "tmp_packages_list.csv"; public static final String SHARED_PREFERENCE_NAME ="preferences"; public static final String KEY_LAST_SYNC_TIME = "lastSyncTimestamp"; - public static final String KEY_OMS_IS_WORKING = "OMS_is_working"; public static final String[] MEDIA_SYNCABLE_CATEGORIES = new String[]{"Images", "Movies", "Music", "Ringtones", "Documents", "Podcasts"}; public static final String[] SETTINGS_SYNCABLE_CATEGORIES = new String[] {"Rom settings"}; - public final static String notificationChannelID ="3310"; - public final static String notificationChannelName="eDrive channel"; - - - public final static String USER_AGENT = "eos("+getBuildTime()+")-eDrive("+ BuildConfig.VERSION_NAME +")"; + public static final String notificationChannelID ="foundation.e.drive"; + public static final String WORK_GENERIC_TAG="eDrive"; + public static final String WORK_INITIALIZATION_TAG="eDrive-init"; + public static final String USER_AGENT = "eos("+getBuildTime()+")-eDrive("+ BuildConfig.VERSION_NAME +")"; /** * Get a readable OS's build date String @@ -53,6 +48,4 @@ public abstract class AppConstants { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd", Locale.getDefault()); return sdf.format(ts); } - - } diff --git a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java index e0f762fcb4f2d104db8baa88938f26276ddbee85..563841fd32463184f9e19c088431767c26959bfa 100644 --- a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java @@ -12,14 +12,16 @@ package foundation.e.drive.utils; import android.accounts.Account; import android.accounts.AccountManager; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.Service; import android.content.ContentResolver; import android.content.Context; import android.media.MediaScannerConnection; import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkInfo; +import android.net.NetworkCapabilities; import android.net.Uri; +import android.os.Build; import android.util.Log; import android.webkit.MimeTypeMap; @@ -31,13 +33,39 @@ import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.resources.files.FileUtils; import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; -import foundation.e.drive.receivers.ScreenOffReceiver; + +import foundation.e.drive.R; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.work.CreateRemoteFolderWorker; +import foundation.e.drive.work.FirstStartWorker; +import foundation.e.drive.work.FullScanWorker; import static foundation.e.drive.utils.AppConstants.MEDIASYNC_PROVIDER_AUTHORITY; import static foundation.e.drive.utils.AppConstants.SETTINGSYNC_PROVIDER_AUTHORITY; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_ENABLE; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_ID; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_LAST_ETAG; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_LAST_MODIFIED; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_LIBELLE; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_LOCAL_PATH; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_MEDIATYPE; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_REMOTE_PATH; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_SCAN_LOCAL; +import static foundation.e.drive.work.CreateRemoteFolderWorker.DATA_KEY_SCAN_REMOTE; import androidx.annotation.NonNull; +import androidx.work.BackoffPolicy; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; /** @@ -45,7 +73,7 @@ import androidx.annotation.NonNull; */ public abstract class CommonUtils { - final private static String TAG = CommonUtils.class.getSimpleName(); + private final static String TAG = CommonUtils.class.getSimpleName(); /** * Set ServiceUncaughtExceptionHandler to be the MainThread Exception Handler @@ -64,23 +92,6 @@ public abstract class CommonUtils { } } - /** - * Unregister from screeOffReceiver component - * - * @param context app context - * @return true if unregistration was successful or false if it encounter an exception - */ - public static boolean unregisterScreenOff(Context context) { - try { - Log.d("TAG", "unregisterReceiver(screenOffReceiver)"); - context.unregisterReceiver(ScreenOffReceiver.getInstance()); - } catch (IllegalArgumentException e) { - Log.w(TAG, "Can't unregister screenOffReceiver "); - return false; - } - return true; - } - /** * This method retrieve Account corresponding to account's name and type @@ -163,7 +174,7 @@ public abstract class CommonUtils { oc.setCredentials(new OwnCloudBasicCredentials(account.name, AccountManager.get(context).getPassword(account))); Log.d(TAG, "user agent: "+AppConstants.USER_AGENT); - if(!AppConstants.USER_AGENT.equals(OwnCloudClientManagerFactory.getUserAgent())) { + if (!AppConstants.USER_AGENT.equals(OwnCloudClientManagerFactory.getUserAgent()) ){ OwnCloudClientManagerFactory.setUserAgent(AppConstants.USER_AGENT); } @@ -200,24 +211,23 @@ public abstract class CommonUtils { * @param context Activity or service which are calling this method * @return True if there is connexion, false either */ - public static boolean haveNetworkConnexion(Context context) { - Log.i(TAG, "haveNetworkConnexion()"); - boolean haveConnectedWifi = false; - boolean haveConnectedMobile = false; - - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo[] netInfo = cm.getAllNetworkInfo(); - for (NetworkInfo ni : netInfo) { - if (ni.getType()== ConnectivityManager.TYPE_WIFI) // Replaced the ni.getTypeName by ni.getType to make the test from ObserverServiceTest to work. But looks a better solution in all case - if (ni.isConnected()) - haveConnectedWifi = true; - if (ni.getType()== ConnectivityManager.TYPE_MOBILE) - if (ni.isConnected()) - haveConnectedMobile = true; + public static boolean haveNetworkConnection(Context context) { + Log.i(TAG, "haveNetworkConnection()"); + + ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork()); + + if (capabilities != null + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) { + return true; } - return haveConnectedWifi || haveConnectedMobile; + return false; } + + /** * Get mimetype of file from the file itself * @@ -293,4 +303,108 @@ public abstract class CommonUtils { + "\n File can be read?: " + f.canRead() + "\n File can be written?: " + f.canWrite(); } + + /** + * Enqueue a unique periodic worker to look for file to be synchronized (remote files + local files + * @param workManager the instance of workManager + */ + public static void registerPeriodicFullScanWorker(WorkManager workManager){ + final Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build(); + + final PeriodicWorkRequest periodicFullScanRequest = + new PeriodicWorkRequest.Builder(FullScanWorker.class, + 31, TimeUnit.MINUTES, + 5, TimeUnit.MINUTES) + .setConstraints(constraints) + .addTag(AppConstants.WORK_GENERIC_TAG) + .build(); + + workManager.enqueueUniquePeriodicWork(FullScanWorker.UNIQUE_WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, periodicFullScanRequest); + } + + + /** + * This method create a chain of WorkRequests to perform Initialization tasks: + * Firstly, it creates WorkRequest to create remote root folders on ecloud + * Then, once all folders are present on cloud, run the FirstStartWorker. + * + * in details: + * - Create 9 remote folders on ecloud + * - Run a first fullscan (ObserverService) + * - start SynchronizationService + * - Active FileObserver + * - Schedule periodic fullscan. + * + * @param syncedFolders List of SyncedFolder for which we want to create a remote folder on ecloud + * @param workManager WorkManager instance to register WorkRequest + */ + public static void registerInitializationWorkers(List syncedFolders, WorkManager workManager){ + if (syncedFolders == null || syncedFolders.isEmpty()) { + Log.e(TAG, "Can't create remote folders. List is empty"); + return; + } + + final Constraints constraints = new Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build(); + + final List workRequests = new ArrayList<>(); + + for (SyncedFolder folder : syncedFolders) { + final OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder( + CreateRemoteFolderWorker.class) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2, TimeUnit.MINUTES) + .setInputData(createDataFromSyncedFolder(folder)) + .addTag(AppConstants.WORK_GENERIC_TAG) + .addTag(AppConstants.WORK_INITIALIZATION_TAG) + .setConstraints(constraints) + .build(); + + workRequests.add(oneTimeWorkRequest); + } + + final OneTimeWorkRequest firstStartRequest = new OneTimeWorkRequest.Builder(FirstStartWorker.class) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2, TimeUnit.MINUTES) + .addTag(AppConstants.WORK_GENERIC_TAG) + .addTag(AppConstants.WORK_INITIALIZATION_TAG) + .build(); + + workManager.beginWith(workRequests) + .then(firstStartRequest) + .enqueue(); + } + + + private static Data createDataFromSyncedFolder(SyncedFolder folder) { + return new Data.Builder() + .putInt(DATA_KEY_ID, folder.getId()) + .putString(DATA_KEY_LIBELLE, folder.getLibelle()) + .putString(DATA_KEY_LOCAL_PATH, folder.getLocalFolder()) + .putString(DATA_KEY_REMOTE_PATH, folder.getRemoteFolder()) + .putString(DATA_KEY_LAST_ETAG, folder.getLastEtag()) + .putLong(DATA_KEY_LAST_MODIFIED, folder.getLastModified()) + .putBoolean(DATA_KEY_SCAN_LOCAL, folder.isScanLocal()) + .putBoolean(DATA_KEY_SCAN_REMOTE, folder.isScanRemote()) + .putBoolean(DATA_KEY_ENABLE, folder.isEnabled()) + .putBoolean(DATA_KEY_MEDIATYPE, folder.isMediaType()) + .build(); + } + + public static void createNotificationChannel(Context context){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + final CharSequence name = context.getString(R.string.notif_channel_name); + final String description = context.getString(R.string.notif_channel_description); + int importance = NotificationManager.IMPORTANCE_DEFAULT; + final NotificationChannel channel = new NotificationChannel(AppConstants.notificationChannelID, name, importance); + channel.setDescription(description); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/utils/JobUtils.java b/app/src/main/java/foundation/e/drive/utils/JobUtils.java deleted file mode 100644 index 790427eb103bdd02a10a0b93fe760a1a2bc4c435..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/utils/JobUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright © Vincent Bourgmayer (/e/ foundation). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the GNU Public License v3.0 - * which accompanies this distribution, and is available at - * http://www.gnu.org/licenses/gpl.html - */ - -package foundation.e.drive.utils; - -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.util.Log; -import foundation.e.drive.jobs.ScannerJob; - - -/** - * @author Vincent Bourgmayer - */ - - -public abstract class JobUtils { - final private static String TAG = JobUtils.class.getSimpleName(); //Tag for log - public final static int ScannerJobId = 3310; - //public final static int InitializerJobId = 3311; - - /** - * Start the scheduledJob for observing remote's folder - * @param context app or service context - */ - public static void scheduleScannerJob(Context context){ - Log.i(TAG, "scheduleJob"); - /* I. Start periodic checkup */ - ComponentName jobService = new ComponentName( context, ScannerJob.class ); - - JobInfo job = new JobInfo.Builder(ScannerJobId, jobService ) - .setPeriodic(1860000, 30000) //31git minutes and 30 secondes - .setPersisted(true) - .setRequiredNetworkType( JobInfo.NETWORK_TYPE_ANY ) - .build(); - - JobScheduler jobScheduler = context.getSystemService( JobScheduler.class ); - - if ( jobScheduler.schedule( job ) == JobScheduler.RESULT_SUCCESS ) { - Log.d(TAG, "Scheduled job created"); - } else { - Log.e(TAG, "Scheduled job not created"); - } - } - - /** - * Try to stop the scheduledJob - * @param context - */ - public static void stopScheduledJob(Context context, int jobId){ - context.getSystemService( JobScheduler.class ).cancel(jobId); - } - - /** - * tell if the scannerJob is already registered or not - * @param context - * @return true if is registered, false either - */ - public static boolean isScannerJobRegistered(Context context){ - return (context.getSystemService( JobScheduler.class).getPendingJob(ScannerJobId) != null); - } - -} diff --git a/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java b/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java index 250e791b2f7bbc9bf706fa16fc423f32c3499e8f..d94e1c21320140d7a649c689c50d134ad322feb3 100644 --- a/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java +++ b/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java @@ -7,7 +7,6 @@ */ package foundation.e.drive.utils; import android.app.Service; -import android.content.Context; import android.os.Environment; import android.util.Log; @@ -18,8 +17,6 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.Thread.UncaughtExceptionHandler; -import foundation.e.drive.services.OperationManagerService; - /** * @author Vincent Bourgmayer */ @@ -48,13 +45,6 @@ public class ServiceExceptionHandler implements UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { Log.d(TAG, "Service class: "+service.getClass().getSimpleName()); - //IF OMS is crashing, set settings that it runs to false; - if(service.getClass().getSimpleName().equals(OperationManagerService.class.getSimpleName())){ - service.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, false) - .apply(); - } if(isExternalStorageAvailable() && !isExternalStorageReadOnly()){ //Get TimeStamp diff --git a/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java b/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..7f828ff94aba24f7b55d1395959ed206882b0bf1 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java @@ -0,0 +1,52 @@ +/** + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.utils; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import foundation.e.drive.services.SynchronizationService; + +/** + * @author Vincent Bourgmayer + */ +public class SynchronizationServiceConnection implements ServiceConnection { + private final static String TAG = SynchronizationServiceConnection.class.getSimpleName(); + + private SynchronizationService synchronizationService; + private boolean boundToSynchronizationService = false; + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + Log.i(TAG, "onServiceConnected: binding to SynchronizationService"); + SynchronizationService.SynchronizationBinder binder = (SynchronizationService.SynchronizationBinder) iBinder; + synchronizationService = binder.getService(); + boundToSynchronizationService = true; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + Log.i(TAG, "onServiceDisconnected: unbinding from SynchronizationService"); + boundToSynchronizationService = false; + } + + public boolean isBoundToSynchronizationService() { + return boundToSynchronizationService; + } + + /** + * Get SynchronizationService. Might be null! + * @return + */ + public SynchronizationService getSynchronizationService() { + return synchronizationService; + } +} diff --git a/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java b/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..8c1443e23312bee849959a772486d8077d388ebb --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/CreateRemoteFolderWorker.java @@ -0,0 +1,127 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.work; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.Data; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; + +import java.io.File; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.AppConstants; +import foundation.e.drive.utils.CommonUtils; + +/** + * Create folder on ecloud for a given local folder + * @author Vincent Bourgmayer + */ +public class CreateRemoteFolderWorker extends Worker { + private static final String TAG = CreateRemoteFolderWorker.class.getSimpleName(); + public static final String DATA_KEY_ID="id"; + public static final String DATA_KEY_LIBELLE="libelle"; + public static final String DATA_KEY_LOCAL_PATH="localPath"; + public static final String DATA_KEY_REMOTE_PATH="remotePath"; + public static final String DATA_KEY_LAST_ETAG="etag"; + public static final String DATA_KEY_LAST_MODIFIED="lModified"; + public static final String DATA_KEY_SCAN_LOCAL="scanLocal"; + public static final String DATA_KEY_SCAN_REMOTE="scanRemote"; + public static final String DATA_KEY_ENABLE="enable"; + public static final String DATA_KEY_MEDIATYPE="mediaType"; + + public CreateRemoteFolderWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + final Context context = getApplicationContext(); + if (!CommonUtils.haveNetworkConnection(context)) { + Log.e(TAG, "Can't create remote folder because there is no unmetered connexion"); + return Result.retry(); + } + + final Account account = getAccount(); + if (account == null) { + Log.e(TAG, "Can't get valid account: stop everything"); + return Result.failure(); + } + + final SyncedFolder syncedFolder = getSyncedFolderFromData(); + Log.d(TAG, "doWork() for :"+syncedFolder.getLocalFolder()); + final File folder = new File(syncedFolder.getLocalFolder() ); + if (!folder.exists()) { + folder.mkdirs(); + syncedFolder.setLastModified(folder.lastModified()); + } + + final OwnCloudClient client = CommonUtils.getOwnCloudClient(account, context); + if (client == null) { + Log.e(TAG, "Can't get OwnCloudClient."); + return Result.retry(); + } + + final CreateFolderRemoteOperation mkcolRequest = + new CreateFolderRemoteOperation(syncedFolder.getRemoteFolder(), true); + + try { + final RemoteOperationResult result = mkcolRequest.execute(client, true); + if (result.isSuccess() || result.getCode() == RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS) { + if(DbHelper.insertSyncedFolder(syncedFolder, context) >= 0 ) { + Log.d(TAG, "Insertion in DB succeed"); + } + return Result.success(); + } + } catch (Exception e) { + Log.e(TAG, "Exception: "+e.getClass().getSimpleName()); + } + return Result.retry(); + } + + private SyncedFolder getSyncedFolderFromData() { + Data data = getInputData(); + SyncedFolder result = new SyncedFolder( + data.getString(DATA_KEY_LIBELLE), + data.getString(DATA_KEY_LOCAL_PATH), + data.getString(DATA_KEY_REMOTE_PATH), + data.getBoolean(DATA_KEY_SCAN_LOCAL, true), + data.getBoolean(DATA_KEY_SCAN_REMOTE, true), + data.getBoolean(DATA_KEY_ENABLE, true), + data.getBoolean(DATA_KEY_MEDIATYPE, true)); + + result.setLastModified(data.getLong(DATA_KEY_LAST_MODIFIED, 0L)); + result.setLastEtag(data.getString(DATA_KEY_LAST_ETAG)); + result.setId(data.getInt(DATA_KEY_ID, -1)); + + return result; + } + + + private Account getAccount() { + SharedPreferences prefs = getApplicationContext().getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + final String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + final String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + + if (accountName.isEmpty() && accountType.isEmpty()) return null; + return CommonUtils.getAccount(accountName, accountType, AccountManager.get(getApplicationContext())); + } +} diff --git a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..98ded19260ee93f02eff0dec5296e10d5f552aa6 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java @@ -0,0 +1,60 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.work; + +import static foundation.e.drive.utils.AppConstants.INITIALFOLDERS_NUMBER; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.WorkManager; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import foundation.e.drive.EdriveApplication; +import foundation.e.drive.utils.AppConstants; +import foundation.e.drive.utils.CommonUtils; + +/** + * This class start eDrive work after initialization. + * It contains job to start FileObserver, periodic fullScan and Synchronization service + * for the first time + * @author Vincent Bourgmayer + */ +public class FirstStartWorker extends Worker { + private static final String TAG = FirstStartWorker.class.getSimpleName(); + public FirstStartWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Log.d(TAG, "doWork()"); + final Context appContext = getApplicationContext(); + appContext.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, + Context.MODE_PRIVATE) + .edit() + .putBoolean(AppConstants.INITIALIZATION_HAS_BEEN_DONE, true) + .putInt(INITIALFOLDERS_NUMBER, 9) + .apply(); + + CommonUtils.registerPeriodicFullScanWorker(WorkManager.getInstance(appContext)); + + getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.SynchronizationService.class)); + getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.ObserverService.class)); + + //all folder have been created + ((EdriveApplication) getApplicationContext()).startRecursiveFileObserver(); + + return Result.success(); + } +} diff --git a/app/src/main/java/foundation/e/drive/work/FullScanWorker.java b/app/src/main/java/foundation/e/drive/work/FullScanWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..a2424a076e77d7a945e241f706f9df2c4392a2c9 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/FullScanWorker.java @@ -0,0 +1,63 @@ +/* + * Copyright © Vincent Bourgmayer (/e/ foundation). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.work; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; + +import foundation.e.drive.services.ObserverService; +import foundation.e.drive.utils.AppConstants; +import foundation.e.drive.utils.CommonUtils; + +/** + * As a first step, this class must replace foundation.e.drive.jobs.ScannerJob + * in order to allow to use Jetpack Work API + * + * In further development it will be a part of Workers that will replace ObserverService + * I will update this header accordingly + * + * @author Vincent Bourgmayer + */ +public class FullScanWorker extends Worker { + private final static String TAG = FullScanWorker.class.getSimpleName(); + public final static String UNIQUE_WORK_NAME = "FullScan"; + + public FullScanWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + } + + @NonNull + @Override + public Result doWork() { + Log.d(TAG, "doWork(): going to send intent to ObserverService"); + final SharedPreferences prefs = getApplicationContext().getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, + Context.MODE_PRIVATE); + final String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); + final String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); + + final Account mAccount = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this.getApplicationContext())); + + if (mAccount != null && CommonUtils.isSettingsSyncEnabled(mAccount) && CommonUtils.isMediaSyncEnabled(mAccount)) { + final Intent observerServiceIntent = new Intent(this.getApplicationContext(), ObserverService.class); + this.getApplicationContext().startService(observerServiceIntent); + } else { + Log.w(TAG, "Intent for ObserverService not send : account is null or \"settings sync\" & \"media sync\" settings are disabled"); + } + + return Result.success(); + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f9814a8960fdc198e3c59e0825e47b42749a8a7..4ba1b93508f6ffa0c5fbd3c7f7e36d625d53674c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,12 @@ /e/ Drive e.foundation.webdav.eelo + eDrive\'s notification + eDrive\'s notification channel + /e/ account\'s quota reached + /e/ account\'s quota nearly reached + eDrive can\'t upload a file that is bigger than remaining quota at: + You filled 99% of your /e/\'s account quota. check: + You filled 90% of your /e/\'s account quota. check: + You filled 80% of your /e/\'s account quota. check: diff --git a/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java b/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java index 1c01da6f62b5e358f9ca67a8150611380e996e57..fbf30d4f08ea23ae20d7b688d4e367693a391e14 100644 --- a/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java +++ b/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java @@ -161,7 +161,7 @@ public class UploadFileOperationTest { boolean checkEtag = false; - UploadFileOperation testOperation = new UploadFileOperation(syncedFileStates.get(0), checkEtag); + UploadFileOperation testOperation = new UploadFileOperation(syncedFileStates.get(0)); testOperation.setContext(RuntimeEnvironment.application); //Without it, it won't update database RemoteOperationResult result = testOperation.execute(client); @@ -189,9 +189,10 @@ public class UploadFileOperationTest { @Test public void nullSyncedFileState_shouldFail(){ + SyncedFileState syncedFileState = null; //Test fails at the moment because of UploadFileOperation's constructor not checking for syncedFileState is null) // check https://gitlab.e.foundation/e/apps/eDrive/-/issues/120 - UploadFileOperation testOperation = new UploadFileOperation(null, false); + UploadFileOperation testOperation = new UploadFileOperation(syncedFileState); testOperation.setContext(RuntimeEnvironment.application); RemoteOperationResult result = testOperation.execute(client); @@ -212,7 +213,7 @@ public class UploadFileOperationTest { Assert.assertTrue("SyncedFileState loaded from DB must have an empty Etag", sfs_fromDB.getLastETAG().isEmpty()); boolean checkEtag = false; - UploadFileOperation testOperation = new UploadFileOperation(syncedFileStates.get(0), checkEtag); + UploadFileOperation testOperation = new UploadFileOperation(syncedFileStates.get(0)); File smallFile = new File(sfs_fromDB.getLocalPath()); Assert.assertTrue("Local file deletion return false instead of true", smallFile.delete()); @@ -230,7 +231,7 @@ public class UploadFileOperationTest { //long freeQuota = getUserRemoteFreeQuota(); Assert.assertFalse("Reading remote free quota fails"+userFreeQuota, -1 == userFreeQuota); //We don't care of parameter of UploadFileOperation's constructor - RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class), false) + RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class)) .checkAvailableSpace(client, (userFreeQuota+1)); Assert.assertEquals("Quota check ("+ userFreeQuota+"vs"+(userFreeQuota+1)+") failed", RemoteOperationResult.ResultCode.QUOTA_EXCEEDED, actualResult.getCode()); } @@ -245,7 +246,7 @@ public class UploadFileOperationTest { //long freeQuota = getUserRemoteFreeQuota(); Assert.assertFalse("Reading remote free quota fails"+userFreeQuota, -1 == userFreeQuota); - RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class), false) + RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class)) .checkAvailableSpace(client, userFreeQuota); Assert.assertEquals("Quota check ("+ userFreeQuota+" vs "+userFreeQuota+") failed", RemoteOperationResult.ResultCode.QUOTA_EXCEEDED, @@ -262,7 +263,7 @@ public class UploadFileOperationTest { //long freeQuota = getUserRemoteFreeQuota(); Assert.assertFalse("Reading remote free quota fails "+userFreeQuota, -1 == userFreeQuota); - RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class), false) + RemoteOperationResult actualResult = new UploadFileOperation(Mockito.mock(SyncedFileState.class)) .checkAvailableSpace(client, (userFreeQuota-1)); Assert.assertEquals("Quota check ("+ userFreeQuota+" vs "+(userFreeQuota-1)+") failed", RemoteOperationResult.ResultCode.OK, diff --git a/app/src/test/java/foundation/e/drive/services/AbstractServiceIT.java b/app/src/test/java/foundation/e/drive/services/AbstractServiceIT.java index 810a7640c7ffd828cc734fa02aab028dddfa6079..27519cb0b452a4d3b71ea3ee59a324c230455f43 100644 --- a/app/src/test/java/foundation/e/drive/services/AbstractServiceIT.java +++ b/app/src/test/java/foundation/e/drive/services/AbstractServiceIT.java @@ -111,7 +111,6 @@ public abstract class AbstractServiceIT { */ protected void registerSharedPref(){ sharedPreferences.edit().putBoolean( AppConstants.INITIALIZATION_HAS_BEEN_DONE, init_done) - .putBoolean(AppConstants.KEY_OMS_IS_WORKING, oms_running) .putString(AccountManager.KEY_ACCOUNT_NAME, TEST_ACCOUNT_NAME) .putString(AccountManager.KEY_ACCOUNT_TYPE, TEST_ACCOUNT_TYPE) .putInt(AppConstants.INITIALFOLDERS_NUMBER, initial_folder_number) diff --git a/app/src/test/java/foundation/e/drive/services/OperationManagerServiceTest.java b/app/src/test/java/foundation/e/drive/services/OperationManagerServiceTest.java deleted file mode 100644 index 0a2fb8f73f1790b30fd3d68758ec7f818d3d7c35..0000000000000000000000000000000000000000 --- a/app/src/test/java/foundation/e/drive/services/OperationManagerServiceTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package foundation.e.drive.services; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.app.job.JobScheduler; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; - -import com.owncloud.android.lib.resources.files.model.RemoteFile; - -import junit.framework.Assert; - -import org.junit.Test; -import org.robolectric.Robolectric; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.shadows.ShadowLog; - -import java.util.List; - -import foundation.e.drive.TestUtils; -import foundation.e.drive.database.DbHelper; -import foundation.e.drive.models.SyncedFileState; -import foundation.e.drive.operations.DownloadFileOperation; -import foundation.e.drive.utils.AppConstants; - - -public class OperationManagerServiceTest extends AbstractServiceIT{ - - public OperationManagerServiceTest(){ - mServiceController = Robolectric.buildService(OperationManagerService.class); - mService = mServiceController.get(); - context = RuntimeEnvironment.application; - accountManager = AccountManager.get(context); - jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - contentResolver = context.getContentResolver(); - sharedPreferences = context.getSharedPreferences( AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - dbHelper = new DbHelper(context); - } - - - /** - * Check that the service stop quickly if no intent is provided - */ - @Test - public void noIntent_shouldStop(){ - mServiceController.create(); - mService.onStartCommand(null,0, 0); - - List logs = ShadowLog.getLogs(); - ShadowLog.LogItem lastLog = logs.get(logs.size()-2); - - Assert.assertEquals("Expected last log was: 'intent was null, flags=0 bits=0' but got:"+lastLog.msg, "intent was null, flags=0 bits=0", lastLog.msg); - } - - - /** - * Start the service with an intent that doesn't contains list of extra data - * (meaning no account and no synchronisation to perform) - */ - @Test - public void intentWithoutExtras_shouldStop(){ - mServiceController.create(); - mService.onStartCommand(new Intent(), 0, 0); - - List logs = ShadowLog.getLogs(); - ShadowLog.LogItem lastLog = logs.get(logs.size()-1); - - Assert.assertEquals("Expected last log was: 'Intent's extras is null.' but got:"+lastLog.msg, "Intent's extras is null.", lastLog.msg); - } - - - /** - * Start the OperationmanagerService with File to upload but no account provided - * in the intent. - * Failure is expected - */ - @Test - public void noAccount_shouldFail(){ - prepareValidAccount(); - final Account account = TestUtils.getValidAccount(); - final Intent intent = new Intent("dummyAction"); - - intent.putExtra("0", - new DownloadFileOperation( - new RemoteFile("/eDrive-test/coco.txt"), - new SyncedFileState(3, "coco.txt", "/tmp/eDrive-test/", "/eDrive-test/coco.txt","", -1, 1, true ) - ) - ); - - - intent.putExtra("account", account); - - final boolean accountRemoved = accountManager.removeAccountExplicitly(account); - Assert.assertTrue("Account removal should return true but returned false", accountRemoved); - - mServiceController.create(); - mService.onStartCommand(intent, 0,0); - - List logs = ShadowLog.getLogs(); - ShadowLog.LogItem lastLog = logs.get(logs.size()-1); - Assert.assertEquals("Expected last log was: 'No Client, Can't Work!' but got:"+lastLog.msg, "No Client, Can't Work!", lastLog.msg); - - } - - -}