add in app log viewer

This commit is contained in:
qimiko committed 2024-01-20 06:20:50 -07:00
1 parent 05eb3af243
commit 397cb9b9bd
8 files changed
+591 -16

No files matched your search

+6 -5
View File
@@ -75,10 +75,10 @@ android {
} }
dependencies { dependencies {
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3:1.1.2' implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.activity:activity-compose:1.8.2'
@@ -90,7 +90,8 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest"
debugImplementation "androidx.compose.ui:ui-tooling-preview"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
} }
+9 -3
View File
@@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-feature <uses-feature
android:name="android.hardware.wifi" android:name="android.hardware.wifi"
@@ -28,6 +27,11 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.GeodeLauncher" android:theme="@style/Theme.GeodeLauncher"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ApplicationLogsActivity"
android:exported="false"
android:label="@string/title_activity_application_logs"
android:theme="@style/Theme.GeodeLauncher" />
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:exported="false" android:exported="false"
@@ -58,11 +62,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider <provider
android:authorities="com.geode.launcher.user"
android:name=".UserDirectoryProvider" android:name=".UserDirectoryProvider"
android:grantUriPermissions="true" android:authorities="com.geode.launcher.user"
android:exported="true" android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"> android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter> <intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" /> <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
@@ -78,4 +83,5 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>
@@ -0,0 +1,275 @@
package com.geode.launcher
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.log.LogLine
import com.geode.launcher.log.LogPriority
import com.geode.launcher.log.LogViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
class ApplicationLogsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ApplicationLogsScreen(onBackPressedDispatcher)
}
}
}
}
}
}
@Composable
fun getLogColor(priority: LogPriority): Color {
val isDark = LocalTheme.current != Theme.LIGHT
// i just copied these from my ide tbh
return if (isDark) {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffbcb500)
LogPriority.FATAL -> Color(0xffb8b6fc)
else -> Color.Unspecified
}
} else {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffeab700)
LogPriority.FATAL -> Color(0xffa142f4)
else -> Color.Unspecified
}
}
}
fun copyLogText(context: Context, logMessage: String) {
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(
ClipData.newPlainText(
context.getString(R.string.application_log_copy_label),
logMessage
)
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.text_copied),
Toast.LENGTH_SHORT
).show()
}
}
@Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current
Column(
modifier = modifier
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
copyLogText(context, logLine.asSimpleString)
}
)
},
) {
Text(
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
style = Typography.labelSmall,
color = getLogColor(logLine.priority),
fontFamily = FontFamily.Monospace
)
Text(
"[${logLine.tag}]: ${logLine.message}",
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationLogsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
logViewModel: LogViewModel = viewModel()
) {
val logLines = logViewModel.lineState
val isLoading by logViewModel.isLoading.collectAsState()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
LaunchedEffect(logLines.size) {
listState.scrollToItem(logLines.size)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = {
if (!logLines.isEmpty()) {
coroutineScope.launch {
val data = logViewModel.getLogData()
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, data)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
}
}) {
Icon(
Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = {
logViewModel.clearLogs()
onBackPressedDispatcher?.onBackPressed()
}) {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.application_logs_delete)
)
}
},
title = {
Text(
stringResource(R.string.title_activity_application_logs),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
) { innerPadding ->
LazyColumn(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceEvenly,
state = listState
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(R.string.application_logs_no_logs),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize()
)
}
}
items(
logLines
) { line ->
LogCard(line)
Divider()
}
}
}
}
@Preview(showSystemUi = true)
@Composable
fun LogsPreview() {
GeodeLauncherTheme {
ApplicationLogsScreen(null)
}
}
@@ -104,7 +104,7 @@ fun onOpenFolder(context: Context) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.export_folder_copied), context.getString(R.string.text_copied),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -224,6 +224,11 @@ fun updateTheme(context: Context, theme: Int) {
} }
} }
fun onOpenLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -267,7 +272,7 @@ fun SettingsScreen(
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.Filled.ArrowBack,
contentDescription = context.getString(R.string.settings_back_icon_alt) contentDescription = context.getString(R.string.back_icon_alt)
) )
} }
}, },
@@ -302,10 +307,6 @@ fun SettingsScreen(
title = stringResource(R.string.preference_black_background_name), title = stringResource(R.string.preference_black_background_name),
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
) )
SettingsCard(
title = context.getString(R.string.preference_load_testing_name),
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
)
SettingsCard( SettingsCard(
title = context.getString(R.string.preference_load_automatically_name), title = context.getString(R.string.preference_load_automatically_name),
description = context.getString(R.string.preference_load_automatically_description), description = context.getString(R.string.preference_load_automatically_description),
@@ -320,6 +321,10 @@ fun SettingsScreen(
title = stringResource(R.string.preferences_open_file_manager), title = stringResource(R.string.preferences_open_file_manager),
onClick = { onOpenFileManager(context) } onClick = { onOpenFileManager(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
} }
OptionsGroup(context.getString(R.string.preference_category_updater)) { OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -0,0 +1,184 @@
package com.geode.launcher.log
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime
import java.io.DataInputStream
import java.io.IOException
fun DataInputStream.readCChar(): Char {
return this.readByte().toInt().toChar()
}
fun DataInputStream.readCString(): String {
val buffer = StringBuilder()
var lastByte = this.readCChar()
while (lastByte != '\u0000') {
buffer.append(lastByte)
lastByte = this.readCChar()
}
return buffer.toString()
}
fun DataInputStream.readLeUShort(): UShort {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val shortValue = (byteB shl 8) or byteA
return shortValue.toUShort()
}
fun DataInputStream.readLeInt(): Int {
val byteA = this.readByte().toInt() and 0xff
val byteB = this.readByte().toInt() and 0xff
val byteC = this.readByte().toInt() and 0xff
val byteD = this.readByte().toInt() and 0xff
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
fun DataInputStream.readLeUInt(): UInt {
val byteA = this.readByte().toUInt() and 0xffu
val byteB = this.readByte().toUInt() and 0xffu
val byteC = this.readByte().toUInt() and 0xffu
val byteD = this.readByte().toUInt() and 0xffu
return (byteD shl 24) or (byteC shl 16) or (byteB shl 8) or byteA
}
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
companion object {
fun fromByte(byte: Byte): LogPriority {
return when (byte.toInt()) {
0x1 -> DEFAULT
0x2 -> VERBOSE
0x3 -> DEBUG
0x4 -> INFO
0x5 -> WARN
0x6 -> ERROR
0x7 -> FATAL
0x8 -> SILENT
else -> UNKNOWN
}
}
}
fun toChar(): Char {
return when (this) {
VERBOSE -> 'V'
DEBUG -> 'D'
INFO -> 'I'
WARN -> 'W'
ERROR -> 'E'
FATAL -> 'F'
SILENT -> 'S'
else -> '?'
}
}
}
/**
* Represents a log entry from logcat.
*/
data class LogLine(
val process: ProcessInformation,
val time: Instant,
val logId: Int,
val priority: LogPriority,
val tag: String,
val message: String
) {
companion object {
private enum class EntryVersion {
V3, V4
}
private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
0x18 -> EntryVersion.V3
0x1c -> EntryVersion.V4
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
}
fun fromInputStream(readStream: DataInputStream): LogLine {
/*
// from android <liblog/include/log/log_read.h>
// there are multiple logger entry formats
// use the header_size to determine the one you have
struct logger_entry_v3 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v3)
int32_t pid; // generating process's pid
int32_t tid; // generating process's tid
int32_t sec; // seconds since Epoch
int32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload
}
struct logger_entry_v4 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
int32_t pid; // generating process's pid
uint32_t tid; // generating process's tid
uint32_t sec; // seconds since Epoch
uint32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload, bottom 4 bits currently
uint32_t uid; // generating process's uid
};
*/
/* val payloadLength = */ readStream.readLeUShort()
val headerSize = readStream.readLeUShort()
val entryVersion = headerSizeToVersion(headerSize)
val pid = readStream.readLeInt()
val tid = readStream.readLeUInt().toInt()
val sec = readStream.readLeUInt().toLong()
val nSec = readStream.readLeUInt().toInt()
val lid = readStream.readLeUInt().toInt()
val uid = if (entryVersion == EntryVersion.V4)
readStream.readLeUInt().toInt() else 0
val processInformation = ProcessInformation(pid, tid, uid)
val time = Instant.fromEpochSeconds(sec, nSec)
val priorityByte = readStream.readByte()
val priority = LogPriority.fromByte(priorityByte)
val tag = readStream.readCString()
val message = readStream.readCString()
return LogLine(
process = processInformation,
priority = priority,
time = time,
logId = lid,
tag = tag,
message = message
)
}
fun showException(exception: Exception) = LogLine(
process = ProcessInformation(0, 0, 0),
time = Clock.System.now(),
logId = 0,
priority = LogPriority.FATAL,
tag = "GeodeLauncher",
message = "Failed to parse log entry with ${exception.stackTraceToString()}"
)
}
val identifier = time.toJavaInstant()
val formattedTime = this.time.toLocalDateTime(TimeZone.currentSystemDefault())
val asSimpleString = "$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.message}"
}
@@ -0,0 +1,86 @@
package com.geode.launcher.log
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import java.io.DataInputStream
import java.io.EOFException
import java.util.LinkedList
/**
* ViewModel to manage log output from logcat.
* Currently, it loads the list of logs at init with no ability to read additional logs.
*/
class LogViewModel: ViewModel() {
// the original version of this code did support streaming btw
// the states were way too annoying to manage across multiple threads
// yay!
private val _lineState = mutableStateListOf<LogLine>()
val lineState = _lineState
private var _isLoading = MutableStateFlow(false)
var isLoading = _isLoading.asStateFlow()
private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>()
val logStream = DataInputStream(
// -B = binary format, -d = dump logs
Runtime.getRuntime().exec("logcat -B -d").inputStream
)
try {
coroutineScope {
// this runs until the stream is exhausted
while (true) {
val line = LogLine.fromInputStream(logStream)
// skip debug/verbose lines
if (line.priority >= LogPriority.INFO) {
logLines += line
}
yield()
}
}
} catch (_: EOFException) {
// ignore, end of file reached
} catch (e: Exception) {
e.printStackTrace()
logLines += LogLine.showException(e)
}
return logLines
}
fun clearLogs() {
viewModelScope.launch(Dispatchers.IO) {
// -c = clear
Runtime.getRuntime().exec("logcat -c")
.waitFor()
}
_lineState.clear()
}
fun getLogData(): String {
return _lineState.joinToString("\n") { it.asSimpleString }
}
init {
_isLoading.value = true
viewModelScope.launch(Dispatchers.IO) {
val lines = logOutput()
_lineState.addAll(lines)
_isLoading.value = false
}
}
}
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
</vector>
+10 -2
View File
@@ -48,10 +48,10 @@
<!-- SettingsActivity strings --> <!-- SettingsActivity strings -->
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
<string name="settings_back_icon_alt">back arrow icon</string> <string name="back_icon_alt">back</string>
<string name="export_folder_tag">Launcher External Folder</string> <string name="export_folder_tag">Launcher External Folder</string>
<string name="export_folder_copied">Copied!</string> <string name="text_copied">Copied to clipboard.</string>
<string name="preference_category_testing">General</string> <string name="preference_category_testing">General</string>
<string name="preference_load_testing_name">Load files from /test</string> <string name="preference_load_testing_name">Load files from /test</string>
@@ -59,6 +59,7 @@
<string name="preference_load_automatically_description">Launches the game after a short delay.</string> <string name="preference_load_automatically_description">Launches the game after a short delay.</string>
<string name="preferences_copy_external_button">Copy external files directory</string> <string name="preferences_copy_external_button">Copy external files directory</string>
<string name="preferences_open_file_manager">Open file manager</string> <string name="preferences_open_file_manager">Open file manager</string>
<string name="preferences_view_logs">View application logs</string>
<string name="preference_theme_name">Theme</string> <string name="preference_theme_name">Theme</string>
<string name="preference_theme_select">Select theme</string> <string name="preference_theme_select">Select theme</string>
@@ -79,4 +80,11 @@
<string name="preference_check_for_updates_downloading">Downloading update…</string> <string name="preference_check_for_updates_downloading">Downloading update…</string>
<string name="preference_check_for_updates_no_internet">No internet found.</string> <string name="preference_check_for_updates_no_internet">No internet found.</string>
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string> <string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
<!-- ApplicationLogsActivity strings -->
<string name="title_activity_application_logs">Application Logs</string>
<string name="application_logs_no_logs">No logs available.</string>
<string name="application_logs_delete">Clear</string>
<string name="application_logs_share">Share</string>
<string name="application_log_copy_label">Log Message</string>
</resources> </resources>