Make device deletion async

This commit is contained in:
José Rebelo
2026-01-06 23:20:58 +01:00
committed by José Rebelo
parent d037be0303
commit 6aaf6d50e8
6 changed files with 208 additions and 35 deletions
+5 -1
View File
@@ -724,7 +724,11 @@
android:name=".activities.BatteryInfoActivity"
android:label="@string/battery_detail_activity_title"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:exported="false"
android:name=".activities.DeviceDeleteActivity"
android:label="@string/controlcenter_delete_device"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.workouts.WorkoutDetailsActivity"
android:label="@string/activity_summary_detail"
@@ -0,0 +1,145 @@
/* Copyright (C) 2026 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities
import android.content.Intent
import android.content.pm.ShortcutManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityDeviceDeleteBinding
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil
import nodomain.freeyourgadget.gadgetbridge.util.GB
import org.slf4j.LoggerFactory
import kotlin.properties.Delegates
class DeviceDeleteActivity : AbstractGBActivity() {
private lateinit var binding: ActivityDeviceDeleteBinding
private lateinit var device: GBDevice
private var deleteFiles by Delegates.notNull<Boolean>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDeviceDeleteBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(EXTRA_DEVICE, GBDevice::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(EXTRA_DEVICE)
}!!
deleteFiles = intent.getBooleanExtra(EXTRA_DELETE_FILES, true)
title = getString(R.string.controlcenter_delete_device_name, device.aliasOrName)
binding.deleteDeviceName.text = getString(R.string.controlcenter_delete_device_name, device.aliasOrName)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setHomeButtonEnabled(false)
startDeviceDelete()
}
private fun startDeviceDelete() {
binding.deleteProgressBar.visibility = View.VISIBLE
binding.deleteProgressBar.keepScreenOn = true
lifecycleScope.launch {
val exception = performDeviceDelete()
handleDeleteResult(exception)
}
}
private suspend fun performDeviceDelete(): Exception? = withContext(Dispatchers.IO) {
try {
val coordinator = device.deviceCoordinator
// Delete device and files
coordinator.deleteDevice(device, deleteFiles)
// Unpair Bluetooth device
BondingUtil.Unpair(this@DeviceDeleteActivity, device.address)
// Remove dynamic shortcut (Android R+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
removeDynamicShortcut(device)
}
} catch (ex: Exception) {
LOG.error("Error deleting device", ex)
return@withContext ex
}
return@withContext null
}
@RequiresApi(api = Build.VERSION_CODES.R)
fun removeDynamicShortcut(device: GBDevice) {
val shortcutManager = applicationContext.getSystemService(SHORTCUT_SERVICE) as ShortcutManager
shortcutManager.removeDynamicShortcuts(mutableListOf<String?>(device.address))
}
private fun handleDeleteResult(exception: Exception?) {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
binding.deleteProgressBar.keepScreenOn = false
binding.deleteProgressBar.visibility = View.GONE
// Refresh device list
val refreshIntent = Intent(DeviceManager.ACTION_REFRESH_DEVICELIST)
LocalBroadcastManager.getInstance(this).sendBroadcast(refreshIntent)
if (exception == null) {
GB.toast(
this,
getString(R.string.device_deleted_successfully),
Toast.LENGTH_SHORT,
GB.INFO
)
finish()
} else {
val errorMsg = getString(
R.string.error_deleting_device,
exception.localizedMessage ?: "Unknown error"
)
binding.deleteStatusText.text = errorMsg
}
}
private fun updateStatus(msg: String) {
binding.deleteStatusText.text = msg
}
companion object {
private val LOG = LoggerFactory.getLogger(DeviceDeleteActivity::class.java)
const val EXTRA_DEVICE = "device"
const val EXTRA_DELETE_FILES = "delete_files"
}
}
@@ -102,6 +102,7 @@ import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DeviceDeleteActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutListActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.BatteryInfoActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
@@ -128,7 +129,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FormatUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -1006,19 +1006,10 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
}
private void removeDevice(GBDevice device, boolean deleteFiles) {
try {
DeviceCoordinator coordinator = device.getDeviceCoordinator();
coordinator.deleteDevice(device, deleteFiles);
BondingUtil.Unpair(context, device.getAddress());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
removeDynamicShortcut(device);
}
} catch (Exception ex) {
GB.toast(context, context.getString(R.string.error_deleting_device, ex.getLocalizedMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
} finally {
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
}
final Intent intent = new Intent(context, DeviceDeleteActivity.class);
intent.putExtra(DeviceDeleteActivity.EXTRA_DEVICE, device);
intent.putExtra(DeviceDeleteActivity.EXTRA_DELETE_FILES, deleteFiles);
context.startActivity(intent);
}
private boolean deviceHasFiles(final GBDevice device) {
@@ -1598,13 +1589,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
);
}
@RequiresApi(api = Build.VERSION_CODES.R)
void removeDynamicShortcut(GBDevice device) {
final ShortcutManager shortcutManager = (ShortcutManager) context.getApplicationContext().getSystemService(Context.SHORTCUT_SERVICE);
shortcutManager.removeDynamicShortcuts(Collections.singletonList(device.getAddress()));
}
/**
* A generator of stable IDs, given a string, since hashCode can easily have collisions.
*/
@@ -201,7 +201,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
@Override
public final void deleteDevice(final GBDevice gbDevice, boolean deleteFiles) throws GBException {
LOG.info("will try to delete device: {}", gbDevice.getName());
LOG.info("Will try to delete device: {}", gbDevice.getName());
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
GBApplication.deviceService(gbDevice).disconnect();
}
@@ -210,7 +210,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
Set<String> lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet());
if (lastDeviceAddresses.contains(gbDevice.getAddress())) {
LOG.debug("#1605 removing last device (one of last devices)");
lastDeviceAddresses = new HashSet<String>(lastDeviceAddresses);
lastDeviceAddresses = new HashSet<>(lastDeviceAddresses);
lastDeviceAddresses.remove(gbDevice.getAddress());
prefs.getPreferences().edit().putStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, lastDeviceAddresses).apply();
}
@@ -222,14 +222,10 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
Device device = DBHelper.findDevice(gbDevice, session);
if (device != null) {
deleteDevice(gbDevice, device, session);
QueryBuilder<?> qb = session.getDeviceAttributesDao().queryBuilder();
qb.where(DeviceAttributesDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> batteryLevelQueryBuilder = session.getBatteryLevelDao().queryBuilder();
batteryLevelQueryBuilder.where(BatteryLevelDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> alarmDeviceQueryBuilder = session.getAlarmDao().queryBuilder();
alarmDeviceQueryBuilder.where(AlarmDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
QueryBuilder<?> healthSyncStateQueryBuilder = session.getHealthConnectSyncStateDao().queryBuilder();
healthSyncStateQueryBuilder.where(HealthConnectSyncStateDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
deleteBy(session.getDeviceAttributesDao(), DeviceAttributesDao.Properties.DeviceId, device.getId());
deleteBy(session.getBatteryLevelDao(), BatteryLevelDao.Properties.DeviceId, device.getId());
deleteBy(session.getAlarmDao(), AlarmDao.Properties.DeviceId, device.getId());
deleteBy(session.getHealthConnectSyncStateDao(), HealthConnectSyncStateDao.Properties.DeviceId, device.getId());
session.getDeviceDao().delete(device);
} else {
LOG.info("device to delete not found in db: {}", gbDevice);
@@ -243,8 +239,14 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
}
}
protected void deleteBy(final AbstractDao<?, ?> dao, final Property property, final Object value) {
LOG.debug("Deleting from {} where {}={}", dao.getTablename(), property.columnName, value);
dao.queryBuilder().where(property.eq(value)).buildDelete().executeDeleteWithoutDetachingEntities();
}
private void deleteDeviceFiles(final GBDevice gbDevice) {
LOG.debug("Deleting device files for {}", gbDevice);
File export = new File("(export)");
try {
export = getWritableExportDirectory(gbDevice, false);
@@ -275,9 +277,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
final Map<AbstractDao<?, ?>, Property> daoMap = getAllDeviceDao(session);
for (final Map.Entry<AbstractDao<?, ?>, Property> e : daoMap.entrySet()) {
e.getKey().queryBuilder()
.where(e.getValue().eq(deviceId))
.buildDelete().executeDeleteWithoutDetachingEntities();
deleteBy(e.getKey(), e.getValue(), deviceId);
}
}
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.DeviceDeleteActivity">
<ProgressBar
android:id="@+id/delete_progress_bar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:indeterminate="true" />
<TextView
android:id="@+id/delete_device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="Device name" />
<TextView
android:id="@+id/delete_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/device_delete_in_progress"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
+2
View File
@@ -3515,6 +3515,8 @@
<string name="controlcenter_connected_fraction">Connected: %1$d/%2$d</string>
<string name="error_setting_parent_folder">Error setting parent folder: %s</string>
<string name="error_deleting_device">Error deleting device: %s</string>
<string name="device_delete_in_progress">Deleting device…</string>
<string name="device_deleted_successfully">Device deleted successfully</string>
<string name="controlcenter_folder_name">Folder name:</string>
<string name="controlcenter_add_new_folder">Add new folder</string>
<string name="unset">Unset</string>