diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000000000000000000000000000000000..681f41ae2aee4749eb4ddda94f8c6a76c825c825 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..3c902cb73cfa1b15a1b48ff20e50a1b293120afa --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java @@ -0,0 +1,42 @@ +package foundation.e.drive.fileFilters; + +import java.io.File; +import java.io.FileFilter; + +import foundation.e.drive.utils.ServiceExceptionHandler; + +public class CrashlogsFileFilter implements FileFilter { + private final static long max_timestamp_delta = 864000000; //10 days in ms (240*3600*1000) + + @Override + public boolean accept(File pathname) { + String fileTimestamp = extractTimestamp(pathname.getName(), + ServiceExceptionHandler.LOG_FILE_NAME_PREFIX, + ServiceExceptionHandler.LOG_FILE_EXTENSION); + + long timestamp; + try { + timestamp = Long.parseLong(fileTimestamp); + }catch (NumberFormatException e){ + //Can't parse the extracted timestamp + //This file has not the expected name. It must be removed + return true; + } + + //if current Date - file date >= max deta allowed + return ((System.currentTimeMillis() - timestamp ) >= max_timestamp_delta); + } + + /** + * Extract the timestamp from the name of the file + * UnitTested! + * @param fileName Filename + * @param prefix prefix to ignore + * @param extension extension to ignore + * @return the timestamp extracted from the name + */ + private String extractTimestamp(String fileName, String prefix, String extension){ + return fileName.substring(prefix.length(), (fileName.length() - extension.length())); + } + +} 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 ee3f8b26dda29d44f650613cc88618eeacb156fb..29c6d0da887c33c5fb0f35afffd1ca6622059c5b 100644 --- a/app/src/main/java/foundation/e/drive/services/InitializerService.java +++ b/app/src/main/java/foundation/e/drive/services/InitializerService.java @@ -34,6 +34,8 @@ 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; @@ -63,6 +65,8 @@ public class InitializerService extends Service implements OnRemoteOperationList @Override 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 ); 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 0514e32827e7a4dc645ff5714dc798f480434ae2..6cf06395b6d618bfcecf7b6fd1dba227e20987ae 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -37,6 +37,7 @@ 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.SyncedFolder; @@ -51,6 +52,7 @@ import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR; import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE; import foundation.e.drive.utils.DavClientProvider; import foundation.e.drive.utils.JobUtils; +import foundation.e.drive.utils.ServiceExceptionHandler; /** * @author Vincent Bourgmayer @@ -75,16 +77,12 @@ public class ObserverService extends Service implements OnRemoteOperationListene this.mSyncedFolders = null; } - @Override - public void onCreate() { - Log.i(TAG, "onCreate()"); - super.onCreate(); - } - @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand("+startId+")"); + CommonUtils.setServiceUnCaughtExceptionHandler(this); + SharedPreferences prefs = this.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); @@ -145,7 +143,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene return super.onStartCommand( intent, flags, startId ); } - /* Common methods */ + //Common methods /** * Start to bind this service to OperationManagerService or start scan if binding is already set. * Method to factorise code that is called from different place @@ -154,12 +152,36 @@ public class ObserverService extends Service implements OnRemoteOperationListene Log.i(TAG, "begin()"); this.isWorking = true; clearCachedFile(); + deleteOldestCrashlogs(); startScan(true); } + /** + * This method remove all the crash-logs file + * in external dir that are 10 days or more old. + */ + private void deleteOldestCrashlogs(){ + Log.i(TAG, "deleteOldestCrashLogs()"); + File[] fileToRemove = getExternalFilesDir(ServiceExceptionHandler.CRASH_LOG_FOLDER) + .listFiles(new CrashlogsFileFilter()); + + int counter = 0; + for (File file : fileToRemove) { + try { + file.delete(); + ++counter; + }catch (SecurityException e){ + e.printStackTrace(); + } + } + Log.d(TAG, counter+" old crashlogs file.s deleted"); + } + + /** * Clear cached file unused: * remove each cached file which isn't in OperationManagerService.lockedSyncedFileState(); + * @TODO rewrite this method! */ private void clearCachedFile(){ Log.i(TAG, "clearCachedFile()"); @@ -201,7 +223,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } Log.d(TAG, logFolderList.toString()); - if (remote) { OwnCloudClient client = DavClientProvider.getInstance().getClientInstance(mAccount, getApplicationContext()); if (client != null) { @@ -249,6 +270,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( result.isSuccess() ){ List resultDatas = result.getData(); diff --git a/app/src/main/java/foundation/e/drive/services/OperationManagerService.java b/app/src/main/java/foundation/e/drive/services/OperationManagerService.java index 4e59a83a551c6546edcbb2d1ab94bbf1fa207a5b..24bb14be7213c4f2835af3d7163307fea63627b1 100644 --- a/app/src/main/java/foundation/e/drive/services/OperationManagerService.java +++ b/app/src/main/java/foundation/e/drive/services/OperationManagerService.java @@ -13,13 +13,11 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Parcelable; -import android.preference.Preference; import android.support.annotation.Nullable; import android.util.Log; import com.owncloud.android.lib.common.OwnCloudClient; @@ -27,7 +25,6 @@ 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 org.apache.commons.httpclient.util.DateUtil; import java.lang.ref.WeakReference; import java.util.Hashtable; @@ -40,6 +37,7 @@ 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 @@ -60,16 +58,6 @@ public class OperationManagerService extends Service implements OnRemoteOperatio @Override public void onDestroy() { Log.i(TAG, "onDestroy()"); - - /*this.mOperationsQueue.clear(); - for(int i =-1, size = mThreadPool.length;++i < size;){ - try{ - if(mThreadPool[i] != null) - mThreadPool[i].interrupt();} - catch(Exception e){ - Log.e(TAG, e.toString()); - } - }*/ super.onDestroy(); } @@ -240,6 +228,8 @@ public class OperationManagerService extends Service implements OnRemoteOperatio public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "onStartCommand()"); + CommonUtils.setServiceUnCaughtExceptionHandler(this); + Bundle extras = intent.getExtras(); Log.d(TAG, "OperationManagerService recieved "+(extras == null ? "null extras": extras.size()+" operations to perform") ); @@ -292,11 +282,9 @@ public class OperationManagerService extends Service implements OnRemoteOperatio }else{ Log.w(TAG, "Intent's extras is null."); } - return super.onStartCommand(intent, flags, startId); } - @Nullable @Override public IBinder onBind(Intent intent) { 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 1694eedd73ccad6e35675d0e875b636152e123e6..50d57ceccf83c6c3c420897f9bfd38ee45283d8f 100644 --- a/app/src/main/java/foundation/e/drive/utils/AppConstants.java +++ b/app/src/main/java/foundation/e/drive/utils/AppConstants.java @@ -27,7 +27,4 @@ public abstract class AppConstants { 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"}; - - - } 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 523e4556015ae84b050ba89e22f01a0bd18862cb..9dc2630b2af25b5295a3cb711c17ebe7bdb664d0 100644 --- a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java @@ -13,6 +13,7 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.Service; import android.content.ContentResolver; import android.content.Context; import android.media.MediaScannerConnection; @@ -40,6 +41,25 @@ import static foundation.e.drive.utils.AppConstants.SETTINGSYNC_PROVIDER_AUTHORI public abstract class CommonUtils { final private static String TAG = CommonUtils.class.getSimpleName(); + + /** + * Set ServiceUncaughtExceptionHandler to be the MainThread Exception Handler + * Or update the service which use it + * @param service current service + */ + public static void setServiceUnCaughtExceptionHandler(Service service){ + + Thread.UncaughtExceptionHandler defaultUEH = Thread.getDefaultUncaughtExceptionHandler(); + if(defaultUEH.getClass().getSimpleName().equals(ServiceExceptionHandler.class.getSimpleName())){ + Log.d("ObserverService", "ServiceExceptionHandler already set!"); + ((ServiceExceptionHandler) defaultUEH).setService(service); + }else{ + Thread.setDefaultUncaughtExceptionHandler(new ServiceExceptionHandler(service)); + } + } + + + /** * Unregister from screeOffReceiver component * @param context app context diff --git a/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java b/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0833f3e3da1a38256a892fe66f220a41a033efda --- /dev/null +++ b/app/src/main/java/foundation/e/drive/utils/ServiceExceptionHandler.java @@ -0,0 +1,118 @@ +/* + * 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.Service; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.Thread.UncaughtExceptionHandler; + +import foundation.e.drive.services.OperationManagerService; + +/** + * @author Vincent Bourgmayer + */ +public class ServiceExceptionHandler implements UncaughtExceptionHandler{ + private final static String TAG = ServiceExceptionHandler.class.getSimpleName(); + public final static String CRASH_LOG_FOLDER = "crash-logs"; + public final static String LOG_FILE_NAME_PREFIX = "eDrive-crash-"; + public final static String LOG_FILE_EXTENSION = ".log"; + private UncaughtExceptionHandler defaultUEH; + private Service service; + + /** + * Change the service that could trigger an UncaughtException + * @param service service instance + */ + public void setService(Service service){ + this.service = service; + } + + public ServiceExceptionHandler(Service service) { + this.service = service; + defaultUEH = Thread.getDefaultUncaughtExceptionHandler(); + } + + @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 + Long timestamp = System.currentTimeMillis(); + + //Create a new file that user can sent to us + String fileName = LOG_FILE_NAME_PREFIX+timestamp+LOG_FILE_EXTENSION; + + File downloadDir = service.getApplication().getExternalFilesDir(CRASH_LOG_FOLDER); + File logFile = new File(downloadDir, fileName); + try { + + FileOutputStream fos = new FileOutputStream(logFile); + fos.write(service.getClass().getSimpleName().getBytes()); + fos.write(getStackTraceAsString(e).getBytes()); + fos.close(); + logFile.setReadable(true, false); + + } catch (IOException exception) { + exception.printStackTrace(); + } + } + //source: https://stackoverflow.com/questions/9050962/rethrow-uncaughtexceptionhandler-exception-after-logging-it/9050990#9050990 + if(defaultUEH != null){ + defaultUEH.uncaughtException(t, e); + }else{ + Log.d(TAG, "there is no ExceptionHandler"); + System.exit(1); //Kill /e/ Drive... + } + } + + //source: https://www.journaldev.com/9400/android-external-storage-read-write-save-file + private static boolean isExternalStorageAvailable() { + String extStorageState = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(extStorageState)) { + return true; + } + return false; + } + + //source: https://www.journaldev.com/9400/android-external-storage-read-write-save-file + private static boolean isExternalStorageReadOnly() { + String extStorageState = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(extStorageState)) { + return true; + } + return false; + } + + /** + * Return the stackTrace of the exception as a String + * @param e the exception + * @return the Stacktrace as a string + */ + private String getStackTraceAsString(Throwable e){ + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + return sw.toString(); + } +} diff --git a/app/src/test/java/foundation/e/drive/Test/FileFilterTest/CrashlogFileFilterTest.java b/app/src/test/java/foundation/e/drive/Test/FileFilterTest/CrashlogFileFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d10b6435d80722cc716586a39ff0bc21ebe4ebad --- /dev/null +++ b/app/src/test/java/foundation/e/drive/Test/FileFilterTest/CrashlogFileFilterTest.java @@ -0,0 +1,48 @@ +package foundation.e.drive.Test.FileFilterTest; + +import org.junit.Assert; +import org.junit.Test; + +public class CrashlogFileFilterTest { + + private String mockFileName(String target, String prefix, String extension){ + return prefix+target+extension; + } + + private String extractTimestamp(String fileName, String prefix, String extension){ + return fileName.substring(prefix.length(), (fileName.length() - extension.length())); + } + + @Test + public void extractTimeStampFromFileNameTest(){ + String prefix = "edrive-"; + String extension = ".log"; + String target = ""; + //Case 1 Empty Target + String base = mockFileName(target, prefix, extension); + Assert.assertEquals("Base length is incorrect", prefix.length()+extension.length(), base.length()); + + String fileTimestamp = extractTimestamp(base, prefix, extension); + Assert.assertEquals("result is not empty String", "", fileTimestamp); + + //Case 2: Prefix is empty + prefix = ""; + target = "1234"; + base = mockFileName(target, prefix, extension); + Assert.assertEquals("Base length is incorrect", prefix.length()+target.length()+extension.length(), base.length()); + + fileTimestamp = extractTimestamp(base, prefix, extension); + Assert.assertEquals("result is not empty String", target, fileTimestamp); + + //Case 3: extension is empty + + prefix = "edrive-"; + extension = ""; + + base = mockFileName(target, prefix, extension); + Assert.assertEquals("Base length is incorrect", prefix.length()+target.length(), base.length()); + + fileTimestamp = extractTimestamp(base, prefix, extension); + Assert.assertEquals("result is not empty String", target, fileTimestamp); + } +}