diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9dee60e..db3e91c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,8 @@ +import java.io.ByteArrayOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") @@ -19,6 +24,10 @@ android { versionCode = 1 versionName = "1.0" + buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") + buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") + buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -79,6 +88,7 @@ dependencies { implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01") implementation("androidx.hilt:hilt-navigation-compose:1.1.0") implementation("androidx.navigation:navigation-compose:2.7.6") + implementation("androidx.profileinstaller:profileinstaller:1.3.1") implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.work:work-runtime-ktx:2.9.0") @@ -97,6 +107,7 @@ dependencies { implementation("com.squareup.okio:okio:3.7.0") implementation("com.tencent:mmkv:1.3.2") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") implementation("io.coil-kt:coil-compose:2.5.0") testImplementation("junit:junit:4.13.2") @@ -106,4 +117,31 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") +} + +// Git is needed in your system PATH for these commands to work. +// If it's not installed, you can return a random value as a workaround +fun Project.getCommitCount(): String { + return runCommand("git rev-list --count HEAD") + // return "1" +} + +fun Project.getGitSha(): String { + return runCommand("git rev-parse --short HEAD") + // return "1" +} + +fun Project.getBuildTime(): String { + val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") + df.timeZone = TimeZone.getTimeZone("UTC") + return df.format(Date()) +} + +fun Project.runCommand(command: String): String { + val byteOut = ByteArrayOutputStream() + project.exec { + commandLine = command.split(" ") + standardOutput = byteOut + } + return String(byteOut.toByteArray()).trim() } \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt index 57333de..847df6e 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/Navigation.kt @@ -140,7 +140,8 @@ fun Navigation( SettingsView( navigateBack = navigateBack, navigateToAppearance = { navController.navigate(APPEARANCE_DESTINATION) }, - navigateToAbout = { navController.navigate(ABOUT_DESTINATION) } + navigateToAbout = { navController.navigate(ABOUT_DESTINATION) }, + navigateToAdvanced = { navController.navigate(ADVANCED_DESTINATION) } ) } @@ -165,6 +166,12 @@ fun Navigation( ) } + composable(ADVANCED_DESTINATION) { + AdvancedView( + navigateBack = navigateBack, + ) + } + composable( route = LIST_DESTINATION, arguments = listOf( diff --git a/app/src/main/java/org/xtimms/tokusho/core/components/PreferenceItem.kt b/app/src/main/java/org/xtimms/tokusho/core/components/PreferenceItem.kt index a2ae13a..a04f420 100644 --- a/app/src/main/java/org/xtimms/tokusho/core/components/PreferenceItem.kt +++ b/app/src/main/java/org/xtimms/tokusho/core/components/PreferenceItem.kt @@ -121,8 +121,13 @@ fun PreferenceItem( Column( modifier = Modifier .weight(1f) - .padding(horizontal = 16.dp) - .padding(end = 8.dp) + .then( + if (icon != null) + Modifier + .padding(horizontal = 16.dp) + .padding(end = 8.dp) + else Modifier.padding(horizontal = 8.dp) + ) ) { PreferenceItemTitle(text = title, enabled = enabled) if (!description.isNullOrEmpty()) PreferenceItemDescription( @@ -465,7 +470,8 @@ fun PreferenceSwitch( ) } Column( - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) .padding(horizontal = 16.dp) ) { PreferenceItemTitle( @@ -542,7 +548,7 @@ fun PreferencesHintCard( @Preview fun PreferenceItemPreview() { Column { - PreferenceItem(title = "title", description = "description", icon = 0) + PreferenceItem(title = "title", description = "description") PreferenceItem(title = "title", description = "description", icon = Icons.Outlined.Update) } } diff --git a/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt b/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt index 07860c1..cf0e1aa 100644 --- a/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt +++ b/app/src/main/java/org/xtimms/tokusho/sections/settings/SettingsView.kt @@ -3,6 +3,7 @@ package org.xtimms.tokusho.sections.settings import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Palette import androidx.compose.runtime.Composable @@ -19,6 +20,7 @@ fun SettingsView( navigateBack: () -> Unit, navigateToAppearance: () -> Unit, navigateToAbout: () -> Unit, + navigateToAdvanced: () -> Unit ) { ScaffoldWithTopAppBar( title = stringResource(R.string.settings), @@ -36,6 +38,14 @@ fun SettingsView( onClick = navigateToAppearance ) } + item { + SettingItem( + title = stringResource(id = R.string.advanced), + description = stringResource(id = R.string.advanced_page), + icon = Icons.Outlined.Code, + onClick = navigateToAdvanced + ) + } item { SettingItem( title = stringResource(id = R.string.about), diff --git a/app/src/main/java/org/xtimms/tokusho/sections/settings/advanced/AdvancedView.kt b/app/src/main/java/org/xtimms/tokusho/sections/settings/advanced/AdvancedView.kt new file mode 100644 index 0000000..ec8a4f0 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/sections/settings/advanced/AdvancedView.kt @@ -0,0 +1,142 @@ +package org.xtimms.tokusho.sections.settings.advanced + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.profileinstaller.ProfileVerifier +import kotlinx.coroutines.guava.await +import org.xtimms.tokusho.BuildConfig +import org.xtimms.tokusho.R +import org.xtimms.tokusho.core.components.PreferenceItem +import org.xtimms.tokusho.core.components.PreferenceSubtitle +import org.xtimms.tokusho.core.components.ScaffoldWithTopAppBar +import org.xtimms.tokusho.utils.WebViewUtil +import org.xtimms.tokusho.utils.lang.toDateTimestampString +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +const val ADVANCED_DESTINATION = "advanced" + +@Composable +fun AdvancedView( + navigateBack: () -> Unit, +) { + + ScaffoldWithTopAppBar( + title = stringResource(R.string.advanced), + navigateBack = navigateBack + ) { padding -> + LazyColumn( + modifier = Modifier + .padding(padding) + ) { + item { + PreferenceSubtitle(text = stringResource(id = R.string.app_info)) + } + item { + PreferenceItem( + title = stringResource(id = R.string.app_version), + description = getVersionName(false) + ) + } + item { + PreferenceItem( + title = stringResource(id = R.string.build_time), + description = getFormattedBuildTime() + ) + } + item { + getProfileVerifierPreference() + } + item { + PreferenceItem( + title = stringResource(id = R.string.webview_version), + description = getWebViewVersion() + ) + } + } + } +} + +@Composable +@ReadOnlyComposable +private fun getWebViewVersion(): String { + return WebViewUtil.getVersion(LocalContext.current) +} + +@Composable +private fun getProfileVerifierPreference() { + val status by produceState(initialValue = "-") { + val result = ProfileVerifier.getCompilationStatusAsync().await().profileInstallResultCode + value = when (result) { + ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE -> "No profile installed" + ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE -> "Compiled" + ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING -> + "Compiled non-matching" + + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ, + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE, + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST, + -> "Error $result" + + ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION -> "Not supported" + ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION -> "Pending compilation" + else -> "Unknown code $result" + } + } + return PreferenceItem( + title = "Profile compilation status", + description = status, + ) +} + +fun getVersionName(withBuildDate: Boolean): String { + return when { + BuildConfig.DEBUG -> { + "Debug ${BuildConfig.COMMIT_SHA}".let { + if (withBuildDate) { + "$it (${getFormattedBuildTime()})" + } else { + it + } + } + } + + else -> { + "Stable ${BuildConfig.VERSION_NAME}".let { + if (withBuildDate) { + "$it (${getFormattedBuildTime()})" + } else { + it + } + } + } + } +} + +internal fun getFormattedBuildTime(): String { + return try { + val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US) + inputDf.timeZone = TimeZone.getTimeZone("UTC") + val buildTime = inputDf.parse(BuildConfig.BUILD_TIME) + + val outputDf = DateFormat.getDateTimeInstance( + DateFormat.MEDIUM, + DateFormat.SHORT, + Locale.getDefault(), + ) + outputDf.timeZone = TimeZone.getDefault() + + buildTime!!.toDateTimestampString(DateFormat.getDateTimeInstance()) + } catch (e: Exception) { + BuildConfig.BUILD_TIME + } +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/utils/WebViewUtil.kt b/app/src/main/java/org/xtimms/tokusho/utils/WebViewUtil.kt new file mode 100644 index 0000000..0610f51 --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/utils/WebViewUtil.kt @@ -0,0 +1,16 @@ +package org.xtimms.tokusho.utils + +import android.content.Context +import android.webkit.WebView + +object WebViewUtil { + + fun getVersion(context: Context): String { + val webView = WebView.getCurrentWebViewPackage() ?: return "o_O" + val pm = context.packageManager + val label = webView.applicationInfo.loadLabel(pm) + val version = webView.versionName + return "$label $version" + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/xtimms/tokusho/utils/lang/Date.kt b/app/src/main/java/org/xtimms/tokusho/utils/lang/Date.kt new file mode 100644 index 0000000..8a7e36a --- /dev/null +++ b/app/src/main/java/org/xtimms/tokusho/utils/lang/Date.kt @@ -0,0 +1,10 @@ +package org.xtimms.tokusho.utils.lang + +import java.text.DateFormat +import java.util.Date + +fun Date.toDateTimestampString(dateFormatter: DateFormat): String { + val date = dateFormatter.format(this) + val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(this) + return "$date $time" +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8091ebf..b1ae4b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,4 +73,10 @@ More Copy to clipboard No description + Advanced + App info + App version + Build time + WebView version + Dump crash logs, debug info \ No newline at end of file