diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index ac58ccc05fc9..7b1f2d615454 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -393,6 +393,11 @@ public Connectivity getConnectivity() { }; PowerManagementService powerManagementServiceMock = new PowerManagementService() { + @Override + public boolean isIgnoringOptimization() { + return true; + } + @NonNull @Override public BatteryStatus getBattery() { diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index 1b0e1b8d3c17..94bf2bddc287 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -225,6 +225,11 @@ public Connectivity getConnectivity() { }; PowerManagementService powerManagementServiceMock = new PowerManagementService() { + @Override + public boolean isIgnoringOptimization() { + return true; + } + @NonNull @Override public BatteryStatus getBattery() { diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index 8072bb5c1805..36bc91ce8c4a 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -78,6 +78,11 @@ public Connectivity getConnectivity() { }; private PowerManagementService powerManagementServiceMock = new PowerManagementService() { + @Override + public boolean isIgnoringOptimization() { + return true; + } + @Override public boolean isPowerSavingEnabled() { return false; @@ -226,6 +231,11 @@ public void testUploadOnChargingOnlyButNotCharging() { @Test public void testUploadOnChargingOnlyAndCharging() { PowerManagementService powerManagementServiceMock = new PowerManagementService() { + @Override + public boolean isIgnoringOptimization() { + return true; + } + @Override public boolean isPowerSavingEnabled() { return false; diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt index 00c568d506ad..5367c66e988e 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt @@ -43,6 +43,9 @@ abstract class FileUploaderIT : AbstractOnServerIT() { } private val powerManagementServiceMock: PowerManagementService = object : PowerManagementService { + override val isIgnoringOptimization: Boolean + get() = true + override val isPowerSavingEnabled: Boolean get() = false diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt index 730ca4ec728a..5b51acaf0518 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt @@ -21,6 +21,11 @@ interface PowerManagementService { */ val isPowerSavingEnabled: Boolean + /** + * Checks app is excluded from battery optimization or not + */ + val isIgnoringOptimization: Boolean + /** * Checks current battery status using platform [android.os.BatteryManager] */ diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index 3c8d56c280de..8d3c9ad89645 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -27,6 +27,12 @@ internal class PowerManagementServiceImpl( } } + override val isIgnoringOptimization: Boolean + get() { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + override val isPowerSavingEnabled: Boolean get() { return platformPowerManager.isPowerSaveMode diff --git a/app/src/main/java/com/nextcloud/ui/component/AutoUploadWarningCardManager.kt b/app/src/main/java/com/nextcloud/ui/component/AutoUploadWarningCardManager.kt new file mode 100644 index 000000000000..d900c09c8f07 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/component/AutoUploadWarningCardManager.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.component + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import android.view.View +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.databinding.AutoUploadBatterySaverWarningBannerBinding +import com.owncloud.android.utils.theme.ViewThemeUtils + +class AutoUploadWarningCardManager( + private val powerManagementService: PowerManagementService, + private val viewThemeUtils: ViewThemeUtils +) { + fun bind(binding: AutoUploadBatterySaverWarningBannerBinding) { + val isBatterySaver = powerManagementService.isPowerSavingEnabled + val isIgnoringOptimization = powerManagementService.isIgnoringOptimization + + binding.root.setVisibleIf(isBatterySaver || isIgnoringOptimization) + + if (isBatterySaver && isIgnoringOptimization) { + binding.title.visibility = View.VISIBLE + binding.batterySaverReason.visibility = View.VISIBLE + binding.batteryOptimizationReason.visibility = View.VISIBLE + } else if (isBatterySaver) { + binding.title.visibility = View.VISIBLE + binding.batterySaverReason.visibility = View.VISIBLE + } else if (isIgnoringOptimization) { + binding.title.visibility = View.VISIBLE + binding.batteryOptimizationReason.visibility = View.VISIBLE + } + + viewThemeUtils.material.themeCardView(binding.root) + } + + // region listen power mode changes + private var binding: AutoUploadBatterySaverWarningBannerBinding? = null + + private val batterySaverReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) { + binding?.let { bind(it) } + } + } + } + + fun register(context: Context, binding: AutoUploadBatterySaverWarningBannerBinding) { + this.binding = binding + bind(binding) + context.registerReceiver(batterySaverReceiver, IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) + } + + fun unregister(context: Context) { + context.unregisterReceiver(batterySaverReceiver) + binding = null + } + // endregion +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt index 9dd1d31c95a7..ac7b3f30b165 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.kt @@ -33,6 +33,7 @@ import com.nextcloud.client.jobs.MediaFoldersDetectionWork import com.nextcloud.client.jobs.NotificationWork import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.SubFolderRule +import com.nextcloud.ui.component.AutoUploadWarningCardManager import com.nextcloud.utils.BatteryOptimizationHelper import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.isDialogFragmentReady @@ -152,6 +153,8 @@ class SyncedFoldersActivity : @Inject lateinit var appInfo: AppInfo + private var autoUploadWarningCardManager: AutoUploadWarningCardManager? = null + lateinit var binding: SyncedFoldersLayoutBinding lateinit var adapter: SyncedFolderAdapter @@ -163,6 +166,7 @@ class SyncedFoldersActivity : super.onCreate(savedInstanceState) binding = SyncedFoldersLayoutBinding.inflate(layoutInflater) setContentView(binding.root) + autoUploadWarningCardManager = AutoUploadWarningCardManager(powerManagementService, viewThemeUtils) if (intent != null && intent.extras != null) { val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) val optionalUser = user @@ -256,6 +260,9 @@ class SyncedFoldersActivity : powerManagementService, connectivityService ) + autoUploadWarningCardManager?.bind(binding.autoUploadBatterySaverWarningCard) + autoUploadWarningCardManager?.register(this, binding.autoUploadBatterySaverWarningCard) + binding.emptyList.emptyListIcon.setImageResource(R.drawable.nav_synced_folders) viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.emptyList.emptyListViewAction) val lm = GridLayoutManager(this, gridWidth) @@ -275,6 +282,11 @@ class SyncedFoldersActivity : } } + override fun onDestroy() { + super.onDestroy() + autoUploadWarningCardManager?.unregister(this) + } + /** * loads all media/synced folders, adds them to the recycler view adapter and shows the list. * diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index cb6c70581622..36958c31d101 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -28,6 +28,7 @@ import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.utils.UploadErrorNotificationManager import com.nextcloud.client.utils.Throttler +import com.nextcloud.ui.component.AutoUploadWarningCardManager import com.nextcloud.utils.extensions.webDavParentPath import com.owncloud.android.R import com.owncloud.android.databinding.UploadListLayoutBinding @@ -72,6 +73,8 @@ class UploadListActivity : @Inject lateinit var uploadFileOperationFactory: UploadFileOperationFactory + private var autoUploadWarningCardManager: AutoUploadWarningCardManager? = null + private var swipeListRefreshLayout: SwipeRefreshLayout? = null private var binding: UploadListLayoutBinding? = null @@ -87,6 +90,7 @@ class UploadListActivity : binding = UploadListLayoutBinding.inflate(layoutInflater) val binding = binding!! setContentView(binding.getRoot()) + autoUploadWarningCardManager = AutoUploadWarningCardManager(powerManagementService, viewThemeUtils) swipeListRefreshLayout = binding.swipeContainingList // this activity has no file really bound, it's for multiple accounts at the same time; should no inherit @@ -117,6 +121,11 @@ class UploadListActivity : adapterHelper ) + binding?.autoUploadBatterySaverWarningCard?.let { + autoUploadWarningCardManager?.register(this, it) + autoUploadWarningCardManager?.bind(it) + } + val lm = GridLayoutManager(this, 1) uploadListAdapter.setLayoutManager(lm) @@ -368,6 +377,11 @@ class UploadListActivity : } } + override fun onDestroy() { + super.onDestroy() + autoUploadWarningCardManager?.unregister(this) + } + companion object { private val TAG: String = UploadListActivity::class.java.getSimpleName() diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt index 050674feae5b..ebf7460b2cab 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/SyncedFolderAdapter.kt @@ -25,6 +25,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.ui.component.AutoUploadWarningCardManager import com.nextcloud.utils.extensions.calculateScanInterval import com.nextcloud.utils.extensions.filterEnabledOrWithoutEnabledParent import com.nextcloud.utils.extensions.hasEnabledParent @@ -263,13 +264,6 @@ class SyncedFolderAdapter( holder.binding.run { headerContainer.visibility = View.VISIBLE - if (section == 0) { - autoUploadBatterySaverWarningCard.root.run { - setVisibleIf(powerManagementService.isPowerSavingEnabled) - viewThemeUtils.material.themeCardView(this) - } - } - val syncedFolder = filteredSyncFolderItems[section] title.text = syncedFolder.folderName diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index 33b0a835cad1..3808c21a1400 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -102,7 +102,6 @@ class UploadListAdapter( bindHeaderTitle(headerViewHolder, group, section) bindHeaderActionButton(headerViewHolder, group) - bindHeaderBatterySaverWarning(headerViewHolder) bindHeaderActionClickListener(headerViewHolder, group) } @@ -130,12 +129,6 @@ class UploadListAdapter( holder.binding.uploadListAction.setImageResource(iconRes) } - private fun bindHeaderBatterySaverWarning(holder: HeaderViewHolder) { - holder.binding.autoUploadBatterySaverWarningCard.root - .setVisibleIf(powerManagementService.isPowerSavingEnabled) - viewThemeUtils.material.themeCardView(holder.binding.autoUploadBatterySaverWarningCard.root) - } - private fun bindHeaderActionClickListener(holder: HeaderViewHolder, group: UploadListSection) { holder.binding.uploadListAction.setOnClickListener { when (group.type) { diff --git a/app/src/main/res/layout/auto_upload_battery_saver_warning_banner.xml b/app/src/main/res/layout/auto_upload_battery_saver_warning_banner.xml index 83274c0eda75..616df3c5c854 100644 --- a/app/src/main/res/layout/auto_upload_battery_saver_warning_banner.xml +++ b/app/src/main/res/layout/auto_upload_battery_saver_warning_banner.xml @@ -5,8 +5,7 @@ ~ SPDX-FileCopyrightText: 2025 Alper Ozturk ~ SPDX-License-Identifier: AGPL-3.0-or-later --> - + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + android:orientation="vertical"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/storage_permission_warning_banner.xml b/app/src/main/res/layout/storage_permission_warning_banner.xml index 993114db9a0e..f42b5a4fd6a3 100644 --- a/app/src/main/res/layout/storage_permission_warning_banner.xml +++ b/app/src/main/res/layout/storage_permission_warning_banner.xml @@ -32,7 +32,7 @@ diff --git a/app/src/main/res/layout/synced_folders_item_header.xml b/app/src/main/res/layout/synced_folders_item_header.xml index 983c109f1f24..071ca508e3da 100644 --- a/app/src/main/res/layout/synced_folders_item_header.xml +++ b/app/src/main/res/layout/synced_folders_item_header.xml @@ -20,22 +20,6 @@ android:paddingStart="@dimen/standard_quarter_padding" android:paddingTop="@dimen/alternate_half_padding"> - - + + - - diff --git a/app/src/main/res/layout/upload_list_layout.xml b/app/src/main/res/layout/upload_list_layout.xml index 8dfbe1fb69d4..3a69b6643177 100755 --- a/app/src/main/res/layout/upload_list_layout.xml +++ b/app/src/main/res/layout/upload_list_layout.xml @@ -22,10 +22,20 @@ + + + android:layout_below="@id/auto_upload_battery_saver_warning_card"> + + android:layout_height="match_parent" + android:padding="@dimen/standard_half_padding" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 694f06afbf28..c0762f04cde4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1483,7 +1483,9 @@ Open in %1$s Delete auto-upload folder? This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. - Auto-upload is paused because Battery Saver is on. + Auto-upload may be limited: + \t \u2022 Battery Saver is active and may pause uploads + \t \u2022 Nextcloud\'s background activity is limited by battery optimization This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway Sync duplication