diff --git a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..47661901f48c89ab30d04f8b3f932e65d92794f8 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java @@ -0,0 +1,36 @@ +/* + * Copyright © ECORP SAS 2022. + * 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.contentScanner; + +import android.accounts.Account; +import android.content.Context; + +import java.util.HashMap; + +import foundation.e.drive.models.SyncRequest; + +/** + * Class encapsulating common code and references for RemoteContentScanner & LocalContentScanner + * @author vincent Bourgmayer + */ +public class AbstractContentScanner { + protected final Context context; + protected final Account account; + protected final HashMap syncRequests; + + /** + * + * @param context Context used to access Database, etc. + * @param account Account used to checked if user has change some synchronization's settings + */ + protected AbstractContentScanner(Context context, Account account) { + syncRequests = new HashMap<>(); + this.context = context; + this.account = account; + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..65fa1dcfb64ac93ac946f71404a7f082da003346 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -0,0 +1,180 @@ +/* + * Copyright © ECORP SAS 2022. + * 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.contentScanner; + +import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; + +import android.accounts.Account; +import android.content.Context; +import android.provider.MediaStore; +import android.util.Log; + +import com.owncloud.android.lib.resources.files.FileUtils; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; + +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.models.SyncedFolder; +import foundation.e.drive.utils.CommonUtils; +import foundation.e.drive.utils.FileDiffUtils; + +/** + * Class encapsulating code for scaning remote content + * @author vincent Bourgmayer + */ +public class RemoteContentScanner extends AbstractContentScanner { + private static final String TAG = RemoteContentScanner.class.getSimpleName(); + + private List syncedFolders; + + /** + * + * @param context Context used to access Database, etc. + * @param account Account used to checked if user has change some synchronization's settings + */ + public RemoteContentScanner(Context context, Account account, List syncedFolder) { + super(context, account); + this.syncedFolders = syncedFolder; + } + + public HashMap scanContent(List remoteFiles, List fileStates) { + + fileStates.removeIf(p -> p.isMediaType() && p.getName().startsWith(".")); //ignore hidden medias from db + + remoteFileLoop: for (final RemoteFile file : remoteFiles) { + final String remoteFilePath = file.getRemotePath(); + final ListIterator iterator = fileStates.listIterator(); + + while (iterator.hasNext()) { + final SyncedFileState fileState = iterator.next(); + if (fileState.getRemotePath().equals(remoteFilePath)) { + onKnownFileFound(file, fileState); + iterator.remove(); + continue remoteFileLoop; + } + } + + onNewFileFound(file); + } + //At this step, we finished to handle each remote file and we may still have synced file but without remote equivalent. + // In most cases, we consider those files as remotly removed files. So we start to delete those local file. + for (SyncedFileState remainingFileState : fileStates) { + onMissingRemoteFile(remainingFileState); + } + return syncRequests; + } + + /** + * A known file has been found + * Check what to do: ignore, update Database with missing input or create a new DownloadOperation + * @param file The remote file + * @param fileState file's latest known state + */ + private void onKnownFileFound(RemoteFile file, SyncedFileState fileState) { + final FileDiffUtils.Action action = getActionForFileDiff(file, fileState); + if (action == FileDiffUtils.Action.Download) { + + this.syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); + + } else if (action == FileDiffUtils.Action.updateDB) { + + fileState.setLastETAG(file.getEtag()); + final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); + if (affectedRows == 0) Log.e(TAG, "Error while updating eTag in DB for: " + file); + + } + } + + /** + * A new remote file has been found + * - Create SyncedFileState for it and insert in DB + * - Create a Download syncRequest for it + * @param file The new remote file + */ + private void onNewFileFound(RemoteFile file) { + final String remoteFilePath = file.getRemotePath(); + final SyncedFolder parentDir = getParentSyncedFolder(remoteFilePath); + if (parentDir == null) return; + + final String fileName = CommonUtils.getFileNameFromPath(remoteFilePath); + + int scannableValue = 0; + if (parentDir.isEnabled()) { + if (parentDir.isScanRemote()) scannableValue++; + if (parentDir.isScanLocal()) scannableValue += 2; + } + + final SyncedFileState newFileState = new SyncedFileState(-1, fileName, parentDir.getLocalFolder() + fileName, remoteFilePath, file.getEtag(), 0, parentDir.getId(), parentDir.isMediaType(), scannableValue); + + //Store it in DB + final int storedId = DbHelper.manageSyncedFileStateDB(newFileState, "INSERT", context); + if (storedId > 0) { + newFileState.setId(storedId); + this.syncRequests.put(storedId, new DownloadRequest(file, newFileState)); + } + } + + + /** + * When a remoteFile doesn't exist anymore we remove it from device & from Database + * @param fileState SyncedFileState for which we lack remote file + */ + private void onMissingRemoteFile(SyncedFileState fileState) { + if (!CommonUtils.isThisSyncAllowed(account, fileState.isMediaType())) { + Log.d(TAG, "Sync of current file: " + fileState.getName() + " isn't allowed"); + return; + } + + //Check that file has already been synced fully + if (!fileState.hasBeenSynchronizedOnce()) { + return; + } + + final File file = new File(fileState.getLocalPath()); + if (!file.exists()) { + return; + } + + context.getContentResolver().delete(MediaStore.Files.getContentUri("external"), + MediaStore.Files.FileColumns.DATA + "=?", + new String[]{CommonUtils.getLocalPath(file)}); + + if (!file.delete()) { //May throw SecurityException or IOException + Log.w(TAG, "local file (" + file.getName() + ") removal failed."); + return; + } + + if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { + Log.e(TAG, "Failed to remove " + file.getName() + " from DB"); + } + } + + /** + * Get SyncedFolder corresponding to parent of remotefile + * @param filePath Remote file path + * @return SyncedFolder or null if none have been found + */ + private SyncedFolder getParentSyncedFolder(String filePath) { + + final String dirPath = filePath.substring(0, filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); + + for (SyncedFolder syncedFolder : syncedFolders) { + if (syncedFolder.getRemoteFolder().equals(dirPath)) { + return syncedFolder; + } + } + return null; + } +} 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 589766be51a4f960b98de40a344c0f00f61a5451..d6bbb566b1a5ac58ab81c58c2fe818f63f514bf3 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -11,7 +11,6 @@ package foundation.e.drive.services; import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; -import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; import android.accounts.Account; import android.accounts.AccountManager; @@ -22,7 +21,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.os.Handler; import android.os.IBinder; -import android.provider.MediaStore; import android.util.Log; import androidx.annotation.Nullable; @@ -42,11 +40,11 @@ import java.util.HashMap; import java.util.List; import java.util.ListIterator; +import foundation.e.drive.contentScanner.RemoteContentScanner; 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.SyncedFileState; import foundation.e.drive.models.SyncedFolder; @@ -77,7 +75,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection(); - /* Lifecycle Methods */ @Override public void onDestroy(){ @@ -87,7 +84,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene this.mSyncedFolders = null; } - @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand("+startId+")"); @@ -255,26 +251,19 @@ public class ObserverService extends Service implements OnRemoteOperationListene return; } - //Display content of SyncedFolderList - StringBuilder logFolderList = new StringBuilder("SyncedFolder: libelle, localFolder, lastmodified, scanLocal, id :"); - for(SyncedFolder sf : mSyncedFolders){ - logFolderList.append("\n").append(sf.getLibelle()).append(", ").append(sf.getLocalFolder()).append(", ").append(sf.getLastModified()).append(", ").append(sf.isScanLocal()).append(", ").append(sf.getId()); - } - Log.d(TAG, logFolderList.toString()); - if (remote) { - OwnCloudClient client = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext()); - if (client != null) { - try { - final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this, this.initialFolderCounter); - loadOperation.execute(client, this, new Handler()); - } catch (IllegalArgumentException e){ - Log.e(TAG, e.toString() ); - } - } else { + final OwnCloudClient client = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext()); + if (client == null) { Log.w(TAG, "OwnCloudClient is null"); return; } + + try { + final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this, this.initialFolderCounter); + loadOperation.execute(client, this, new Handler()); + } catch (IllegalArgumentException e){ + Log.e(TAG, "Can't execute ListFileRemoteOperation", e); + } } else { scanLocalFiles(); } @@ -307,29 +296,33 @@ public class ObserverService extends Service implements OnRemoteOperationListene @Override public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result ) { Log.i( TAG, "onRemoteOperationFinish()" ); - if ( ! (operation instanceof ListFileRemoteOperation)) { return;} + if (!(operation instanceof ListFileRemoteOperation)) return; + if (result.isSuccess()) { + final List remoteFiles = ((RemoteOperationResult>)result).getResultData(); + if (remoteFiles != null) { + final ListFileRemoteOperation listFileOperation = (ListFileRemoteOperation) operation; - mSyncedFolders = listFileOperation.getSyncedFolderList(); + mSyncedFolders = listFileOperation.getSyncedFolderList(); //The list may have been reduced if some directory hasn't changed + final List syncedFileStateList = DbHelper.getSyncedFileStatesByFolders(this, getIdsFromFolderToScan()); - //At least one list is not empty - if (!remoteFiles.isEmpty() || !syncedFileStateList.isEmpty() && CommonUtils.isMediaSyncEnabled(mAccount)) { - handleRemoteFiles(remoteFiles, syncedFileStateList); - } else { - Log.v(TAG, "No remote files nor syncedFileStates. Go next step"); + if (!remoteFiles.isEmpty() || !syncedFileStateList.isEmpty()) { + final RemoteContentScanner scanner = new RemoteContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStateList)); } } } else { - Log.w(TAG, "ListRemoteFileOperation doesn't return a success: " + result.getHttpCode()); + Log.w(TAG, "ListRemoteFileOperation failed. Http code: " + result.getHttpCode()); } - this.startScan(false); - Log.v(TAG, "operationsForIntent contains " + syncRequests.size()); - if (syncRequests != null && !syncRequests.isEmpty()) { + startScan(false); + Log.v(TAG, "syncRequests contains " + syncRequests.size()); + + if (!syncRequests.isEmpty()) { passSyncRequestsToSynchronizationService(); } else { Log.w(TAG, "There is no file to sync."); @@ -354,15 +347,13 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - - /** * Method to get Id of SyncedFolder to scan * @return List id of SyncedFolder to scan */ - private List getIdsFromFolderToScan(){ + private List getIdsFromFolderToScan() { List result = new ArrayList<>(); - for(int i = -1, size = this.mSyncedFolders.size(); ++i < size;){ + for (int i = -1, size = this.mSyncedFolders.size(); ++i < size;) { SyncedFolder syncedFolder = this.mSyncedFolders.get(i); if (syncedFolder.isToSync() ){ result.add( (long) syncedFolder.getId() ); @@ -371,151 +362,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene return result; } - /* methods related to Server Scanning */ - - /** - * decide what to do with remote files and decide - * @param remoteFiles Remote Files to inspect - * @param syncedFileStates SyncedFileState to inspect - */ - private void handleRemoteFiles(List remoteFiles, List syncedFileStates ){ - Log.i(TAG, "handleRemoteFiles()"); - Log.d(TAG, "start to loop through remoteFiles"); - - ListIterator syncedFileListIterator; - for( int i =-1, size = remoteFiles.size(); ++i < size; ){ - - final RemoteFile remoteFile = remoteFiles.get(i); - final String remoteFilePath = remoteFile.getRemotePath(); - - // hidden file from server has already been filtered in previous step - boolean correspondant_found = false; - - syncedFileListIterator = syncedFileStates.listIterator(); //reset listiterator - Log.d( TAG, "start to loop through syncedFileList for: "+remoteFilePath); - - while( syncedFileListIterator.hasNext() ){ - SyncedFileState syncedFileState = syncedFileListIterator.next(); - - //ignore hidden file from db - if (syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")){ - syncedFileListIterator.remove(); - continue; - } - - if (remoteFilePath.equals(syncedFileState.getRemotePath())) { - Log.d(TAG, "correspondant found for "+remoteFilePath ); - correspondant_found = true; - - final Action action = getActionForFileDiff(remoteFile, syncedFileState); - if (action == Action.Download) { - - Log.i(TAG, "Add download operation for file " + syncedFileState.getId()); - this.syncRequests.put(syncedFileState.getId(), new DownloadRequest(remoteFile, syncedFileState)); - - } else if (action == Action.updateDB) { - - syncedFileState.setLastETAG(remoteFile.getEtag()); - final int affectedRows = DbHelper.manageSyncedFileStateDB(syncedFileState, "UPDATE", this); - if (affectedRows == 0) Log.e(TAG, "Error while updating eTag in DB for: " + remoteFilePath); - } - syncedFileListIterator.remove(); - break; - } - } - - if (correspondant_found) continue; - - Log.v(TAG, remoteFilePath + "is a new file"); - - //Extract parent folder's path of remote file - final String parentOfKnownPath = remoteFilePath.substring(0, remoteFilePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); - - //look for parent folder in SyncedFolders - for (int j = -1, mSyncedFolderSize = this.mSyncedFolders.size(); ++j < mSyncedFolderSize;) { - if ( mSyncedFolders.get(j).getRemoteFolder().equals( parentOfKnownPath ) ) { //We have found the parent folder - final SyncedFolder parentFolder = mSyncedFolders.get(j); - - final String fileName = CommonUtils.getFileNameFromPath(remoteFilePath); - - 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(), scannableValue); - - //Store it in DB - int storedId = DbHelper.manageSyncedFileStateDB(newRemoteFile, "INSERT", this); - if (storedId > 0) { - newRemoteFile.setId(storedId); - Log.i(TAG, "Add download operation for new file "+storedId); - //Create Download operation and add it into Bundle - this.syncRequests.put(storedId, new DownloadRequest(remoteFile, newRemoteFile)); - - } else { - Log.w(TAG, "Can't save new remote File in DB. Ignore file."); - - } - } else { - Log.w(TAG, "Can't get filename from path: " + remoteFilePath); - } - break; - } - } - } - //At this step, we finished to handle each remote file and we may still have synced file but without remote equivalent. - // In most cases, we consider those files as remotly removed files. So we start to delete those local file. - Log.v( TAG, "Start to handle remotly missing file" ); - handleRemoteRemainingSyncedFileState( syncedFileStates ); - } - - /** - * Handle the list of syncedFileState which don't have remoteFile anymore. - * @param syncedFileStates SyncedFileState for which no remote equivalent has been found - */ - private void handleRemoteRemainingSyncedFileState(List syncedFileStates){ - - Log.i( TAG, "handleRemoteRemainingSyncedFileState()" ); - - //Loop through remaining file state - for(int i = -1, size = syncedFileStates.size(); ++i < size; ){ - - final SyncedFileState syncedFileState = syncedFileStates.get(i); - - 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.hasBeenSynchronizedOnce()) { - continue; - } - - final File file = new File(syncedFileStates.get(i).getLocalPath()); - - //Try to remove local file - if (file.exists()) { - int rowAffected = getContentResolver().delete(MediaStore.Files.getContentUri("external"), - MediaStore.Files.FileColumns.DATA + "=?", - new String[]{CommonUtils.getLocalPath(file)}); - Log.d(TAG, "deleted rows by mediastore : " + rowAffected); - - if (!file.delete()) { //May throw SecurityException or IOException - Log.w(TAG, "local file (" + file.getName() + ") removal failed."); - continue; - } - } - - if (DbHelper.manageSyncedFileStateDB(syncedFileState, "DELETE", this) <= 0) { - Log.e(TAG, "Failed to remove " + file.getName() + " from DB"); - } - } - } - /* end of methods related to server scanning */ /* Methods related to device Scanning */ @@ -523,7 +369,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene * Generate a .txt file containing list of all installed packages with their version name * I.e : " com.android.my_example_package,7.1.2 " */ - private void generateAppListFile(){ + private void generateAppListFile() { Log.i(TAG, "generateAppListFile()"); List packagesInfo = getPackageManager().getInstalledPackages(0);