diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt index e8e62350..85be609e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt @@ -2,12 +2,12 @@ package io.rebble.cobble.bluetooth sealed class ConnectionState { object Disconnected : ConnectionState() - class WaitingForBluetoothToEnable(val watch: PebbleDevice?) : ConnectionState() - class WaitingForReconnect(val watch: PebbleDevice?) : ConnectionState() - class Connecting(val watch: PebbleDevice?) : ConnectionState() - class Negotiating(val watch: PebbleDevice?) : ConnectionState() - class Connected(val watch: PebbleDevice) : ConnectionState() - class RecoveryMode(val watch: PebbleDevice) : ConnectionState() + data class WaitingForBluetoothToEnable(val watch: PebbleDevice?) : ConnectionState() + data class WaitingForReconnect(val watch: PebbleDevice?) : ConnectionState() + data class Connecting(val watch: PebbleDevice?) : ConnectionState() + data class Negotiating(val watch: PebbleDevice?) : ConnectionState() + data class Connected(val watch: PebbleDevice) : ConnectionState() + data class RecoveryMode(val watch: PebbleDevice) : ConnectionState() } val ConnectionState.watchOrNull: PebbleDevice? diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt index cea5f718..1e8091cd 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt @@ -14,7 +14,7 @@ class DebugFlutterBridge @Inject constructor( bridgeLifecycleController.setupControl(Pigeons.DebugControl::setup, this) } - override fun collectLogs() { - collectAndShareLogs(context) + override fun collectLogs(rwsId: String) { + collectAndShareLogs(context, rwsId) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index 058855dc..473c2541 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -1,9 +1,13 @@ package io.rebble.cobble.log +import android.companion.CompanionDeviceManager import android.content.ClipData import android.content.Context import android.content.Intent +import android.os.Build import androidx.core.content.FileProvider +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.bluetooth.watchOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -13,20 +17,57 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Calendar +import java.util.TimeZone import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream +private fun generateDebugInfo(context: Context, rwsId: String): String { + val sdkVersion = Build.VERSION.SDK_INT + val device = Build.DEVICE + val model = Build.MODEL + val product = Build.PRODUCT + val manufacturer = Build.MANUFACTURER + + val inj = (context.applicationContext as CobbleApplication).component + val connectionLooper = inj.createConnectionLooper() + val connectionState = connectionLooper.connectionState.value + + val associatedDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) + deviceManager.associations + } else { + null + } + return """ + SDK Version: $sdkVersion + Device: $device + Model: $model + Product: $product + Manufacturer: $manufacturer + Connection State: $connectionState + Associated devices: $associatedDevices + RWS ID: + $rwsId + """.trimIndent() +} + /** * This should be eventually moved to flutter. Written it in Kotlin for now so we can use it while * testing other things. */ -fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { +fun collectAndShareLogs(context: Context, rwsId: String) = GlobalScope.launch(Dispatchers.IO) { val logsFolder = File(context.cacheDir, "logs") - - val targetFile = File(logsFolder, "logs.zip") + val date = LocalDateTime.now(ZoneId.of("UTC")).format(DateTimeFormatter.ISO_DATE_TIME) + val targetFile = File(logsFolder, "logs-${date}.zip") var zipOutputStream: ZipOutputStream? = null + val debugInfo = generateDebugInfo(context, rwsId) try { zipOutputStream = ZipOutputStream(FileOutputStream(targetFile)) for (file in logsFolder.listFiles() ?: emptyArray()) { @@ -44,6 +85,9 @@ fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { inputStream.close() zipOutputStream.closeEntry() } + zipOutputStream.putNextEntry(ZipEntry("debug_info.txt")) + zipOutputStream.write(debugInfo.toByteArray()) + zipOutputStream.closeEntry() } catch (e: Exception) { Timber.e(e, "Zip writing error") } finally { @@ -63,9 +107,9 @@ fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { activityIntent.putExtra(Intent.EXTRA_STREAM, targetUri) activityIntent.setType("application/octet-stream") - activityIntent.setClipData(ClipData.newUri(context.getContentResolver(), + activityIntent.clipData = ClipData.newUri(context.contentResolver, "Cobble Logs", - targetUri)) + targetUri) activityIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index 9c6a027e..b18e3eed 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -4248,7 +4248,7 @@ public void error(Throwable error) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DebugControl { - void collectLogs(); + void collectLogs(@NonNull String rwsId); /** The codec used by DebugControl. */ static @NonNull MessageCodec getCodec() { @@ -4264,8 +4264,10 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable DebugContr channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String rwsIdArg = (String) args.get(0); try { - api.collectLogs(); + api.collectLogs(rwsIdArg); wrapped.add(0, null); } catch (Throwable exception) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 8f8e2281..50fce97f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -2,7 +2,9 @@ package io.rebble.cobble.bluetooth import android.Manifest import android.bluetooth.BluetoothDevice +import android.content.pm.PackageManager import androidx.annotation.RequiresPermission +import androidx.core.app.ActivityCompat import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -23,6 +25,15 @@ data class PebbleDevice ( emulated, bluetoothDevice?.address ?: throw IllegalArgumentException() ) + + override fun toString(): String { + val start = "< PebbleDevice emulated=$emulated, address=$address, bluetoothDevice=< BluetoothDevice address=${bluetoothDevice?.address}" + return try { + "$start, name=${bluetoothDevice?.name}, type=${bluetoothDevice?.type} > >" + } catch (e: SecurityException) { + "$start, name=unknown, type=unknown > >" + } + } } sealed class SingleConnectionStatus { diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index b545a9a8..15af7850 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -514,7 +514,7 @@ extern void IntentControlSetup(id binaryMessenger, NSObj NSObject *DebugControlGetCodec(void); @protocol DebugControl -- (void)collectLogsWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)collectLogsRwsId:(NSString *)rwsId error:(FlutterError *_Nullable *_Nonnull)error; @end extern void DebugControlSetup(id binaryMessenger, NSObject *_Nullable api); diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 50204045..44ef6e9c 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -2500,10 +2500,12 @@ void DebugControlSetup(id binaryMessenger, NSObject codec = StandardMessageCodec(); - Future collectLogs() async { + Future collectLogs(String arg_rwsId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DebugControl.collectLogs', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send(null) as List?; + await channel.send([arg_rwsId]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index 1f4aa5f0..d449fea4 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -1,4 +1,7 @@ +import 'package:cobble/domain/api/auth/auth.dart'; +import 'package:cobble/domain/api/auth/user.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; @@ -83,7 +86,22 @@ class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { ), ), CobbleButton( - onPressed: () => debug.collectLogs(), + onPressed: () async { + AuthService auth = await ref.read(authServiceProvider.future); + User user = await auth.user; + String id = user.uid.toString(); + String bootOverrideCount = user.bootOverrides?.length.toString() ?? "0"; + String subscribed = user.isSubscribed.toString(); + String timelineTtl = user.timelineTtl.toString(); + debug.collectLogs( + """ +User ID: $id +Boot override count: $bootOverrideCount +Subscribed: $subscribed +Timeline TTL: $timelineTtl + """, + ); + }, label: "Share application logs", ), ], diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index 1bc65a4b..17ed3430 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -337,7 +337,7 @@ abstract class IntentControl { @HostApi() abstract class DebugControl { - void collectLogs(); + void collectLogs(String rwsId); } @HostApi()