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/UploadWarningCard.kt b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt new file mode 100644 index 000000000000..423f20235840 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/component/UploadWarningCard.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.component + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.PowerManager +import android.provider.Settings +import android.view.View +import androidx.core.net.toUri +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.databinding.UploadWarningCardBinding +import com.owncloud.android.utils.theme.ViewThemeUtils + +class UploadWarningCard( + private val context: Context, + private val powerManagementService: PowerManagementService, + private val viewThemeUtils: ViewThemeUtils +) { + fun bind(binding: UploadWarningCardBinding) { + val isBatterySaver = powerManagementService.isPowerSavingEnabled + val isIgnoringOptimization = powerManagementService.isIgnoringOptimization + + binding.root.setVisibleIf(isBatterySaver || !isIgnoringOptimization) + + if (isBatterySaver) { + viewThemeUtils.material.themeCardView(binding.batterySaverLayout) + binding.batterySaverLayout.visibility = View.VISIBLE + binding.batterySaverButton.setOnClickListener { + openBatterySaverPage() + } + } else { + binding.batterySaverLayout.visibility = View.GONE + } + + if (!isIgnoringOptimization) { + viewThemeUtils.material.themeCardView(binding.backgroundActivityLimitedLayout) + binding.backgroundActivityLimitedLayout.visibility = View.VISIBLE + binding.backgroundActivityLimitedButton.setOnClickListener { + showIgnoreBatteryOptimizationDialog() + } + } else { + binding.backgroundActivityLimitedLayout.visibility = View.GONE + } + } + + // region listen power mode changes + private var binding: UploadWarningCardBinding? = 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: UploadWarningCardBinding) { + 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 + + /** + * Opens page for OS's battery saver screen. + */ + private fun openBatterySaverPage() { + context.startActivity(Intent(Settings.ACTION_BATTERY_SAVER_SETTINGS)) + } + + /** + * Shows dialog to allow background usage for app. + */ + @SuppressLint("BatteryLife") + private fun showIgnoreBatteryOptimizationDialog() { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + intent.data = "package:${context.packageName}".toUri() + context.startActivity(intent) + } +} 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..ad2d9f4b4a7e 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.UploadWarningCard 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 uploadWarningCard: UploadWarningCard? = 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) + uploadWarningCard = UploadWarningCard(this, powerManagementService, viewThemeUtils) if (intent != null && intent.extras != null) { val accountName = intent.extras!!.getString(NotificationWork.KEY_NOTIFICATION_ACCOUNT) val optionalUser = user @@ -207,6 +211,7 @@ class SyncedFoldersActivity : override fun onResume() { super.onResume() highlightNavigationViewItem(menuItemId) + uploadWarningCard?.bind(binding.autoUploadBatterySaverWarningCard) } fun setupStoragePermissionWarningBanner() { @@ -256,6 +261,8 @@ class SyncedFoldersActivity : powerManagementService, connectivityService ) + uploadWarningCard?.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() + uploadWarningCard?.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..5755eca72413 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.UploadWarningCard 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 uploadWarningCard: UploadWarningCard? = 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()) + uploadWarningCard = UploadWarningCard(this, 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,10 @@ class UploadListActivity : adapterHelper ) + binding?.autoUploadBatterySaverWarningCard?.let { + uploadWarningCard?.register(this, it) + } + val lm = GridLayoutManager(this, 1) uploadListAdapter.setLayoutManager(lm) @@ -256,6 +264,13 @@ class UploadListActivity : } } + override fun onResume() { + super.onResume() + binding?.autoUploadBatterySaverWarningCard?.let { + uploadWarningCard?.bind(it) + } + } + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { if (operation !is CheckCurrentCredentialsOperation) { super.onRemoteOperationFinish(operation, result) @@ -368,6 +383,11 @@ class UploadListActivity : } } + override fun onDestroy() { + super.onDestroy() + uploadWarningCard?.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..126af0068702 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 @@ -263,13 +263,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 a7b2c3c776bb..1f60d8558591 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 deleted file mode 100644 index 83274c0eda75..000000000000 --- a/app/src/main/res/layout/auto_upload_battery_saver_warning_banner.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - \ 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..ed23628e89a6 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/layout/upload_warning_card.xml b/app/src/main/res/layout/upload_warning_card.xml new file mode 100644 index 000000000000..ecef4c5986a0 --- /dev/null +++ b/app/src/main/res/layout/upload_warning_card.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fac148f5684..e8464c0c43c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1482,8 +1482,11 @@ This folder is best viewed in %1$s. Open in %1$s Delete auto-upload folder? + Battery Saver is active and may pause uploads + Nextcloud\'s background activity is limited by battery optimization + Open battery saver settings + Allow This will remove the folder and auto-upload configuration. Any unfinished uploads will be canceled. - Auto-upload is paused because Battery Saver is on. This folder is already included in the parent folder’s sync, which may cause duplicate uploads Sync anyway Sync duplication