Bläddra i källkod

Merge pull request #2406 from nextcloud/new-media-detection

New media detection
Tobias Kaminsky 6 år sedan
förälder
incheckning
7e5ee956e9

+ 23 - 2
src/main/java/com/owncloud/android/MainApp.java

@@ -44,6 +44,7 @@ import android.text.TextUtils;
 import android.view.WindowManager;
 
 import com.evernote.android.job.JobManager;
+import com.evernote.android.job.JobRequest;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.authentication.PassCodeManager;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
@@ -56,6 +57,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datastorage.DataStorageProvider;
 import com.owncloud.android.datastorage.StoragePoint;
 import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.jobs.MediaFoldersDetectionJob;
 import com.owncloud.android.jobs.NCJobCreator;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory.Policy;
@@ -77,6 +79,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
@@ -164,6 +167,20 @@ public class MainApp extends MultiDexApplication {
         initContactsBackup();
         notificationChannels();
 
+
+        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
+                .setPeriodic(TimeUnit.MINUTES.toMillis(15), TimeUnit.MINUTES.toMillis(5))
+                .setUpdateCurrent(true)
+                .build()
+                .schedule();
+
+        new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
+                .startNow()
+                .setUpdateCurrent(false)
+                .build()
+                .schedule();
+
+
         // register global protection with pass code
         registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
 
@@ -336,6 +353,10 @@ public class MainApp extends MultiDexApplication {
                 createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_PUSH,
                         R.string.notification_channel_push_name, R.string
                                 .notification_channel_push_description, context, NotificationManager.IMPORTANCE_DEFAULT);
+
+                createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string
+                        .notification_channel_general_name, R.string.notification_channel_general_description,
+                        context, NotificationManager.IMPORTANCE_DEFAULT);
             } else {
                 Log_OC.e(TAG, "Notification manager is null");
             }
@@ -546,8 +567,8 @@ public class MainApp extends MultiDexApplication {
 
             SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
 
-            final List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null);
-            final List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null);
+            final List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1, null, true);
+            final List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null, true);
 
             ArrayList<Long> idsToDelete = new ArrayList<>();
             List<SyncedFolder> syncedFolders = syncedFolderProvider.getSyncedFolders();

+ 56 - 0
src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.java

@@ -0,0 +1,56 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2018 Mario Danic
+ *
+ * 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.datamodel;
+
+import java.util.List;
+
+public class MediaFoldersModel {
+    private List<String> imageMediaFolders;
+    private List<String> videoMediaFolders;
+
+    /**
+     * default constructor.
+     */
+    public MediaFoldersModel() {
+        // keep default constructor for GSON
+    }
+
+    public MediaFoldersModel(List<String> imageMediaFolders, List<String> videoMediaFolders) {
+        this.imageMediaFolders = imageMediaFolders;
+        this.videoMediaFolders = videoMediaFolders;
+    }
+
+    public List<String> getImageMediaFolders() {
+        return imageMediaFolders;
+    }
+
+    public void setImageMediaFolders(List<String> imageMediaFolders) {
+        this.imageMediaFolders = imageMediaFolders;
+    }
+
+    public List<String> getVideoMediaFolders() {
+        return videoMediaFolders;
+    }
+
+    public void setVideoMediaFolders(List<String> videoMediaFolders) {
+        this.videoMediaFolders = videoMediaFolders;
+    }
+}

+ 6 - 6
src/main/java/com/owncloud/android/datamodel/MediaProvider.java

@@ -66,14 +66,14 @@ public class MediaProvider {
      * @return list with media folders
      */
     public static List<MediaFolder> getImageFolders(ContentResolver contentResolver, int itemLimit,
-                                                    @Nullable final Activity activity) {
+                                                    @Nullable final Activity activity, boolean getWithoutActivity) {
         // check permissions
         checkPermissions(activity);
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if (activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
             cursorFolders = contentResolver.query(IMAGES_MEDIA_URI, IMAGES_FOLDER_PROJECTION, null, null,
                     IMAGES_FOLDER_SORT_ORDER);
         }
@@ -171,14 +171,14 @@ public class MediaProvider {
     }
 
     public static List<MediaFolder> getVideoFolders(ContentResolver contentResolver, int itemLimit,
-                                                    @Nullable final Activity activity) {
+                                                    @Nullable final Activity activity, boolean getWithoutActivity) {
         // check permissions
         checkPermissions(activity);
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if (activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
             cursorFolders = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEOS_FOLDER_PROJECTION,
                     null, null, null);
         } 

+ 50 - 1
src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -78,6 +78,24 @@ public class SyncedFolderProvider extends Observable {
         }
     }
 
+    public int countEnabledSyncedFolders() {
+        int count = 0;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED + " = ?",
+                new String[]{"1"},
+                null
+        );
+
+        if (cursor != null) {
+             count = cursor.getCount();
+             cursor.close();
+        }
+
+        return count;
+    }
+
     /**
      * get all synced folder entries.
      *
@@ -160,6 +178,37 @@ public class SyncedFolderProvider extends Observable {
         return result;
     }
 
+    public SyncedFolder findByLocalPathAndAccount(String localPath, Account account) {
+
+        SyncedFolder result = null;
+        Cursor cursor = mContentResolver.query(
+                ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
+                null,
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " == \"" + localPath + "\"" + " AND " +
+                ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " == " + account.name,
+                null,
+                null
+        );
+
+        if (cursor != null && cursor.getCount() == 1) {
+            result = createSyncedFolderFromCursor(cursor);
+        } else {
+            if (cursor == null) {
+                Log_OC.e(TAG, "Sync folder db cursor for local path=" + localPath + " in NULL.");
+            } else {
+                Log_OC.e(TAG, cursor.getCount() + " items for local path=" + localPath
+                        + " available in sync folder db. Expected 1. Failed to update sync folder db.");
+            }
+        }
+
+        if (cursor != null) {
+            cursor.close();
+        }
+
+        return result;
+
+    }
+
     /**
      * find a synced folder by local path.
      *
@@ -171,7 +220,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
         );

+ 173 - 0
src/main/java/com/owncloud/android/jobs/MediaFoldersDetectionJob.java

@@ -0,0 +1,173 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2018 Mario Danic
+ * Copyright (C) 2018 Andy Scherzinger
+ *
+ * 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.jobs;
+
+
+import android.accounts.Account;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+
+import com.evernote.android.job.Job;
+import com.google.gson.Gson;
+import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
+import com.owncloud.android.datamodel.MediaFolder;
+import com.owncloud.android.datamodel.MediaFoldersModel;
+import com.owncloud.android.datamodel.MediaProvider;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
+import com.owncloud.android.ui.activity.ManageAccountsActivity;
+import com.owncloud.android.ui.activity.SyncedFoldersActivity;
+import com.owncloud.android.ui.notifications.NotificationUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaFoldersDetectionJob extends Job {
+    public static final String TAG = "MediaFoldersDetectionJob";
+
+    public static final String KEY_MEDIA_FOLDER_PATH = "KEY_MEDIA_FOLDER_PATH";
+    public static final String KEY_MEDIA_FOLDER_TYPE = "KEY_MEDIA_FOLDER_TYPE";
+
+    private static final String ACCOUNT_NAME_GLOBAL = "global";
+    private static final String KEY_MEDIA_FOLDERS = "media_folders";
+
+    @NonNull
+    @Override
+    protected Result onRunJob(@NonNull Params params) {
+        Context context = getContext();
+        ContentResolver contentResolver = context.getContentResolver();
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(contentResolver);
+        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver);
+        Gson gson = new Gson();
+        String arbitraryDataString;
+        MediaFoldersModel mediaFoldersModel;
+
+        List<MediaFolder> imageMediaFolders = MediaProvider.getImageFolders(contentResolver, 1,
+                null, true);
+        List<MediaFolder> videoMediaFolders = MediaProvider.getVideoFolders(contentResolver, 1, null,
+                true);
+
+        List<String> imageMediaFolderPaths = new ArrayList<>();
+        List<String> videoMediaFolderPaths = new ArrayList<>();
+
+        for (MediaFolder imageMediaFolder: imageMediaFolders) {
+            imageMediaFolderPaths.add(imageMediaFolder.absolutePath);
+        }
+
+        for (MediaFolder videoMediaFolder: videoMediaFolders) {
+            imageMediaFolderPaths.add(videoMediaFolder.absolutePath);
+        }
+
+        arbitraryDataString = arbitraryDataProvider.getValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS);
+        if (!TextUtils.isEmpty(arbitraryDataString)) {
+            mediaFoldersModel = gson.fromJson(arbitraryDataString, MediaFoldersModel.class);
+
+            // Store updated values
+            arbitraryDataProvider.storeOrUpdateKeyValue(ACCOUNT_NAME_GLOBAL, KEY_MEDIA_FOLDERS, gson.toJson(new
+                    MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths)));
+
+            imageMediaFolderPaths.removeAll(mediaFoldersModel.getImageMediaFolders());
+            videoMediaFolderPaths.removeAll(mediaFoldersModel.getVideoMediaFolders());
+
+            if (imageMediaFolderPaths.size() > 0 || videoMediaFolderPaths.size() > 0) {
+                Account[] accounts = AccountUtils.getAccounts(getContext());
+                List<Account> accountList = new ArrayList<>();
+                for (Account account : accounts) {
+                    if (!arbitraryDataProvider.getBooleanValue(account, ManageAccountsActivity.PENDING_FOR_REMOVAL)) {
+                        accountList.add(account);
+                    }
+                }
+
+                for (Account account : accountList) {
+                    for (String imageMediaFolder : imageMediaFolderPaths) {
+                        if (syncedFolderProvider.findByLocalPathAndAccount(imageMediaFolder,
+                                account) == null) {
+                            sendNotification(String.format(context.getString(R.string.new_media_folder_detected),
+                                    context.getString(R.string.new_media_folder_photos)),
+                                    imageMediaFolder.substring(imageMediaFolder.lastIndexOf('/') + 1,
+                                            imageMediaFolder.length()),
+                                    account, imageMediaFolder, 1);
+                        }
+                    }
+
+                    for (String videoMediaFolder : videoMediaFolderPaths) {
+                        if (syncedFolderProvider.findByLocalPathAndAccount(videoMediaFolder,
+                                account) == null) {
+                            sendNotification(String.format(context.getString(R.string.new_media_folder_detected),
+                                    context.getString(R.string.new_media_folder_videos)),
+                                    videoMediaFolder.substring(videoMediaFolder.lastIndexOf('/') + 1,
+                                            videoMediaFolder.length()),
+                                    account, videoMediaFolder, 2);
+                        }
+                    }
+                }
+            }
+
+        } else {
+            mediaFoldersModel = new MediaFoldersModel(imageMediaFolderPaths, videoMediaFolderPaths);
+            arbitraryDataProvider.storeOrUpdateKeyValue("global", "media_folders", gson.toJson(mediaFoldersModel));
+        }
+
+        return Result.SUCCESS;
+    }
+
+    private void sendNotification(String contentTitle, String subtitle,  Account account,
+                                  String path, int type) {
+        Context context = getContext();
+        Intent intent = new Intent(getContext(), SyncedFoldersActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.putExtra(NotificationJob.KEY_NOTIFICATION_ACCOUNT, account.name);
+        intent.putExtra(KEY_MEDIA_FOLDER_PATH, path);
+        intent.putExtra(KEY_MEDIA_FOLDER_TYPE, type);
+        intent.putExtra(SyncedFoldersActivity.EXTRA_SHOW_SIDEBAR, true);
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(
+                context, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL)
+                .setSmallIcon(R.drawable.notification_icon)
+                .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.notification_icon))
+                .setColor(ThemeUtils.primaryColor(getContext()))
+                .setSubText(account.name)
+                .setContentTitle(contentTitle)
+                .setContentText(subtitle)
+                .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
+                .setAutoCancel(true)
+                .setContentIntent(pendingIntent);
+
+        NotificationManager notificationManager = (NotificationManager)
+                context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        if (notificationManager != null) {
+            notificationManager.notify(0, notificationBuilder.build());
+        }
+    }
+}

+ 2 - 0
src/main/java/com/owncloud/android/jobs/NCJobCreator.java

@@ -43,6 +43,8 @@ public class NCJobCreator implements JobCreator {
                 return new OfflineSyncJob();
             case NotificationJob.TAG:
                 return new NotificationJob();
+            case MediaFoldersDetectionJob.TAG:
+                return new MediaFoldersDetectionJob();
             default:
                 return null;
         }

+ 23 - 8
src/main/java/com/owncloud/android/jobs/NContentObserverJob.java

@@ -1,4 +1,4 @@
-/**
+/*
  * Nextcloud Android client application
  *
  * @author Mario Danic
@@ -28,11 +28,13 @@ import android.support.annotation.RequiresApi;
 
 import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.support.PersistableBundleCompat;
+import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.utils.FilesSyncHelper;
 import com.owncloud.android.utils.PowerUtils;
 
 /*
     Job that triggers new FilesSyncJob in case new photo or video were detected
+    and starts a job to find new media folders
  */
 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
 public class NContentObserverJob extends JobService {
@@ -42,26 +44,39 @@ public class NContentObserverJob extends JobService {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             if (params.getJobId() == FilesSyncHelper.ContentSyncJobId && params.getTriggeredContentAuthorities()
                     != null && params.getTriggeredContentUris() != null
-                    && params.getTriggeredContentUris().length > 0
-                    && !PowerUtils.isPowerSaveMode(getApplicationContext())) {
+                    && params.getTriggeredContentUris().length > 0) {
 
-                PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
-                persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true);
+                checkAndStartFileSyncJob();
 
-                new JobRequest.Builder(FilesSyncJob.TAG)
+                new JobRequest.Builder(MediaFoldersDetectionJob.TAG)
                         .startNow()
-                        .setExtras(persistableBundleCompat)
                         .setUpdateCurrent(false)
                         .build()
                         .schedule();
+
             }
 
-            FilesSyncHelper.scheduleNJobs(true, getApplicationContext());
+            FilesSyncHelper.scheduleJobOnN();
         }
 
         return true;
     }
 
+    private void checkAndStartFileSyncJob() {
+        if (!PowerUtils.isPowerSaveMode(getApplicationContext()) &&
+                new SyncedFolderProvider(getContentResolver()).countEnabledSyncedFolders() > 0) {
+            PersistableBundleCompat persistableBundleCompat = new PersistableBundleCompat();
+            persistableBundleCompat.putBoolean(FilesSyncJob.SKIP_CUSTOM, true);
+
+            new JobRequest.Builder(FilesSyncJob.TAG)
+                    .startNow()
+                    .setExtras(persistableBundleCompat)
+                    .setUpdateCurrent(false)
+                    .build()
+                    .schedule();
+        }
+    }
+
     @Override
     public boolean onStopJob(JobParameters params) {
         return false;

+ 69 - 47
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -37,6 +37,7 @@ import android.support.v4.widget.DrawerLayout;
 import android.support.v7.app.ActionBar;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.MenuItem;
 import android.view.View;
@@ -56,6 +57,8 @@ import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.jobs.MediaFoldersDetectionJob;
+import com.owncloud.android.jobs.NotificationJob;
 import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
 import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
 import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
@@ -85,10 +88,9 @@ import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED
 public class SyncedFoldersActivity extends FileActivity implements SyncedFolderAdapter.ClickListener,
         SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener {
 
-    private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
-    public static final String[] PRIORITIZED_FOLDERS = new String[] { "Camera", "Screenshots" };
+    public static final String[] PRIORITIZED_FOLDERS = new String[]{"Camera", "Screenshots"};
     public static final String EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR";
-
+    private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
     private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
 
     private RecyclerView mRecyclerView;
@@ -100,6 +102,9 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
     private boolean showSidebar = true;
     private RelativeLayout mCustomFolderRelativeLayout;
 
+    private String path;
+    private int type;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -110,6 +115,21 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
 
         setContentView(R.layout.synced_folders_layout);
 
+        String account;
+        Account currentAccount;
+        if (getIntent() != null && getIntent().getExtras() != null) {
+            account = getIntent().getExtras().getString(NotificationJob.KEY_NOTIFICATION_ACCOUNT);
+            currentAccount = AccountUtils.getCurrentOwnCloudAccount(getApplicationContext());
+
+            if (account != null && currentAccount != null && !account.equalsIgnoreCase(currentAccount.name)) {
+                AccountUtils.setCurrentOwnCloudAccount(getApplicationContext(), account);
+                setAccount(AccountUtils.getCurrentOwnCloudAccount(this));
+            }
+
+            path = getIntent().getStringExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_PATH);
+            type = getIntent().getIntExtra(MediaFoldersDetectionJob.KEY_MEDIA_FOLDER_TYPE, -1);
+        }
+
         // setup toolbar
         setupToolbar();
         CollapsingToolbarLayout mCollapsingToolbarLayout = findViewById(R.id.collapsing_toolbar);
@@ -199,9 +219,9 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         }
         setListShown(false);
         final List<MediaFolder> mediaFolders = MediaProvider.getImageFolders(getContentResolver(),
-                perFolderMediaItemLimit, SyncedFoldersActivity.this);
+                perFolderMediaItemLimit, this, false);
         mediaFolders.addAll(MediaProvider.getVideoFolders(getContentResolver(), perFolderMediaItemLimit,
-                SyncedFoldersActivity.this));
+                this, false));
 
         List<SyncedFolder> syncedFolderArrayList = mSyncedFolderProvider.getSyncedFolders();
         List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
@@ -218,41 +238,13 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         mAdapter.setSyncFolderItems(syncFolderItems);
         mAdapter.notifyDataSetChanged();
         setListShown(true);
-    }
-
-    /**
-     * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
-     *
-     * @param syncedFolders the synced folders
-     * @param mediaFolders  the media folders
-     * @return the merged list of SyncedFolderItems
-     */
-    @NonNull
-    private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
-                                                          @NonNull List<MediaFolder> mediaFolders) {
-        Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
-        List<SyncedFolderDisplayItem> result = new ArrayList<>();
-
-        for (MediaFolder mediaFolder : mediaFolders) {
-            if (syncedFoldersMap.containsKey(mediaFolder.absolutePath+"-"+mediaFolder.type)) {
-                SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath+"-"+mediaFolder.type);
-                syncedFoldersMap.remove(mediaFolder.absolutePath+"-"+mediaFolder.type);
 
-                if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
-                    result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
-                } else {
-                    result.add(createSyncedFolder(syncedFolder, mediaFolder));
-                }
-            } else {
-                result.add(createSyncedFolderFromMediaFolder(mediaFolder));
+        if (!TextUtils.isEmpty(path)) {
+            int section = mAdapter.getSectionByLocalPathAndType(path, type);
+            if (section >= 0) {
+                onSyncFolderSettingsClick(section, mAdapter.get(section));
             }
         }
-
-        for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
-            result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
-        }
-
-        return result;
     }
 
     /**
@@ -296,13 +288,48 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                     }
                 }
                 return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
-                            f2.getFolderName().toLowerCase(Locale.getDefault()));
+                        f2.getFolderName().toLowerCase(Locale.getDefault()));
             }
         });
 
         return syncFolderItemList;
     }
 
+    /**
+     * merges two lists of {@link SyncedFolder} and {@link MediaFolder} items into one of SyncedFolderItems.
+     *
+     * @param syncedFolders the synced folders
+     * @param mediaFolders  the media folders
+     * @return the merged list of SyncedFolderItems
+     */
+    @NonNull
+    private List<SyncedFolderDisplayItem> mergeFolderData(List<SyncedFolder> syncedFolders,
+                                                          @NonNull List<MediaFolder> mediaFolders) {
+        Map<String, SyncedFolder> syncedFoldersMap = createSyncedFoldersMap(syncedFolders);
+        List<SyncedFolderDisplayItem> result = new ArrayList<>();
+
+        for (MediaFolder mediaFolder : mediaFolders) {
+            if (syncedFoldersMap.containsKey(mediaFolder.absolutePath + "-" + mediaFolder.type)) {
+                SyncedFolder syncedFolder = syncedFoldersMap.get(mediaFolder.absolutePath + "-" + mediaFolder.type);
+                syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type);
+
+                if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
+                    result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
+                } else {
+                    result.add(createSyncedFolder(syncedFolder, mediaFolder));
+                }
+            } else {
+                result.add(createSyncedFolderFromMediaFolder(mediaFolder));
+            }
+        }
+
+        for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
+            result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
+        }
+
+        return result;
+    }
+
     @NonNull
     private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
 
@@ -420,7 +447,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         Map<String, SyncedFolder> result = new HashMap<>();
         if (syncFolders != null) {
             for (SyncedFolder syncFolder : syncFolders) {
-                result.put(syncFolder.getLocalPath()+"-"+syncFolder.getType(), syncFolder);
+                result.put(syncFolder.getLocalPath() + "-" + syncFolder.getType(), syncFolder);
             }
         }
         return result;
@@ -501,8 +528,6 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + syncedFolderDisplayItem.getId();
             arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
         }
-        FilesSyncHelper.scheduleNJobs(false, getApplicationContext());
-
     }
 
     @Override
@@ -522,12 +547,12 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
             OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
             mSyncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
-        } if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
+        }
+        if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
                 && resultCode == RESULT_OK && mSyncedFolderPreferencesDialogFragment != null) {
             String localPath = data.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
             mSyncedFolderPreferencesDialogFragment.setLocalFolderSummary(localPath);
-        }
-        else {
+        } else {
             super.onActivityResult(requestCode, resultCode, data);
         }
     }
@@ -554,7 +579,6 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                     String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + newCustomFolder.getId();
                     arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
                 }
-                FilesSyncHelper.scheduleNJobs(false, getApplicationContext());
             }
             mAdapter.addSyncFolderItem(newCustomFolder);
         } else {
@@ -574,7 +598,6 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                         String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
                         arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
                     }
-                    FilesSyncHelper.scheduleNJobs(false, getApplicationContext());
                 }
             } else {
                 // existing synced folder setup to be updated
@@ -585,7 +608,6 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                     String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
                     arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
                 }
-                FilesSyncHelper.scheduleNJobs(false, getApplicationContext());
             }
 
             mAdapter.setSyncFolderItem(syncedFolder.getSection(), item);

+ 18 - 0
src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -108,6 +108,24 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SyncedFold
         return mSyncFolderItems.get(section);
     }
 
+    /**
+     * returns the section of a synced folder for the given local path and type.
+     *
+     * @param localPath the local path of the synced folder
+     * @param type      the of the synced folder
+     * @return the section index of the looked up synced folder, <code>-1</code> if not present
+     */
+    public int getSectionByLocalPathAndType(String localPath, int type) {
+        for (int i = 0; i < mSyncFolderItems.size(); i++) {
+            if (mSyncFolderItems.get(i).getLocalPath().equalsIgnoreCase(localPath) &&
+                    mSyncFolderItems.get(i).getType().getId().equals(type)) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
     @Override
     public void onBindHeaderViewHolder(final MainViewHolder holder, final int section, boolean expanded) {
         holder.mainHeaderContainer.setVisibility(View.VISIBLE);

+ 1 - 0
src/main/java/com/owncloud/android/ui/notifications/NotificationUtils.java

@@ -34,6 +34,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 public class NotificationUtils {
 
+    public static final String NOTIFICATION_CHANNEL_GENERAL = "NOTIFICATION_CHANNEL_GENERAL";
     public static final String NOTIFICATION_CHANNEL_DOWNLOAD = "NOTIFICATION_CHANNEL_DOWNLOAD";
     public static final String NOTIFICATION_CHANNEL_UPLOAD = "NOTIFICATION_CHANNEL_UPLOAD";
     public static final String NOTIFICATION_CHANNEL_MEDIA = "NOTIFICATION_CHANNEL_MEDIA";

+ 5 - 62
src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -1,4 +1,4 @@
-/**
+/*
  * Nextcloud Android client application
  *
  * @author Mario Danic
@@ -60,7 +60,6 @@ import org.lukhnos.nnio.file.attribute.BasicFileAttributes;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -240,52 +239,6 @@ public class FilesSyncHelper {
         }).start();
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.N)
-    public static boolean isContentObserverJobScheduled() {
-        JobScheduler js = MainApp.getAppContext().getSystemService(JobScheduler.class);
-        List<JobInfo> jobs = js.getAllPendingJobs();
-
-        if (jobs == null || jobs.size() == 0) {
-            return false;
-        }
-
-        for (int i = 0; i < jobs.size(); i++) {
-            if (jobs.get(i).getId() == ContentSyncJobId) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    public static void scheduleNJobs(boolean force, Context context) {
-        SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(context.getContentResolver());
-
-
-        boolean hasVideoFolders = false;
-        boolean hasImageFolders = false;
-
-        if (syncedFolderProvider.getSyncedFolders() != null) {
-            for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
-                if (MediaFolderType.VIDEO == syncedFolder.getType()) {
-                    hasVideoFolders = true;
-                } else if (MediaFolderType.IMAGE == syncedFolder.getType()) {
-                    hasImageFolders = true;
-                }
-            }
-        }
-
-        if (hasImageFolders || hasVideoFolders) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                scheduleJobOnN(hasImageFolders, hasVideoFolders, force);
-            }
-        } else {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                cancelJobOnN();
-            }
-        }
-    }
-
     public static void scheduleFilesSyncIfNeeded(Context context) {
         // always run this because it also allows us to perform retries of manual uploads
         new JobRequest.Builder(FilesSyncJob.TAG)
@@ -294,8 +247,8 @@ public class FilesSyncHelper {
                 .build()
                 .schedule();
 
-        if (context != null) {
-            scheduleNJobs(false, context);
+        if (context != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            scheduleJobOnN();
         }
     }
 
@@ -310,21 +263,11 @@ public class FilesSyncHelper {
         }
     }
 
-
-    @RequiresApi(api = Build.VERSION_CODES.N)
-    private static void cancelJobOnN() {
-        JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
-        if (isContentObserverJobScheduled()) {
-            jobScheduler.cancel(ContentSyncJobId);
-        }
-    }
-
     @RequiresApi(api = Build.VERSION_CODES.N)
-    private static void scheduleJobOnN(boolean hasImageFolders, boolean hasVideoFolders,
-                                       boolean force) {
+    public static void scheduleJobOnN() {
         JobScheduler jobScheduler = MainApp.getAppContext().getSystemService(JobScheduler.class);
 
-        if ((hasImageFolders || hasVideoFolders) && (!isContentObserverJobScheduled() || force)) {
+        if (jobScheduler != null) {
             JobInfo.Builder builder = new JobInfo.Builder(ContentSyncJobId, new ComponentName(MainApp.getAppContext(),
                     NContentObserverJob.class.getName()));
             builder.addTriggerContentUri(new JobInfo.TriggerContentUri(android.provider.MediaStore.

+ 7 - 1
src/main/res/values/strings.xml

@@ -652,7 +652,6 @@
     <string name="contacts_preferences_backup_scheduled">Backup scheduled and will start shortly</string>
     <string name="contacts_preferences_import_scheduled">Import scheduled and will start shortly</string>
 
-    <!-- Notifications -->
     <string name="drawer_logout">Log out</string>
     <string name="picture_set_as_no_app">No app found to set a picture with</string>
     <string name="privacy">Privacy</string>
@@ -781,6 +780,7 @@
     <string name="notification_channel_push_name">Push notifications</string>
     <string name="notification_channel_push_description">Show push notifications sent by the server: Mentions in comments, reception of new remote shares, announcements posted by an admin etc.</string>
     <string name="sendbutton_description">Send button icon</string>
+
     <string name="oauth_2_0_auth_end_point_address_hint">Auth end point address</string>
     <string name="oauth_2_0_access_end_point_address_hint">Access end point address</string>
     <string name="hidden_character" translatable="false">*</string>
@@ -804,6 +804,12 @@
     <string name="error_comment_file">Error commenting file</string>
     <string name="file_version_restored_successfully">Successfully restored file version.</string>
     <string name="file_version_restored_error">Error restoring file version!</string>
+    <string name="notification_channel_general_name">General notifications</string>
+    <string name="notification_channel_general_description">Show notifications for new media folders and similar</string>
+    <string name="new_media_folder_detected">New %1$s media folder detected.</string>
+    <string name="new_media_folder_photos">photo</string>
+    <string name="new_media_folder_videos">video</string>
+
     <string name="outdated_server">The server has reached end of life, please upgrade!</string>
     <string name="dismiss">Dismiss</string>
     <string name="feedback_no_mail_app">No app available to send mails!</string>