Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Check Auto Upload Sub Folder #14547

Merged
merged 21 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.utils.extensions

import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.datamodel.SyncedFolderDisplayItem
import java.io.File

fun List<SyncedFolderDisplayItem>.filterEnabledOrWithoutEnabledParent(): List<SyncedFolderDisplayItem> {
return filter { it.isEnabled || !hasEnabledParent(it.localPath) }
}

@Suppress("ReturnCount")
fun List<SyncedFolder>.hasEnabledParent(localPath: String?): Boolean {
localPath ?: return false

val localFile = File(localPath).takeIf { it.exists() } ?: return false
val parent = localFile.parentFile ?: return false

return any { it.isEnabled && File(it.localPath).exists() && File(it.localPath) == parent } ||
hasEnabledParent(parent.absolutePath)
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.ui.adapter.SyncedFolderAdapter
import com.owncloud.android.ui.decoration.MediaGridItemDecoration
import com.owncloud.android.ui.dialog.ConfirmationDialogFragment
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.OnSyncedFolderPreferenceListener
import com.owncloud.android.ui.dialog.parcel.SyncedFolderParcelable
Expand All @@ -77,6 +78,7 @@ class SyncedFoldersActivity :

companion object {
private const val SYNCED_FOLDER_PREFERENCES_DIALOG_TAG = "SYNCED_FOLDER_PREFERENCES_DIALOG"
private const val SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG_TAG"

// yes, there is a typo in this value
private const val KEY_SYNCED_FOLDER_INITIATED_PREFIX = "syncedFolderIntitiated_"
Expand Down Expand Up @@ -274,6 +276,7 @@ class SyncedFoldersActivity :
if (adapter.itemCount > 0 && !force) {
return
}

showLoadingContent()
lifecycleScope.launch(Dispatchers.IO) {
val mediaFolders = MediaProvider.getImageFolders(
Expand All @@ -292,19 +295,23 @@ class SyncedFoldersActivity :
viewThemeUtils
)
)

val syncedFolderArrayList = syncedFolderProvider.syncedFolders
val currentAccountSyncedFoldersList: MutableList<SyncedFolder> = ArrayList()
val user = userAccountManager.user
for (syncedFolder in syncedFolderArrayList) {
if (syncedFolder.account == user.accountName) {
val folder = File(syncedFolder.localPath)

// delete non-existing & disabled synced folders
if (!File(syncedFolder.localPath).exists() && !syncedFolder.isEnabled) {
if (!folder.exists() && !syncedFolder.isEnabled) {
syncedFolderProvider.deleteSyncedFolder(syncedFolder.id)
} else {
currentAccountSyncedFoldersList.add(syncedFolder)
}
}
}

val syncFolderItems = sortSyncedFolderItems(
mergeFolderData(currentAccountSyncedFoldersList, mediaFolders)
).filterNotNull()
Expand Down Expand Up @@ -709,6 +716,22 @@ class SyncedFoldersActivity :
}
}

override fun showSubFolderWarningDialog() {
val dialog = ConfirmationDialogFragment.newInstance(
messageResId = R.string.auto_upload_sub_folder_warning,
messageArguments = null,
titleResId = R.string.sync_duplication,
titleIconId = R.drawable.ic_info,
positiveButtonTextId = R.string.dialog_close,
negativeButtonTextId = -1,
neutralButtonTextId = -1
)

if (isDialogFragmentReady(dialog)) {
dialog.show(supportFragmentManager, SUB_FOLDER_WARNING_DIALOG_TAG)
}
}

private fun saveOrUpdateSyncedFolder(item: SyncedFolderDisplayItem) {
if (item.id == SyncedFolder.UNPERSISTED_ID) {
// newly set up folder sync config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@
import android.widget.TextView;

import com.nextcloud.client.account.User;
import com.nextcloud.client.core.Clock;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.utils.extensions.ActivityExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.nextcloud.utils.extensions.SyncedFolderExtensionsKt;
import com.owncloud.android.R;
import com.owncloud.android.databinding.UploadFilesLayoutBinding;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.ui.adapter.StoragePathAdapter;
import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
Expand All @@ -60,6 +63,7 @@

import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat;
Expand Down Expand Up @@ -90,10 +94,15 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
private static final String ENCRYPTED_FOLDER_KEY = "encrypted_folder";

private static final String QUERY_TO_MOVE_DIALOG_TAG = "QUERY_TO_MOVE";
private static final String SUB_FOLDER_WARNING_DIALOG_TAG = "SUB_FOLDER_WARNING_DIALOG";
private static final String TAG = "UploadFilesActivity";
private static final String WAIT_DIALOG_TAG = "WAIT";

@Inject AppPreferences preferences;

@Inject
Clock clock;

private Account mAccountOnCreation;
private ArrayAdapter<String> mDirectories;
private boolean mLocalFolderPickerMode;
Expand Down Expand Up @@ -615,6 +624,17 @@ public boolean isWithinEncryptedFolder() {
return isWithinEncryptedFolder;
}

private boolean isGivenLocalPathHasEnabledParent() {
if (mCurrentDir == null) {
return false;
}

final var chosenPath = mCurrentDir.getPath();
final var syncedFolderProvider = new SyncedFolderProvider(getContentResolver(), preferences, clock);
final var syncedFolders = syncedFolderProvider.getSyncedFolders();
return SyncedFolderExtensionsKt.hasEnabledParent(syncedFolders, chosenPath);
}

/**
* Performs corresponding action when user presses 'Cancel' or 'Upload' button
* <p>
Expand All @@ -639,7 +659,11 @@ public void onClick(View v) {
}
setResult(RESULT_OK, data);

finish();
if (isGivenLocalPathHasEnabledParent()) {
showSubFolderWarningDialog();
} else {
finish();
}
} else {
String[] selectedFilePaths = mFileListFragment.getCheckedFilePaths();
boolean isPositionZero = (binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0);
Expand All @@ -651,6 +675,39 @@ public void onClick(View v) {
}
}

private void showSubFolderWarningDialog() {
final var dialog = ConfirmationDialogFragment.newInstance(
R.string.auto_upload_sub_folder_warning,
null,
R.string.sync_duplication,
R.drawable.ic_info,
R.string.sync_anyway,
R.string.common_cancel,
-1);

dialog.setOnConfirmationListener(new ConfirmationDialogFragmentListener() {
@Override
public void onConfirmation(@Nullable String callerTag) {
finish();
}

@Override
public void onNeutral(@Nullable String callerTag) {

}

@Override
public void onCancel(@Nullable String callerTag) {

}
});

final var isDialogFragmentReady = ActivityExtensionsKt.isDialogFragmentReady(this, dialog);
if (isDialogFragmentReady) {
dialog.show(getSupportFragmentManager(), SUB_FOLDER_WARNING_DIALOG_TAG);
}
}

@Override
public void onConfirmation(String callerTag) {
Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.PopupMenu
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter
import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.core.Clock
import com.nextcloud.utils.extensions.filterEnabledOrWithoutEnabledParent
import com.nextcloud.utils.extensions.hasEnabledParent
import com.nextcloud.utils.extensions.setVisibleIf
import com.owncloud.android.R
import com.owncloud.android.databinding.GridSyncItemBinding
import com.owncloud.android.databinding.SyncedFoldersEmptyBinding
Expand All @@ -39,7 +43,7 @@ import java.util.concurrent.Executors
/**
* Adapter to display all auto-synced folders and/or instant upload media folders.
*/
@Suppress("LongParameterList")
@Suppress("LongParameterList", "TooManyFunctions")
class SyncedFolderAdapter(
private val context: Context,
private val clock: Clock,
Expand Down Expand Up @@ -71,11 +75,12 @@ class SyncedFolderAdapter(
}
}

fun setSyncFolderItems(syncFolderItems: List<SyncedFolderDisplayItem>) {
this.syncFolderItems.clear()
this.syncFolderItems.addAll(syncFolderItems)
fun setSyncFolderItems(newList: List<SyncedFolderDisplayItem>) {
val filteredList = newList.filterEnabledOrWithoutEnabledParent()
syncFolderItems.clear()
syncFolderItems.addAll(filteredList)

filterHiddenItems(this.syncFolderItems, hideItems)?.let {
filterHiddenItems(syncFolderItems, hideItems)?.let {
filteredSyncFolderItems.clear()
filteredSyncFolderItems.addAll(it)
}
Expand Down Expand Up @@ -283,6 +288,23 @@ class SyncedFolderAdapter(
)
}
}

initSubFolderWarningButton(holder, section)
}
}

private fun initSubFolderWarningButton(holder: HeaderViewHolder, section: Int) {
val syncFolderItem = filteredSyncFolderItems[section]
val isGivenLocalPathHasEnabledParent =
filteredSyncFolderItems.hasEnabledParent(syncFolderItem.localPath)
holder.binding.subFolderWarningButton.run {
setVisibleIf(isGivenLocalPathHasEnabledParent)
if (isVisible) {
viewThemeUtils.platform.themeImageButton(this)
setOnClickListener {
clickListener.showSubFolderWarningDialog()
}
}
}
}

Expand Down Expand Up @@ -421,6 +443,7 @@ class SyncedFolderAdapter(
fun onSyncStatusToggleClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?)
fun onSyncFolderSettingsClick(section: Int, syncedFolderDisplayItem: SyncedFolderDisplayItem?)
fun onVisibilityToggleClick(section: Int, item: SyncedFolderDisplayItem?)
fun showSubFolderWarningDialog()
}

internal class HeaderViewHolder(var binding: SyncedFoldersItemHeaderBinding) : SectionedViewHolder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
Expand Down Expand Up @@ -60,6 +61,10 @@ open class ConfirmationDialogFragment : DialogFragment(), Injectable {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val messageArguments = requireArguments().getStringArray(ARG_MESSAGE_ARGUMENTS) ?: arrayOf<String>()
val titleId = requireArguments().getInt(ARG_TITLE_ID, -1)

val defaultTitleIconId = com.owncloud.android.R.drawable.ic_warning
val titleIconId = requireArguments().getInt(ARG_TITLE_ICON_ID, defaultTitleIconId)

val messageId = requireArguments().getInt(ARG_MESSAGE_RESOURCE_ID, -1)
val positiveButtonTextId = requireArguments().getInt(ARG_POSITIVE_BTN_RES, -1)
val negativeButtonTextId = requireArguments().getInt(ARG_NEGATIVE_BTN_RES, -1)
Expand All @@ -69,10 +74,21 @@ open class ConfirmationDialogFragment : DialogFragment(), Injectable {
val message = getString(messageId, *messageArguments)

val builder = MaterialAlertDialogBuilder(requireActivity())
.setIcon(com.owncloud.android.R.drawable.ic_warning)
.setIconAttribute(R.attr.alertDialogIcon)
.setMessage(message)

if (titleIconId == defaultTitleIconId) {
builder
.setIcon(titleIconId)
.setIconAttribute(R.attr.alertDialogIcon)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only setIconAttribute for defaultTitleIconId otherwise it will override given titleIconId

} else {
val icon = ContextCompat.getDrawable(requireContext(), titleIconId)?.apply {
setTint(requireContext().getColor(com.owncloud.android.R.color.text_color))
}

builder
.setIcon(icon)
}

if (titleId == 0) {
builder.setTitle(R.string.dialog_alert_title)
} else if (titleId != -1) {
Expand Down Expand Up @@ -113,6 +129,7 @@ open class ConfirmationDialogFragment : DialogFragment(), Injectable {
const val ARG_MESSAGE_RESOURCE_ID = "resource_id"
const val ARG_MESSAGE_ARGUMENTS = "string_array"
const val ARG_TITLE_ID = "title_id"
const val ARG_TITLE_ICON_ID = "title_icon_id"
const val ARG_POSITIVE_BTN_RES = "positive_btn_res"
const val ARG_NEUTRAL_BTN_RES = "neutral_btn_res"
const val ARG_NEGATIVE_BTN_RES = "negative_btn_res"
Expand All @@ -125,31 +142,39 @@ open class ConfirmationDialogFragment : DialogFragment(), Injectable {
* @param messageArguments Arguments to complete the message, if it's a format string. May be null.
* @param titleResId Resource id for a text to show in the title. 0 for default alert title, -1 for no
* title.
* @param titleIconId Resource id for a icon to show in the dialog.
* @param positiveButtonTextId Resource id for the text of the positive button. -1 for no positive button.
* @param neutralButtonTextId Resource id for the text of the neutral button. -1 for no neutral button.
* @param negativeButtonTextId Resource id for the text of the negative button. -1 for no negative button.
* @return Dialog ready to show.
*/
@Suppress("LongParameterList")
@JvmStatic
@JvmOverloads
fun newInstance(
messageResId: Int,
messageArguments: Array<String?>?,
titleResId: Int,
titleIconId: Int = com.owncloud.android.R.drawable.ic_warning,
positiveButtonTextId: Int,
negativeButtonTextId: Int,
neutralButtonTextId: Int
): ConfirmationDialogFragment {
check(messageResId != -1) { "Calling confirmation dialog without message resource" }
val frag = ConfirmationDialogFragment()
val args = Bundle()
args.putInt(ARG_MESSAGE_RESOURCE_ID, messageResId)
args.putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments)
args.putInt(ARG_TITLE_ID, titleResId)
args.putInt(ARG_POSITIVE_BTN_RES, positiveButtonTextId)
args.putInt(ARG_NEGATIVE_BTN_RES, negativeButtonTextId)
args.putInt(ARG_NEUTRAL_BTN_RES, neutralButtonTextId)
frag.arguments = args
return frag

val bundle = Bundle().apply {
putInt(ARG_MESSAGE_RESOURCE_ID, messageResId)
putStringArray(ARG_MESSAGE_ARGUMENTS, messageArguments)
putInt(ARG_TITLE_ID, titleResId)
putInt(ARG_TITLE_ICON_ID, titleIconId)
putInt(ARG_POSITIVE_BTN_RES, positiveButtonTextId)
putInt(ARG_NEGATIVE_BTN_RES, negativeButtonTextId)
putInt(ARG_NEUTRAL_BTN_RES, neutralButtonTextId)
}

return ConfirmationDialogFragment().apply {
arguments = bundle
}
}
}
}
Loading
Loading