mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
android: add troubleshooter for easier log access
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
|
<protected-broadcast android:name="batterywidget.impl.action.update_bluetooth_data" />
|
||||||
|
|
||||||
@@ -126,6 +127,16 @@
|
|||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ import me.kavishdevar.librepods.screens.HeadTrackingScreen
|
|||||||
import me.kavishdevar.librepods.screens.LongPress
|
import me.kavishdevar.librepods.screens.LongPress
|
||||||
import me.kavishdevar.librepods.screens.Onboarding
|
import me.kavishdevar.librepods.screens.Onboarding
|
||||||
import me.kavishdevar.librepods.screens.RenameScreen
|
import me.kavishdevar.librepods.screens.RenameScreen
|
||||||
|
import me.kavishdevar.librepods.screens.TroubleshootingScreen
|
||||||
import me.kavishdevar.librepods.services.AirPodsService
|
import me.kavishdevar.librepods.services.AirPodsService
|
||||||
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
import me.kavishdevar.librepods.ui.theme.LibrePodsTheme
|
||||||
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
import me.kavishdevar.librepods.utils.AirPodsNotifications
|
||||||
@@ -277,6 +278,9 @@ fun Main() {
|
|||||||
composable("app_settings") {
|
composable("app_settings") {
|
||||||
AppSettingsScreen(navController)
|
AppSettingsScreen(navController)
|
||||||
}
|
}
|
||||||
|
composable("troubleshooting") {
|
||||||
|
TroubleshootingScreen(navController)
|
||||||
|
}
|
||||||
composable("head_tracking") {
|
composable("head_tracking") {
|
||||||
HeadTrackingScreen(navController)
|
HeadTrackingScreen(navController)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,15 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -414,6 +418,24 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { navController.navigate("troubleshooting") },
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (isSystemInDarkTheme()) Color(0xFF1C1C1E) else Color(0xFFF2F2F7),
|
||||||
|
contentColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Troubleshoot Connection",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontFamily = FontFamily(Font(R.font.sf_pro))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -618,6 +618,49 @@ fun AppSettingsScreen(navController: NavController) {
|
|||||||
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
modifier = Modifier.padding(8.dp, bottom = 2.dp, top = 24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
backgroundColor,
|
||||||
|
RoundedCornerShape(14.dp)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
navController.navigate("troubleshooting")
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.troubleshooting),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.troubleshooting_description),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = textColor.copy(0.6f),
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { showResetDialog = true },
|
onClick = { showResetDialog = true },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -804,6 +804,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
notificationManager.notify(1, updatedNotification)
|
notificationManager.notify(1, updatedNotification)
|
||||||
notificationManager.cancel(2)
|
notificationManager.cancel(2)
|
||||||
} else if (!socket.isConnected && isConnectedLocally) {
|
} else if (!socket.isConnected && isConnectedLocally) {
|
||||||
|
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
|
||||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1423,6 +1424,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
fun connectToSocket(device: BluetoothDevice) {
|
fun connectToSocket(device: BluetoothDevice) {
|
||||||
|
Log.d("AirPodsService", "<LogCollector:Start> Connecting to socket")
|
||||||
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;")
|
||||||
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a")
|
||||||
if (isConnectedLocally != true && !CrossDevice.isAvailable) {
|
if (isConnectedLocally != true && !CrossDevice.isAvailable) {
|
||||||
@@ -1446,12 +1448,15 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
config.deviceName,
|
config.deviceName,
|
||||||
batteryNotification.getBattery()
|
batteryNotification.getBattery()
|
||||||
)
|
)
|
||||||
|
Log.d("AirPodsService", "<LogCollector:Complete:Success> Socket connected")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
|
||||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!socket.isConnected) {
|
if (!socket.isConnected) {
|
||||||
|
Log.d("AirPodsService", "<LogCollector:Complete:Failed> Socket not connected")
|
||||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1784,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
} else {
|
} else {
|
||||||
Log.d("AirPodsService", "Can't send packet: Socket not initialized or connected")
|
Log.d("AirPodsService", "Can't send packet: Socket not initialized or connected")
|
||||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AirPodsService", "Error sending packet: ${e.message}")
|
Log.e("AirPodsService", "Error sending packet: ${e.message}")
|
||||||
@@ -1799,7 +1803,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList
|
|||||||
socket.outputStream?.flush()
|
socket.outputStream?.flush()
|
||||||
} else {
|
} else {
|
||||||
Log.d("AirPodsService", "Can't send packet: Socket not initialized or connected")
|
Log.d("AirPodsService", "Can't send packet: Socket not initialized or connected")
|
||||||
showSocketConnectionFailureNotification("Socket created, but not connected. Is the Bluetooth process hooked?")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AirPodsService", "Error sending packet: ${e.message}")
|
Log.e("AirPodsService", "Error sending packet: ${e.message}")
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* LibrePods - AirPods liberated from Apple's ecosystem
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 LibrePods contributors
|
||||||
|
*
|
||||||
|
* This program 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.
|
||||||
|
*
|
||||||
|
* This program 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 me.kavishdevar.librepods.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
class LogCollector(private val context: Context) {
|
||||||
|
private var isCollecting = false
|
||||||
|
private var logProcess: Process? = null
|
||||||
|
|
||||||
|
suspend fun openXposedSettings(context: Context) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val command = if (android.os.Build.VERSION.SDK_INT >= 29) {
|
||||||
|
"am broadcast -a android.telephony.action.SECRET_CODE -d android_secret_code://5776733 android"
|
||||||
|
} else {
|
||||||
|
"am broadcast -a android.provider.Telephony.SECRET_CODE -d android_secret_code://5776733 android"
|
||||||
|
}
|
||||||
|
|
||||||
|
executeRootCommand(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearLogs() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
executeRootCommand("logcat -c")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun killBluetoothService() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
executeRootCommand("killall com.android.bluetooth")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getPackageUIDs(): Pair<String?, String?> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
val btUid = executeRootCommand("dumpsys package com.android.bluetooth | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
|
||||||
|
.trim()
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
val appUid = executeRootCommand("dumpsys package me.kavishdevar.librepods | grep -m 1 \"uid=\" | sed -E 's/.*uid=([0-9]+).*/\\1/'")
|
||||||
|
.trim()
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
Pair(btUid, appUid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun startLogCollection(listener: (String) -> Unit, connectionDetectedCallback: () -> Unit): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
isCollecting = true
|
||||||
|
val (btUid, appUid) = getPackageUIDs()
|
||||||
|
|
||||||
|
val uidFilter = buildString {
|
||||||
|
if (!btUid.isNullOrEmpty() && !appUid.isNullOrEmpty()) {
|
||||||
|
append("$btUid,$appUid")
|
||||||
|
} else if (!btUid.isNullOrEmpty()) {
|
||||||
|
append(btUid)
|
||||||
|
} else if (!appUid.isNullOrEmpty()) {
|
||||||
|
append(appUid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val command = if (uidFilter.isNotEmpty()) {
|
||||||
|
"su -c logcat --uid=$uidFilter -v threadtime"
|
||||||
|
} else {
|
||||||
|
"su -c logcat -v threadtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
val logs = StringBuilder()
|
||||||
|
try {
|
||||||
|
logProcess = Runtime.getRuntime().exec(command)
|
||||||
|
val reader = BufferedReader(InputStreamReader(logProcess!!.inputStream))
|
||||||
|
var line: String? = null
|
||||||
|
var connectionDetected = false
|
||||||
|
|
||||||
|
while (isCollecting && reader.readLine().also { line = it } != null) {
|
||||||
|
line?.let {
|
||||||
|
if (it.contains("<LogCollector:")) {
|
||||||
|
logs.append("\n=============\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.append(it).append("\n")
|
||||||
|
listener(it)
|
||||||
|
|
||||||
|
if (it.contains("<LogCollector:")) {
|
||||||
|
logs.append("=============\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectionDetected) {
|
||||||
|
if (it.contains("<LogCollector:Complete:Success>")) {
|
||||||
|
connectionDetected = true
|
||||||
|
connectionDetectedCallback()
|
||||||
|
} else if (it.contains("<LogCollector:Complete:Failed>")) {
|
||||||
|
connectionDetected = true
|
||||||
|
connectionDetectedCallback()
|
||||||
|
} else if (it.contains("<LogCollector:Start>")) {
|
||||||
|
}
|
||||||
|
else if (it.contains("AirPodsService") && it.contains("Connected to device")) {
|
||||||
|
connectionDetected = true
|
||||||
|
connectionDetectedCallback()
|
||||||
|
} else if (it.contains("AirPodsService") && it.contains("Connection failed")) {
|
||||||
|
connectionDetected = true
|
||||||
|
connectionDetectedCallback()
|
||||||
|
} else if (it.contains("AirPodsService") && it.contains("Device disconnected")) {
|
||||||
|
}
|
||||||
|
else if (it.contains("BluetoothService") && it.contains("CONNECTION_STATE_CONNECTED")) {
|
||||||
|
connectionDetected = true
|
||||||
|
connectionDetectedCallback()
|
||||||
|
} else if (it.contains("BluetoothService") && it.contains("CONNECTION_STATE_DISCONNECTED")) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logs.append("Error collecting logs: ${e.message}").append("\n")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopLogCollection() {
|
||||||
|
isCollecting = false
|
||||||
|
logProcess?.destroy()
|
||||||
|
logProcess = null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveLogToInternalStorage(fileName: String, content: String): File? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val logsDir = File(context.filesDir, "logs")
|
||||||
|
if (!logsDir.exists()) {
|
||||||
|
logsDir.mkdir()
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = File(logsDir, fileName)
|
||||||
|
file.writeText(content)
|
||||||
|
return@withContext file
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun addLogMarker(markerType: LogMarkerType, details: String = "") {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val timestamp = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US)
|
||||||
|
.format(java.util.Date())
|
||||||
|
|
||||||
|
val marker = when (markerType) {
|
||||||
|
LogMarkerType.START -> "<LogCollector:Start> [$timestamp] Beginning connection test"
|
||||||
|
LogMarkerType.SUCCESS -> "<LogCollector:Complete:Success> [$timestamp] Connection test completed successfully"
|
||||||
|
LogMarkerType.FAILURE -> "<LogCollector:Complete:Failed> [$timestamp] Connection test failed"
|
||||||
|
LogMarkerType.CUSTOM -> "<LogCollector:Custom:$details> [$timestamp]"
|
||||||
|
}
|
||||||
|
|
||||||
|
val command = "log -t AirPodsService \"$marker\""
|
||||||
|
executeRootCommand(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LogMarkerType {
|
||||||
|
START,
|
||||||
|
SUCCESS,
|
||||||
|
FAILURE,
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeRootCommand(command: String): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val process = Runtime.getRuntime().exec("su -c $command")
|
||||||
|
val reader = BufferedReader(InputStreamReader(process.inputStream))
|
||||||
|
val output = StringBuilder()
|
||||||
|
var line: String?
|
||||||
|
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
output.append(line).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
process.waitFor()
|
||||||
|
output.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
android/app/src/main/res/drawable/ic_save.xml
Normal file
10
android/app/src/main/res/drawable/ic_save.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z" />
|
||||||
|
</vector>
|
||||||
@@ -59,4 +59,9 @@
|
|||||||
<string name="ear_detection">Automatic Ear Detection</string>
|
<string name="ear_detection">Automatic Ear Detection</string>
|
||||||
<string name="auto_play">Auto Play</string>
|
<string name="auto_play">Auto Play</string>
|
||||||
<string name="auto_pause">Auto Pause</string>
|
<string name="auto_pause">Auto Pause</string>
|
||||||
|
<string name="troubleshooting">Troubleshooting</string>
|
||||||
|
<string name="troubleshooting_description">Collect logs to diagnose issues with AirPods connection</string>
|
||||||
|
<string name="collect_logs">Collect Logs</string>
|
||||||
|
<string name="saved_logs">Saved Logs</string>
|
||||||
|
<string name="no_logs_found">No saved logs found</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
4
android/app/src/main/res/xml/file_paths.xml
Normal file
4
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<files-path name="logs" path="logs/"/>
|
||||||
|
</paths>
|
||||||
Reference in New Issue
Block a user