mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Make device deletion async
This commit is contained in:
@@ -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"
|
||||
|
||||
+145
@@ -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"
|
||||
}
|
||||
}
|
||||
+5
-21
@@ -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.
|
||||
*/
|
||||
|
||||
+13
-13
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user