Procházet zdrojové kódy

Merge pull request #615 from nextcloud/rewrite-auto-upload

Rewrite auto upload
Mario Đanić před 8 roky
rodič
revize
f0bbb4b562
30 změnil soubory, kde provedl 1327 přidání a 490 odebrání
  1. 1 1
      .drone.yml
  2. 9 5
      AndroidManifest.xml
  3. 10 0
      CHANGELOG.md
  4. 15 3
      build.gradle
  5. 5 0
      res/xml/backup_config.xml
  6. 89 43
      src/com/owncloud/android/MainApp.java
  7. 4 1
      src/com/owncloud/android/datamodel/SyncedFolder.java
  8. 8 0
      src/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java
  9. 43 10
      src/com/owncloud/android/datamodel/SyncedFolderProvider.java
  10. 28 21
      src/com/owncloud/android/datamodel/UploadsStorageManager.java
  11. 22 2
      src/com/owncloud/android/db/PreferenceManager.java
  12. 1 1
      src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java
  13. 11 0
      src/com/owncloud/android/files/services/FileDownloader.java
  14. 14 0
      src/com/owncloud/android/files/services/FileUploader.java
  15. 37 26
      src/com/owncloud/android/operations/UploadFileOperation.java
  16. 186 0
      src/com/owncloud/android/services/AdvancedFileAlterationListener.java
  17. 81 0
      src/com/owncloud/android/services/AutoUploadJob.java
  18. 40 0
      src/com/owncloud/android/services/NCJobCreator.java
  19. 40 0
      src/com/owncloud/android/services/ShutdownReceiver.java
  20. 0 93
      src/com/owncloud/android/services/SyncedFolderJobService.java
  21. 402 0
      src/com/owncloud/android/services/observer/AdvancedFileAlterationObserver.java
  22. 0 78
      src/com/owncloud/android/services/observer/SyncedFolderObserver.java
  23. 122 31
      src/com/owncloud/android/services/observer/SyncedFolderObserverService.java
  24. 2 2
      src/com/owncloud/android/ui/activity/DrawerActivity.java
  25. 2 2
      src/com/owncloud/android/ui/activity/FileDisplayActivity.java
  26. 47 6
      src/com/owncloud/android/ui/activity/FolderSyncActivity.java
  27. 2 2
      src/com/owncloud/android/ui/activity/Preferences.java
  28. 34 23
      src/com/owncloud/android/ui/adapter/FolderSyncAdapter.java
  29. 72 31
      src/com/owncloud/android/utils/FileStorageUtils.java
  30. 0 109
      src/com/owncloud/android/utils/RecursiveFileObserver.java

+ 1 - 1
.drone.yml

@@ -1,6 +1,6 @@
 pipeline:
   test:
-    image: nextcloudci/android:android-7
+    image: nextcloudci/android:android-14
     commands:
       - echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI -c 20M
       - emulator -avd test -no-window &

+ 9 - 5
AndroidManifest.xml

@@ -25,7 +25,7 @@
 
     <uses-sdk
         android:minSdkVersion="14"
-        android:targetSdkVersion="24" />
+        android:targetSdkVersion="25" />
 
     <!-- GET_ACCOUNTS is needed for API < 23.
         For API >= 23 results in the addition of CONTACTS group to the list of permissions that may be
@@ -61,6 +61,7 @@
         android:name=".MainApp"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
+        android:fullBackupContent="@xml/backup_config"
         android:theme="@style/Theme.ownCloud.Toolbar"
         android:manageSpaceActivity="com.owncloud.android.ui.activity.ManageSpaceActivity">
         <activity
@@ -128,10 +129,6 @@
                 android:name="android.content.SyncAdapter"
                 android:resource="@xml/syncadapter_files" />
         </service>
-        <service
-            android:name=".services.SyncedFolderJobService"
-            android:permission="android.permission.BIND_JOB_SERVICE"
-            android:exported="true"/>
 
         <provider
             android:name=".providers.FileContentProvider"
@@ -244,6 +241,13 @@
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
             </intent-filter>
         </receiver>
+        <receiver android:name=".services.ShutdownReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.ACTION_SHUTDOWN" />
+                <action android:name="android.intent.action.QUICKBOOT_POWEROFF" />
+            </intent-filter>
+        </receiver>
+
 
         <service android:name=".services.observer.FileObserverService" />
 

+ 10 - 0
CHANGELOG.md

@@ -1,3 +1,13 @@
+## 1.4.2 (March , 2017)
+- Auto Upload (Android 6+)
+- Auto Upload improvements and fixes
+- Filtering improvements
+- Fix for Android permissions (removed read phone state permission)
+- Fix re-upload of files
+- Avoid toggling favourite for all selected files
+- Link to providers list in the setup screen
+- further bugfixes and improvements
+
 ## 1.4.1 (January 27, 2017)
 - Share URLs to Nextcloud
 - Improve performance of Auto Upload view

+ 15 - 3
build.gradle

@@ -8,6 +8,9 @@
 buildscript {
     repositories {
         jcenter()
+        maven {
+            url 'https://oss.sonatype.org/content/repositories/snapshots/'
+        }
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:2.2.3'
@@ -20,7 +23,7 @@ apply plugin: 'pmd'
 apply plugin: 'findbugs'
 
 ext {
-    supportLibraryVersion = '24.2.1'
+    supportLibraryVersion = '25.0.0'
 
     travisBuild = System.getenv("TRAVIS") == "true"
 
@@ -40,15 +43,18 @@ repositories {
 dependencies {
     /// dependencies for app building
     compile name: 'touch-image-view'
+    compile 'com.android.support:multidex:1.0.1'
 
-    compile 'com.github.nextcloud:android-library:1.0.11'
+    compile 'com.github.nextcloud:android-library:1.0.12'
     compile "com.android.support:support-v4:${supportLibraryVersion}"
     compile "com.android.support:design:${supportLibraryVersion}"
     compile 'com.jakewharton:disklrucache:2.0.2'
     compile "com.android.support:appcompat-v7:${supportLibraryVersion}"
     compile 'com.getbase:floatingactionbutton:1.10.1'
     compile 'com.google.code.findbugs:annotations:2.0.1'
-
+    compile group: 'commons-io', name: 'commons-io', version: '2.4'
+    compile 'com.google.android.gms:play-services:10.2.0'
+    compile 'com.github.evernote:android-job:v1.1.7'
 
     /// dependencies for local unit tests
     testCompile 'junit:junit:4.12'
@@ -89,6 +95,11 @@ android {
         htmlReport true
         htmlOutput file("$project.buildDir/reports/lint/lint.html")
     }
+
+    dexOptions {
+        javaMaxHeapSize "4g"
+    }
+
     compileSdkVersion 24
     buildToolsVersion "24.0.2"
 
@@ -100,6 +111,7 @@ android {
         testInstrumentationRunnerArgument "TEST_PASSWORD", "\"$System.env.OCTEST_APP_PASSWORD\""
         testInstrumentationRunnerArgument "TEST_SERVER_URL", "\"$System.env.OCTEST_SERVER_BASE_URL\""
 
+        multiDexEnabled true
         applicationId "com.nextcloud.client"
     }
 

+ 5 - 0
res/xml/backup_config.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<full-backup-content>
+    <exclude domain="sharedpref" path="evernote_jobs.xml" />
+    <exclude domain="database" path="evernote_jobs.db" />
+</full-backup-content>

+ 89 - 43
src/com/owncloud/android/MainApp.java

@@ -1,27 +1,25 @@
 /**
- *   ownCloud Android client application
- *
- *   @author masensio
- *   @author David A. Velasco
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * ownCloud Android client application
  *
+ * @author masensio
+ * @author David A. Velasco
+ * Copyright (C) 2015 ownCloud Inc.
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 package com.owncloud.android;
 
 import android.app.Activity;
-import android.app.Application;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -32,29 +30,38 @@ import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.IBinder;
-import android.preference.PreferenceManager;
+import android.support.multidex.MultiDexApplication;
+import android.support.v4.util.Pair;
 
+import com.evernote.android.job.JobManager;
 import com.owncloud.android.authentication.PassCodeManager;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
+import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory.Policy;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.services.NCJobCreator;
 import com.owncloud.android.services.observer.SyncedFolderObserverService;
 import com.owncloud.android.ui.activity.Preferences;
 import com.owncloud.android.ui.activity.WhatsNewActivity;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 
 /**
  * Main Application of the project
- * 
+ *
  * Contains methods to build the "static" strings. These strings were before constants in different
  * classes
  */
-public class MainApp extends Application {
+public class MainApp extends MultiDexApplication {
 
     private static final String TAG = MainApp.class.getSimpleName();
 
@@ -76,14 +83,16 @@ public class MainApp extends Application {
     @SuppressWarnings("unused")
     private boolean mBound;
 
-    @SuppressFBWarnings("ST")    public void onCreate(){
+    @SuppressFBWarnings("ST")
+    public void onCreate() {
         super.onCreate();
+        JobManager.create(this).addJobCreator(new NCJobCreator());
         MainApp.mContext = getApplicationContext();
 
         SharedPreferences appPrefs =
                 PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
         MainApp.storagePath = appPrefs.getString(Preferences.PreferenceKeys.STORAGE_PATH, Environment.
-                              getExternalStorageDirectory().getAbsolutePath());
+                getExternalStorageDirectory().getAbsolutePath());
 
         boolean isSamlAuth = AUTH_ON.equals(getString(R.string.auth_method_saml_web_sso));
 
@@ -96,7 +105,7 @@ public class MainApp extends Application {
 
         // initialise thumbnails cache on background thread
         new ThumbnailsCacheManager.InitDiskCacheTask().execute();
-        
+
         if (BuildConfig.DEBUG) {
 
             String dataFolder = getDataFolder();
@@ -108,30 +117,33 @@ public class MainApp extends Application {
             Log_OC.d("Debug", "start logging");
         }
 
+        cleanOldEntries();
+
         Log_OC.d("SyncedFolderObserverService", "Start service SyncedFolderObserverService");
         Intent i = new Intent(this, SyncedFolderObserverService.class);
         startService(i);
         bindService(i, syncedFolderObserverServiceConnection, Context.BIND_AUTO_CREATE);
 
+
         // register global protection with pass code
-        registerActivityLifecycleCallbacks( new ActivityLifecycleCallbacks() {
+        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
 
             @Override
             public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
-                Log_OC.d(activity.getClass().getSimpleName(),  "onCreate(Bundle) starting" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onCreate(Bundle) starting");
                 WhatsNewActivity.runIfNeeded(activity);
                 PassCodeManager.getPassCodeManager().onActivityCreated(activity);
             }
 
             @Override
             public void onActivityStarted(Activity activity) {
-                Log_OC.d(activity.getClass().getSimpleName(),  "onStart() starting" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onStart() starting");
                 PassCodeManager.getPassCodeManager().onActivityStarted(activity);
             }
 
             @Override
             public void onActivityResumed(Activity activity) {
-                Log_OC.d(activity.getClass().getSimpleName(), "onResume() starting" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onResume() starting");
             }
 
             @Override
@@ -141,18 +153,18 @@ public class MainApp extends Application {
 
             @Override
             public void onActivityStopped(Activity activity) {
-                Log_OC.d(activity.getClass().getSimpleName(), "onStop() ending" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onStop() ending");
                 PassCodeManager.getPassCodeManager().onActivityStopped(activity);
             }
 
             @Override
             public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
-                Log_OC.d(activity.getClass().getSimpleName(), "onSaveInstanceState(Bundle) starting" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onSaveInstanceState(Bundle) starting");
             }
 
             @Override
             public void onActivityDestroyed(Activity activity) {
-                Log_OC.d(activity.getClass().getSimpleName(), "onDestroy() ending" );
+                Log_OC.d(activity.getClass().getSimpleName(), "onDestroy() ending");
             }
         });
     }
@@ -161,11 +173,11 @@ public class MainApp extends Application {
         return MainApp.mContext;
     }
 
-    public static String getStoragePath(){
+    public static String getStoragePath() {
         return MainApp.storagePath;
     }
 
-    public static void setStoragePath(String path){
+    public static void setStoragePath(String path) {
         MainApp.storagePath = path;
     }
 
@@ -192,42 +204,42 @@ public class MainApp extends Application {
     public static String getAuthority() {
         return getAppContext().getResources().getString(R.string.authority);
     }
-    
+
     //  From AccountAuthenticator
     //  public static final String AUTH_TOKEN_TYPE = "org.owncloud";
     public static String getAuthTokenType() {
         return getAppContext().getResources().getString(R.string.authority);
     }
-    
+
     //  From ProviderMeta 
     //  public static final String DB_FILE = "owncloud.db";
     public static String getDBFile() {
         return getAppContext().getResources().getString(R.string.db_file);
     }
-    
+
     //  From ProviderMeta
     //  private final String mDatabaseName = "ownCloud";
     public static String getDBName() {
         return getAppContext().getResources().getString(R.string.db_name);
     }
-     
+
     /**
      * name of data_folder, e.g., "owncloud"
      */
     public static String getDataFolder() {
         return getAppContext().getResources().getString(R.string.data_folder);
     }
-    
+
     // log_name
     public static String getLogName() {
         return getAppContext().getResources().getString(R.string.log_name);
     }
 
-    public static void showOnlyFilesOnDevice(boolean state){
+    public static void showOnlyFilesOnDevice(boolean state) {
         mOnlyOnDevice = state;
     }
 
-    public static boolean isOnlyOnDevice(){
+    public static boolean isOnlyOnDevice() {
         return mOnlyOnDevice;
     }
 
@@ -257,12 +269,47 @@ public class MainApp extends Application {
         return userAgent;
     }
 
+    private void cleanOldEntries() {
+        // previous versions of application created broken entries in the SyncedFolderProvider
+        // database, and this cleans all that and leaves 1 (newest) entry per synced folder
+
+        if (!PreferenceManager.getLegacyClean(this)) {
+            SyncedFolderProvider syncedFolderProvider =
+                    new SyncedFolderProvider(MainApp.getAppContext().getContentResolver());
+
+            List<SyncedFolder> syncedFolderList = syncedFolderProvider.getSyncedFolders();
+            Map<Pair<String, String>, Long> syncedFolders = new HashMap<>();
+            ArrayList<Long> ids = new ArrayList<>();
+            for (SyncedFolder syncedFolder : syncedFolderList) {
+                Pair<String, String> checkPair = new Pair<>(syncedFolder.getAccount(), syncedFolder.getLocalPath());
+                if (syncedFolders.containsKey(checkPair)) {
+                    if (syncedFolder.getId() > syncedFolders.get(checkPair)) {
+                        syncedFolders.put(checkPair, syncedFolder.getId());
+                    }
+                } else {
+                    syncedFolders.put(checkPair, syncedFolder.getId());
+                }
+            }
+
+            for (Long idValue : syncedFolders.values()) {
+                ids.add(idValue);
+            }
+
+            if (ids.size() > 0) {
+                syncedFolderProvider.deleteSyncedFoldersNotInList(mContext, ids);
+            } else {
+                PreferenceManager.setLegacyClean(this, true);
+            }
+        }
+    }
+
     /** Defines callbacks for service binding, passed to bindService() */
     private ServiceConnection syncedFolderObserverServiceConnection = new ServiceConnection() {
 
         @Override
         public void onServiceConnected(ComponentName className, IBinder service) {
-            SyncedFolderObserverService.SyncedFolderObserverBinder binder = (SyncedFolderObserverService.SyncedFolderObserverBinder) service;
+            SyncedFolderObserverService.SyncedFolderObserverBinder binder =
+                    (SyncedFolderObserverService.SyncedFolderObserverBinder) service;
             mObserverService = binder.getService();
             mBound = true;
         }
@@ -272,5 +319,4 @@ public class MainApp extends Application {
             mBound = false;
         }
     };
-
 }

+ 4 - 1
src/com/owncloud/android/datamodel/SyncedFolder.java

@@ -21,11 +21,14 @@
 
 package com.owncloud.android.datamodel;
 
+import java.io.Serializable;
+
 /**
  * Synced folder entity containing all information per synced folder.
  */
-public class SyncedFolder {
+public class SyncedFolder implements Serializable {
     public static final long UNPERSISTED_ID = Long.MIN_VALUE;
+    private static final long serialVersionUID = -793476118299906429L;
     private long id = UNPERSISTED_ID;
     private String localPath;
     private String remotePath;

+ 8 - 0
src/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java

@@ -57,6 +57,14 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
         this.numberOfFiles = numberOfFiles;
     }
 
+    public SyncedFolderDisplayItem(long id, String localPath, String remotePath, Boolean wifiOnly, Boolean chargingOnly,
+                                   Boolean subfolderByDate, String account, Integer uploadAction, Boolean enabled,
+                                   String folderName) {
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled);
+        this.folderName = folderName;
+    }
+
+
     public List<String> getFilePaths() {
         return filePaths;
     }

+ 43 - 10
src/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -21,11 +21,13 @@ package com.owncloud.android.datamodel;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.support.annotation.NonNull;
 
 import com.owncloud.android.MainApp;
+import com.owncloud.android.db.PreferenceManager;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.lib.common.utils.Log_OC;
 
@@ -165,7 +167,7 @@ public class SyncedFolderProvider extends Observable {
         Cursor cursor = mContentResolver.query(
                 ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
                 null,
-                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + "==" + localPath,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + "== \"" + localPath + "\"",
                 null,
                 null
         );
@@ -186,6 +188,27 @@ public class SyncedFolderProvider extends Observable {
 
     }
 
+    /**
+     * delete any records of synchronized folders that are not within the given list of ids.
+     *
+     * @param context the context.
+     * @param ids     the list of ids to be excluded from deletion.
+     * @return number of deleted records.
+     */
+    public int deleteSyncedFoldersNotInList(Context context, ArrayList<Long> ids) {
+        int result = mContentResolver.delete(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                ProviderMeta.ProviderTableMeta._ID + " NOT IN (?)",
+                new String[]{String.valueOf(ids)}
+        );
+
+        if (result > 0 && context != null) {
+            PreferenceManager.setLegacyClean(context, true);
+        }
+
+        return result;
+    }
+
     /**
      * update given synced folder.
      *
@@ -214,21 +237,29 @@ public class SyncedFolderProvider extends Observable {
     /**
      * maps a cursor into a SyncedFolder object.
      *
-     * @param cursor the cursor
+     * @param cursor the db cursor
      * @return the mapped SyncedFolder, null if cursor is null
      */
     private SyncedFolder createSyncedFolderFromCursor(Cursor cursor) {
         SyncedFolder syncedFolder = null;
         if (cursor != null) {
             long id = cursor.getLong(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta._ID));
-            String localPath = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH));
-            String remotePath = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH));
-            Boolean wifiOnly = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1;
-            Boolean chargingOnly = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1;
-            Boolean subfolderByDate = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1;
-            String accountName = cursor.getString(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT));
-            Integer uploadAction = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION));
-            Boolean enabled = cursor.getInt(cursor.getColumnIndex(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1;
+            String localPath = cursor.getString(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH));
+            String remotePath = cursor.getString(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH));
+            Boolean wifiOnly = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1;
+            Boolean chargingOnly = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1;
+            Boolean subfolderByDate = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1;
+            String accountName = cursor.getString(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT));
+            Integer uploadAction = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_UPLOAD_ACTION));
+            Boolean enabled = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED)) == 1;
 
             syncedFolder = new SyncedFolder(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate,
                     accountName, uploadAction, enabled);
@@ -258,6 +289,8 @@ public class SyncedFolderProvider extends Observable {
 
     /**
      * Inform all observers about data change.
+     *
+     * @param syncedFolder changed, synchronized folder
      */
     private void notifyFolderSyncObservers(SyncedFolder syncedFolder) {
         MainApp.getSyncedFolderObserverService().restartObserver(syncedFolder);

+ 28 - 21
src/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -20,17 +20,17 @@
  */
 package com.owncloud.android.datamodel;
 
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
-import android.os.PersistableBundle;
 import android.support.annotation.RequiresApi;
 
+import com.evernote.android.job.JobManager;
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import com.owncloud.android.db.UploadResult;
@@ -44,6 +44,7 @@ import java.util.Calendar;
 import java.util.Collections;
 import java.util.List;
 import java.util.Observable;
+import java.util.Set;
 
 /**
  * Database helper for storing list of files to be uploaded, including status
@@ -395,17 +396,17 @@ public class UploadsStorageManager extends Observable {
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
     private List<OCUpload> getPendingJobs() {
-        JobScheduler js = (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+        Set<JobRequest> jobRequests = JobManager.create(mContext).getAllJobRequests();
 
         ArrayList<OCUpload> list = new ArrayList<>();
 
-        for (JobInfo ji: js.getAllPendingJobs()) {
-            PersistableBundle extras = ji.getExtras();
-            OCUpload upload  = new OCUpload(extras.getString("filePath"),
-                    extras.getString("remotePath"),
-                    extras.getString("account"));
+        for (JobRequest ji : jobRequests) {
+            PersistableBundleCompat extras = ji.getExtras();
+                OCUpload upload = new OCUpload(extras.getString("filePath", ""),
+                        extras.getString("remotePath", ""),
+                        extras.getString("account", ""));
 
-            list.add(upload);
+                list.add(upload);
         }
 
         return list;
@@ -413,13 +414,14 @@ public class UploadsStorageManager extends Observable {
 
     @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
     public void cancelPendingJob(String accountName, String remotePath){
-        JobScheduler js = (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
-
-        for (JobInfo ji: js.getAllPendingJobs()) {
-            PersistableBundle extras = ji.getExtras();
-            if (remotePath.equalsIgnoreCase(extras.getString("remotePath")) &&
-                accountName.equalsIgnoreCase(extras.getString("account"))){
-                js.cancel(ji.getId());
+        JobManager jobManager = JobManager.create(mContext);
+        Set<JobRequest> jobRequests = jobManager.getAllJobRequests();
+
+        for (JobRequest ji : jobRequests) {
+            PersistableBundleCompat extras = ji.getExtras();
+            if (remotePath.equalsIgnoreCase(extras.getString("remotePath", "")) &&
+                    accountName.equalsIgnoreCase(extras.getString("account", ""))) {
+                jobManager.cancel(ji.getJobId());
                 break;
             }
         }
@@ -429,13 +431,16 @@ public class UploadsStorageManager extends Observable {
      * Get all failed uploads.
      */
     public OCUpload[] getFailedUploads() {
-        return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value, null);
+
+        return getUploads(
+                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value, null);
     }
 
     /**
      * Get all uploads which where successfully completed.
      */
     public OCUpload[] getFinishedUploads() {
+
         return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value, null);
     }
 
@@ -444,8 +449,8 @@ public class UploadsStorageManager extends Observable {
      * @return      Array of failed uploads, except for those that were not performed due to lack of Wifi connection.
      */
     public OCUpload[] getFailedButNotDelayedUploads() {
-        return getUploads(
-            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
+
+        return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
                 ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
                 ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
             null
@@ -459,7 +464,7 @@ public class UploadsStorageManager extends Observable {
     public long clearFailedButNotDelayedUploads() {
         long result = getDB().delete(
             ProviderTableMeta.CONTENT_URI_UPLOADS,
-            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
+                ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND +
                 ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + AND +
                 ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue(),
             null
@@ -472,6 +477,7 @@ public class UploadsStorageManager extends Observable {
     }
 
     public long clearSuccessfulUploads() {
+
         long result = getDB().delete(
                 ProviderTableMeta.CONTENT_URI_UPLOADS,
                 ProviderTableMeta.UPLOADS_STATUS + "=="+ UploadStatus.UPLOAD_SUCCEEDED.value, null
@@ -484,6 +490,7 @@ public class UploadsStorageManager extends Observable {
     }
 
     public long clearAllFinishedButNotDelayedUploads() {
+
         String[] whereArgs = new String[2];
         whereArgs[0] = String.valueOf(UploadStatus.UPLOAD_SUCCEEDED.value);
         whereArgs[1] = String.valueOf(UploadStatus.UPLOAD_FAILED.value);

+ 22 - 2
src/com/owncloud/android/db/PreferenceManager.java

@@ -44,6 +44,7 @@ public abstract class PreferenceManager {
     private static final String PREF__INSTANT_UPLOAD_ON_WIFI = "instant_upload_on_wifi";
     private static final String PREF__INSTANT_VIDEO_UPLOAD_ON_WIFI = "instant_video_upload_on_wifi";
     private static final String PREF__INSTANT_VIDEO_UPLOAD_PATH_USE_SUBFOLDERS = "instant_video_upload_path_use_subfolders";
+    private static final String PREF__LEGACY_CLEAN = "legacyClean";
 
     public static boolean instantPictureUploadEnabled(Context context) {
         return getDefaultSharedPreferences(context).getBoolean(PREF__INSTANT_UPLOADING, false);
@@ -184,6 +185,26 @@ public abstract class PreferenceManager {
         saveBooleanPreference(context, AUTO_PREF__SORT_ASCENDING, ascending);
     }
 
+    /**
+     * Gets the legacy cleaning flag last set.
+     *
+     * @param context Caller {@link Context}, used to access to shared preferences manager.
+     * @return ascending order     the alegacy cleaning flag, default is false
+     */
+    public static boolean getLegacyClean(Context context) {
+        return getDefaultSharedPreferences(context).getBoolean(PREF__LEGACY_CLEAN, false);
+    }
+
+    /**
+     * Saves the legacy cleaning flag which the user has set last.
+     *
+     * @param context   Caller {@link Context}, used to access to shared preferences manager.
+     * @param legacyClean flag if it is a legacy cleaning
+     */
+    public static void setLegacyClean(Context context, boolean legacyClean) {
+        saveBooleanPreference(context, PREF__LEGACY_CLEAN, legacyClean);
+    }
+
     /**
      * Gets the uploader behavior which the user has set last.
      *
@@ -191,8 +212,7 @@ public abstract class PreferenceManager {
      * @return uploader behavior     the uploader behavior
      */
     public static int getUploaderBehaviour(Context context) {
-        return getDefaultSharedPreferences(context)
-                .getInt(AUTO_PREF__UPLOADER_BEHAVIOR, 1);
+        return getDefaultSharedPreferences(context).getInt(AUTO_PREF__UPLOADER_BEHAVIOR, 1);
     }
 
     /**

+ 1 - 1
src/com/owncloud/android/files/InstantUploadBroadcastReceiver.java

@@ -58,7 +58,7 @@ public class InstantUploadBroadcastReceiver extends BroadcastReceiver {
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
             Log_OC.d(TAG, "Received: " + intent.getAction());
             if (intent.getAction().equals(NEW_PHOTO_ACTION_UNOFFICIAL)) {
                 handleNewPictureAction(context, intent);

+ 11 - 0
src/com/owncloud/android/files/services/FileDownloader.java

@@ -23,6 +23,7 @@ package com.owncloud.android.files.services;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -79,6 +80,8 @@ public class FileDownloader extends Service
     public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
     public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
 
+    private static final int FOREGROUND_SERVICE_ID = 412;
+
     private static final String TAG = FileDownloader.class.getSimpleName();
 
     private Looper mServiceLooper;
@@ -96,6 +99,7 @@ public class FileDownloader extends Service
     private NotificationCompat.Builder mNotificationBuilder;
     private int mLastPercent;
 
+    private Notification mNotification;
 
     public static String getDownloadAddedMessage() {
         return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
@@ -120,6 +124,10 @@ public class FileDownloader extends Service
         mServiceHandler = new ServiceHandler(mServiceLooper, this);
         mBinder = new FileDownloaderBinder();
 
+        mNotification = new NotificationCompat.Builder(this).setContentTitle(getApplicationContext().
+                getResources().getString(R.string.app_name))
+                .build();
+
         // add AccountsUpdatedListener
         AccountManager am = AccountManager.get(getApplicationContext());
         am.addOnAccountsUpdatedListener(this, null, false);
@@ -156,6 +164,8 @@ public class FileDownloader extends Service
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log_OC.d(TAG, "Starting command with id " + startId);
 
+        startForeground(FOREGROUND_SERVICE_ID, mNotification);
+
         if (!intent.hasExtra(EXTRA_ACCOUNT) ||
                 !intent.hasExtra(EXTRA_FILE)
                 ) {
@@ -383,6 +393,7 @@ public class FileDownloader extends Service
                 }
             }
             Log_OC.d(TAG, "Stopping after command with id " + msg.arg1);
+            mService.stopForeground(true);
             mService.stopSelf(msg.arg1);
         }
     }

+ 14 - 0
src/com/owncloud/android/files/services/FileUploader.java

@@ -27,6 +27,7 @@ package com.owncloud.android.files.services;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -103,11 +104,15 @@ public class FileUploader extends Service
     public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
     public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
 
+    private static final int FOREGROUND_SERVICE_ID = 411;
+
     public static final String KEY_FILE = "FILE";
     public static final String KEY_LOCAL_FILE = "LOCAL_FILE";
     public static final String KEY_REMOTE_FILE = "REMOTE_FILE";
     public static final String KEY_MIME_TYPE = "MIME_TYPE";
 
+    private Notification mNotification;
+
     /**
      * Call this Service with only this Intent key if all pending uploads are to be retried.
      */
@@ -348,6 +353,10 @@ public class FileUploader extends Service
 
         mUploadsStorageManager = new UploadsStorageManager(getContentResolver(), getApplicationContext());
 
+        mNotification = new NotificationCompat.Builder(this).setContentTitle(getApplicationContext().
+                getResources().getString(R.string.app_name))
+                .build();
+
         int failedCounter = mUploadsStorageManager.failInProgressUploads(
             UploadResult.SERVICE_INTERRUPTED    // Add UploadResult.KILLED?
         );
@@ -401,6 +410,8 @@ public class FileUploader extends Service
     public int onStartCommand(Intent intent, int flags, int startId) {
         Log_OC.d(TAG, "Starting command with id " + startId);
 
+        startForeground(FOREGROUND_SERVICE_ID, mNotification);
+
         boolean retry = intent.getBooleanExtra(KEY_RETRY, false);
         AbstractList<String> requestedUploads = new Vector<String>();
 
@@ -845,6 +856,7 @@ public class FileUploader extends Service
 
     }
 
+
     /**
      * Upload worker. Performs the pending uploads in the order they were
      * requested.
@@ -876,7 +888,9 @@ public class FileUploader extends Service
                 }
             }
             Log_OC.d(TAG, "Stopping command after id " + msg.arg1);
+            mService.stopForeground(true);
             mService.stopSelf(msg.arg1);
+
         }
     }
 

+ 37 - 26
src/com/owncloud/android/operations/UploadFileOperation.java

@@ -390,31 +390,7 @@ public class UploadFileOperation extends SyncOperation {
 
             /// move local temporal file or original file to its corresponding
             // location in the ownCloud local folder
-            if (result.isSuccess()) {
-                if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) {
-                    String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
-                    if (mOriginalStoragePath.equals(temporalPath)) {
-                        // delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader)
-                        temporalFile = new File(temporalPath);
-                        temporalFile.delete();
-                    }
-                    mFile.setStoragePath("");
-
-                } else if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_DELETE) {
-                    originalFile.delete();
-                } else {
-                    mFile.setStoragePath(expectedPath);
-
-                    if (temporalFile != null) {         // FileUploader.LOCAL_BEHAVIOUR_COPY
-                        move(temporalFile, expectedFile);
-                    } else {                            // FileUploader.LOCAL_BEHAVIOUR_MOVE
-                        move(originalFile, expectedFile);
-                        getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
-                    }
-                    FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
-                }
-
-            } else if (result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED ) {
+            if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED ) {
                 result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
             }
 
@@ -450,8 +426,43 @@ public class UploadFileOperation extends SyncOperation {
         }
 
         if (result.isSuccess()) {
-            saveUploadedFile(client);
 
+            if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_FORGET) {
+
+                String temporalPath = FileStorageUtils.getTemporalPath(mAccount.name) + mFile.getRemotePath();
+                if (mOriginalStoragePath.equals(temporalPath)) {
+                    // delete local file is was pre-copied in temporary folder (see .ui.helpers.UriUploader)
+                    temporalFile = new File(temporalPath);
+                    temporalFile.delete();
+                }
+                mFile.setStoragePath("");
+                saveUploadedFile(client);
+
+
+            } else if (mLocalBehaviour == FileUploader.LOCAL_BEHAVIOUR_DELETE) {
+                originalFile.delete();
+                getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
+                saveUploadedFile(client);
+            } else {
+
+                if (temporalFile != null) {         // FileUploader.LOCAL_BEHAVIOUR_COPY
+                    try {
+                        move(temporalFile, expectedFile);
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                } else {                            // FileUploader.LOCAL_BEHAVIOUR_MOVE
+                    try {
+                        move(originalFile, expectedFile);
+                    } catch (IOException e) {
+                        e.printStackTrace();
+                    }
+                    getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath());
+                }
+                mFile.setStoragePath(expectedFile.getAbsolutePath());
+                saveUploadedFile(client);
+                FileDataStorageManager.triggerMediaScan(expectedFile.getAbsolutePath());
+            }
         } else if (result.getCode() == ResultCode.SYNC_CONFLICT) {
             getStorageManager().saveConflict(mFile, mFile.getEtagInConflict());
         }

+ 186 - 0
src/com/owncloud/android/services/AdvancedFileAlterationListener.java

@@ -0,0 +1,186 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.services;
+
+import android.content.Context;
+import android.media.ExifInterface;
+import android.os.Handler;
+import android.text.TextUtils;
+
+import com.evernote.android.job.JobRequest;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.FileStorageUtils;
+
+import org.apache.commons.io.monitor.FileAlterationListener;
+import org.apache.commons.io.monitor.FileAlterationObserver;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+/**
+ * Magical file alteration listener
+ */
+
+public class AdvancedFileAlterationListener implements FileAlterationListener {
+
+    public static final String TAG = "AdvancedFileAlterationListener";
+    public static final int DELAY_INVOCATION_MS = 2500;
+    private Context context;
+
+    private SyncedFolder syncedFolder;
+
+    private Map<String, Runnable> uploadMap = new HashMap<>();
+    private Handler handler = new Handler();
+
+    public AdvancedFileAlterationListener(SyncedFolder syncedFolder) {
+        super();
+
+        context = MainApp.getAppContext();
+        this.syncedFolder = syncedFolder;
+    }
+
+    @Override
+    public void onStart(FileAlterationObserver observer) {
+        // This method is intentionally empty
+    }
+
+    @Override
+    public void onDirectoryCreate(File directory) {
+        // This method is intentionally empty
+    }
+
+    @Override
+    public void onDirectoryChange(File directory) {
+        // This method is intentionally empty
+    }
+
+    @Override
+    public void onDirectoryDelete(File directory) {
+        // This method is intentionally empty
+    }
+
+    @Override
+    public void onFileCreate(final File file) {
+        onFileCreate(file, DELAY_INVOCATION_MS);
+    }
+
+    public void onFileCreate(final File file, int delay) {
+        if (file != null) {
+            uploadMap.put(file.getAbsolutePath(), null);
+
+            String mimetypeString = FileStorageUtils.getMimeTypeFromName(file.getAbsolutePath());
+            Long lastModificationTime = file.lastModified();
+            final Locale currentLocale = context.getResources().getConfiguration().locale;
+
+            if ("image/jpeg".equalsIgnoreCase(mimetypeString) || "image/tiff".equalsIgnoreCase(mimetypeString)) {
+                try {
+                    ExifInterface exifInterface = new ExifInterface(file.getAbsolutePath());
+                    String exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME);
+                    if (!TextUtils.isEmpty(exifDate)) {
+                        ParsePosition pos = new ParsePosition(0);
+                        SimpleDateFormat sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale);
+                        sFormatter.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
+                        Date dateTime = sFormatter.parse(exifDate, pos);
+                        lastModificationTime = dateTime.getTime();
+                    }
+
+                } catch (IOException e) {
+                    Log_OC.d(TAG, "Failed to get the proper time " + e.getLocalizedMessage());
+                }
+            }
+
+
+            final Long finalLastModificationTime = lastModificationTime;
+
+            Runnable runnable = new Runnable() {
+                @Override
+                public void run() {
+                    PersistableBundleCompat bundle = new PersistableBundleCompat();
+                    bundle.putString(AutoUploadJob.LOCAL_PATH, file.getAbsolutePath());
+                    bundle.putString(AutoUploadJob.REMOTE_PATH, FileStorageUtils.getInstantUploadFilePath(
+                            currentLocale,
+                            syncedFolder.getRemotePath(), file.getName(),
+                            finalLastModificationTime,
+                            syncedFolder.getSubfolderByDate()));
+                    bundle.putString(AutoUploadJob.ACCOUNT, syncedFolder.getAccount());
+                    bundle.putInt(AutoUploadJob.UPLOAD_BEHAVIOUR, syncedFolder.getUploadAction());
+
+                    new JobRequest.Builder(AutoUploadJob.TAG)
+                            .setExecutionWindow(30_000L, 80_000L)
+                            .setRequiresCharging(syncedFolder.getChargingOnly())
+                            .setRequiredNetworkType(syncedFolder.getWifiOnly() ? JobRequest.NetworkType.UNMETERED :
+                                    JobRequest.NetworkType.ANY)
+                            .setExtras(bundle)
+                            .setPersisted(false)
+                            .setRequirementsEnforced(true)
+                            .setUpdateCurrent(false)
+                            .build()
+                            .schedule();
+
+                    uploadMap.remove(file.getAbsolutePath());
+                }
+            };
+
+            uploadMap.put(file.getAbsolutePath(), runnable);
+            handler.postDelayed(runnable, delay);
+        }
+    }
+
+    @Override
+    public void onFileChange(File file) {
+        onFileChange(file, 2500);
+    }
+
+    public void onFileChange(File file, int delay) {
+        Runnable runnable;
+        if ((runnable = uploadMap.get(file.getAbsolutePath())) != null) {
+            handler.removeCallbacks(runnable);
+            handler.postDelayed(runnable, delay);
+        }
+    }
+
+    @Override
+    public void onFileDelete(File file) {
+        Runnable runnable;
+        if ((runnable = uploadMap.get(file.getAbsolutePath())) != null) {
+            handler.removeCallbacks(runnable);
+            uploadMap.remove(file.getAbsolutePath());
+        }
+    }
+
+    @Override
+    public void onStop(FileAlterationObserver observer) {
+        // This method is intentionally empty
+    }
+
+    public int getActiveTasksCount() {
+        return uploadMap.size();
+    }
+}

+ 81 - 0
src/com/owncloud/android/services/AutoUploadJob.java

@@ -0,0 +1,81 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2016 Tobias Kaminsky
+ * Copyright (C) 2016 Nextcloud
+ * <p>
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.services;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.evernote.android.job.Job;
+import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.MainApp;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.operations.UploadFileOperation;
+import com.owncloud.android.utils.MimeTypeUtil;
+
+import java.io.File;
+
+public class AutoUploadJob extends Job {
+    public static final String TAG = "AutoUploadJob";
+
+    public static final String LOCAL_PATH = "filePath";
+    public static final String REMOTE_PATH = "remotePath";
+    public static final String ACCOUNT = "account";
+    public static final String UPLOAD_BEHAVIOUR = "uploadBehaviour";
+
+    @NonNull
+    @Override
+    protected Result onRunJob(Params params) {
+        final Context context = MainApp.getAppContext();
+        PersistableBundleCompat bundle = params.getExtras();
+        final String filePath = bundle.getString(LOCAL_PATH, "");
+        final String remotePath = bundle.getString(REMOTE_PATH, "");
+        final Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString(ACCOUNT, ""));
+        final Integer uploadBehaviour = bundle.getInt(UPLOAD_BEHAVIOUR, -1);
+
+
+        File file = new File(filePath);
+
+
+        // File can be deleted between job generation and job execution. If file does not exist, just ignore it
+        if (file.exists()) {
+            final String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
+
+            final FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
+            requester.uploadNewFile(
+                    context,
+                    account,
+                    filePath,
+                    remotePath,
+                    uploadBehaviour,
+                    mimeType,
+                    true,           // create parent folder if not existent
+                    UploadFileOperation.CREATED_AS_INSTANT_PICTURE
+            );
+        }
+
+
+        return Result.SUCCESS;
+    }
+}

+ 40 - 0
src/com/owncloud/android/services/NCJobCreator.java

@@ -0,0 +1,40 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.services;
+
+import com.evernote.android.job.Job;
+import com.evernote.android.job.JobCreator;
+
+/**
+ * Job creator for android-job
+ */
+
+public class NCJobCreator implements JobCreator {
+    @Override
+    public Job create(String tag) {
+        switch (tag) {
+            case AutoUploadJob.TAG:
+                return new AutoUploadJob();
+            default:
+                return null;
+        }
+    }
+}

+ 40 - 0
src/com/owncloud/android/services/ShutdownReceiver.java

@@ -0,0 +1,40 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.services;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.owncloud.android.MainApp;
+
+/**
+ * Handles shutdown procedure - basically just waits a little bit for all jobs to finish
+ */
+
+public class ShutdownReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        if (MainApp.getSyncedFolderObserverService() != null) {
+            MainApp.getSyncedFolderObserverService().onDestroy();
+        }
+    }
+}

+ 0 - 93
src/com/owncloud/android/services/SyncedFolderJobService.java

@@ -1,93 +0,0 @@
-/**
- *   Nextcloud Android client application
- *
- *   @author Tobias Kaminsky
- *   Copyright (C) 2016 Tobias Kaminsky
- *   Copyright (C) 2016 Nextcloud
- *
- *   This program is free software; you can redistribute it and/or
- *   modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- *   License as published by the Free Software Foundation; either
- *   version 3 of the License, or any later version.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- *   You should have received a copy of the GNU Affero General Public
- *   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.owncloud.android.services;
-
-import android.accounts.Account;
-import android.annotation.TargetApi;
-import android.app.job.JobParameters;
-import android.app.job.JobService;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.os.PersistableBundle;
-
-import com.owncloud.android.MainApp;
-import com.owncloud.android.authentication.AccountUtils;
-import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.operations.UploadFileOperation;
-import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.MimeTypeUtil;
-
-import java.io.File;
-
-@TargetApi(Build.VERSION_CODES.LOLLIPOP)
-public class SyncedFolderJobService extends JobService {
-    private static final String TAG = "SyncedFolderJobService";
-
-    public static final String LOCAL_PATH = "filePath";
-    public static final String REMOTE_PATH = "remotePath";
-    public static final String ACCOUNT = "account";
-    public static final String UPLOAD_BEHAVIOUR = "uploadBehaviour";
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        return START_REDELIVER_INTENT;
-    }
-
-    @Override
-    public boolean onStartJob(JobParameters params) {
-        Context context = MainApp.getAppContext();
-        PersistableBundle bundle = params.getExtras();
-        String filePath = bundle.getString(LOCAL_PATH);
-        String remotePath = bundle.getString(REMOTE_PATH);
-        Account account = AccountUtils.getOwnCloudAccountByName(context, bundle.getString(ACCOUNT));
-        Integer uploadBehaviour = bundle.getInt(UPLOAD_BEHAVIOUR);
-
-        Log_OC.d(TAG, "startJob: " + params.getJobId() + ", filePath: " + filePath);
-
-        File file = new File(filePath);
-
-        // File can be deleted between job generation and job execution. If file does not exist, just ignore it
-        if (file.exists()) {
-            String mimeType = MimeTypeUtil.getBestMimeTypeByFilename(file.getAbsolutePath());
-
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.uploadNewFile(
-                    context,
-                    account,
-                    filePath,
-                    remotePath,
-                    uploadBehaviour,
-                    mimeType,
-                    true,           // create parent folder if not existent
-                    UploadFileOperation.CREATED_AS_INSTANT_PICTURE
-            );
-        }
-        return false;
-    }
-
-    @Override
-    public boolean onStopJob(JobParameters params) {
-        return false;
-    }
-}

+ 402 - 0
src/com/owncloud/android/services/observer/AdvancedFileAlterationObserver.java

@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Original source code:
+ * https://github.com/apache/commons-io/blob/master/src/main/java/org/apache/commons/io/monitor/FileAlterationObserver.java
+ *
+ * Modified by Mario Danic
+ * Changes are Copyright (C) 2017 Mario Danic
+ * Copyright (C) 2017 Nextcloud GmbH
+ *
+ * All changes are under the same licence as the original.
+ *
+ */
+package com.owncloud.android.services.observer;
+
+import android.os.SystemClock;
+
+import com.owncloud.android.datamodel.SyncedFolder;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.services.AdvancedFileAlterationListener;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.comparator.NameFileComparator;
+import org.apache.commons.io.monitor.FileAlterationObserver;
+import org.apache.commons.io.monitor.FileEntry;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class AdvancedFileAlterationObserver extends FileAlterationObserver implements Serializable {
+
+    private static final long serialVersionUID = 1185122225658782848L;
+    private static final int DELAY_INVOCATION_MS = 2500;
+    private final List<AdvancedFileAlterationListener> listeners = new CopyOnWriteArrayList<>();
+    private FileEntry rootEntry;
+    private FileFilter fileFilter;
+    private Comparator<File> comparator;
+    private SyncedFolder syncedFolder;
+
+    private static final FileEntry[] EMPTY_ENTRIES = new FileEntry[0];
+    
+    public AdvancedFileAlterationObserver(SyncedFolder syncedFolder, FileFilter fileFilter) {
+        super(syncedFolder.getLocalPath(), fileFilter);
+
+        this.rootEntry = new FileEntry(new File(syncedFolder.getLocalPath()));
+        this.fileFilter = fileFilter;
+        this.syncedFolder = syncedFolder;
+        comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
+    }
+
+    public long getSyncedFolderID() {
+        return syncedFolder.getId();
+    }
+
+    public SyncedFolder getSyncedFolder() {
+        return syncedFolder;
+    }
+
+    /**
+     * Return the directory being observed.
+     *
+     * @return the directory being observed
+     */
+    public File getDirectory() {
+        return rootEntry.getFile();
+    }
+
+    /**
+     * Return the fileFilter.
+     *
+     * @return the fileFilter
+     * @since 2.1
+     */
+    public FileFilter getFileFilter() {
+        return fileFilter;
+    }
+
+    public FileEntry getRootEntry() {
+        return rootEntry;
+    }
+
+    public void setRootEntry(FileEntry rootEntry) {
+        this.rootEntry = rootEntry;
+    }
+
+    /**
+     * Add a file system listener.
+     *
+     * @param listener The file system listener
+     */
+    public void addListener(final AdvancedFileAlterationListener listener) {
+        if (listener != null) {
+            listeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove a file system listener.
+     *
+     * @param listener The file system listener
+     */
+    public void removeListener(final AdvancedFileAlterationListener listener) {
+        if (listener != null) {
+            while (listeners.remove(listener)) {
+            }
+        }
+    }
+
+    /**
+     * Returns the set of registered file system listeners.
+     *
+     * @return The file system listeners
+     */
+    public Iterable<AdvancedFileAlterationListener> getMagicListeners() {
+        return listeners;
+    }
+
+    /**
+     * Does nothing - hack for the monitor
+     *
+     *
+     */
+    public void initialize() {
+        // does nothing - hack the monitor
+    }
+
+
+    /**
+     * Initializes everything
+     *
+     * @throws Exception if an error occurs
+     */
+    public void init() throws Exception {
+        rootEntry.refresh(rootEntry.getFile());
+        final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry);
+        rootEntry.setChildren(children);
+    }
+
+
+    /**
+     * Final processing.
+     *
+     * @throws Exception if an error occurs
+     */
+    public void destroy() throws Exception {
+        Iterator iterator = getMagicListeners().iterator();
+        while (iterator.hasNext()) {
+            AdvancedFileAlterationListener AdvancedFileAlterationListener = (AdvancedFileAlterationListener) iterator.next();
+            while (AdvancedFileAlterationListener.getActiveTasksCount() > 0) {
+                SystemClock.sleep(250);
+            }
+        }
+    }
+
+    public void checkAndNotifyNow() {
+                /* fire onStart() */
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            listener.onStart(this);
+        }
+
+        /* fire directory/file events */
+        final File rootFile = rootEntry.getFile();
+        if (rootFile.exists()) {
+            checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile), 0);
+        } else if (rootEntry.isExists()) {
+            try {
+                // try to init once more
+                init();
+                if (rootEntry.getFile().exists()) {
+                    checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootEntry.getFile()), 0);
+                } else {
+                    checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, 0);
+                }
+            } catch (Exception e) {
+                Log_OC.d("AdvancedFileAlterationObserver", "Failed getting an observer to intialize " + e);
+                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, 0);
+            }
+        } // else didn't exist and still doesn't
+
+        /* fire onStop() */
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            listener.onStop(this);
+        }
+    }
+    
+    /**
+     * Check whether the file and its children have been created, modified or deleted.
+     */
+    public void checkAndNotify() {
+
+        /* fire onStart() */
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            listener.onStart(this);
+        }
+
+        /* fire directory/file events */
+        final File rootFile = rootEntry.getFile();
+        if (rootFile.exists()) {
+            checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile), DELAY_INVOCATION_MS);
+        } else if (rootEntry.isExists()) {
+            try {
+                // try to init once more
+                init();
+                if (rootEntry.getFile().exists()) {
+                    checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootEntry.getFile()),
+                            DELAY_INVOCATION_MS);
+                } else {
+                    checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, DELAY_INVOCATION_MS);
+                }
+            } catch (Exception e) {
+                Log_OC.d("AdvancedFileAlterationObserver", "Failed getting an observer to intialize " + e);
+                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, DELAY_INVOCATION_MS);
+            }
+        } // else didn't exist and still doesn't
+
+        /* fire onStop() */
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            listener.onStop(this);
+        }
+    }
+
+    /**
+     * Compare two file lists for files which have been created, modified or deleted.
+     *
+     * @param parent   The parent entry
+     * @param previous The original list of files
+     * @param files    The current list of files
+     */
+    private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files, int delay) {
+        if (files != null && files.length > 0) {
+            int c = 0;
+            final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : EMPTY_ENTRIES;
+            for (final FileEntry entry : previous) {
+                while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
+                    current[c] = createFileEntry(parent, files[c]);
+                    doCreate(current[c], delay);
+                    c++;
+                }
+                if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
+                    doMatch(entry, files[c], delay);
+                    checkAndNotify(entry, entry.getChildren(), listFiles(files[c]), delay);
+                    current[c] = entry;
+                    c++;
+                } else {
+                    checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY, delay);
+                    doDelete(entry);
+                }
+            }
+            for (; c < files.length; c++) {
+                current[c] = createFileEntry(parent, files[c]);
+                doCreate(current[c], delay);
+            }
+            parent.setChildren(current);
+        }
+    }
+
+    /**
+     * Create a new file entry for the specified file.
+     *
+     * @param parent The parent file entry
+     * @param file   The file to create an entry for
+     * @return A new file entry
+     */
+    private FileEntry createFileEntry(final FileEntry parent, final File file) {
+        final FileEntry entry = parent.newChildInstance(file);
+        entry.refresh(file);
+        final FileEntry[] children = doListFiles(file, entry);
+        entry.setChildren(children);
+        return entry;
+    }
+
+    /**
+     * List the files
+     *
+     * @param file  The file to list files for
+     * @param entry the parent entry
+     * @return The child files
+     */
+    private FileEntry[] doListFiles(File file, FileEntry entry) {
+        final File[] files = listFiles(file);
+        final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : EMPTY_ENTRIES;
+        for (int i = 0; i < files.length; i++) {
+            children[i] = createFileEntry(entry, files[i]);
+        }
+        return children;
+    }
+
+    /**
+     * Fire directory/file created events to the registered listeners.
+     *
+     * @param entry The file entry
+     */
+    private void doCreate(final FileEntry entry, int delay) {
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            if (entry.isDirectory()) {
+                listener.onDirectoryCreate(entry.getFile());
+            } else {
+                listener.onFileCreate(entry.getFile(), delay);
+            }
+        }
+        final FileEntry[] children = entry.getChildren();
+        for (final FileEntry aChildren : children) {
+            doCreate(aChildren, delay);
+        }
+    }
+
+    /**
+     * Fire directory/file change events to the registered listeners.
+     *
+     * @param entry The previous file system entry
+     * @param file  The current file
+     */
+    private void doMatch(final FileEntry entry, final File file, int delay) {
+        if (entry.refresh(file)) {
+            for (final AdvancedFileAlterationListener listener : listeners) {
+                if (entry.isDirectory()) {
+                    listener.onDirectoryChange(file);
+                } else {
+                    listener.onFileChange(file, delay);
+                }
+            }
+        }
+    }
+
+    /**
+     * Fire directory/file delete events to the registered listeners.
+     *
+     * @param entry The file entry
+     */
+    private void doDelete(final FileEntry entry) {
+        for (final AdvancedFileAlterationListener listener : listeners) {
+            if (entry.isDirectory()) {
+                listener.onDirectoryDelete(entry.getFile());
+            } else {
+                listener.onFileDelete(entry.getFile());
+            }
+        }
+    }
+
+    /**
+     * List the contents of a directory
+     *
+     * @param file The file to list the contents of
+     * @return the directory contents or a zero length array if
+     * the empty or the file is not a directory
+     */
+    private File[] listFiles(final File file) {
+        File[] children = null;
+        if (file.isDirectory()) {
+            children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
+        }
+        if (children == null) {
+            children = FileUtils.EMPTY_FILE_ARRAY;
+        }
+        if (comparator != null && children.length > 1) {
+            Arrays.sort(children, comparator);
+        }
+        return children;
+    }
+
+    /**
+     * Provide a String representation of this observer.
+     *
+     * @return a String representation of this observer
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(getClass().getSimpleName());
+        builder.append("[file='");
+        builder.append(getDirectory().getPath());
+        builder.append('\'');
+        if (fileFilter != null) {
+            builder.append(", ");
+            builder.append(fileFilter.toString());
+        }
+        builder.append(", listeners=");
+        builder.append(listeners.size());
+        builder.append("]");
+        return builder.toString();
+    }
+
+}

+ 0 - 78
src/com/owncloud/android/services/observer/SyncedFolderObserver.java

@@ -1,78 +0,0 @@
-package com.owncloud.android.services.observer;
-
-import android.annotation.TargetApi;
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.content.ComponentName;
-import android.content.Context;
-import android.os.Build;
-import android.os.FileObserver;
-import android.os.PersistableBundle;
-import android.util.Log;
-
-import com.owncloud.android.MainApp;
-import com.owncloud.android.datamodel.SyncedFolder;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.services.SyncedFolderJobService;
-import com.owncloud.android.utils.FileStorageUtils;
-import com.owncloud.android.utils.RecursiveFileObserver;
-
-import java.io.File;
-import java.util.Date;
-
-class SyncedFolderObserver extends RecursiveFileObserver {
-
-    private Context context;
-
-    public static final String TAG = "SyncedFolderObserver";
-    private SyncedFolder syncedFolder;
-
-
-    public SyncedFolderObserver(SyncedFolder syncedFolder) {
-        super(syncedFolder.getLocalPath(), FileObserver.CREATE + FileObserver.MOVED_TO);
-
-        context = MainApp.getAppContext();
-        this.syncedFolder = syncedFolder;
-        Log_OC.d("SyncedFolderObserver", "Started watching: " + syncedFolder.getLocalPath());
-    }
-
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    @Override
-    public void onEvent(int event, String path) {
-        Log.d(TAG, "Event: " + event + " Path: " + path);
-
-        File temp = new File(path);
-
-        // do not upload "null"-files, test if file exists and is a real file
-        if (!temp.getName().equalsIgnoreCase("null") && temp.isFile() && !temp.getName().endsWith(".tmp")) {
-            PersistableBundle bundle = new PersistableBundle();
-            // TODO extract
-            bundle.putString(SyncedFolderJobService.LOCAL_PATH, path);
-            bundle.putString(SyncedFolderJobService.REMOTE_PATH, FileStorageUtils.getInstantUploadFilePath(
-                                                                 syncedFolder.getRemotePath(), temp.getName(),
-                                                                 new Date().getTime(),
-                                                                 syncedFolder.getSubfolderByDate()));
-            bundle.putString(SyncedFolderJobService.ACCOUNT, syncedFolder.getAccount());
-            bundle.putInt(SyncedFolderJobService.UPLOAD_BEHAVIOUR, syncedFolder.getUploadAction());
-
-            JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
-
-            Long date = new Date().getTime();
-            JobInfo job = new JobInfo.Builder(
-                    date.intValue(),
-                    new ComponentName(context, SyncedFolderJobService.class))
-                    .setRequiresCharging(syncedFolder.getChargingOnly())
-                    .setMinimumLatency(10000)
-                    .setRequiredNetworkType(syncedFolder.getWifiOnly() ? JobInfo.NETWORK_TYPE_UNMETERED : JobInfo.NETWORK_TYPE_ANY)
-                    .setExtras(bundle)
-                    .setPersisted(true)
-                    .build();
-
-            Integer result = js.schedule(job);
-            if (result <= 0) {
-                Log_OC.d(TAG, "Job failed to start: " + result);
-            }
-        }
-    }
-}

+ 122 - 31
src/com/owncloud/android/services/observer/SyncedFolderObserverService.java

@@ -1,3 +1,25 @@
+/**
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * Copyright (C) 2016 Tobias Kaminsky, Andy Scherzinger
+ * Copyright (C) 2017 Mario Danic
+ * <p>
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ * <p>
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * <p>
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
 package com.owncloud.android.services.observer;
 
 import android.app.Service;
@@ -9,65 +31,134 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.services.AdvancedFileAlterationListener;
 
-import java.util.HashMap;
+import org.apache.commons.io.monitor.FileAlterationMonitor;
+import org.apache.commons.io.monitor.FileAlterationObserver;
+
+import java.io.File;
+import java.io.FileFilter;
 
 public class SyncedFolderObserverService extends Service {
     private static final String TAG = "SyncedFolderObserverService";
-    private SyncedFolderProvider mProvider;
-    private HashMap<String, SyncedFolderObserver> syncedFolderMap = new HashMap<>();
-    private final IBinder mBinder = new SyncedFolderObserverBinder();
 
+    private static final int MONITOR_SCAN_INTERVAL = 1000;
+
+    private final IBinder mBinder = new SyncedFolderObserverBinder();
+    private FileAlterationMonitor monitor;
+    private FileFilter fileFilter;
+    
     @Override
     public void onCreate() {
-        mProvider = new SyncedFolderProvider(MainApp.getAppContext().getContentResolver());
-    }
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(MainApp.getAppContext().
+                getContentResolver());
+        monitor = new FileAlterationMonitor(MONITOR_SCAN_INTERVAL);
 
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log_OC.d(TAG, "start");
-        for (SyncedFolder syncedFolder : mProvider.getSyncedFolders()) {
+        fileFilter = new FileFilter() {
+            @Override
+            public boolean accept(File pathname) {
+                return !pathname.getName().startsWith(".") && !pathname.getName().endsWith(".tmp");
+            }
+        };
+
+
+        for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
             if (syncedFolder.isEnabled()) {
-                Log_OC.d(TAG, "start observer: " + syncedFolder.getLocalPath());
-                SyncedFolderObserver observer = new SyncedFolderObserver(syncedFolder);
-                observer.startWatching();
-                syncedFolderMap.put(syncedFolder.getLocalPath(), observer);
+                AdvancedFileAlterationObserver observer = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
+
+                try {
+                    observer.init();
+                    observer.addListener(new AdvancedFileAlterationListener(syncedFolder));
+                    monitor.addObserver(observer);
+                } catch (Exception e) {
+                    Log_OC.d(TAG, "Failed getting an observer to intialize " + e);
+                }
+
             }
         }
 
-        return Service.START_NOT_STICKY;
+
+        try {
+            monitor.start();
+        } catch (Exception e) {
+            Log_OC.d(TAG, "Something went very wrong at onStartCommand");
+        }
+
     }
 
     @Override
     public void onDestroy() {
-        for (SyncedFolderObserver observer : syncedFolderMap.values()) {
-            observer.stopWatching();
-            syncedFolderMap.remove(observer);
+
+        super.onDestroy();
+        for (FileAlterationObserver fileAlterationObserver : monitor.getObservers()) {
+            AdvancedFileAlterationObserver advancedFileAlterationObserver = (AdvancedFileAlterationObserver)
+                    fileAlterationObserver;
+            try {
+                monitor.removeObserver(advancedFileAlterationObserver);
+                advancedFileAlterationObserver.checkAndNotifyNow();
+                advancedFileAlterationObserver.destroy();
+            } catch (Exception e) {
+                Log_OC.d(TAG, "Something went very wrong on trying to destroy observers");
+            }
         }
     }
 
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        return Service.START_NOT_STICKY;
+    }
+
     /**
      * Restart oberver if it is enabled
      * If syncedFolder exists already, use it, otherwise create new observer
+     *
      * @param syncedFolder
      */
-    public void restartObserver(SyncedFolder syncedFolder){
-        if (syncedFolderMap.containsKey(syncedFolder.getLocalPath())) {
-            Log_OC.d(TAG, "stop observer: " + syncedFolder.getLocalPath());
-            syncedFolderMap.get(syncedFolder.getLocalPath()).stopWatching();
-            syncedFolderMap.remove(syncedFolder.getLocalPath());
+
+    public void restartObserver(SyncedFolder syncedFolder) {
+        boolean found = false;
+        AdvancedFileAlterationObserver advancedFileAlterationObserver;
+        for (FileAlterationObserver fileAlterationObserver : monitor.getObservers()) {
+            advancedFileAlterationObserver =
+                    (AdvancedFileAlterationObserver) fileAlterationObserver;
+            if (advancedFileAlterationObserver.getSyncedFolderID() == syncedFolder.getId()) {
+                monitor.removeObserver(fileAlterationObserver);
+                advancedFileAlterationObserver.checkAndNotifyNow();
+                try {
+                    advancedFileAlterationObserver.destroy();
+                } catch (Exception e) {
+                    Log_OC.d(TAG, "Failed to destroy the observer in restart");
+                }
+
+                if (syncedFolder.isEnabled()) {
+                    try {
+                        advancedFileAlterationObserver = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
+                        advancedFileAlterationObserver.init();
+                        advancedFileAlterationObserver.addListener(new AdvancedFileAlterationListener(syncedFolder));
+                        monitor.addObserver(advancedFileAlterationObserver);
+                    } catch (Exception e) {
+                        Log_OC.d(TAG, "Failed getting an observer to intialize");
+                    }
+                } else {
+                    monitor.removeObserver(fileAlterationObserver);
+                }
+                found = true;
+                break;
+            }
         }
 
-        if (syncedFolder.isEnabled()) {
-            Log_OC.d(TAG, "start observer: " + syncedFolder.getLocalPath());
-            if (syncedFolderMap.containsKey(syncedFolder.getLocalPath())) {
-                syncedFolderMap.get(syncedFolder.getLocalPath()).startWatching();
-            } else {
-                SyncedFolderObserver observer = new SyncedFolderObserver(syncedFolder);
-                observer.startWatching();
-                syncedFolderMap.put(syncedFolder.getLocalPath(), observer);
+        if (!found && syncedFolder.isEnabled()) {
+            try {
+                advancedFileAlterationObserver = new AdvancedFileAlterationObserver(syncedFolder, fileFilter);
+                advancedFileAlterationObserver.init();
+                advancedFileAlterationObserver.addListener(new AdvancedFileAlterationListener(syncedFolder));
+                monitor.addObserver(advancedFileAlterationObserver);
+            } catch (Exception e) {
+                Log_OC.d(TAG, "Failed getting an observer to intialize");
             }
+
         }
+
     }
 
     @Override

+ 2 - 2
src/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -175,8 +175,8 @@ public abstract class DrawerActivity extends ToolbarActivity implements DisplayU
 
             setupQuotaElement();
 
-            // show folder sync menu item only for Android 7+
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            // show folder sync menu item only for Android 6+
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
                 mNavigationView.getMenu().removeItem(R.id.nav_folder_sync);
             }
         }

+ 2 - 2
src/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -256,8 +256,8 @@ public class FileDisplayActivity extends HookActivity
      * Opens a pop up info for the new instant upload and disabled the old instant upload.
      */
     private void upgradeNotificationForInstantUpload() {
-        // check for Android 7+ if legacy instant upload is activated --> disable + show info
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+        // check for Android 6+ if legacy instant upload is activated --> disable + show info
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                 (PreferenceManager.instantPictureUploadEnabled(this) ||
                         PreferenceManager.instantPictureUploadEnabled(this))) {
 

+ 47 - 6
src/com/owncloud/android/ui/activity/FolderSyncActivity.java

@@ -21,6 +21,7 @@
 
 package com.owncloud.android.ui.activity;
 
+import android.accounts.Account;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.Handler;
@@ -48,6 +49,7 @@ import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
 import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
 import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -130,7 +132,16 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
             public void run() {
                 final List<MediaFolder> mediaFolders = MediaProvider.getMediaFolders(getContentResolver(),
                         perFolderMediaItemLimit);
-                syncFolderItems = sortSyncedFolderItems(mergeFolderData(mSyncedFolderProvider.getSyncedFolders(),
+                List<SyncedFolder> syncedFolderArrayList = mSyncedFolderProvider.getSyncedFolders();
+                List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<SyncedFolder>();
+                Account currentAccount = AccountUtils.getCurrentOwnCloudAccount(FolderSyncActivity.this);
+                for (SyncedFolder syncedFolder : syncedFolderArrayList) {
+                    if (syncedFolder.getAccount().equals(currentAccount.name)) {
+                        currentAccountSyncedFoldersList.add(syncedFolder);
+                    }
+                }
+
+                syncFolderItems = sortSyncedFolderItems(mergeFolderData(currentAccountSyncedFoldersList,
                         mediaFolders));
 
                 mHandler.post(new TimerTask() {
@@ -157,15 +168,22 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
         List<SyncedFolderDisplayItem> result = new ArrayList<>();
 
+
         for (MediaFolder mediaFolder : mediaFolders) {
             if (syncedFoldersMap.containsKey(mediaFolder.absolutePath)) {
                 SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath);
+                syncedFoldersMap.remove(mediaFolder.absolutePath);
                 result.add(createSyncedFolder(syncedFolder, mediaFolder));
             } else {
                 result.add(createSyncedFolderFromMediaFolder(mediaFolder));
             }
         }
 
+        for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
+            SyncedFolderDisplayItem syncedFolderDisplayItem = createSyncedFolderWithoutMediaFolder(syncedFolder);
+            result.add(syncedFolderDisplayItem);
+        }
+
         return result;
     }
 
@@ -210,6 +228,21 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         return syncFolderItemList;
     }
 
+    @NonNull
+    private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
+        return new SyncedFolderDisplayItem(
+                syncedFolder.getId(),
+                syncedFolder.getLocalPath(),
+                syncedFolder.getRemotePath(),
+                syncedFolder.getWifiOnly(),
+                syncedFolder.getChargingOnly(),
+                syncedFolder.getSubfolderByDate(),
+                syncedFolder.getAccount(),
+                syncedFolder.getUploadAction(),
+                syncedFolder.isEnabled(),
+                new File(syncedFolder.getLocalPath()).getName());
+    }
+
     /**
      * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
      *
@@ -273,7 +306,6 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
         }
         return result;
     }
-
     /**
      * show/hide recycler view list or the empty message / progress info.
      *
@@ -289,7 +321,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        boolean result;
+        boolean result = true;
         switch (item.getItemId()) {
             case android.R.id.home: {
                 if (isDrawerOpen()) {
@@ -297,6 +329,7 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
                 } else {
                     openDrawer();
                 }
+                break;
             }
 
             default:
@@ -323,9 +356,13 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
     @Override
     public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
         if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
-            mSyncedFolderProvider.updateFolderSyncEnabled(syncedFolderDisplayItem.getId(), syncedFolderDisplayItem.isEnabled());
+            mSyncedFolderProvider.updateFolderSyncEnabled(syncedFolderDisplayItem.getId(),
+                    syncedFolderDisplayItem.isEnabled());
         } else {
-            mSyncedFolderProvider.storeFolderSync(syncedFolderDisplayItem);
+            long storedId = mSyncedFolderProvider.storeFolderSync(syncedFolderDisplayItem);
+            if (storedId != -1) {
+                syncedFolderDisplayItem.setId(storedId);
+            }
         }
     }
 
@@ -362,7 +399,11 @@ public class FolderSyncActivity extends FileActivity implements FolderSyncAdapte
 
         if (syncedFolder.getId() == UNPERSISTED_ID) {
             // newly set up folder sync config
-            mSyncedFolderProvider.storeFolderSync(item);
+            long storedId = mSyncedFolderProvider.storeFolderSync(item);
+            if (storedId != -1) {
+                item.setId(storedId);
+            }
+
         } else {
             // existing synced folder setup to be updated
             mSyncedFolderProvider.updateSyncFolder(item);

+ 2 - 2
src/com/owncloud/android/ui/activity/Preferences.java

@@ -368,8 +368,8 @@ public class Preferences extends PreferenceActivity
 
         mPrefInstantUploadCategory = (PreferenceCategory) findPreference("instant_uploading_category");
 
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-            // Instant upload via preferences on pre Android Nougat
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            // Instant upload via preferences on pre Android Marshmallow
             mPrefInstantUploadPath = findPreference("instant_upload_path");
             if (mPrefInstantUploadPath != null) {
 

+ 34 - 23
src/com/owncloud/android/ui/adapter/FolderSyncAdapter.java

@@ -77,7 +77,11 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
 
     @Override
     public int getItemCount(int section) {
-        return mSyncFolderItems.get(section).getFilePaths().size();
+        if (mSyncFolderItems.get(section).getFilePaths() != null) {
+            return mSyncFolderItems.get(section).getFilePaths().size();
+        } else {
+            return 1;
+        }
     }
 
     @Override
@@ -108,37 +112,44 @@ public class FolderSyncAdapter extends SectionedRecyclerViewAdapter<FolderSyncAd
     @Override
     public void onBindViewHolder(MainViewHolder holder, int section, int relativePosition, int absolutePosition) {
 
-        File file = new File(mSyncFolderItems.get(section).getFilePaths().get(relativePosition));
+        if (mSyncFolderItems.get(section).getFilePaths() != null) {
+            File file = new File(mSyncFolderItems.get(section).getFilePaths().get(relativePosition));
 
-        ThumbnailsCacheManager.MediaThumbnailGenerationTask task =
-                new ThumbnailsCacheManager.MediaThumbnailGenerationTask(holder.image);
+            ThumbnailsCacheManager.MediaThumbnailGenerationTask task =
+                    new ThumbnailsCacheManager.MediaThumbnailGenerationTask(holder.image);
 
-        ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
-                new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
-                        mContext.getResources(),
-                        ThumbnailsCacheManager.mDefaultImg,
-                        task
-                );
-        holder.image.setImageDrawable(asyncDrawable);
+            ThumbnailsCacheManager.AsyncMediaThumbnailDrawable asyncDrawable =
+                    new ThumbnailsCacheManager.AsyncMediaThumbnailDrawable(
+                            mContext.getResources(),
+                            ThumbnailsCacheManager.mDefaultImg,
+                            task
+                    );
+            holder.image.setImageDrawable(asyncDrawable);
 
-        task.execute(file);
+            task.execute(file);
 
-        // set proper tag
-        holder.image.setTag(file.hashCode());
+            // set proper tag
+            holder.image.setTag(file.hashCode());
 
-        holder.itemView.setTag(relativePosition % mGridWidth);
+            holder.itemView.setTag(relativePosition % mGridWidth);
+
+            if (mSyncFolderItems.get(section).getNumberOfFiles() > mGridTotal && relativePosition >= mGridTotal - 1) {
+                holder.counterValue.setText(Long.toString(mSyncFolderItems.get(section).getNumberOfFiles() - mGridTotal));
+                holder.counterBar.setVisibility(View.VISIBLE);
+                holder.thumbnailDarkener.setVisibility(View.VISIBLE);
+            } else {
+                holder.counterBar.setVisibility(View.GONE);
+                holder.thumbnailDarkener.setVisibility(View.GONE);
+            }
 
-        if (mSyncFolderItems.get(section).getNumberOfFiles() > mGridTotal && relativePosition >= mGridTotal - 1) {
-            holder.counterValue.setText(Long.toString(mSyncFolderItems.get(section).getNumberOfFiles() - mGridTotal));
+            //holder.itemView.setTag(String.format(Locale.getDefault(), "%d:%d:%d", section, relativePos, absolutePos));
+            //holder.itemView.setOnClickListener(this);
+        } else {
+            holder.itemView.setTag(relativePosition % mGridWidth);
+            holder.counterValue.setText(Long.toString(0));
             holder.counterBar.setVisibility(View.VISIBLE);
             holder.thumbnailDarkener.setVisibility(View.VISIBLE);
-        } else {
-            holder.counterBar.setVisibility(View.GONE);
-            holder.thumbnailDarkener.setVisibility(View.GONE);
         }
-
-        //holder.itemView.setTag(String.format(Locale.getDefault(), "%d:%d:%d", section, relativePos, absolutePos));
-        //holder.itemView.setOnClickListener(this);
     }
 
     @Override

+ 72 - 31
src/com/owncloud/android/utils/FileStorageUtils.java

@@ -39,6 +39,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -47,6 +48,7 @@ import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.TimeZone;
 import java.util.Vector;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -62,6 +64,7 @@ public class FileStorageUtils {
     public static final Integer SORT_NAME = 0;
     public static final Integer SORT_DATE = 1;
     public static final Integer SORT_SIZE = 2;
+    public static final String PATTERN_YYYY_MM = "yyyy/MM/";
     public static Integer mSortOrder = SORT_NAME;
     public static Boolean mSortAscending = true;
 
@@ -122,42 +125,70 @@ public class FileStorageUtils {
      * string is returned
      *
      * @param date: date in microseconds since 1st January 1970
-     * @return
+     * @return string: yyyy/mm/
      */
-    private static String getSubpathFromDate(long date) {
+    private static String getSubpathFromDate(long date, Locale currentLocale) {
         if (date == 0) {
             return "";
         }
-        try {
-            SimpleDateFormat formatter = new SimpleDateFormat(
-                    "yyyy" + OCFile.PATH_SEPARATOR + "MM" + OCFile.PATH_SEPARATOR, Locale.ENGLISH);
-            return formatter.format(new Date(date));
-        }
-        catch(RuntimeException ex) {
-            Log_OC.w(TAG, "could not extract date from timestamp");
+
+        Date d = new Date(date);
+
+        DateFormat df = new SimpleDateFormat(PATTERN_YYYY_MM, currentLocale);
+        df.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
+
+        return df.format(d);
+    }
+
+    private static String getSubpathFromDate(long date) {
+        if (date == 0) {
             return "";
         }
+
+        Date d = new Date(date);
+
+        DateFormat df = new SimpleDateFormat(PATTERN_YYYY_MM);
+
+        return df.format(d);
     }
 
     /**
-     * Returns the InstantUploadFilePath on the owncloud instance
+     * Returns the InstantUploadFilePath on the nextcloud instance
      *
-     * @param fileName
+     * @param fileName complete file name
      * @param dateTaken: Time in milliseconds since 1970 when the picture was taken.
-     * @return
+     * @return instantUpload path, eg. /Camera/2017/01/fileName
      */
-    public static String getInstantUploadFilePath(String remotePath, String fileName, long dateTaken,
+    public static String getInstantUploadFilePath(Locale current,
+                                                  String remotePath,
+                                                  String fileName,
+                                                  long dateTaken,
+                                                  Boolean subfolderByDate) {
+        String subPath = "";
+        if (subfolderByDate) {
+           subPath = getSubpathFromDate(dateTaken, current);
+        }
+
+        return remotePath + OCFile.PATH_SEPARATOR  + subPath + (fileName == null ? "" : fileName);
+    }
+
+    public static String getInstantUploadFilePath(String remotePath,
+                                                  String fileName,
+                                                  long dateTaken,
                                                   Boolean subfolderByDate) {
         String subPath = "";
         if (subfolderByDate) {
-           subPath = getSubpathFromDate(dateTaken);
+            subPath = getSubpathFromDate(dateTaken);
         }
+
         return remotePath + OCFile.PATH_SEPARATOR + subPath + (fileName == null ? "" : fileName);
     }
 
+
     /**
-     * Gets the composed path when video is or must be stored
-     * @param context
+     * Gets the composed path when video is or must be stored.
+     *
+     * @param context app context
      * @param fileName: video file name
      * @return String: video file path composed
      */
@@ -169,8 +200,7 @@ public class FileStorageUtils {
         if (com.owncloud.android.db.PreferenceManager.instantVideoUploadPathUseSubfolders(context)) {
             subPath = getSubpathFromDate(dateTaken);
         }
-        return uploadVideoPath + OCFile.PATH_SEPARATOR + subPath
-                + (fileName == null ? "" : fileName);
+        return uploadVideoPath + subPath + (fileName == null ? "" : fileName);
     }
     
     public static String getParentPath(String remotePath) {
@@ -241,7 +271,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts all filenames, regarding last user decision
+     * Sorts all filenames, regarding last user decision.
+     *
+     * @param files of files to sort
      */
     public static File[] sortLocalFolder(File[] files){
         switch (mSortOrder){
@@ -260,8 +292,9 @@ public class FileStorageUtils {
     }
     
     /**
-     * Sorts list by Date
-     * @param files
+     * Sorts list by Date.
+     *
+     * @param files list of files to sort
      */
     public static Vector<OCFile> sortOCFilesByDate(Vector<OCFile> files){
         final int multiplier = mSortAscending ? 1 : -1;
@@ -278,8 +311,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Date
-     * @param filesArray
+     * Sorts list by Date.
+     *
+     * @param filesArray list of files to sort
      */
     public static File[] sortLocalFilesByDate(File[] filesArray){
         final int multiplier = mSortAscending ? 1 : -1;
@@ -299,7 +333,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Size
+     * Sorts list by Size.
+     *
+     * @param files list of files to sort
      */
     public static Vector<OCFile> sortOCFilesBySize(Vector<OCFile> files){
         final int multiplier = mSortAscending ? 1 : -1;
@@ -326,7 +362,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Size
+     * Sorts list by Size.
+     *
+     * @param filesArray list of files to sort
      */
     public static File[] sortLocalFilesBySize(File[] filesArray) {
         final int multiplier = mSortAscending ? 1 : -1;
@@ -356,8 +394,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Name
-     * @param files     files to sort
+     * Sorts list by Name.
+     *
+     * @param files files to sort
      */
     @SuppressFBWarnings(value = "Bx")
     public static Vector<OCFile> sortOCFilesByName(Vector<OCFile> files){
@@ -380,8 +419,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Name
-     * @param filesArray    files to sort
+     * Sorts list by Name.
+     *
+     * @param filesArray files to sort
      */
     public static File[] sortLocalFilesByName(File[] filesArray) {
         final int multiplier = mSortAscending ? 1 : -1;
@@ -407,8 +447,9 @@ public class FileStorageUtils {
     }
 
     /**
-     * Sorts list by Favourites
-     * @param files     files to sort
+     * Sorts list by Favourites.
+     *
+     * @param files files to sort
      */
     public static Vector<OCFile> sortOCFilesByFavourite(Vector<OCFile> files){
         Collections.sort(files, new Comparator<OCFile>() {

+ 0 - 109
src/com/owncloud/android/utils/RecursiveFileObserver.java

@@ -1,109 +0,0 @@
-/**
- *   ownCloud Android client application
- *
- *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2015 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   This program is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-package com.owncloud.android.utils;
-
-import android.os.FileObserver;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Stack;
-
-public class RecursiveFileObserver extends FileObserver {
-
-    private final List<SingleFileObserver> mObservers =  new ArrayList<>();
-    private boolean watching = false;
-    private String mPath;
-    private int mMask;
-    
-    public RecursiveFileObserver(String path) {
-        this(path, ALL_EVENTS);
-    }
-    
-    public RecursiveFileObserver(String path, int mask) {
-        super(path, mask);
-        mPath = path;
-        mMask = mask;
-    }
-
-    @Override
-    public void startWatching() {
-        if (watching) {
-            return;
-        }
-        watching = true;
-        final Stack<String> stack = new Stack<String>();
-        stack.push(mPath);
-        
-        while (!stack.empty()) {
-            String parent = stack.pop();
-            mObservers.add(new SingleFileObserver(parent, mMask));
-            File path = new File(parent);
-            File[] files = path.listFiles();
-            if (files == null) {
-                continue;
-            }
-            for (final File file : files) {
-                if (file.isDirectory() && !file.getName().equals(".")
-                        && !file.getName().equals("..")) {
-                    stack.push(file.getPath());
-                }
-            }
-        }
-        for (int i = 0; i < mObservers.size(); i++) {
-            mObservers.get(i).startWatching();
-        }
-    }
-    
-    @Override
-    public void stopWatching() {
-        if (!watching) {
-            return;
-        }
-
-        for (int i = 0; i < mObservers.size(); ++i) {
-            mObservers.get(i).stopWatching();
-        }
-        mObservers.clear();
-        watching = false;
-    }
-    
-    @Override
-    public void onEvent(int event, String path) {
-        
-    }
-    
-    private class SingleFileObserver extends FileObserver {
-        private String mPath;
-
-        SingleFileObserver(String path, int mask) {
-            super(path, mask);
-            mPath = path;
-        }
-        
-        @Override
-        public void onEvent(int event, String path) {
-            String newPath = mPath + "/" + path;
-            RecursiveFileObserver.this.onEvent(event, newPath);
-        } 
-        
-    }
-}