From 5f9b66014a77b9473b968157d452773f22541333 Mon Sep 17 00:00:00 2001 From: Kshitij Chauhan Date: Sun, 28 Nov 2021 11:13:23 +0530 Subject: [PATCH] Migrate project to Jetpack Compose (#27) * feat: Switch to versions catalog and AGP 7 * feat: Refactor library module to use jetpack compose * feat: Migrate sample app to jetpack compose * fix: Use JDK 11 in CI * fix: Update ktlint * lint: Fix lint errors * fix: Use the same version for compose material and tooling libs * fix: Inline single use composables, make filenames consistent with composables, use version references in build.gradle * fix: Replace deprecated Handler constructor --- .github/workflows/android.yml | 4 +- .github/workflows/style-check.yml | 4 +- .idea/compiler.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 23 ++ .idea/misc.xml | 24 +- app/build.gradle | 15 +- app/src/main/AndroidManifest.xml | 6 +- .../com/haroldadmin/crashyapp/MainActivity.kt | 15 +- .../crashyapp/ui/pages/HomePage.kt | 41 ++++ .../crashyapp/ui/theme/CrashyAppTheme.kt | 20 ++ app/src/main/res/layout/activity_main.xml | 32 --- app/src/main/res/values/colors.xml | 11 - app/src/main/res/values/strings.xml | 3 - app/src/main/res/values/styles.xml | 11 +- build.gradle | 6 +- gradle/libs.versions.toml | 12 +- what-the-stack/build.gradle | 16 +- .../whatthestack/ExceptionProcessor.kt | 2 +- .../whatthestack/WhatTheStackActivity.kt | 109 ++------- .../whatthestack/WhatTheStackConnection.kt | 2 +- .../WhatTheStackExceptionHandler.kt | 18 +- .../whatthestack/WhatTheStackService.kt | 7 +- .../ui/components/OutlinedIconButton.kt | 38 +++ .../ui/components/OverlineLabel.kt | 20 ++ .../whatthestack/ui/pages/ExceptionPage.kt | 229 ++++++++++++++++++ .../whatthestack/ui/preview/SampleData.kt | 32 +++ .../ui/theme/WhatTheStackTheme.kt | 38 +++ .../res/drawable/ic_baseline_refresh_24.xml | 3 +- .../drawable/ic_outline_content_copy_24.xml | 3 +- .../main/res/drawable/ic_outline_share_24.xml | 3 +- .../main/res/drawable/ic_round_search_24.xml | 3 +- .../res/layout/activity_what_the_stack.xml | 183 -------------- what-the-stack/src/main/res/values/colors.xml | 12 - .../src/main/res/values/strings.xml | 13 +- what-the-stack/src/main/res/values/styles.xml | 12 +- .../whatthestack/ExceptionProcessingTest.kt | 2 +- 36 files changed, 563 insertions(+), 411 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 app/src/main/java/com/haroldadmin/crashyapp/ui/pages/HomePage.kt create mode 100644 app/src/main/java/com/haroldadmin/crashyapp/ui/theme/CrashyAppTheme.kt delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/values/colors.xml delete mode 100644 app/src/main/res/values/strings.xml create mode 100644 what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OutlinedIconButton.kt create mode 100644 what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OverlineLabel.kt create mode 100644 what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/pages/ExceptionPage.kt create mode 100644 what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/preview/SampleData.kt create mode 100644 what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/theme/WhatTheStackTheme.kt delete mode 100644 what-the-stack/src/main/res/layout/activity_what_the_stack.xml delete mode 100644 what-the-stack/src/main/res/values/colors.xml diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index be23f69..427ed23 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: set up JDK 1.8 + - name: set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Build with Gradle run: ./gradlew build test \ No newline at end of file diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml index 9cd7ce0..37d611f 100644 --- a/.github/workflows/style-check.yml +++ b/.github/workflows/style-check.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v1 - - name: set up JDK 1.8 + - name: set up JDK 11 uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 11 - name: Ktlint check run: ./gradlew ktlintCheck \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 61a9130..fb7f4a8 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..875a112 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bb9736..be9f721 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,29 @@ + + + - + diff --git a/app/build.gradle b/app/build.gradle index e469a44..297b44a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,7 +41,11 @@ android { } buildFeatures { - viewBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion "1.0.5" } } @@ -53,9 +57,12 @@ dependencies { implementation libs.kotlinStdLib implementation libs.appCompat implementation libs.coreKtx - implementation libs.fragmentKtx - implementation libs.constraintLayout - implementation libs.materialComponents + + implementation libs.composeActivity + implementation libs.composeMaterial + implementation libs.composeTooling + implementation libs.accompanistSysUi + implementation libs.accompanistInsets testImplementation libs.junit diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6840f78..b7bfb9e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,11 +5,13 @@ - + diff --git a/app/src/main/java/com/haroldadmin/crashyapp/MainActivity.kt b/app/src/main/java/com/haroldadmin/crashyapp/MainActivity.kt index 4c890cc..a4cfa9d 100644 --- a/app/src/main/java/com/haroldadmin/crashyapp/MainActivity.kt +++ b/app/src/main/java/com/haroldadmin/crashyapp/MainActivity.kt @@ -1,20 +1,19 @@ package com.haroldadmin.crashyapp import android.os.Bundle +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import com.haroldadmin.crashyapp.databinding.ActivityMainBinding +import com.haroldadmin.crashyapp.ui.pages.HomePage +import com.haroldadmin.crashyapp.ui.theme.CrashyAppTheme class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - - binding.crashButton.setOnClickListener { - throw BecauseICanException() + setContent { + CrashyAppTheme { + HomePage() + } } } } - -private class BecauseICanException : Exception("This exception is thrown purely because it can be thrown") diff --git a/app/src/main/java/com/haroldadmin/crashyapp/ui/pages/HomePage.kt b/app/src/main/java/com/haroldadmin/crashyapp/ui/pages/HomePage.kt new file mode 100644 index 0000000..d245e77 --- /dev/null +++ b/app/src/main/java/com/haroldadmin/crashyapp/ui/pages/HomePage.kt @@ -0,0 +1,41 @@ +package com.haroldadmin.crashyapp.ui.pages + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun HomePage() { + Scaffold( + topBar = { + TopAppBar { + Text(text = "Crashy App", style = MaterialTheme.typography.h6) + } + } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .fillMaxHeight() + ) { + Text( + text = "Press the button below to see error screen from WhatTheStack!", + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { throw BecauseICanException() }) { + Text(text = "Crash!") + } + } + } +} + +private class BecauseICanException : + Exception("This exception is thrown purely because it can be thrown") diff --git a/app/src/main/java/com/haroldadmin/crashyapp/ui/theme/CrashyAppTheme.kt b/app/src/main/java/com/haroldadmin/crashyapp/ui/theme/CrashyAppTheme.kt new file mode 100644 index 0000000..c14ff85 --- /dev/null +++ b/app/src/main/java/com/haroldadmin/crashyapp/ui/theme/CrashyAppTheme.kt @@ -0,0 +1,20 @@ +package com.haroldadmin.crashyapp.ui.theme + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val ColorPalette = lightColors( + primary = Color(0xffd32f2f), + primaryVariant = Color(0xff9a0007), + secondary = Color(0xff616161), + secondaryVariant = Color(0x33373737), +) + +@Composable +fun CrashyAppTheme( + content: @Composable () -> Unit +) { + MaterialTheme(colors = ColorPalette, content = content) +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 0131968..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f12742b..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - #d32f2f - #ff6659 - #9a0007 - #616161 - #8e8e8e - #373737 - #ffffff - #ffffff - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index d1882f5..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Crashy App - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 05ba9fc..f3f0b8f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,13 +1,4 @@ - - - diff --git a/build.gradle b/build.gradle index 613bb94..658afa7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ buildscript { ext.buildConfig = [ "applicationId": "com.haroldadmin.crashyapp", - "compileSdk" : 29, + "compileSdk" : 31, "minSdk" : 21, - "targetSdk" : 29, + "targetSdk" : 31, "versionCode" : 1, "versionName" : "0.0.1" ] @@ -33,7 +33,7 @@ allprojects { subprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" ktlint { - version = "0.36.0" + version = "0.43.0" ignoreFailures = false disabledRules = ["no-wildcard-imports"] } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a3d007..35a7f95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kotlin = "1.5.20" -agp = "4.2.0" +kotlin = "1.5.31" +agp = "7.0.3" ktlint = "10.1.0" appCompat = "1.3.0" coreTest = "2.0.0" @@ -17,9 +17,17 @@ espressoCore = "3.2.0" mockk = "1.9.3" robolectric = "4.3.1" startup = "1.0.0" +composeActivity = "1.3.1" +compose = "1.0.5" +accompanist = "0.21.3-beta" [libraries] +composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } +composeMaterial = { module = "androidx.compose.material:material", version.ref = "compose" } +composeTooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +accompanistSysUi = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } +accompanistInsets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanist" } agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } ktlintGradlePlugin = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint" } diff --git a/what-the-stack/build.gradle b/what-the-stack/build.gradle index 564ebb8..a9122bc 100644 --- a/what-the-stack/build.gradle +++ b/what-the-stack/build.gradle @@ -36,7 +36,11 @@ android { } buildFeatures { - viewBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion libs.versions.compose.get() } } @@ -46,11 +50,13 @@ dependencies { implementation libs.kotlinStdLib implementation libs.appCompat implementation libs.coreKtx - implementation libs.fragmentKtx - implementation libs.constraintLayout - implementation libs.materialComponents implementation libs.startup - implementation libs.insetter + + implementation libs.composeActivity + implementation libs.composeMaterial + implementation libs.composeTooling + implementation libs.accompanistSysUi + implementation libs.accompanistInsets testImplementation libs.junit testImplementation libs.mockk diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ExceptionProcessor.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ExceptionProcessor.kt index d02d8bb..2d564a5 100644 --- a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ExceptionProcessor.kt +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ExceptionProcessor.kt @@ -1,7 +1,7 @@ package com.haroldadmin.whatthestack import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize /** * Represents the data of the exception to be displayed to the user. diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackActivity.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackActivity.kt index 54c46c6..108afee 100644 --- a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackActivity.kt +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackActivity.kt @@ -1,18 +1,14 @@ package com.haroldadmin.whatthestack -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.text.method.ScrollingMovementMethod +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat -import com.google.android.material.snackbar.Snackbar -import com.haroldadmin.whatthestack.databinding.ActivityWhatTheStackBinding -import dev.chrisbanes.insetter.Insetter -import dev.chrisbanes.insetter.windowInsetTypesOf +import com.google.accompanist.insets.ProvideWindowInsets +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.haroldadmin.whatthestack.ui.pages.ExceptionPage +import com.haroldadmin.whatthestack.ui.theme.SystemBarsColor +import com.haroldadmin.whatthestack.ui.theme.WhatTheStackTheme /** * An Activity which displays various pieces of information regarding the exception which @@ -20,90 +16,27 @@ import dev.chrisbanes.insetter.windowInsetTypesOf */ class WhatTheStackActivity : AppCompatActivity() { - private lateinit var binding: ActivityWhatTheStackBinding - - private val clipboardManager: ClipboardManager by lazy { - getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityWhatTheStackBinding.inflate(layoutInflater) - setContentView(binding.root) - WindowCompat.setDecorFitsSystemWindows(window, false) - Insetter.builder() - .padding(windowInsetTypesOf(statusBars = true, navigationBars = true)) - .applyToView(binding.root) - - val type = intent.getStringExtra(KEY_EXCEPTION_TYPE) - val cause = intent.getStringExtra(KEY_EXCEPTION_CAUSE) - val message = intent.getStringExtra(KEY_EXCEPTION_MESSAGE) - val stackTrace = intent.getStringExtra(KEY_EXCEPTION_STACKTRACE) - - binding.stacktrace.apply { - text = stackTrace - setHorizontallyScrolling(true) - movementMethod = ScrollingMovementMethod() - } - - binding.exceptionName.apply { - text = getString(R.string.exception_name, type) - } - - binding.exceptionCause.apply { - text = getString(R.string.exception_cause, cause) - } - - binding.exceptionMessage.apply { - text = getString(R.string.exception_message, message) - } - - binding.copyStacktrace.apply { - setOnClickListener { - val clipping = ClipData.newPlainText("stacktrace", stackTrace) - clipboardManager.setPrimaryClip(clipping) - snackbar { R.string.copied_message } - } - } - - binding.shareStacktrace.apply { - setOnClickListener { - val sendIntent: Intent = Intent().apply { - this.action = Intent.ACTION_SEND - this.putExtra(Intent.EXTRA_TEXT, stackTrace) - this.type = "text/plain" + val type = intent.getStringExtra(KEY_EXCEPTION_TYPE) ?: "" + val message = intent.getStringExtra(KEY_EXCEPTION_MESSAGE) ?: "" + val stackTrace = intent.getStringExtra(KEY_EXCEPTION_STACKTRACE) ?: "" + + setContent { + val sysUiController = rememberSystemUiController() + sysUiController.setSystemBarsColor(SystemBarsColor) + + WhatTheStackTheme { + ProvideWindowInsets { + ExceptionPage( + type = type, + message = message, + stackTrace = stackTrace + ) } - - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) - } - } - - binding.launchApplication.apply { - setOnClickListener { - context.packageManager.getLaunchIntentForPackage(applicationContext.packageName) - ?.let { - it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(it) - } } } - - binding.searchStackoverflow.apply { - setOnClickListener { - val searchQuery = "$cause: $message" - val searchIntent: Intent = Intent().apply { - this.action = Intent.ACTION_VIEW - this.data = Uri.parse(generateStackoverflowSearchUrl(searchQuery)) - } - startActivity(searchIntent) - } - } - } - - private inline fun snackbar(messageProvider: () -> Int) { - Snackbar.make(binding.nestedScrollRoot, messageProvider(), Snackbar.LENGTH_SHORT).show() } } diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackConnection.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackConnection.kt index 16df4d5..3e1f45b 100644 --- a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackConnection.kt +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackConnection.kt @@ -5,7 +5,7 @@ import android.content.ServiceConnection import android.os.IBinder internal class WhatTheStackConnection( - private val onDisconnected: () -> Unit = { Unit }, + private val onDisconnected: () -> Unit = { }, private val onConnected: (IBinder) -> Unit ) : ServiceConnection { diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackExceptionHandler.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackExceptionHandler.kt index d581270..98de608 100644 --- a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackExceptionHandler.kt +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackExceptionHandler.kt @@ -19,14 +19,16 @@ internal class WhatTheStackExceptionHandler( val exceptionData = e.process() - service.send(Message().apply { - data = bundleOf( - KEY_EXCEPTION_TYPE to exceptionData.type, - KEY_EXCEPTION_CAUSE to exceptionData.cause, - KEY_EXCEPTION_MESSAGE to exceptionData.message, - KEY_EXCEPTION_STACKTRACE to exceptionData.stacktrace - ) - }) + service.send( + Message().apply { + data = bundleOf( + KEY_EXCEPTION_TYPE to exceptionData.type, + KEY_EXCEPTION_CAUSE to exceptionData.cause, + KEY_EXCEPTION_MESSAGE to exceptionData.message, + KEY_EXCEPTION_STACKTRACE to exceptionData.stacktrace + ) + } + ) defaultHandler?.uncaughtException(t, e) ?: Process.killProcess(Process.myPid()) } diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackService.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackService.kt index 19a8cd5..e93833b 100644 --- a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackService.kt +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/WhatTheStackService.kt @@ -3,10 +3,7 @@ package com.haroldadmin.whatthestack import android.app.Service import android.content.Context import android.content.Intent -import android.os.Handler -import android.os.IBinder -import android.os.Message -import android.os.Messenger +import android.os.* /** * A Bound Service which runs in a separate process than the application. @@ -28,7 +25,7 @@ class WhatTheStackService : Service() { internal class WhatTheStackHandler( private val applicationContext: Context - ) : Handler() { + ) : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { val type = msg.data.getString(KEY_EXCEPTION_TYPE) val cause = msg.data.getString(KEY_EXCEPTION_CAUSE) diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OutlinedIconButton.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OutlinedIconButton.kt new file mode 100644 index 0000000..6d47bcc --- /dev/null +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OutlinedIconButton.kt @@ -0,0 +1,38 @@ +package com.haroldadmin.whatthestack.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign + +@Composable +internal fun OutlinedIconButton( + text: String, + @DrawableRes iconId: Int, + onClick: () -> Unit, + contentDescription: String, + modifier: Modifier = Modifier, +) { + OutlinedButton( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + backgroundColor = MaterialTheme.colors.background, + contentColor = MaterialTheme.colors.onBackground, + disabledContentColor = MaterialTheme.colors.onBackground.copy(alpha = 0.5f) + ), + ) { + Icon( + painter = painterResource(id = iconId), + contentDescription = contentDescription + ) + Text( + text = text, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + } +} diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OverlineLabel.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OverlineLabel.kt new file mode 100644 index 0000000..3017f02 --- /dev/null +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/components/OverlineLabel.kt @@ -0,0 +1,20 @@ +package com.haroldadmin.whatthestack.ui.components + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * A text label with the "overline" typography style + */ +@Composable +internal fun OverlineLabel(label: String, modifier: Modifier = Modifier) { + Text( + text = label, + style = MaterialTheme.typography.overline, + color = if (isSystemInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primaryVariant, + modifier = modifier + ) +} diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/pages/ExceptionPage.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/pages/ExceptionPage.kt new file mode 100644 index 0000000..da2efaa --- /dev/null +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/pages/ExceptionPage.kt @@ -0,0 +1,229 @@ +package com.haroldadmin.whatthestack.ui.pages + +import android.content.Intent +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.net.Uri +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.insets.navigationBarsHeight +import com.google.accompanist.insets.statusBarsHeight +import com.haroldadmin.whatthestack.R +import com.haroldadmin.whatthestack.generateStackoverflowSearchUrl +import com.haroldadmin.whatthestack.ui.components.OutlinedIconButton +import com.haroldadmin.whatthestack.ui.components.OverlineLabel +import com.haroldadmin.whatthestack.ui.preview.SampleData +import com.haroldadmin.whatthestack.ui.theme.WhatTheStackTheme +import kotlinx.coroutines.launch + +@Composable +fun ExceptionPage( + type: String, + message: String, + stackTrace: String +) { + val clipboard = LocalClipboardManager.current + val context = LocalContext.current + val scaffoldState = rememberScaffoldState() + val coroutineScope = rememberCoroutineScope() + + val snackbarMessage = stringResource(id = R.string.copied_message) + + Scaffold(scaffoldState = scaffoldState) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.statusBarsHeight(additional = 8.dp)) + PageHeader() + ExceptionDetails( + type = type, + message = message, + modifier = Modifier.padding(vertical = 8.dp) + ) + ExceptionOptions( + onCopy = { + coroutineScope.launch { + clipboard.setText(AnnotatedString(stackTrace)) + scaffoldState.snackbarHostState.showSnackbar(snackbarMessage) + } + }, + onShare = { + val sendIntent: Intent = Intent().apply { + this.action = Intent.ACTION_SEND + this.putExtra(Intent.EXTRA_TEXT, stackTrace) + this.type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, "Stacktrace") + context.startActivity(shareIntent) + }, + onSearch = { + val searchQuery = "$type: $message" + val url = generateStackoverflowSearchUrl(searchQuery) + val searchIntent = Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse(url) + } + context.startActivity(searchIntent) + }, + onRestart = { + val applicationContext = context.applicationContext + val packageManager = applicationContext.packageManager + val packageName = applicationContext.packageName + + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + if (launchIntent != null) { + launchIntent.flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + context.startActivity(launchIntent) + } + } + ) + Stacktrace( + stackTrace = stackTrace, + modifier = Modifier.padding(top = 8.dp) + ) + Spacer(modifier = Modifier.navigationBarsHeight(additional = 8.dp)) + } + } +} + +@Composable +fun PageHeader() { + Text( + stringResource(id = R.string.header_text), + style = MaterialTheme.typography.h4, + modifier = Modifier.padding(vertical = 4.dp), + color = MaterialTheme.colors.onBackground + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.explanation_text), + color = MaterialTheme.colors.onBackground + ) +} + +@Composable +fun ExceptionDetails(type: String, message: String, modifier: Modifier) { + Column(modifier = modifier) { + OverlineLabel(label = stringResource(id = R.string.exception_name)) + Text( + text = type, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colors.onBackground + ) + Spacer(modifier = Modifier.height(8.dp)) + OverlineLabel(label = stringResource(id = R.string.exception_message)) + Text( + text = message, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colors.onBackground + ) + } +} + +@Composable +fun ExceptionOptions( + onCopy: () -> Unit, + onShare: () -> Unit, + onRestart: () -> Unit, + onSearch: () -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + OutlinedIconButton( + text = stringResource(id = R.string.copy_stacktrace), + iconId = R.drawable.ic_outline_content_copy_24, + onClick = onCopy, + contentDescription = "Copy", + modifier = Modifier.padding(vertical = 4.dp), + ) + OutlinedIconButton( + text = stringResource(id = R.string.share_stacktrace), + iconId = R.drawable.ic_outline_share_24, + onClick = onShare, + contentDescription = "Share", + modifier = Modifier.padding(vertical = 4.dp) + ) + OutlinedIconButton( + text = stringResource(id = R.string.search_stackoverflow), + iconId = R.drawable.ic_round_search_24, + onClick = onSearch, + contentDescription = "Search Stackoverflow", + modifier = Modifier.padding(vertical = 4.dp) + ) + OutlinedIconButton( + text = stringResource(id = R.string.restart_application), + iconId = R.drawable.ic_baseline_refresh_24, + onClick = onRestart, + contentDescription = "Restart" + ) + } +} + +@Composable +fun Stacktrace(stackTrace: String, modifier: Modifier) { + Column(modifier) { + OverlineLabel(label = stringResource(id = R.string.stacktrace)) + Surface( + color = Color.LightGray.copy(alpha = 0.5f), + modifier = Modifier.padding(top = 4.dp) + ) { + SelectionContainer { + Text( + text = stackTrace, + style = MaterialTheme.typography.body2.copy(fontSize = 12.sp), + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colors.onBackground, + modifier = Modifier + .padding(4.dp) + .horizontalScroll(rememberScrollState()) + ) + } + } + } +} + +@Preview +@Composable +fun ExceptionPagePreview() { + WhatTheStackTheme { + ExceptionPage( + type = SampleData.ExceptionType, + message = SampleData.ExceptionMessage, + stackTrace = SampleData.Stacktrace + ) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +fun ExceptionPagePreviewNightMode() { + WhatTheStackTheme { + ExceptionPage( + type = SampleData.ExceptionType, + message = SampleData.ExceptionMessage, + stackTrace = SampleData.Stacktrace + ) + } +} diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/preview/SampleData.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/preview/SampleData.kt new file mode 100644 index 0000000..a16438f --- /dev/null +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/preview/SampleData.kt @@ -0,0 +1,32 @@ +package com.haroldadmin.whatthestack.ui.preview + +object SampleData { + const val ExceptionType = "Runtime Exception" + + const val ExceptionMessage = "This exception was thrown purely because it can be thrown" + + const val Stacktrace = + """java.lang.RuntimeException: java.lang.reflect.InvocationTargetException + at com.android.internal.os.RuntimeInitMethodAndArgsCaller.run(RuntimeInit.java:558) + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003) +Caused by: java.lang.reflect.InvocationTargetException + at java.lang.reflect.Method.invoke(Native Method) + at com.android.internal.os.RuntimeInitMethodAndArgsCaller.run(RuntimeInit.java:548) + ... 1 more +Caused by: com.haroldadmin.crashyapp.BecauseICanException: This exception is thrown purely because it can be thrown + at com.haroldadmin.crashyapp.MainActivity.onCreatelambda-0(MainActivity.kt:15) + at com.haroldadmin.crashyapp.MainActivity.r8lambdapFZVHP1EeT4E2LW7TLA5yGBRTTk(Unknown Source:0) + at com.haroldadmin.crashyapp.MainActivityxternalSyntheticLambda0.onClick(Unknown Source:0) + at android.view.View.performClick(View.java:7441) + at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119) + at android.view.View.performClickInternal(View.java:7418) + at android.view.View.access$3700(View.java:835) + at android.view.ViewPerformClick.run(View.java:28676) + at android.os.Handler.handleCallback(Handler.java:938) + at android.os.Handler.dispatchMessage(Handler.java:99) + at android.os.Looper.loopOnce(Looper.java:201) + at android.os.Looper.loop(Looper.java:288) + at android.app.ActivityThread.main(ActivityThread.java:7839) + ... 3 more +""" +} diff --git a/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/theme/WhatTheStackTheme.kt b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/theme/WhatTheStackTheme.kt new file mode 100644 index 0000000..120e11f --- /dev/null +++ b/what-the-stack/src/main/java/com/haroldadmin/whatthestack/ui/theme/WhatTheStackTheme.kt @@ -0,0 +1,38 @@ +package com.haroldadmin.whatthestack.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val DarkColorPalette = darkColors( + primary = Color(0xffd32f2f), + primaryVariant = Color(0xffff6659), + secondary = Color(0xff616161), + secondaryVariant = Color(0xff373737), +) + +private val LightColorPalette = lightColors( + primary = Color(0xffd32f2f), + primaryVariant = Color(0xff9a0007), + secondary = Color(0xff616161), + secondaryVariant = Color(0x33373737), +) + +internal val SystemBarsColor = Color(0x33373737) + +@Composable +fun WhatTheStackTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme(colors = colors, content = content) +} diff --git a/what-the-stack/src/main/res/drawable/ic_baseline_refresh_24.xml b/what-the-stack/src/main/res/drawable/ic_baseline_refresh_24.xml index f2be45b..5ab492c 100644 --- a/what-the-stack/src/main/res/drawable/ic_baseline_refresh_24.xml +++ b/what-the-stack/src/main/res/drawable/ic_baseline_refresh_24.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="24"> diff --git a/what-the-stack/src/main/res/drawable/ic_outline_content_copy_24.xml b/what-the-stack/src/main/res/drawable/ic_outline_content_copy_24.xml index 79c5a76..eb384e5 100644 --- a/what-the-stack/src/main/res/drawable/ic_outline_content_copy_24.xml +++ b/what-the-stack/src/main/res/drawable/ic_outline_content_copy_24.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorOnPrimary"> + android:viewportHeight="24"> diff --git a/what-the-stack/src/main/res/drawable/ic_outline_share_24.xml b/what-the-stack/src/main/res/drawable/ic_outline_share_24.xml index 3a6a059..394e3ef 100644 --- a/what-the-stack/src/main/res/drawable/ic_outline_share_24.xml +++ b/what-the-stack/src/main/res/drawable/ic_outline_share_24.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="24"> diff --git a/what-the-stack/src/main/res/drawable/ic_round_search_24.xml b/what-the-stack/src/main/res/drawable/ic_round_search_24.xml index c1818d5..b86e6fc 100644 --- a/what-the-stack/src/main/res/drawable/ic_round_search_24.xml +++ b/what-the-stack/src/main/res/drawable/ic_round_search_24.xml @@ -2,8 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="24"> diff --git a/what-the-stack/src/main/res/layout/activity_what_the_stack.xml b/what-the-stack/src/main/res/layout/activity_what_the_stack.xml deleted file mode 100644 index 7e85fa7..0000000 --- a/what-the-stack/src/main/res/layout/activity_what_the_stack.xml +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/what-the-stack/src/main/res/values/colors.xml b/what-the-stack/src/main/res/values/colors.xml deleted file mode 100644 index 9f135e1..0000000 --- a/what-the-stack/src/main/res/values/colors.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - #d32f2f - #ff6659 - #9a0007 - #616161 - #8e8e8e - #373737 - #33373737 - #ffffff - #ffffff - \ No newline at end of file diff --git a/what-the-stack/src/main/res/values/strings.xml b/what-the-stack/src/main/res/values/strings.xml index 2fecf52..54394d8 100644 --- a/what-the-stack/src/main/res/values/strings.xml +++ b/what-the-stack/src/main/res/values/strings.xml @@ -26,14 +26,13 @@ at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)  - Exception: %1$s - Cause: %1$s + Exception Stacktrace - Message: %1$s - Copy - Share - Restart + Message + Copy Stacktrace + Share Stacktrace + Restart Application Relaunch App Stacktrace copied! - Stackoverflow + Search Stackoverflow \ No newline at end of file diff --git a/what-the-stack/src/main/res/values/styles.xml b/what-the-stack/src/main/res/values/styles.xml index 98b8f55..0870988 100644 --- a/what-the-stack/src/main/res/values/styles.xml +++ b/what-the-stack/src/main/res/values/styles.xml @@ -1,14 +1,4 @@ - +