浏览代码

Merge pull request #9922 from nextcloud/syncedfolders-async

SyncedFoldersActivity: asynchronous loading
Álvaro Brey 3 年之前
父节点
当前提交
4a621176a4

+ 1 - 1
detekt.yml

@@ -1,5 +1,5 @@
 build:
 build:
-  maxIssues: 10
+  maxIssues: 11
   weights:
   weights:
     # complexity: 2
     # complexity: 2
     # LongParameterList: 1
     # LongParameterList: 1

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-633
+627

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 93 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 92 warnings</span>

+ 0 - 829
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -1,829 +0,0 @@
-/*
- * Nextcloud Android client application
- *
- * @author Andy Scherzinger
- * Copyright (C) 2016 Andy Scherzinger
- * Copyright (C) 2016 Nextcloud
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
- *
- * 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.ui.activity;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.PowerManager;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.core.Clock;
-import com.nextcloud.client.device.PowerManagementService;
-import com.nextcloud.client.di.Injectable;
-import com.nextcloud.client.jobs.BackgroundJobManager;
-import com.nextcloud.client.jobs.MediaFoldersDetectionWork;
-import com.nextcloud.client.jobs.NotificationWork;
-import com.nextcloud.client.preferences.AppPreferences;
-import com.nextcloud.java.util.Optional;
-import com.owncloud.android.BuildConfig;
-import com.owncloud.android.MainApp;
-import com.owncloud.android.R;
-import com.owncloud.android.databinding.SyncedFoldersLayoutBinding;
-import com.owncloud.android.datamodel.ArbitraryDataProvider;
-import com.owncloud.android.datamodel.MediaFolder;
-import com.owncloud.android.datamodel.MediaFolderType;
-import com.owncloud.android.datamodel.MediaProvider;
-import com.owncloud.android.datamodel.OCFile;
-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.files.services.NameCollisionPolicy;
-import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
-import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
-import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
-import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable;
-import com.owncloud.android.utils.PermissionUtil;
-import com.owncloud.android.utils.SyncedFolderUtils;
-import com.owncloud.android.utils.theme.ThemeButtonUtils;
-import com.owncloud.android.utils.theme.ThemeUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.drawerlayout.widget.DrawerLayout;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentTransaction;
-import androidx.lifecycle.Lifecycle;
-import androidx.recyclerview.widget.GridLayoutManager;
-
-import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
-import static com.owncloud.android.datamodel.SyncedFolderDisplayItem.UNPERSISTED_ID;
-
-/**
- * Activity displaying all auto-synced folders and/or instant upload media folders.
- */
-public class SyncedFoldersActivity extends FileActivity implements SyncedFolderAdapter.ClickListener,
-        SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener, Injectable {
-
-    private static final String[] PRIORITIZED_FOLDERS = new String[]{"Camera", "Screenshots"};
-    private static final String SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG";
-    private static final String TAG = SyncedFoldersActivity.class.getSimpleName();
-
-    private SyncedFoldersLayoutBinding binding;
-    private SyncedFolderAdapter adapter;
-    private SyncedFolderProvider syncedFolderProvider;
-    private SyncedFolderPreferencesDialogFragment syncedFolderPreferencesDialogFragment;
-
-    private String path;
-    private int type;
-    @Inject AppPreferences preferences;
-    @Inject PowerManagementService powerManagementService;
-    @Inject Clock clock;
-    @Inject BackgroundJobManager backgroundJobManager;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        binding = SyncedFoldersLayoutBinding.inflate(getLayoutInflater());
-        setContentView(binding.getRoot());
-
-        if (getIntent() != null && getIntent().getExtras() != null) {
-            final String accountName = getIntent().getExtras().getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT);
-            Optional<User> optionalUser = getUser();
-            if (optionalUser.isPresent() && accountName != null) {
-                User user = optionalUser.get();
-                if (!accountName.equalsIgnoreCase(user.getAccountName())) {
-                    accountManager.setCurrentOwnCloudAccount(accountName);
-                    setUser(getUserAccountManager().getUser());
-                }
-            }
-
-            path = getIntent().getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH);
-            type = getIntent().getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1);
-
-            // Cancel notification
-            int notificationId = getIntent().getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0);
-            NotificationManager notificationManager =
-                (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE);
-            notificationManager.cancel(notificationId);
-        }
-
-        // setup toolbar
-        setupToolbar();
-        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_synced_folders));
-
-        setupDrawer();
-        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
-
-        if (getSupportActionBar() != null) {
-            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-        }
-
-        if (mDrawerToggle != null) {
-            mDrawerToggle.setDrawerIndicatorEnabled(false);
-        }
-
-        // TODO: The content loading should be done asynchronously
-        setupContent();
-
-        if (ThemeUtils.themingEnabled(this)) {
-            setTheme(R.style.FallbackThemingTheme);
-        }
-
-        binding.emptyList.emptyListViewAction.setOnClickListener(v -> showHiddenItems());
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.activity_synced_folders, menu);
-
-        if (powerManagementService.isPowerSavingExclusionAvailable()) {
-            MenuItem item = menu.findItem(R.id.action_disable_power_save_check);
-            item.setVisible(true);
-
-            item.setChecked(preferences.isPowerCheckDisabled());
-
-            item.setOnMenuItemClickListener(this::onDisablePowerSaveCheckClicked);
-        }
-
-        return true;
-    }
-
-    private boolean onDisablePowerSaveCheckClicked(MenuItem powerCheck) {
-        if (!powerCheck.isChecked()) {
-            showPowerCheckDialog();
-        }
-
-        preferences.setPowerCheckDisabled(!powerCheck.isChecked());
-        powerCheck.setChecked(!powerCheck.isChecked());
-
-        return true;
-    }
-
-    private void showPowerCheckDialog() {
-        AlertDialog alertDialog = new AlertDialog.Builder(this)
-            .setView(findViewById(R.id.root_layout))
-            .setPositiveButton(R.string.common_ok, (dialog, which) -> dialog.dismiss())
-            .setTitle(R.string.autoupload_disable_power_save_check)
-            .setMessage(getString(R.string.power_save_check_dialog_message))
-            .show();
-
-        ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE));
-    }
-
-    /**
-     * sets up the UI elements and loads all media/synced folders.
-     */
-    private void setupContent() {
-        final int gridWidth = getResources().getInteger(R.integer.media_grid_width);
-        boolean lightVersion = getResources().getBoolean(R.bool.syncedFolder_light);
-        adapter = new SyncedFolderAdapter(this, clock, gridWidth, this, lightVersion);
-        syncedFolderProvider = new SyncedFolderProvider(getContentResolver(), preferences, clock);
-        binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders);
-        ThemeButtonUtils.colorPrimaryButton(binding.emptyList.emptyListViewAction, this);
-
-        final GridLayoutManager lm = new GridLayoutManager(this, gridWidth);
-        adapter.setLayoutManager(lm);
-        int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
-        binding.list.addItemDecoration(new MediaGridItemDecoration(spacing));
-        binding.list.setLayoutManager(lm);
-        binding.list.setAdapter(adapter);
-
-        load(gridWidth * 2, false);
-    }
-
-    public void showHiddenItems() {
-        if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
-            adapter.toggleHiddenItemsVisibility();
-            binding.emptyList.emptyListView.setVisibility(View.GONE);
-            binding.list.setVisibility(View.VISIBLE);
-        }
-    }
-
-    /**
-     * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
-     *
-     * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
-     */
-    private void load(final int perFolderMediaItemLimit, boolean force) {
-        if (adapter.getItemCount() > 0 && !force) {
-            return;
-        }
-        showLoadingContent();
-        final List<MediaFolder> mediaFolders = MediaProvider.getImageFolders(getContentResolver(),
-                perFolderMediaItemLimit, this, false);
-        mediaFolders.addAll(MediaProvider.getVideoFolders(getContentResolver(), perFolderMediaItemLimit,
-                this, false));
-
-        List<SyncedFolder> syncedFolderArrayList = syncedFolderProvider.getSyncedFolders();
-        List<SyncedFolder> currentAccountSyncedFoldersList = new ArrayList<>();
-        User user = getUserAccountManager().getUser();
-        for (SyncedFolder syncedFolder : syncedFolderArrayList) {
-            if (syncedFolder.getAccount().equals(user.getAccountName())) {
-                // delete non-existing & disabled synced folders
-                if (!new File(syncedFolder.getLocalPath()).exists() && !syncedFolder.isEnabled()) {
-                    syncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
-                } else {
-                    currentAccountSyncedFoldersList.add(syncedFolder);
-                }
-            }
-        }
-
-        List<SyncedFolderDisplayItem> syncFolderItems = sortSyncedFolderItems(
-                mergeFolderData(currentAccountSyncedFoldersList, mediaFolders));
-
-        adapter.setSyncFolderItems(syncFolderItems);
-        adapter.notifyDataSetChanged();
-        showList();
-
-        if (!TextUtils.isEmpty(path)) {
-            int section = adapter.getSectionByLocalPathAndType(path, type);
-            if (section >= 0) {
-                onSyncFolderSettingsClick(section, adapter.get(section));
-            }
-        }
-    }
-
-    /**
-     * Sorts list of {@link SyncedFolderDisplayItem}s.
-     *
-     * @param syncFolderItemList list of items to be sorted
-     * @return sorted list of items
-     */
-    public static List<SyncedFolderDisplayItem> sortSyncedFolderItems(List<SyncedFolderDisplayItem>
-                                                                              syncFolderItemList) {
-        Collections.sort(syncFolderItemList, (f1, f2) -> {
-            if (f1 == null && f2 == null) {
-                return 0;
-            } else if (f1 == null) {
-                return -1;
-            } else if (f2 == null) {
-                return 1;
-            } else if (f1.isEnabled() && f2.isEnabled()) {
-                if (f1.getFolderName() == null) {
-                    return -1;
-                }
-                if (f2.getFolderName() == null) {
-                    return 1;
-                }
-
-                return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
-                    f2.getFolderName().toLowerCase(Locale.getDefault()));
-            } else if (f1.getFolderName() == null && f2.getFolderName() == null) {
-                return 0;
-            } else if (f1.isEnabled()) {
-                return -1;
-            } else if (f2.isEnabled()) {
-                return 1;
-            } else if (f1.getFolderName() == null) {
-                return -1;
-            } else if (f2.getFolderName() == null) {
-                return 1;
-            }
-
-            for (String folder : PRIORITIZED_FOLDERS) {
-                if (folder.equals(f1.getFolderName()) && folder.equals(f2.getFolderName())) {
-                    return 0;
-                } else if (folder.equals(f1.getFolderName())) {
-                    return -1;
-                } else if (folder.equals(f2.getFolderName())) {
-                    return 1;
-                }
-            }
-            return f1.getFolderName().toLowerCase(Locale.getDefault()).compareTo(
-                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 (syncedFolder != null && SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder)) {
-                    if (MediaFolderType.CUSTOM == syncedFolder.getType()) {
-                        result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
-                    } else {
-                        result.add(createSyncedFolder(syncedFolder, mediaFolder));
-                    }
-                }
-            } else {
-                if (SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) {
-                    result.add(createSyncedFolderFromMediaFolder(mediaFolder));
-                }
-            }
-        }
-
-        for (SyncedFolder syncedFolder : syncedFoldersMap.values()) {
-            result.add(createSyncedFolderWithoutMediaFolder(syncedFolder));
-        }
-
-        return result;
-    }
-
-    @NonNull
-    private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull SyncedFolder syncedFolder) {
-
-        File localFolder = new File(syncedFolder.getLocalPath());
-        File[] files = SyncedFolderUtils.getFileList(localFolder);
-        List<String> filePaths = getDisplayFilePathList(files);
-
-        return new SyncedFolderDisplayItem(
-            syncedFolder.getId(),
-            syncedFolder.getLocalPath(),
-            syncedFolder.getRemotePath(),
-            syncedFolder.isWifiOnly(),
-            syncedFolder.isChargingOnly(),
-            syncedFolder.isExisting(),
-            syncedFolder.isSubfolderByDate(),
-            syncedFolder.getAccount(),
-            syncedFolder.getUploadAction(),
-            syncedFolder.getNameCollisionPolicyInt(),
-            syncedFolder.isEnabled(),
-            clock.getCurrentTime(),
-            filePaths,
-            localFolder.getName(),
-            files.length,
-            syncedFolder.getType(),
-            syncedFolder.isHidden());
-    }
-
-    /**
-     * creates a SyncedFolderDisplayItem merging a {@link SyncedFolder} and a {@link MediaFolder} object instance.
-     *
-     * @param syncedFolder the synced folder object
-     * @param mediaFolder  the media folder object
-     * @return the created SyncedFolderDisplayItem
-     */
-    @NonNull
-    private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull MediaFolder mediaFolder) {
-        return new SyncedFolderDisplayItem(
-            syncedFolder.getId(),
-            syncedFolder.getLocalPath(),
-            syncedFolder.getRemotePath(),
-            syncedFolder.isWifiOnly(),
-            syncedFolder.isChargingOnly(),
-            syncedFolder.isExisting(),
-            syncedFolder.isSubfolderByDate(),
-            syncedFolder.getAccount(),
-            syncedFolder.getUploadAction(),
-            syncedFolder.getNameCollisionPolicyInt(),
-            syncedFolder.isEnabled(),
-            clock.getCurrentTime(),
-            mediaFolder.filePaths,
-            mediaFolder.folderName,
-            mediaFolder.numberOfFiles,
-            mediaFolder.type,
-            syncedFolder.isHidden());
-    }
-
-    /**
-     * creates a {@link SyncedFolderDisplayItem} based on a {@link MediaFolder} object instance.
-     *
-     * @param mediaFolder the media folder object
-     * @return the created SyncedFolderDisplayItem
-     */
-    @NonNull
-    private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull MediaFolder mediaFolder) {
-        return new SyncedFolderDisplayItem(
-                UNPERSISTED_ID,
-                mediaFolder.absolutePath,
-                getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
-                true,
-                false,
-                true,
-                false,
-                getAccount().name,
-                FileUploader.LOCAL_BEHAVIOUR_FORGET,
-                NameCollisionPolicy.ASK_USER.serialize(),
-                false,
-                clock.getCurrentTime(),
-                mediaFolder.filePaths,
-                mediaFolder.folderName,
-                mediaFolder.numberOfFiles,
-                mediaFolder.type,
-                false);
-    }
-
-    private List<String> getDisplayFilePathList(File... files) {
-        List<String> filePaths = null;
-
-        if (files != null && files.length > 0) {
-            filePaths = new ArrayList<>();
-            for (int i = 0; i < 7 && i < files.length; i++) {
-                filePaths.add(files[i].getAbsolutePath());
-            }
-        }
-
-        return filePaths;
-    }
-
-    /**
-     * creates a lookup map for a list of given {@link SyncedFolder}s with their local path as the key.
-     *
-     * @param syncFolders list of {@link SyncedFolder}s
-     * @return the lookup map for {@link SyncedFolder}s
-     */
-    @NonNull
-    private Map<String, SyncedFolder> createSyncedFoldersMap(List<SyncedFolder> syncFolders) {
-        Map<String, SyncedFolder> result = new HashMap<>();
-        if (syncFolders != null) {
-            for (SyncedFolder syncFolder : syncFolders) {
-                result.put(syncFolder.getLocalPath() + "-" + syncFolder.getType(), syncFolder);
-            }
-        }
-        return result;
-    }
-
-    /**
-     * show recycler view list or the empty message info (in case list is empty).
-     */
-    private void showList() {
-        binding.list.setVisibility(View.VISIBLE);
-        binding.loadingContent.setVisibility(View.GONE);
-        checkAndShowEmptyListContent();
-    }
-
-    private void checkAndShowEmptyListContent() {
-        if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() > adapter.getSectionCount()) {
-            binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-            int hiddenFoldersCount = adapter.getHiddenFolderCount();
-
-            showEmptyContent(getString(R.string.drawer_synced_folders),
-                             getResources().getQuantityString(R.plurals.synced_folders_show_hidden_folders,
-                                                              hiddenFoldersCount,
-                                                              hiddenFoldersCount),
-                             getResources().getQuantityString(R.plurals.synced_folders_show_hidden_folders,
-                                                              hiddenFoldersCount,
-                                                              hiddenFoldersCount));
-        } else if (adapter.getSectionCount() == 0 && adapter.getUnfilteredSectionCount() == 0) {
-            binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-            showEmptyContent(getString(R.string.drawer_synced_folders),
-                             getString(R.string.synced_folders_no_results));
-        } else {
-            binding.emptyList.emptyListView.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        boolean result = true;
-        int itemId = item.getItemId();
-        if (itemId == android.R.id.home) {
-            finish();
-        } else if (itemId == R.id.action_create_custom_folder) {
-            Log.d(TAG, "Show custom folder dialog");
-            SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
-                    UNPERSISTED_ID,
-                    null,
-                    null,
-                    true,
-                    false,
-                    true,
-                    false,
-                    getAccount().name,
-                    FileUploader.LOCAL_BEHAVIOUR_FORGET,
-                    NameCollisionPolicy.ASK_USER.serialize(),
-                    false,
-                    clock.getCurrentTime(),
-                    null,
-                    MediaFolderType.CUSTOM,
-                    false);
-            onSyncFolderSettingsClick(0, emptyCustomFolder);
-
-            result = super.onOptionsItemSelected(item);
-        } else {
-            result = super.onOptionsItemSelected(item);
-        }
-
-        return result;
-    }
-
-    @Override
-    public void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
-        if (syncedFolderDisplayItem.getId() > UNPERSISTED_ID) {
-            syncedFolderProvider.updateSyncedFolderEnabled(syncedFolderDisplayItem.getId(),
-                                                           syncedFolderDisplayItem.isEnabled());
-        } else {
-            long storedId = syncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem);
-            if (storedId != -1) {
-                syncedFolderDisplayItem.setId(storedId);
-            }
-        }
-
-        if (syncedFolderDisplayItem.isEnabled()) {
-            backgroundJobManager.startImmediateFilesSyncJob(false, false);
-            showBatteryOptimizationInfo();
-        }
-    }
-
-    @Override
-    public void onSyncFolderSettingsClick(int section, SyncedFolderDisplayItem syncedFolderDisplayItem) {
-        FragmentManager fm = getSupportFragmentManager();
-        FragmentTransaction ft = fm.beginTransaction();
-        ft.addToBackStack(null);
-
-        syncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
-                syncedFolderDisplayItem, section);
-        syncedFolderPreferencesDialogFragment.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG);
-    }
-
-    @Override
-    public void onVisibilityToggleClick(int section, SyncedFolderDisplayItem syncedFolder) {
-        syncedFolder.setHidden(!syncedFolder.isHidden());
-
-        saveOrUpdateSyncedFolder(syncedFolder);
-        adapter.setSyncFolderItem(section, syncedFolder);
-
-        checkAndShowEmptyListContent();
-    }
-
-    private void showEmptyContent(String headline, String message, String action) {
-        showEmptyContent(headline, message);
-        binding.emptyList.emptyListViewAction.setText(action);
-        binding.emptyList.emptyListViewAction.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListViewText.setVisibility(View.GONE);
-    }
-
-    private void showLoadingContent() {
-        binding.loadingContent.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
-    }
-
-    private void showEmptyContent(String headline, String message) {
-        binding.emptyList.emptyListViewAction.setVisibility(View.GONE);
-        binding.emptyList.emptyListView.setVisibility(View.VISIBLE);
-        binding.list.setVisibility(View.GONE);
-        binding.loadingContent.setVisibility(View.GONE);
-
-        binding.emptyList.emptyListViewHeadline.setText(headline);
-        binding.emptyList.emptyListViewText.setText(message);
-        binding.emptyList.emptyListViewText.setVisibility(View.VISIBLE);
-        binding.emptyList.emptyListIcon.setVisibility(View.VISIBLE);
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER
-                && resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null) {
-            OCFile chosenFolder = data.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER);
-            syncedFolderPreferencesDialogFragment.setRemoteFolderSummary(chosenFolder.getRemotePath());
-        }
-        if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER
-                && resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null) {
-            String localPath = data.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
-            syncedFolderPreferencesDialogFragment.setLocalFolderSummary(localPath);
-        } else {
-            super.onActivityResult(requestCode, resultCode, data);
-        }
-    }
-
-    @Override
-    public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
-        // custom folders newly created aren't in the list already,
-        // so triggering a refresh
-        if (MediaFolderType.CUSTOM == syncedFolder.getType() && syncedFolder.getId() == UNPERSISTED_ID) {
-            SyncedFolderDisplayItem newCustomFolder = new SyncedFolderDisplayItem(
-                SyncedFolder.UNPERSISTED_ID,
-                syncedFolder.getLocalPath(),
-                syncedFolder.getRemotePath(),
-                syncedFolder.isWifiOnly(),
-                syncedFolder.isChargingOnly(),
-                syncedFolder.isExisting(),
-                syncedFolder.isSubfolderByDate(),
-                syncedFolder.getAccount(),
-                syncedFolder.getUploadAction(),
-                syncedFolder.getNameCollisionPolicy().serialize(),
-                syncedFolder.isEnabled(),
-                clock.getCurrentTime(),
-                new File(syncedFolder.getLocalPath()).getName(),
-                syncedFolder.getType(),
-                syncedFolder.isHidden());
-
-            saveOrUpdateSyncedFolder(newCustomFolder);
-            adapter.addSyncFolderItem(newCustomFolder);
-        } else {
-            SyncedFolderDisplayItem item = adapter.get(syncedFolder.getSection());
-            updateSyncedFolderItem(item,
-                                   syncedFolder.getId(),
-                                   syncedFolder.getLocalPath(),
-                                   syncedFolder.getRemotePath(),
-                                   syncedFolder.isWifiOnly(),
-                                   syncedFolder.isChargingOnly(),
-                                   syncedFolder.isExisting(),
-                                   syncedFolder.isSubfolderByDate(),
-                                   syncedFolder.getUploadAction(),
-                                   syncedFolder.getNameCollisionPolicy().serialize(),
-                                   syncedFolder.isEnabled());
-
-            saveOrUpdateSyncedFolder(item);
-
-            // TODO test if notifyItemChanged is sufficient (should improve performance)
-            adapter.notifyDataSetChanged();
-        }
-
-        syncedFolderPreferencesDialogFragment = null;
-
-        if (syncedFolder.isEnabled()) {
-            showBatteryOptimizationInfo();
-        }
-    }
-
-    private void saveOrUpdateSyncedFolder(SyncedFolderDisplayItem item) {
-        if (item.getId() == UNPERSISTED_ID) {
-            // newly set up folder sync config
-            storeSyncedFolder(item);
-        } else {
-            // existing synced folder setup to be updated
-            syncedFolderProvider.updateSyncFolder(item);
-            if (item.isEnabled()) {
-                backgroundJobManager.startImmediateFilesSyncJob(false, false);
-            } else {
-                String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
-
-                ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
-                    getContentResolver());
-                arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
-            }
-        }
-    }
-
-    private void storeSyncedFolder(SyncedFolderDisplayItem item) {
-        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(MainApp.getAppContext().
-            getContentResolver());
-        long storedId = syncedFolderProvider.storeSyncedFolder(item);
-        if (storedId != -1) {
-            item.setId(storedId);
-            if (item.isEnabled()) {
-                backgroundJobManager.startImmediateFilesSyncJob(false, false);
-            } else {
-                String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
-                arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
-            }
-        }
-    }
-
-    @Override
-    public void onCancelSyncedFolderPreference() {
-        syncedFolderPreferencesDialogFragment = null;
-    }
-
-    @Override
-    public void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder) {
-        syncedFolderProvider.deleteSyncedFolder(syncedFolder.getId());
-        adapter.removeItem(syncedFolder.getSection());
-    }
-
-    /**
-     * update given synced folder with the given values.
-     *
-     * @param item            the synced folder to be updated
-     * @param localPath       the local path
-     * @param remotePath      the remote path
-     * @param wifiOnly        upload on wifi only
-     * @param chargingOnly    upload on charging only
-     * @param existing        also upload existing
-     * @param subfolderByDate created sub folders
-     * @param uploadAction    upload action
-     * @param nameCollisionPolicy what to do on name collision
-     * @param enabled         is sync enabled
-     */
-    private void updateSyncedFolderItem(SyncedFolderDisplayItem item,
-                                                           long id,
-                                                           String localPath,
-                                                           String remotePath,
-                                                           boolean wifiOnly,
-                                                           boolean chargingOnly,
-                                                           boolean existing,
-                                                           boolean subfolderByDate,
-                                                           Integer uploadAction,
-                                                           Integer nameCollisionPolicy,
-                                                           boolean enabled) {
-        item.setId(id);
-        item.setLocalPath(localPath);
-        item.setRemotePath(remotePath);
-        item.setWifiOnly(wifiOnly);
-        item.setChargingOnly(chargingOnly);
-        item.setExisting(existing);
-        item.setSubfolderByDate(subfolderByDate);
-        item.setUploadAction(uploadAction);
-        item.setNameCollisionPolicy(nameCollisionPolicy);
-        item.setEnabled(enabled, clock.getCurrentTime());
-    }
-
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
-                                           @NonNull int[] grantResults) {
-        switch (requestCode) {
-            case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: {
-                // If request is cancelled, result arrays are empty.
-                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                    // permission was granted
-                    int gridWidth = getResources().getInteger(R.integer.media_grid_width);
-                    load(gridWidth * 2, true);
-                } else {
-                    // permission denied --> do nothing
-                    return;
-                }
-                return;
-            }
-            default:
-                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-        }
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-    }
-
-    private void showBatteryOptimizationInfo() {
-        if (powerManagementService.isPowerSavingExclusionAvailable() || checkIfBatteryOptimizationEnabled()) {
-            AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog)
-                .setTitle(getString(R.string.battery_optimization_title))
-                .setMessage(getString(R.string.battery_optimization_message))
-                .setPositiveButton(getString(R.string.battery_optimization_disable), (dialog, which) -> {
-                    // show instant upload
-                    @SuppressLint("BatteryLife")
-                    Intent intent = new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
-                                               Uri.parse("package:" + BuildConfig.APPLICATION_ID));
-
-                    if (intent.resolveActivity(getPackageManager()) != null) {
-                        startActivity(intent);
-                    }
-                })
-                .setNeutralButton(getString(R.string.battery_optimization_close), (dialog, which) -> dialog.dismiss())
-                .setIcon(R.drawable.ic_battery_alert);
-
-            if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) {
-                AlertDialog alertDialog = alertDialogBuilder.show();
-                ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE),
-                                                       alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL));
-            }
-        }
-    }
-
-    /**
-     * Check if battery optimization is enabled. If unknown, fallback to true.
-     *
-     * @return true if battery optimization is enabled
-     */
-    private boolean checkIfBatteryOptimizationEnabled() {
-        PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
-
-        if (powerManager == null) {
-            return true;
-        }
-
-        return !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID);
-    }
-}

+ 809 - 0
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt

@@ -0,0 +1,809 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2016 Andy Scherzinger
+ * Copyright (C) 2016 Nextcloud
+ * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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.ui.activity
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.PowerManager
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.app.AlertDialog
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.lifecycle.Lifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import com.nextcloud.client.core.Clock
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.nextcloud.client.jobs.MediaFoldersDetectionWork
+import com.nextcloud.client.jobs.NotificationWork
+import com.nextcloud.client.preferences.AppPreferences
+import com.owncloud.android.BuildConfig
+import com.owncloud.android.MainApp
+import com.owncloud.android.R
+import com.owncloud.android.databinding.SyncedFoldersLayoutBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.datamodel.MediaFolder
+import com.owncloud.android.datamodel.MediaFolderType
+import com.owncloud.android.datamodel.MediaProvider
+import com.owncloud.android.datamodel.OCFile
+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.files.services.NameCollisionPolicy
+import com.owncloud.android.ui.adapter.SyncedFolderAdapter
+import com.owncloud.android.ui.decoration.MediaGridItemDecoration
+import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment
+import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener
+import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable
+import com.owncloud.android.utils.PermissionUtil
+import com.owncloud.android.utils.SyncedFolderUtils
+import com.owncloud.android.utils.theme.ThemeButtonUtils
+import com.owncloud.android.utils.theme.ThemeUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import java.io.File
+import java.util.Locale
+import javax.inject.Inject
+
+/**
+ * Activity displaying all auto-synced folders and/or instant upload media folders.
+ */
+@Suppress("TooManyFunctions")
+class SyncedFoldersActivity :
+    FileActivity(),
+    SyncedFolderAdapter.ClickListener,
+    OnSyncedFolderPreferenceListener,
+    Injectable {
+
+    companion object {
+        private const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG"
+        // yes, there is a typo in this value
+        private const val KEY_SYNCED_FOLDER_INITIATED_PREFIX = "syncedFolderIntitiated_"
+        private val PRIORITIZED_FOLDERS = arrayOf("Camera", "Screenshots")
+        private val TAG = SyncedFoldersActivity::class.java.simpleName
+
+        /**
+         * Sorts list of [SyncedFolderDisplayItem]s.
+         *
+         * @param syncFolderItemList list of items to be sorted
+         * @return sorted list of items
+         */
+        @JvmStatic
+        @Suppress("ComplexMethod")
+        fun sortSyncedFolderItems(syncFolderItemList: List<SyncedFolderDisplayItem?>): List<SyncedFolderDisplayItem?> {
+            return syncFolderItemList.sortedWith { f1, f2 ->
+                if (f1 == null && f2 == null) {
+                    0
+                } else if (f1 == null) {
+                    -1
+                } else if (f2 == null) {
+                    1
+                } else if (f1.isEnabled && f2.isEnabled) {
+                    when {
+                        f1.folderName == null -> -1
+                        f2.folderName == null -> 1
+                        else -> f1.folderName.lowercase(Locale.getDefault()).compareTo(
+                            f2.folderName.lowercase(Locale.getDefault())
+                        )
+                    }
+                } else if (f1.folderName == null && f2.folderName == null) {
+                    0
+                } else if (f1.isEnabled) {
+                    -1
+                } else if (f2.isEnabled) {
+                    1
+                } else if (f1.folderName == null) {
+                    -1
+                } else if (f2.folderName == null) {
+                    1
+                } else {
+                    for (folder in PRIORITIZED_FOLDERS) {
+                        if (folder == f1.folderName && folder == f2.folderName) {
+                            return@sortedWith 0
+                        } else if (folder == f1.folderName) {
+                            return@sortedWith -1
+                        } else if (folder == f2.folderName) {
+                            return@sortedWith 1
+                        }
+                    }
+                    f1.folderName.lowercase(Locale.getDefault()).compareTo(
+                        f2.folderName.lowercase(Locale.getDefault())
+                    )
+                }
+            }
+        }
+    }
+
+    @Inject
+    lateinit var preferences: AppPreferences
+
+    @Inject
+    lateinit var powerManagementService: PowerManagementService
+
+    @Inject
+    lateinit var clock: Clock
+
+    @Inject
+    lateinit var backgroundJobManager: BackgroundJobManager
+
+    private lateinit var binding: SyncedFoldersLayoutBinding
+    private lateinit var adapter: SyncedFolderAdapter
+    private lateinit var syncedFolderProvider: SyncedFolderProvider
+
+    private var syncedFolderPreferencesDialogFragment: SyncedFolderPreferencesDialogFragment? = null
+    private var path: String? = null
+    private var type = 0
+    private var loadJob: Job? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = SyncedFoldersLayoutBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+        if (intent != null && intent.extras != null) {
+            val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT)
+            val optionalUser = user
+            if (optionalUser.isPresent && accountName != null) {
+                val user = optionalUser.get()
+                if (!accountName.equals(user.accountName, ignoreCase = true)) {
+                    accountManager.setCurrentOwnCloudAccount(accountName)
+                    setUser(userAccountManager.user)
+                }
+            }
+            path = intent.getStringExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_PATH)
+            type = intent.getIntExtra(MediaFoldersDetectionWork.KEY_MEDIA_FOLDER_TYPE, -1)
+
+            // Cancel notification
+            val notificationId = intent.getIntExtra(MediaFoldersDetectionWork.NOTIFICATION_ID, 0)
+            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+            notificationManager.cancel(notificationId)
+        }
+
+        // setup toolbar
+        setupToolbar()
+        updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_synced_folders))
+        setupDrawer()
+        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
+        if (supportActionBar != null) {
+            supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+        }
+        if (mDrawerToggle != null) {
+            mDrawerToggle.isDrawerIndicatorEnabled = false
+        }
+
+        setupContent()
+        if (ThemeUtils.themingEnabled(this)) {
+            setTheme(R.style.FallbackThemingTheme)
+        }
+        binding.emptyList.emptyListViewAction.setOnClickListener { showHiddenItems() }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        val inflater = menuInflater
+        inflater.inflate(R.menu.activity_synced_folders, menu)
+        if (powerManagementService.isPowerSavingExclusionAvailable) {
+            val item = menu.findItem(R.id.action_disable_power_save_check)
+            item.isVisible = true
+            item.isChecked = preferences.isPowerCheckDisabled
+            item.setOnMenuItemClickListener { powerCheck -> onDisablePowerSaveCheckClicked(powerCheck) }
+        }
+        return true
+    }
+
+    private fun onDisablePowerSaveCheckClicked(powerCheck: MenuItem): Boolean {
+        if (!powerCheck.isChecked) {
+            showPowerCheckDialog()
+        }
+        preferences.isPowerCheckDisabled = !powerCheck.isChecked
+        powerCheck.isChecked = !powerCheck.isChecked
+        return true
+    }
+
+    private fun showPowerCheckDialog() {
+        val alertDialog = AlertDialog.Builder(this)
+            .setView(findViewById(R.id.root_layout))
+            .setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() }
+            .setTitle(R.string.autoupload_disable_power_save_check)
+            .setMessage(getString(R.string.power_save_check_dialog_message))
+            .show()
+        ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE))
+    }
+
+    /**
+     * sets up the UI elements and loads all media/synced folders.
+     */
+    private fun setupContent() {
+        val gridWidth = resources.getInteger(R.integer.media_grid_width)
+        val lightVersion = resources.getBoolean(R.bool.syncedFolder_light)
+        adapter = SyncedFolderAdapter(this, clock, gridWidth, this, lightVersion)
+        syncedFolderProvider = SyncedFolderProvider(contentResolver, preferences, clock)
+        binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders)
+        ThemeButtonUtils.colorPrimaryButton(binding.emptyList.emptyListViewAction, this)
+        val lm = GridLayoutManager(this, gridWidth)
+        adapter.setLayoutManager(lm)
+        val spacing = resources.getDimensionPixelSize(R.dimen.media_grid_spacing)
+        binding.list.addItemDecoration(MediaGridItemDecoration(spacing))
+        binding.list.layoutManager = lm
+        binding.list.adapter = adapter
+        load(getItemsDisplayedPerFolder(), false)
+    }
+
+    private fun showHiddenItems() {
+        if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount > adapter.sectionCount) {
+            adapter.toggleHiddenItemsVisibility()
+            binding.emptyList.emptyListView.visibility = View.GONE
+            binding.list.visibility = View.VISIBLE
+        }
+    }
+
+    /**
+     * loads all media/synced folders, adds them to the recycler view adapter and shows the list.
+     *
+     * @param perFolderMediaItemLimit the amount of media items to be loaded/shown per media folder
+     */
+    @SuppressLint("NotifyDataSetChanged")
+    private fun load(perFolderMediaItemLimit: Int, force: Boolean) {
+        if (adapter.itemCount > 0 && !force) {
+            return
+        }
+        showLoadingContent()
+        loadJob = CoroutineScope(Dispatchers.IO).launch {
+            loadJob?.cancel()
+            val mediaFolders = MediaProvider.getImageFolders(
+                contentResolver,
+                perFolderMediaItemLimit, this@SyncedFoldersActivity, false
+            )
+            mediaFolders.addAll(
+                MediaProvider.getVideoFolders(
+                    contentResolver, perFolderMediaItemLimit,
+                    this@SyncedFoldersActivity, false
+                )
+            )
+            val syncedFolderArrayList = syncedFolderProvider.syncedFolders
+            val currentAccountSyncedFoldersList: MutableList<SyncedFolder> = ArrayList()
+            val user = userAccountManager.user
+            for (syncedFolder in syncedFolderArrayList) {
+                if (syncedFolder.account == user.accountName) {
+                    // delete non-existing & disabled synced folders
+                    if (!File(syncedFolder.localPath).exists() && !syncedFolder.isEnabled) {
+                        syncedFolderProvider.deleteSyncedFolder(syncedFolder.id)
+                    } else {
+                        currentAccountSyncedFoldersList.add(syncedFolder)
+                    }
+                }
+            }
+            val syncFolderItems = sortSyncedFolderItems(
+                mergeFolderData(currentAccountSyncedFoldersList, mediaFolders)
+            )
+            CoroutineScope(Dispatchers.Main).launch {
+                adapter.setSyncFolderItems(syncFolderItems)
+                adapter.notifyDataSetChanged()
+                showList()
+                if (!TextUtils.isEmpty(path)) {
+                    val section = adapter.getSectionByLocalPathAndType(path, type)
+                    if (section >= 0) {
+                        onSyncFolderSettingsClick(section, adapter[section])
+                    }
+                }
+                loadJob = null
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        loadJob?.cancel()
+    }
+
+    /**
+     * merges two lists of [SyncedFolder] and [MediaFolder] items into one of SyncedFolderItems.
+     *
+     * @param syncedFolders the synced folders
+     * @param mediaFolders  the media folders
+     * @return the merged list of SyncedFolderItems
+     */
+    private fun mergeFolderData(
+        syncedFolders: List<SyncedFolder>,
+        mediaFolders: List<MediaFolder>
+    ): List<SyncedFolderDisplayItem?> {
+        val syncedFoldersMap = createSyncedFoldersMap(syncedFolders)
+        val result: MutableList<SyncedFolderDisplayItem?> = ArrayList()
+        for (mediaFolder in mediaFolders) {
+            if (syncedFoldersMap.containsKey(mediaFolder.absolutePath + "-" + mediaFolder.type)) {
+                val syncedFolder = syncedFoldersMap[mediaFolder.absolutePath + "-" + mediaFolder.type]
+                syncedFoldersMap.remove(mediaFolder.absolutePath + "-" + mediaFolder.type)
+                if (syncedFolder != null && SyncedFolderUtils.isQualifyingMediaFolder(syncedFolder)) {
+                    if (MediaFolderType.CUSTOM == syncedFolder.type) {
+                        result.add(createSyncedFolderWithoutMediaFolder(syncedFolder))
+                    } else {
+                        result.add(createSyncedFolder(syncedFolder, mediaFolder))
+                    }
+                }
+            } else {
+                if (SyncedFolderUtils.isQualifyingMediaFolder(mediaFolder)) {
+                    result.add(createSyncedFolderFromMediaFolder(mediaFolder))
+                }
+            }
+        }
+        for (syncedFolder in syncedFoldersMap.values) {
+            result.add(createSyncedFolderWithoutMediaFolder(syncedFolder))
+        }
+        return result
+    }
+
+    private fun createSyncedFolderWithoutMediaFolder(syncedFolder: SyncedFolder): SyncedFolderDisplayItem {
+        val localFolder = File(syncedFolder.localPath)
+        val files = SyncedFolderUtils.getFileList(localFolder)
+        val filePaths = getDisplayFilePathList(files.toList())
+        return SyncedFolderDisplayItem(
+            syncedFolder.id,
+            syncedFolder.localPath,
+            syncedFolder.remotePath,
+            syncedFolder.isWifiOnly,
+            syncedFolder.isChargingOnly,
+            syncedFolder.isExisting,
+            syncedFolder.isSubfolderByDate,
+            syncedFolder.account,
+            syncedFolder.uploadAction,
+            syncedFolder.nameCollisionPolicyInt,
+            syncedFolder.isEnabled,
+            clock.currentTime,
+            filePaths,
+            localFolder.name,
+            files.size.toLong(),
+            syncedFolder.type,
+            syncedFolder.isHidden
+        )
+    }
+
+    /**
+     * creates a SyncedFolderDisplayItem merging a [SyncedFolder] and a [MediaFolder] object instance.
+     *
+     * @param syncedFolder the synced folder object
+     * @param mediaFolder  the media folder object
+     * @return the created SyncedFolderDisplayItem
+     */
+    private fun createSyncedFolder(syncedFolder: SyncedFolder, mediaFolder: MediaFolder): SyncedFolderDisplayItem {
+        return SyncedFolderDisplayItem(
+            syncedFolder.id,
+            syncedFolder.localPath,
+            syncedFolder.remotePath,
+            syncedFolder.isWifiOnly,
+            syncedFolder.isChargingOnly,
+            syncedFolder.isExisting,
+            syncedFolder.isSubfolderByDate,
+            syncedFolder.account,
+            syncedFolder.uploadAction,
+            syncedFolder.nameCollisionPolicyInt,
+            syncedFolder.isEnabled,
+            clock.currentTime,
+            mediaFolder.filePaths,
+            mediaFolder.folderName,
+            mediaFolder.numberOfFiles,
+            mediaFolder.type,
+            syncedFolder.isHidden
+        )
+    }
+
+    /**
+     * creates a [SyncedFolderDisplayItem] based on a [MediaFolder] object instance.
+     *
+     * @param mediaFolder the media folder object
+     * @return the created SyncedFolderDisplayItem
+     */
+    private fun createSyncedFolderFromMediaFolder(mediaFolder: MediaFolder): SyncedFolderDisplayItem {
+        return SyncedFolderDisplayItem(
+            SyncedFolder.UNPERSISTED_ID,
+            mediaFolder.absolutePath,
+            getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
+            true,
+            false,
+            true,
+            false,
+            account.name,
+            FileUploader.LOCAL_BEHAVIOUR_FORGET,
+            NameCollisionPolicy.ASK_USER.serialize(),
+            false,
+            clock.currentTime,
+            mediaFolder.filePaths,
+            mediaFolder.folderName,
+            mediaFolder.numberOfFiles,
+            mediaFolder.type,
+            false
+        )
+    }
+
+    private fun getItemsDisplayedPerFolder(): Int {
+        return resources.getInteger(R.integer.media_grid_width) * 2
+    }
+
+    private fun getDisplayFilePathList(files: List<File>?): List<String>? {
+        if (!files.isNullOrEmpty()) {
+            return files.take(getItemsDisplayedPerFolder())
+                .map { it.absolutePath }
+        }
+        return null
+    }
+
+    /**
+     * creates a lookup map for a list of given [SyncedFolder]s with their local path as the key.
+     *
+     * @param syncFolders list of [SyncedFolder]s
+     * @return the lookup map for [SyncedFolder]s
+     */
+    private fun createSyncedFoldersMap(syncFolders: List<SyncedFolder>?): MutableMap<String, SyncedFolder> {
+        val result: MutableMap<String, SyncedFolder> = HashMap()
+        if (syncFolders != null) {
+            for (syncFolder in syncFolders) {
+                result[syncFolder.localPath + "-" + syncFolder.type] = syncFolder
+            }
+        }
+        return result
+    }
+
+    /**
+     * show recycler view list or the empty message info (in case list is empty).
+     */
+    private fun showList() {
+        binding.list.visibility = View.VISIBLE
+        binding.loadingContent.visibility = View.GONE
+        checkAndShowEmptyListContent()
+    }
+
+    private fun checkAndShowEmptyListContent() {
+        if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount > adapter.sectionCount) {
+            binding.emptyList.emptyListView.visibility = View.VISIBLE
+            val hiddenFoldersCount = adapter.hiddenFolderCount
+            showEmptyContent(
+                getString(R.string.drawer_synced_folders),
+                resources.getQuantityString(
+                    R.plurals.synced_folders_show_hidden_folders,
+                    hiddenFoldersCount,
+                    hiddenFoldersCount
+                ),
+                resources.getQuantityString(
+                    R.plurals.synced_folders_show_hidden_folders,
+                    hiddenFoldersCount,
+                    hiddenFoldersCount
+                )
+            )
+        } else if (adapter.sectionCount == 0 && adapter.unfilteredSectionCount == 0) {
+            binding.emptyList.emptyListView.visibility = View.VISIBLE
+            showEmptyContent(
+                getString(R.string.drawer_synced_folders),
+                getString(R.string.synced_folders_no_results)
+            )
+        } else {
+            binding.emptyList.emptyListView.visibility = View.GONE
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        var result = true
+        when (item.itemId) {
+            android.R.id.home -> finish()
+            R.id.action_create_custom_folder -> {
+                Log.d(TAG, "Show custom folder dialog")
+                val emptyCustomFolder = SyncedFolderDisplayItem(
+                    SyncedFolder.UNPERSISTED_ID,
+                    null,
+                    null,
+                    true,
+                    false,
+                    true,
+                    false,
+                    account.name,
+                    FileUploader.LOCAL_BEHAVIOUR_FORGET,
+                    NameCollisionPolicy.ASK_USER.serialize(),
+                    false,
+                    clock.currentTime,
+                    null,
+                    MediaFolderType.CUSTOM,
+                    false
+                )
+                onSyncFolderSettingsClick(0, emptyCustomFolder)
+                result = super.onOptionsItemSelected(item)
+            }
+            else -> result = super.onOptionsItemSelected(item)
+        }
+        return result
+    }
+
+    override fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) {
+        if (syncedFolderDisplayItem.id > SyncedFolder.UNPERSISTED_ID) {
+            syncedFolderProvider.updateSyncedFolderEnabled(
+                syncedFolderDisplayItem.id,
+                syncedFolderDisplayItem.isEnabled
+            )
+        } else {
+            val storedId = syncedFolderProvider.storeSyncedFolder(syncedFolderDisplayItem)
+            if (storedId != -1L) {
+                syncedFolderDisplayItem.id = storedId
+            }
+        }
+        if (syncedFolderDisplayItem.isEnabled) {
+            backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false)
+            showBatteryOptimizationInfo()
+        }
+    }
+
+    override fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem) {
+        val fm = supportFragmentManager
+        val ft = fm.beginTransaction()
+        ft.addToBackStack(null)
+        syncedFolderPreferencesDialogFragment = SyncedFolderPreferencesDialogFragment.newInstance(
+            syncedFolderDisplayItem, section
+        ).also {
+            it.show(ft, SYNCED_FOLDER_PREFERENCES_DIALOG_TAG)
+        }
+    }
+
+    override fun onVisibilityToggleClick(section: Int, syncedFolder: SyncedFolderDisplayItem) {
+        syncedFolder.isHidden = !syncedFolder.isHidden
+        saveOrUpdateSyncedFolder(syncedFolder)
+        adapter.setSyncFolderItem(section, syncedFolder)
+        checkAndShowEmptyListContent()
+    }
+
+    private fun showEmptyContent(headline: String, message: String, action: String) {
+        showEmptyContent(headline, message)
+        binding.emptyList.emptyListViewAction.text = action
+        binding.emptyList.emptyListViewAction.visibility = View.VISIBLE
+        binding.emptyList.emptyListViewText.visibility = View.GONE
+    }
+
+    private fun showLoadingContent() {
+        binding.loadingContent.visibility = View.VISIBLE
+        binding.emptyList.emptyListViewAction.visibility = View.GONE
+    }
+
+    private fun showEmptyContent(headline: String, message: String) {
+        binding.emptyList.emptyListViewAction.visibility = View.GONE
+        binding.emptyList.emptyListView.visibility = View.VISIBLE
+        binding.list.visibility = View.GONE
+        binding.loadingContent.visibility = View.GONE
+        binding.emptyList.emptyListViewHeadline.text = headline
+        binding.emptyList.emptyListViewText.text = message
+        binding.emptyList.emptyListViewText.visibility = View.VISIBLE
+        binding.emptyList.emptyListIcon.visibility = View.VISIBLE
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_REMOTE_FOLDER &&
+            resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null
+        ) {
+            val chosenFolder: OCFile = data!!.getParcelableExtra(FolderPickerActivity.EXTRA_FOLDER)!!
+            syncedFolderPreferencesDialogFragment!!.setRemoteFolderSummary(chosenFolder.remotePath)
+        } else if (
+            requestCode == SyncedFolderPreferencesDialogFragment.REQUEST_CODE__SELECT_LOCAL_FOLDER &&
+            resultCode == RESULT_OK && syncedFolderPreferencesDialogFragment != null
+        ) {
+            val localPath = data!!.getStringExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES)
+            syncedFolderPreferencesDialogFragment!!.setLocalFolderSummary(localPath)
+        } else {
+            super.onActivityResult(requestCode, resultCode, data)
+        }
+    }
+
+    override fun onSaveSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) {
+        // custom folders newly created aren't in the list already,
+        // so triggering a refresh
+        if (MediaFolderType.CUSTOM == syncedFolder.type && syncedFolder.id == SyncedFolder.UNPERSISTED_ID) {
+            val newCustomFolder = SyncedFolderDisplayItem(
+                SyncedFolder.UNPERSISTED_ID,
+                syncedFolder.localPath,
+                syncedFolder.remotePath,
+                syncedFolder.isWifiOnly,
+                syncedFolder.isChargingOnly,
+                syncedFolder.isExisting,
+                syncedFolder.isSubfolderByDate,
+                syncedFolder.account,
+                syncedFolder.uploadAction,
+                syncedFolder.nameCollisionPolicy.serialize(),
+                syncedFolder.isEnabled,
+                clock.currentTime,
+                File(syncedFolder.localPath).name,
+                syncedFolder.type,
+                syncedFolder.isHidden
+            )
+            saveOrUpdateSyncedFolder(newCustomFolder)
+            adapter.addSyncFolderItem(newCustomFolder)
+        } else {
+            val item = adapter[syncedFolder.section]
+            updateSyncedFolderItem(
+                item,
+                syncedFolder.id,
+                syncedFolder.localPath,
+                syncedFolder.remotePath,
+                syncedFolder.isWifiOnly,
+                syncedFolder.isChargingOnly,
+                syncedFolder.isExisting,
+                syncedFolder.isSubfolderByDate,
+                syncedFolder.uploadAction,
+                syncedFolder.nameCollisionPolicy.serialize(),
+                syncedFolder.isEnabled
+            )
+            saveOrUpdateSyncedFolder(item)
+
+            // TODO test if notifyItemChanged is sufficient (should improve performance)
+            adapter.notifyDataSetChanged()
+        }
+        syncedFolderPreferencesDialogFragment = null
+        if (syncedFolder.isEnabled) {
+            showBatteryOptimizationInfo()
+        }
+    }
+
+    private fun saveOrUpdateSyncedFolder(item: SyncedFolderDisplayItem) {
+        if (item.id == SyncedFolder.UNPERSISTED_ID) {
+            // newly set up folder sync config
+            storeSyncedFolder(item)
+        } else {
+            // existing synced folder setup to be updated
+            syncedFolderProvider.updateSyncFolder(item)
+            if (item.isEnabled) {
+                backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false)
+            } else {
+                val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
+                val arbitraryDataProvider = ArbitraryDataProvider(MainApp.getAppContext().contentResolver)
+                arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey)
+            }
+        }
+    }
+
+    private fun storeSyncedFolder(item: SyncedFolderDisplayItem) {
+        val arbitraryDataProvider = ArbitraryDataProvider(MainApp.getAppContext().contentResolver)
+        val storedId = syncedFolderProvider.storeSyncedFolder(item)
+        if (storedId != -1L) {
+            item.id = storedId
+            if (item.isEnabled) {
+                backgroundJobManager.startImmediateFilesSyncJob(skipCustomFolders = false, overridePowerSaving = false)
+            } else {
+                val syncedFolderInitiatedKey = KEY_SYNCED_FOLDER_INITIATED_PREFIX + item.id
+                arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey)
+            }
+        }
+    }
+
+    override fun onCancelSyncedFolderPreference() {
+        syncedFolderPreferencesDialogFragment = null
+    }
+
+    override fun onDeleteSyncedFolderPreference(syncedFolder: SyncedFolderParcelable) {
+        syncedFolderProvider.deleteSyncedFolder(syncedFolder.id)
+        adapter.removeItem(syncedFolder.section)
+    }
+
+    /**
+     * update given synced folder with the given values.
+     *
+     * @param item            the synced folder to be updated
+     * @param localPath       the local path
+     * @param remotePath      the remote path
+     * @param wifiOnly        upload on wifi only
+     * @param chargingOnly    upload on charging only
+     * @param existing        also upload existing
+     * @param subfolderByDate created sub folders
+     * @param uploadAction    upload action
+     * @param nameCollisionPolicy what to do on name collision
+     * @param enabled         is sync enabled
+     */
+    @Suppress("LongParameterList")
+    private fun updateSyncedFolderItem(
+        item: SyncedFolderDisplayItem,
+        id: Long,
+        localPath: String,
+        remotePath: String,
+        wifiOnly: Boolean,
+        chargingOnly: Boolean,
+        existing: Boolean,
+        subfolderByDate: Boolean,
+        uploadAction: Int,
+        nameCollisionPolicy: Int,
+        enabled: Boolean
+    ) {
+        item.id = id
+        item.localPath = localPath
+        item.remotePath = remotePath
+        item.isWifiOnly = wifiOnly
+        item.isChargingOnly = chargingOnly
+        item.isExisting = existing
+        item.isSubfolderByDate = subfolderByDate
+        item.uploadAction = uploadAction
+        item.setNameCollisionPolicy(nameCollisionPolicy)
+        item.setEnabled(enabled, clock.currentTime)
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<String>,
+        grantResults: IntArray
+    ) {
+        when (requestCode) {
+            PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> {
+
+                // If request is cancelled, result arrays are empty.
+                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    // permission was granted
+
+                    load(getItemsDisplayedPerFolder(), true)
+                } else {
+                    // permission denied --> do nothing
+                    return
+                }
+                return
+            }
+            else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        }
+    }
+
+    private fun showBatteryOptimizationInfo() {
+        if (powerManagementService.isPowerSavingExclusionAvailable || checkIfBatteryOptimizationEnabled()) {
+            val alertDialogBuilder = AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog)
+                .setTitle(getString(R.string.battery_optimization_title))
+                .setMessage(getString(R.string.battery_optimization_message))
+                .setPositiveButton(getString(R.string.battery_optimization_disable)) { _, _ ->
+                    // show instant upload
+                    @SuppressLint("BatteryLife") val intent = Intent(
+                        Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
+                        Uri.parse("package:" + BuildConfig.APPLICATION_ID)
+                    )
+                    if (intent.resolveActivity(packageManager) != null) {
+                        startActivity(intent)
+                    }
+                }
+                .setNeutralButton(getString(R.string.battery_optimization_close)) { dialog, _ -> dialog.dismiss() }
+                .setIcon(R.drawable.ic_battery_alert)
+            if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+                val alertDialog = alertDialogBuilder.show()
+                ThemeButtonUtils.themeBorderlessButton(
+                    alertDialog.getButton(AlertDialog.BUTTON_POSITIVE),
+                    alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)
+                )
+            }
+        }
+    }
+
+    /**
+     * Check if battery optimization is enabled. If unknown, fallback to true.
+     *
+     * @return true if battery optimization is enabled
+     */
+    private fun checkIfBatteryOptimizationEnabled(): Boolean {
+        val powerManager = getSystemService(POWER_SERVICE) as PowerManager?
+        return when {
+            powerManager != null -> !powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
+            else -> true
+        }
+    }
+}

+ 5 - 1
src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.java

@@ -47,6 +47,8 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
 
 
@@ -68,6 +70,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
     private static final int VIEW_TYPE_HEADER = 2;
     private static final int VIEW_TYPE_HEADER = 2;
     private static final int VIEW_TYPE_FOOTER = 3;
     private static final int VIEW_TYPE_FOOTER = 3;
     private boolean hideItems;
     private boolean hideItems;
+    private final Executor thumbnailThreadPool;
 
 
     public SyncedFolderAdapter(Context context, Clock clock, int gridWidth, ClickListener listener, boolean light) {
     public SyncedFolderAdapter(Context context, Clock clock, int gridWidth, ClickListener listener, boolean light) {
         this.context = context;
         this.context = context;
@@ -79,6 +82,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
         filteredSyncFolderItems = new ArrayList<>();
         filteredSyncFolderItems = new ArrayList<>();
         this.light = light;
         this.light = light;
         this.hideItems = true;
         this.hideItems = true;
+        this.thumbnailThreadPool = Executors.newCachedThreadPool();
 
 
         shouldShowHeadersForEmptySections(true);
         shouldShowHeadersForEmptySections(true);
         shouldShowFooters(true);
         shouldShowFooters(true);
@@ -341,7 +345,7 @@ public class SyncedFolderAdapter extends SectionedRecyclerViewAdapter<SectionedV
                     );
                     );
             holder.binding.thumbnail.setImageDrawable(asyncDrawable);
             holder.binding.thumbnail.setImageDrawable(asyncDrawable);
 
 
-            task.execute(file);
+            task.executeOnExecutor(thumbnailThreadPool, file);
 
 
             // set proper tag
             // set proper tag
             holder.binding.thumbnail.setTag(file.hashCode());
             holder.binding.thumbnail.setTag(file.hashCode());