Adds debug log recording.

This commit is contained in:
darken
2021-12-29 22:18:53 +01:00
parent 7e52722123
commit 817c65ee3b
28 changed files with 834 additions and 25 deletions
+1
View File
@@ -105,6 +105,7 @@ android {
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlin.time.ExperimentalTime",
"-Xuse-experimental=kotlin.ExperimentalUnsignedTypes",
"-Xuse-experimental=kotlin.contracts.ExperimentalContracts",
"-Xopt-in=kotlin.RequiresOptIn"
]
}
+22 -4
View File
@@ -11,9 +11,9 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name="eu.darken.cap.App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:name="eu.darken.cap.App"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -21,8 +21,8 @@
<activity
android:name="eu.darken.cap.main.ui.MainActivity"
android:label="@string/app_name"
android:exported="true">
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -43,6 +43,24 @@
</intent-filter>
</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_provider_paths" />
</provider>
<!-- Debug stuff-->
<activity
android:name=".common.debug.recording.ui.RecorderActivity"
android:theme="@style/AppThemeFloating" />
<service android:name=".common.debug.recording.core.RecorderService" />
<!-- Worker stuff-->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="connectedDevice"
@@ -51,7 +69,7 @@
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove"></provider>
tools:node="remove" />
</application>
</manifest>
+1 -1
View File
@@ -5,8 +5,8 @@ import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.getkeepsafe.relinker.ReLinker
import dagger.hilt.android.HiltAndroidApp
import eu.darken.cap.bugreporting.BugReporter
import eu.darken.cap.common.coroutine.AppScope
import eu.darken.cap.common.debug.bugreporting.BugReporter
import eu.darken.cap.common.debug.logging.*
import eu.darken.cap.monitor.core.worker.MonitorControl
import kotlinx.coroutines.CoroutineScope
@@ -13,4 +13,6 @@ object BuildConfigWrap {
val VERSION_CODE: Long = BuildConfig.VERSION_CODE.toLong()
val VERSION_NAME: String = BuildConfig.VERSION_NAME
val GIT_SHA: String = BuildConfig.GITSHA
val VERSION_DESCRIPTION: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA]"
}
@@ -0,0 +1,40 @@
package eu.darken.cap.common.compression
import eu.darken.cap.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
// https://stackoverflow.com/a/48598099/1251958
class Zipper {
@Throws(Exception::class)
fun zip(files: Array<String>, zipFile: String) {
var origin: BufferedInputStream?
val out = ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile)))
for (i in files.indices) {
log(TAG, VERBOSE) { "Compressing ${files[i]} into $zipFile" }
origin = BufferedInputStream(FileInputStream(files[i]), BUFFER)
val entry = ZipEntry(files[i].substring(files[i].lastIndexOf("/") + 1))
out.putNextEntry(entry)
origin.use { input -> input.copyTo(out) }
}
out.finish()
out.close()
}
companion object {
internal val TAG = logTag("Zipper")
const val BUFFER = 2048
}
}
@@ -1,4 +1,4 @@
package eu.darken.cap.bugreporting
package eu.darken.cap.common.debug.bugreporting
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -1,4 +1,4 @@
package eu.darken.cap.bugreporting
package eu.darken.cap.common.debug.bugreporting
import android.content.Context
import com.bugsnag.android.Bugsnag
@@ -1,4 +1,4 @@
package eu.darken.cap.bugreporting
package eu.darken.cap.common.debug.bugreporting
import com.bugsnag.android.Bugsnag
import eu.darken.cap.common.debug.logging.Logging.Priority.VERBOSE
@@ -0,0 +1,79 @@
package eu.darken.cap.common.debug.logging
import android.annotation.SuppressLint
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStreamWriter
import java.time.Instant
@SuppressLint("LogNotTimber")
class FileLogger(private val logFile: File) : Logging.Logger {
private var logWriter: OutputStreamWriter? = null
@SuppressLint("SetWorldReadable")
@Synchronized
fun start() {
if (logWriter != null) return
logFile.parentFile!!.mkdirs()
if (logFile.createNewFile()) {
Log.i(TAG, "File logger writing to " + logFile.path)
}
if (logFile.setReadable(true, false)) {
Log.i(TAG, "Debug run log read permission set")
}
try {
logWriter = OutputStreamWriter(FileOutputStream(logFile, true))
logWriter!!.write("=== BEGIN ===\n")
logWriter!!.write("Logfile: $logFile\n")
logWriter!!.flush()
Log.i(TAG, "File logger started.")
} catch (e: IOException) {
e.printStackTrace()
logFile.delete()
if (logWriter != null) logWriter!!.close()
}
}
@Synchronized
fun stop() {
logWriter?.let {
logWriter = null
try {
it.write("=== END ===\n")
it.close()
} catch (ignore: IOException) {
}
Log.i(TAG, "File logger stopped.")
}
}
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
logWriter?.let {
try {
it.write("${Instant.ofEpochMilli(System.currentTimeMillis())} ${priority.shortLabel}/$tag: $message\n")
it.flush()
} catch (e: IOException) {
Log.e(TAG, "Failed to write log line.", e)
try {
it.close()
} catch (ignore: Exception) {
}
logWriter = null
}
}
}
override fun toString(): String = "FileLogger(file=$logFile)"
companion object {
private val TAG = logTag("Debug", "FileLogger")
}
}
@@ -0,0 +1,48 @@
package eu.darken.cap.common.debug.recording.core
import eu.darken.cap.common.debug.logging.FileLogger
import eu.darken.cap.common.debug.logging.Logging
import eu.darken.cap.common.debug.logging.Logging.Priority.INFO
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import javax.inject.Inject
class Recorder @Inject constructor() {
private val mutex = Mutex()
private var fileLogger: FileLogger? = null
val isRecording: Boolean
get() = path != null
var path: File? = null
private set
suspend fun start(path: File) = mutex.withLock {
if (fileLogger != null) return@withLock
this.path = path
fileLogger = FileLogger(path)
fileLogger?.let {
it.start()
Logging.install(it)
log(TAG, INFO) { "Now logging to file!" }
}
}
suspend fun stop() = mutex.withLock {
fileLogger?.let {
log(TAG, INFO) { "Stopping file-logger-tree: $it" }
Logging.remove(it)
it.stop()
fileLogger = null
this.path = null
}
}
companion object {
internal val TAG = logTag("Debug", "Log", "Recorder")
}
}
@@ -0,0 +1,123 @@
package eu.darken.cap.common.debug.recording.core
import android.content.Context
import android.content.Intent
import android.os.Environment
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.cap.common.BuildConfigWrap
import eu.darken.cap.common.coroutine.AppScope
import eu.darken.cap.common.coroutine.DispatcherProvider
import eu.darken.cap.common.debug.logging.Logging.Priority.ERROR
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
import eu.darken.cap.common.debug.recording.ui.RecorderActivity
import eu.darken.cap.common.flow.DynamicStateFlow
import eu.darken.cap.common.startServiceCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecorderModule @Inject constructor(
@ApplicationContext private val context: Context,
@AppScope private val appScope: CoroutineScope,
private val dispatcherProvider: DispatcherProvider,
) {
private val triggerFile = try {
File(context.getExternalFilesDir(null), FORCE_FILE)
} catch (e: Exception) {
File(
Environment.getExternalStorageDirectory(),
"/Android/data/${BuildConfigWrap.APPLICATION_ID}/files/$FORCE_FILE"
)
}
private val internalState = DynamicStateFlow(TAG, appScope + dispatcherProvider.IO) {
val triggerFileExists = triggerFile.exists()
State(shouldRecord = triggerFileExists)
}
val state: Flow<State> = internalState.flow
init {
internalState.flow
.onEach {
log(TAG) { "New Recorder state: $internalState" }
internalState.updateBlocking {
if (!isRecording && shouldRecord) {
val newRecorder = Recorder()
newRecorder.start(createRecordingFilePath())
triggerFile.createNewFile()
context.startServiceCompat(Intent(context, RecorderService::class.java))
copy(
recorder = newRecorder
)
} else if (!shouldRecord && isRecording) {
val currentLog = recorder!!.path!!
recorder.stop()
if (triggerFile.exists() && !triggerFile.delete()) {
log(TAG, ERROR) { "Failed to delete trigger file" }
}
val intent = RecorderActivity.getLaunchIntent(context, currentLog.path).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
copy(
recorder = null,
lastLogPath = currentLog
)
} else {
this
}
}
}
.launchIn(appScope)
}
private fun createRecordingFilePath() = File(
File(context.cacheDir, "debug/logs"),
"bb_logfile_${System.currentTimeMillis()}.log"
)
suspend fun startRecorder(): File {
internalState.updateBlocking {
copy(shouldRecord = true)
}
return internalState.flow.filter { it.isRecording }.first().currentLogPath!!
}
suspend fun stopRecorder(): File? {
val currentPath = internalState.value().currentLogPath ?: return null
internalState.updateBlocking {
copy(shouldRecord = false)
}
internalState.flow.filter { !it.isRecording }.first()
return currentPath
}
data class State(
val shouldRecord: Boolean = false,
internal val recorder: Recorder? = null,
val lastLogPath: File? = null,
) {
val isRecording: Boolean
get() = recorder != null
val currentLogPath: File?
get() = recorder?.path
}
companion object {
internal val TAG = logTag("Debug", "Log", "Recorder", "Module")
private const val FORCE_FILE = "bb_force_debug_run"
}
}
@@ -0,0 +1,115 @@
package eu.darken.cap.common.debug.recording.core
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.cap.R
import eu.darken.cap.common.BuildConfigWrap
import eu.darken.cap.common.coroutine.DispatcherProvider
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
import eu.darken.cap.common.notifications.PendingIntentCompat
import eu.darken.cap.common.smart.SmartService
import eu.darken.cap.main.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class RecorderService : SmartService() {
private lateinit var builder: NotificationCompat.Builder
@Inject lateinit var recorderModule: RecorderModule
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var dispatcherProvider: DispatcherProvider
private val recorderScope by lazy {
CoroutineScope(SupervisorJob() + dispatcherProvider.IO)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIF_CHANID_DEBUG,
getString(R.string.debug_notification_channel_label),
NotificationManager.IMPORTANCE_MIN
)
notificationManager.createNotificationChannel(channel)
}
val openIntent = Intent(this, MainActivity::class.java)
val openPi = PendingIntent.getActivity(
this,
0,
openIntent,
PendingIntentCompat.FLAG_IMMUTABLE
)
val stopIntent = Intent(this, RecorderService::class.java)
stopIntent.action = STOP_ACTION
val stopPi = PendingIntent.getService(
this,
0,
stopIntent,
PendingIntentCompat.FLAG_IMMUTABLE
)
builder = NotificationCompat.Builder(this, NOTIF_CHANID_DEBUG)
.setChannelId(NOTIF_CHANID_DEBUG)
.setContentIntent(openPi)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.drawable.ic_baseline_bug_report_24)
.setContentText("Idle")
.setContentTitle(getString(R.string.app_name))
.addAction(NotificationCompat.Action.Builder(0, getString(R.string.general_done_action), stopPi).build())
startForeground(NOTIFICATION_ID, builder.build())
recorderModule.state
.onEach {
if (it.isRecording) {
builder.setContentText("Recording debug log: ${it.currentLogPath?.path}")
notificationManager.notify(NOTIFICATION_ID, builder.build())
} else {
stopForeground(true)
stopSelf()
}
}
.launchIn(recorderScope)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log(TAG) { "onStartCommand(intent=$intent, flags=$flags, startId=$startId" }
if (intent?.action == STOP_ACTION) {
recorderScope.launch {
recorderModule.stopRecorder()
}
}
return START_STICKY
}
override fun onDestroy() {
recorderScope.coroutineContext.cancel()
super.onDestroy()
}
companion object {
private val TAG = logTag("Debug", "Log", "Recorder", "Service")
private val NOTIF_CHANID_DEBUG = "${BuildConfigWrap.APPLICATION_ID}.notification.channel.debug"
private const val STOP_ACTION = "STOP_SERVICE"
private const val NOTIFICATION_ID = 53
}
}
@@ -0,0 +1,58 @@
package eu.darken.cap.common.debug.recording.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.format.Formatter
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.cap.common.debug.logging.logTag
import eu.darken.cap.common.error.asErrorDialogBuilder
import eu.darken.cap.common.smart.SmartActivity
import eu.darken.cap.databinding.CoreDebugRecordingActivityBinding
@AndroidEntryPoint
class RecorderActivity : SmartActivity() {
private lateinit var ui: CoreDebugRecordingActivityBinding
private val vm: RecorderActivityVM by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ui = CoreDebugRecordingActivityBinding.inflate(layoutInflater)
setContentView(ui.root)
vm.state.observe2 { state ->
ui.loadingIndicator.isInvisible = !state.loading
ui.share.isInvisible = state.loading
ui.recordingPath.text = state.normalPath
if (state.normalSize != -1L) {
ui.recordingSize.text = Formatter.formatShortFileSize(this, state.normalSize)
}
if (state.compressedSize != -1L) {
ui.recordingSizeCompressed.text = Formatter.formatShortFileSize(this, state.compressedSize)
}
}
vm.errorEvents.observe2 {
it.asErrorDialogBuilder(this).show()
}
ui.share.setOnClickListener { vm.share() }
vm.shareEvent.observe2 { startActivity(it) }
}
companion object {
internal val TAG = logTag("Debug", "Log", "RecorderActivity")
const val RECORD_PATH = "logPath"
fun getLaunchIntent(context: Context, path: String): Intent {
val intent = Intent(context, RecorderActivity::class.java)
intent.putExtra(RECORD_PATH, path)
return intent
}
}
}
@@ -0,0 +1,113 @@
package eu.darken.cap.common.debug.recording.ui
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.cap.R
import eu.darken.cap.common.BuildConfigWrap
import eu.darken.cap.common.compression.Zipper
import eu.darken.cap.common.coroutine.DispatcherProvider
import eu.darken.cap.common.debug.logging.logTag
import eu.darken.cap.common.flow.DynamicStateFlow
import eu.darken.cap.common.flow.onError
import eu.darken.cap.common.flow.replayingShare
import eu.darken.cap.common.livedata.SingleLiveEvent
import eu.darken.cap.common.smart.Smart2VM
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import java.io.File
import javax.inject.Inject
@HiltViewModel
class RecorderActivityVM @Inject constructor(
handle: SavedStateHandle,
dispatcherProvider: DispatcherProvider,
@ApplicationContext private val context: Context,
) : Smart2VM(dispatcherProvider) {
private val recordedPath = handle.get<String>(RecorderActivity.RECORD_PATH)!!
private val pathCache = MutableStateFlow(recordedPath)
private val resultCacheObs = pathCache
.map { path -> Pair(path, File(path).length()) }
.replayingShare(vmScope)
private val resultCacheCompressedObs = resultCacheObs
.map { uncompressed ->
val zipped = "${uncompressed.first}.zip"
Zipper().zip(arrayOf(uncompressed.first), zipped)
Pair(zipped, File(zipped).length())
}
.replayingShare(vmScope + dispatcherProvider.IO)
private val stater = DynamicStateFlow(TAG, vmScope) { State() }
val state = stater.asLiveData2()
val shareEvent = SingleLiveEvent<Intent>()
init {
resultCacheObs
.onEach { (path, size) ->
stater.updateBlocking { copy(normalPath = path, normalSize = size) }
}
.launchInViewModel()
resultCacheCompressedObs
.onEach { (path, size) ->
stater.updateBlocking {
copy(
compressedPath = path,
compressedSize = size,
loading = false
)
}
}
.onError { errorEvents.postValue(it) }
.launchInViewModel()
}
fun share() = launch {
val (path, size) = resultCacheCompressedObs.first()
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = FileProvider.getUriForFile(
context,
BuildConfigWrap.APPLICATION_ID + ".provider",
File(path)
)
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
type = "application/zip"
addCategory(Intent.CATEGORY_DEFAULT)
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION})")
putExtra(Intent.EXTRA_TEXT, "Your text here.")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val chooserIntent = Intent.createChooser(intent, context.getString(R.string.debug_log_file_label))
shareEvent.postValue(chooserIntent)
}
data class State(
val normalPath: String? = null,
val normalSize: Long = -1L,
val compressedPath: String? = null,
val compressedSize: Long = -1L,
val loading: Boolean = true
)
companion object {
private val TAG = logTag("Debug", "Recorder", "VM")
}
}
@@ -0,0 +1,12 @@
package eu.darken.cap.common.notifications
import android.app.PendingIntent
import eu.darken.cap.common.hasApiLevel
object PendingIntentCompat {
val FLAG_IMMUTABLE: Int = if (hasApiLevel(31)) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
}
@@ -3,6 +3,7 @@ package eu.darken.cap.common.smart
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import eu.darken.cap.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
@@ -36,4 +37,8 @@ abstract class SmartActivity : AppCompatActivity() {
super.onActivityResult(requestCode, resultCode, data)
}
fun <T> LiveData<T>.observe2(callback: (T) -> Unit) {
observe(this@SmartActivity) { callback.invoke(it) }
}
}
@@ -40,8 +40,22 @@ class MainFragment : Smart2Fragment(R.layout.main_fragment) {
ui.apply {
list.setupDefaults(adapter)
toolbar.subtitle =
"v${BuildConfigWrap.VERSION_NAME} (${BuildConfigWrap.VERSION_CODE}) [${BuildConfigWrap.GIT_SHA}]"
}
ui.toolbar.apply {
subtitle = BuildConfigWrap.VERSION_DESCRIPTION
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_item_debuglog -> {
vm.toggleDebugLog()
true
}
R.id.menu_item_settings -> {
vm.goToSettings()
true
}
else -> false
}
}
}
vm.listItems.observe2(ui) {
@@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.cap.common.coroutine.DispatcherProvider
import eu.darken.cap.common.debug.recording.core.RecorderModule
import eu.darken.cap.common.hasApiLevel
import eu.darken.cap.common.livedata.SingleLiveEvent
import eu.darken.cap.common.permissions.Permission
@@ -22,6 +23,7 @@ class MainFragmentVM @Inject constructor(
handle: SavedStateHandle,
@ApplicationContext private val context: Context,
dispatcherProvider: DispatcherProvider,
private val recorderModule: RecorderModule,
) : Smart2VM(dispatcherProvider = dispatcherProvider) {
@@ -65,4 +67,12 @@ class MainFragmentVM @Inject constructor(
if (granted) permissionCheckTrigger.value = UUID.randomUUID()
}
fun toggleDebugLog() = launch {
recorderModule.startRecorder()
}
fun goToSettings() = launch {
}
}
@@ -8,7 +8,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.work.ForegroundInfo
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -17,6 +16,7 @@ import eu.darken.cap.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.cap.common.debug.logging.log
import eu.darken.cap.common.debug.logging.logTag
import eu.darken.cap.common.hasApiLevel
import eu.darken.cap.common.notifications.PendingIntentCompat
import eu.darken.cap.main.ui.MainActivity
import eu.darken.cap.pods.core.PodDevice
import javax.inject.Inject
@@ -30,21 +30,19 @@ class MonitorNotifications @Inject constructor(
private val builder: NotificationCompat.Builder
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_channel_device_status_label),
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(channel)
}
NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_channel_device_status_label),
NotificationManager.IMPORTANCE_LOW
).run { notificationManager.createNotificationChannel(this) }
val openIntent = Intent(context, MainActivity::class.java)
val openPi = if (hasApiLevel(31)) {
PendingIntent.getActivity(context, 0, openIntent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, openIntent, 0)
}
val openPi = PendingIntent.getActivity(
context,
0,
openIntent,
PendingIntentCompat.FLAG_IMMUTABLE
)
builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setChannelId(NOTIFICATION_CHANNEL_ID)
@@ -2,7 +2,7 @@ package eu.darken.cap.pods.core.airpods
import android.bluetooth.le.ScanResult
import dagger.Reusable
import eu.darken.cap.bugreporting.Bugs
import eu.darken.cap.common.debug.bugreporting.Bugs
import eu.darken.cap.common.debug.logging.Logging.Priority.INFO
import eu.darken.cap.common.debug.logging.Logging.Priority.WARN
import eu.darken.cap.common.debug.logging.asLog
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView style="@style/MyCardView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="320dp">
<TextView
android:id="@+id/label_path"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="Recorded file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/recording_path"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/label_path"
tools:text="/storage/emulated/0/Android/data/eu.darken.cap/cache/log_files/some_log_file_123124.log" />
<TextView
android:id="@+id/label_size"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_size_label"
app:layout_constraintEnd_toStartOf="@+id/label_compressed_size"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recording_path" />
<TextView
android:id="@+id/recording_size"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toStartOf="@+id/recording_size_compressed"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/label_size"
app:layout_constraintTop_toBottomOf="@+id/label_size"
tools:text="72 MB" />
<TextView
android:id="@+id/label_compressed_size"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_size_compressed_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/label_size"
app:layout_constraintTop_toBottomOf="@+id/recording_path" />
<TextView
android:id="@+id/recording_size_compressed"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/label_compressed_size"
app:layout_constraintStart_toEndOf="@+id/recording_size"
app:layout_constraintTop_toBottomOf="@+id/label_compressed_size"
tools:text="745 KB" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/loading_indicator"
style="@style/Widget.Material3.CircularProgressIndicator.Medium"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/share"
app:layout_constraintEnd_toEndOf="@+id/share"
app:layout_constraintStart_toStartOf="@+id/share"
app:layout_constraintTop_toTopOf="@+id/share" />
<com.google.android.material.button.MaterialButton
android:id="@+id/share"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/general_share_action"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recording_size_compressed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -13,6 +13,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main"
app:title="@string/app_name" />
<androidx.recyclerview.widget.RecyclerView
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item_debuglog"
android:title="@string/debug_debuglog_record_action" />
<item
android:id="@+id/menu_item_settings"
android:title="@string/general_settings_label" />
</menu>
+8
View File
@@ -7,4 +7,12 @@
<string name="notification_channel_device_status_label">Device status</string>
<string name="permission_bluetooth_scan_label">BLUETOOTH SCAN</string>
<string name="permission_bluetooth_scan_description">Required to be able to discover and pair nearby Bluetooth devices.</string>
<string name="general_share_action">Share</string>
<string name="general_done_action">Done</string>
<string name="debug_debuglog_size_label">Size</string>
<string name="debug_debuglog_size_compressed_label">Compressed size</string>
<string name="debug_notification_channel_label">Debug notifications</string>
<string name="debug_debuglog_file_label">Recorded log file</string>
<string name="debug_debuglog_record_action">Record debug log</string>
<string name="general_settings_label">Settings</string>
</resources>
+10
View File
@@ -29,4 +29,14 @@
<item name="colorPrimaryInverse">@color/md_theme_light_primaryInverse</item>
</style>
<style name="AppThemeFloating" parent="AppTheme">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
</style>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path
name="debug_logs"
path="debug/logs" />
</paths>