From 68c1dbbfb36072db83ec6726bbd6bde6c437adc4 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:48:45 +0000 Subject: [PATCH 01/37] Add https://mrmans0n.github.io/compose-rules/. --- TODO | 1 - app/build.gradle | 1 + detekt.yml | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 0610b51..8a5ae08 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,4 @@ * Try out JetPack compose. - - add detekt plugin. * NDK crash reporting diff --git a/app/build.gradle b/app/build.gradle index 99fff37..3e711da 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.6") detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-ruleauthors:1.23.6") + detektPlugins 'io.nlopez.compose.rules:detekt:0.3.12' } task writeVersionFile { diff --git a/detekt.yml b/detekt.yml index a3968ad..51d7095 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,3 +1,91 @@ empty-blocks: EmptyFunctionBlock: ignoreOverridden: true +Compose: + ComposableAnnotationNaming: + active: true + ComposableNaming: + active: true + # -- You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters) + # allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter + ComposableParamOrder: + active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType + CompositionLocalAllowlist: + active: true + # -- You can optionally define a list of CompositionLocals that are allowed here + # allowedCompositionLocals: LocalSomething,LocalSomethingElse + CompositionLocalNaming: + active: true + ContentEmitterReturningValues: + active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + DefaultsVisibility: + active: true + LambdaParameterInRestartableEffect: + active: true + # -- You can optionally have a list of types to be treated as lambdas (e.g. typedefs or fun interfaces not picked up automatically) + # treatAsLambda: MyLambdaType + ModifierClickableOrder: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierComposable: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierMissing: + active: true + # -- You can optionally control the visibility of which composables to check for here + # -- Possible values are: `only_public`, `public_and_internal` and `all` (default is `only_public`) + # checkModifiersForVisibility: only_public + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierNaming: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierNotUsedAtRoot: + active: true + # -- You can optionally add your own composables here + # contentEmitters: MyComposable,MyOtherComposable + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierReused: + active: true + # -- You can optionally add your own Modifier types + # customModifiers: BananaModifier,PotatoModifier + ModifierWithoutDefault: + active: true + MultipleEmitters: + active: true + # -- You can optionally add your own composables here that will count as content emitters + # contentEmitters: MyComposable,MyOtherComposable + # -- You can add composables here that you don't want to count as content emitters (e.g. custom dialogs or modals) + # contentEmittersDenylist: MyNonEmitterComposable + MutableParams: + active: true + MutableStateParam: + active: true + PreviewAnnotationNaming: + active: true + PreviewPublic: + active: true + RememberMissing: + active: true + RememberContentMissing: + active: true + UnstableCollections: + active: true + ViewModelForwarding: + active: true + # -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here: + # allowedStateHolderNames: .*ViewModel,.*Presenter + # -- You can optionally add an allowlist for Composable names that won't be affected by this rule + # allowedForwarding: .*Content,.*FancyStuff + ViewModelInjection: + active: true + # -- You can optionally add your own ViewModel factories here + # viewModelFactories: hiltViewModel,potatoViewModel From 3123acff5c9cccfefd7940184cc4667a4c214b51 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 29 Mar 2024 15:09:25 +0000 Subject: [PATCH 02/37] Add JetPack Compose dependencies and config. --- app/build.gradle | 17 +++++++++++++++++ build.gradle | 2 +- dependencies.gradle | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 dependencies.gradle diff --git a/app/build.gradle b/app/build.gradle index 3e711da..621472f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,8 @@ plugins { id 'io.gitlab.arturbosch.detekt' } +apply from: '../dependencies.gradle' + static String getBranchName() { final branchCommand = ["git", "symbolic-ref", "HEAD", "--short"] return branchCommand.execute().text.trim() @@ -57,6 +59,7 @@ android { buildFeatures { viewBinding = true buildConfig = true + compose = true } signingConfigs { release { @@ -75,6 +78,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + composeOptions { + kotlinCompilerExtensionVersion = project.ext.kotlinCompilerExtensionVersion + } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -100,7 +106,16 @@ detekt { dependencies { def lifecycle_version = "2.6.2" def room_version = "2.6.1" + def composeBom = platform('androidx.compose:compose-bom:2024.03.00') implementation fileTree(dir: 'libs', include: ['*.jar']) + + // JetPack Compose + implementation composeBom + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" implementation 'androidx.annotation:annotation:1.7.0' @@ -127,6 +142,8 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation composeBom + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.6") detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-ruleauthors:1.23.6") detektPlugins 'io.nlopez.compose.rules:detekt:0.3.12' diff --git a/build.gradle b/build.gradle index b6a8faf..7c9f460 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.9.21' + apply from: 'dependencies.gradle' repositories { google() mavenCentral() diff --git a/dependencies.gradle b/dependencies.gradle new file mode 100644 index 0000000..eccbfe4 --- /dev/null +++ b/dependencies.gradle @@ -0,0 +1,5 @@ +ext { + kotlin_version = '1.9.21' + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin#pre-release_kotlin_compatibility + kotlinCompilerExtensionVersion = '1.5.7' +} From a9fe1282c06c7feff60376aa2363e9998e4cb68c Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 16:32:14 +0100 Subject: [PATCH 03/37] Start to reimplement OnboardingActivity using Jetpack Compose. --- app/build.gradle | 1 + .../merging/onboarding/OnboardingActivity.kt | 139 +++++++++++------- 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 621472f..bb9e9c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,6 +113,7 @@ dependencies { implementation composeBom implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.activity:activity-compose' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index fe43a2e..c048b9f 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -1,18 +1,30 @@ package uk.me.jeremygreen.merging.onboarding import android.content.Intent -import android.graphics.Color -import android.net.Uri import android.os.Bundle -import android.view.View -import android.webkit.WebView -import android.webkit.WebViewClient +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import uk.me.jeremygreen.merging.databinding.OnboardingBinding +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.main.MainActivity import uk.me.jeremygreen.merging.model.AppViewModel - internal class OnboardingActivity: AppCompatActivity() { companion object { @@ -23,67 +35,84 @@ internal class OnboardingActivity: AppCompatActivity() { } - private lateinit var binding: OnboardingBinding private lateinit var appViewModel: AppViewModel // Activity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = OnboardingBinding.inflate(layoutInflater) - setContentView(binding.root) appViewModel = AppViewModel.getInstance(this, application) - setSupportActionBar(binding.onboardingToolbar) - binding.onboardingWebView.loadUrl(PRIVACY_HTML) - binding.onboardingWebView.setBackgroundColor(Color.TRANSPARENT) - binding.onboardingWebView.webViewClient = object : WebViewClient() { - override fun onPageFinished(view: WebView?, url: String?) { - binding.onboardingAcceptCheckbox.visibility = View.VISIBLE - } - override fun shouldOverrideUrlLoading(webView: WebView?, url: String): Boolean { - if (url.startsWith("mailto:")) { - val intent = Intent(Intent.ACTION_VIEW) - intent.setData(Uri.parse(url)) - startActivity(intent) - return true - } - return false - } - } - } - - // Activity - override fun onResume() { - super.onResume() - binding.onboardingAcceptButton.setOnClickListener { - binding.onboardingAcceptButton.setOnClickListener(null) - appViewModel.acceptOnboarding(version) - val intent = Intent(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME - startActivity(intent) - finish() - } - binding.onboardingAcceptCheckbox.setOnClickListener { - updateFabState() + setContent { + Onboarding() } - updateFabState() + //setSupportActionBar(binding.onboardingToolbar) +// binding.onboardingWebView.loadUrl(PRIVACY_HTML) +// binding.onboardingWebView.setBackgroundColor(Color.TRANSPARENT) +// binding.onboardingWebView.webViewClient = object : WebViewClient() { +// override fun onPageFinished(view: WebView?, url: String?) { +// binding.onboardingAcceptCheckbox.visibility = View.VISIBLE +// } +// override fun shouldOverrideUrlLoading(webView: WebView?, url: String): Boolean { +// if (url.startsWith("mailto:")) { +// val intent = Intent(Intent.ACTION_VIEW) +// intent.setData(Uri.parse(url)) +// startActivity(intent) +// return true +// } +// return false +// } +// } } /** * Update FloatingActionButton properties etc. */ - private fun updateFabState() { - if (binding.onboardingAcceptCheckbox.isChecked) { - binding.onboardingAcceptButton.show() - } else { - binding.onboardingAcceptButton.hide() - } - } +// private fun updateFabState() { +// if (binding.onboardingAcceptCheckbox.isChecked) { +// binding.onboardingAcceptButton.show() +// } else { +// binding.onboardingAcceptButton.hide() +// } +// } - // Activity - override fun onPause() { - binding.onboardingAcceptButton.setOnClickListener(null) - binding.onboardingAcceptCheckbox.setOnClickListener(null) - super.onPause() + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Onboarding() { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { Text(text = stringResource(R.string.appName)) } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { appViewModel.acceptOnboarding(version) + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME + startActivity(intent) + finish() + + } + ) { + Icon( + Icons.Default.Add, + contentDescription = "Add" + ) + } + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(text = "onboarding text") + } + } } } From 3ece853dabbb883ed4927acf8930bfbfa7d7de4a Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 16:35:04 +0100 Subject: [PATCH 04/37] TODO --- TODO | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/TODO b/TODO index 8a5ae08..0f5dcc3 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,12 @@ -* Try out JetPack compose. +* Try out Jetpack Compose. + - checkbox + - webview + - webview rendering refinements + - is double click protection required? + - remove onboarding.xml + - styling of onboarding doesn't match rest of app + - convert all app to Jetpack Compose? + - adjust styling? * NDK crash reporting From 4ae64a5d8d5a1f6353fa2a3db29f3115c108b1e1 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 17:01:22 +0100 Subject: [PATCH 05/37] Add Switch for onboarding text agreement. --- TODO | 6 ++- .../merging/onboarding/OnboardingActivity.kt | 38 +++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/TODO b/TODO index 0f5dcc3..a904078 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,11 @@ * Try out Jetpack Compose. - - checkbox + X checkbox - webview + - remove onboarding.xml - webview rendering refinements + - checkbox animation + - AnimatedVisibility? - is double click protection required? - - remove onboarding.xml - styling of onboarding doesn't match rest of app - convert all app to Jetpack Compose? - adjust styling? diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index c048b9f..f3d2877 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -14,10 +14,15 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -78,6 +83,7 @@ internal class OnboardingActivity: AppCompatActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun Onboarding() { + var agreed by rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { TopAppBar( @@ -89,19 +95,22 @@ internal class OnboardingActivity: AppCompatActivity() { ) }, floatingActionButton = { - FloatingActionButton( - onClick = { appViewModel.acceptOnboarding(version) - val intent = Intent(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME - startActivity(intent) - finish() - + if (agreed) { + FloatingActionButton( + onClick = { + appViewModel.acceptOnboarding(version) + val intent = Intent(this, MainActivity::class.java) + intent.flags = + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME + startActivity(intent) + finish() + }, + ) { + Icon( + Icons.Default.Add, + contentDescription = "Add" + ) } - ) { - Icon( - Icons.Default.Add, - contentDescription = "Add" - ) } } ) { innerPadding -> @@ -111,6 +120,11 @@ internal class OnboardingActivity: AppCompatActivity() { verticalArrangement = Arrangement.spacedBy(16.dp), ) { Text(text = "onboarding text") + Switch( + checked = agreed, + onCheckedChange = { + agreed = it + }) } } } From 712e06f4157b5f69233d372fb418c6a315cfd5a8 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 18:07:45 +0100 Subject: [PATCH 06/37] Scale in/out animation for FloatingActionButton. --- TODO | 3 +-- .../merging/onboarding/OnboardingActivity.kt | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index a904078..a1207aa 100644 --- a/TODO +++ b/TODO @@ -3,8 +3,7 @@ - webview - remove onboarding.xml - webview rendering refinements - - checkbox animation - - AnimatedVisibility? + X checkbox animation - is double click protection required? - styling of onboarding doesn't match rest of app - convert all app to Jetpack Compose? diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index f3d2877..071c776 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -4,6 +4,9 @@ import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -95,11 +98,15 @@ internal class OnboardingActivity: AppCompatActivity() { ) }, floatingActionButton = { - if (agreed) { + AnimatedVisibility( + visible = agreed, + enter = scaleIn(), + exit = scaleOut() + ) { FloatingActionButton( onClick = { appViewModel.acceptOnboarding(version) - val intent = Intent(this, MainActivity::class.java) + val intent = Intent(this@OnboardingActivity, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_TASK_ON_HOME startActivity(intent) From 8c16184b33a90e3c73e4f474455e26409032ce27 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 21:13:41 +0100 Subject: [PATCH 07/37] Add WebView for onboarding text. --- .../merging/onboarding/OnboardingActivity.kt | 49 +++++++------------ 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index 071c776..418e242 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -2,6 +2,7 @@ package uk.me.jeremygreen.merging.onboarding import android.content.Intent import android.os.Bundle +import android.webkit.WebView import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility @@ -29,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.main.MainActivity import uk.me.jeremygreen.merging.model.AppViewModel @@ -52,40 +54,11 @@ internal class OnboardingActivity: AppCompatActivity() { setContent { Onboarding() } - //setSupportActionBar(binding.onboardingToolbar) -// binding.onboardingWebView.loadUrl(PRIVACY_HTML) -// binding.onboardingWebView.setBackgroundColor(Color.TRANSPARENT) -// binding.onboardingWebView.webViewClient = object : WebViewClient() { -// override fun onPageFinished(view: WebView?, url: String?) { -// binding.onboardingAcceptCheckbox.visibility = View.VISIBLE -// } -// override fun shouldOverrideUrlLoading(webView: WebView?, url: String): Boolean { -// if (url.startsWith("mailto:")) { -// val intent = Intent(Intent.ACTION_VIEW) -// intent.setData(Uri.parse(url)) -// startActivity(intent) -// return true -// } -// return false -// } -// } } - /** - * Update FloatingActionButton properties etc. - */ -// private fun updateFabState() { -// if (binding.onboardingAcceptCheckbox.isChecked) { -// binding.onboardingAcceptButton.show() -// } else { -// binding.onboardingAcceptButton.hide() -// } -// } - - @OptIn(ExperimentalMaterial3Api::class) @Composable - fun Onboarding() { + private fun Onboarding() { var agreed by rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { @@ -126,7 +99,7 @@ internal class OnboardingActivity: AppCompatActivity() { .padding(innerPadding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Text(text = "onboarding text") + WebView(url = PRIVACY_HTML) Switch( checked = agreed, onCheckedChange = { @@ -136,4 +109,18 @@ internal class OnboardingActivity: AppCompatActivity() { } } + @Composable + private fun WebView( + @Suppress("SameParameterValue") url: String + ) { + AndroidView( + factory = { context -> + return@AndroidView WebView(context) + }, + update = { + it.loadUrl(url) + } + ) + } + } From 39933cea3004868e3f21abf40e5cc702a0df763e Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 21:14:29 +0100 Subject: [PATCH 08/37] Remove unused onboarding.xml. --- app/src/main/res/layout/onboarding.xml | 61 -------------------------- 1 file changed, 61 deletions(-) delete mode 100644 app/src/main/res/layout/onboarding.xml diff --git a/app/src/main/res/layout/onboarding.xml b/app/src/main/res/layout/onboarding.xml deleted file mode 100644 index 2117bb8..0000000 --- a/app/src/main/res/layout/onboarding.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file From e823f1be6562c5537fce9751619a38b4dad3c39e Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 21:26:16 +0100 Subject: [PATCH 09/37] Suppress detekt warning about Composable function names that start with capital letter. https://detekt.dev/docs/introduction/compose/#functionnaming-for-compose. --- detekt.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/detekt.yml b/detekt.yml index 51d7095..c5dbb84 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,6 +1,10 @@ empty-blocks: EmptyFunctionBlock: ignoreOverridden: true +naming: + FunctionNaming: + ignoreAnnotated: + - 'Composable' Compose: ComposableAnnotationNaming: active: true From 9c5c40fb452372e14f33602c03ebe476e5de8182 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 3 May 2024 21:27:19 +0100 Subject: [PATCH 10/37] TODO --- TODO | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TODO b/TODO index a1207aa..3014c1d 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,5 @@ * Try out Jetpack Compose. - X checkbox - - webview - - remove onboarding.xml - - webview rendering refinements - X checkbox animation + - fix layout - is double click protection required? - styling of onboarding doesn't match rest of app - convert all app to Jetpack Compose? From 466f76dfc925334470411415a53bc8145f08b7af Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Mon, 6 May 2024 16:48:15 +0100 Subject: [PATCH 11/37] Add back code that delays showing rest of onboarding UI until the WebView has loaded. --- .../merging/onboarding/OnboardingActivity.kt | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index 418e242..bf7bd51 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -3,6 +3,7 @@ package uk.me.jeremygreen.merging.onboarding import android.content.Intent import android.os.Bundle import android.webkit.WebView +import android.webkit.WebViewClient import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility @@ -25,6 +26,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -60,6 +62,7 @@ internal class OnboardingActivity: AppCompatActivity() { @Composable private fun Onboarding() { var agreed by rememberSaveable { mutableStateOf(false) } + var webViewLoaded by remember { mutableStateOf(false) } Scaffold( topBar = { TopAppBar( @@ -99,23 +102,32 @@ internal class OnboardingActivity: AppCompatActivity() { .padding(innerPadding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - WebView(url = PRIVACY_HTML) - Switch( - checked = agreed, - onCheckedChange = { - agreed = it - }) + WebView(url = PRIVACY_HTML, onLoaded = { webViewLoaded = true }) + if (webViewLoaded) { + Switch( + checked = agreed, + onCheckedChange = { + agreed = it + }) + } } } } @Composable private fun WebView( - @Suppress("SameParameterValue") url: String + @Suppress("SameParameterValue") url: String, + onLoaded: () -> Unit ) { AndroidView( factory = { context -> - return@AndroidView WebView(context) + return@AndroidView WebView(context).apply { + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + onLoaded() + } + } + } }, update = { it.loadUrl(url) From c5bc3ee408e6a930914a7d3327d27448b36f3443 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Mon, 6 May 2024 16:48:32 +0100 Subject: [PATCH 12/37] Code formatting. --- .../uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index bf7bd51..c97f230 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -98,8 +98,7 @@ internal class OnboardingActivity: AppCompatActivity() { } ) { innerPadding -> Column( - modifier = Modifier - .padding(innerPadding), + modifier = Modifier.padding(innerPadding), verticalArrangement = Arrangement.spacedBy(16.dp), ) { WebView(url = PRIVACY_HTML, onLoaded = { webViewLoaded = true }) From 88df056d84eee57c4a1bac0e6d8de5e2ad7a7a0f Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Mon, 6 May 2024 17:00:20 +0100 Subject: [PATCH 13/37] Indent onboarding accept Switch. --- TODO | 2 -- .../merging/onboarding/OnboardingActivity.kt | 15 ++++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/TODO b/TODO index 3014c1d..35b11c4 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,4 @@ * Try out Jetpack Compose. - - fix layout - - is double click protection required? - styling of onboarding doesn't match rest of app - convert all app to Jetpack Compose? - adjust styling? diff --git a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt index c97f230..51f48f4 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/onboarding/OnboardingActivity.kt @@ -103,11 +103,16 @@ internal class OnboardingActivity: AppCompatActivity() { ) { WebView(url = PRIVACY_HTML, onLoaded = { webViewLoaded = true }) if (webViewLoaded) { - Switch( - checked = agreed, - onCheckedChange = { - agreed = it - }) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Switch( + checked = agreed, + onCheckedChange = { + agreed = it + } + ) + } } } } From 5c4e5ccf819bc3db83cd2bd8789d9e3e409a6e6e Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Mon, 6 May 2024 17:26:44 +0100 Subject: [PATCH 14/37] Change AboutActivity to JetPack compose. --- TODO | 1 + .../merging/about/AboutActivity.kt | 79 +++++++++++++++---- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/TODO b/TODO index 35b11c4..ce980c2 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,5 @@ * Try out Jetpack Compose. + - deduplicate shared layout code. - styling of onboarding doesn't match rest of app - convert all app to Jetpack Compose? - adjust styling? diff --git a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt index e017de6..c91223f 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt @@ -1,7 +1,26 @@ package uk.me.jeremygreen.merging.about import android.os.Bundle +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import uk.me.jeremygreen.merging.BuildConfig import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.databinding.AboutBinding @@ -10,29 +29,57 @@ internal class AboutActivity: AppCompatActivity() { private val versionName by lazy { packageManager.getPackageInfo(packageName, 0).versionName } - private lateinit var binding: AboutBinding - // Activity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = AboutBinding.inflate(layoutInflater) - setContentView(binding.root) - setSupportActionBar(binding.aboutToolbar) - binding.appVersion.text = getString(R.string.version, BuildConfig.APPLICATION_ID, versionName) + setContent { About() } } - // Activity - override fun onResume() { - super.onResume() - binding.aboutToolbar.setNavigationOnClickListener { - finish() + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun About() { + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { Text(text = stringResource(R.string.actionAbout)) }, + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + } + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(stringResource(R.string.version, BuildConfig.APPLICATION_ID, versionName)) + } + } } } - // Activity - override fun onPause() { - super.onPause() - binding.aboutToolbar.setNavigationOnClickListener(null) - } +// // Activity +// override fun onResume() { +// super.onResume() +// binding.aboutToolbar.setNavigationOnClickListener { +// finish() +// } +// } +// +// // Activity +// override fun onPause() { +// super.onPause() +// binding.aboutToolbar.setNavigationOnClickListener(null) +// } } From f216387807351e16175a5b168bdfe367775188ad Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Mon, 6 May 2024 22:40:40 +0100 Subject: [PATCH 15/37] detekt wants reusable Composible functions to have modifier. Avoid by making the component private. https://mrmans0n.github.io/compose-rules/rules/#when-should-i-expose-modifier-parameters. Remove commented out code too. --- .../jeremygreen/merging/about/AboutActivity.kt | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt index c91223f..9fc65c2 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt @@ -37,7 +37,7 @@ internal class AboutActivity: AppCompatActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable - fun About() { + private fun About() { Scaffold( topBar = { TopAppBar( @@ -68,18 +68,4 @@ internal class AboutActivity: AppCompatActivity() { } } -// // Activity -// override fun onResume() { -// super.onResume() -// binding.aboutToolbar.setNavigationOnClickListener { -// finish() -// } -// } -// -// // Activity -// override fun onPause() { -// super.onPause() -// binding.aboutToolbar.setNavigationOnClickListener(null) -// } - } From efa820a754fa749211cc24ea1de37351614f95bb Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Tue, 7 May 2024 09:57:17 +0100 Subject: [PATCH 16/37] Refactor AboutActivity About component. --- .../merging/about/AboutActivity.kt | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt index 9fc65c2..f56a391 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt @@ -46,26 +46,34 @@ internal class AboutActivity: AppCompatActivity() { titleContentColor = MaterialTheme.colorScheme.primary, ), title = { Text(text = stringResource(R.string.actionAbout)) }, - navigationIcon = { - IconButton(onClick = { finish() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - tint = Color.White - ) - } - } + navigationIcon = { BackButton() } ) } ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding), ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(stringResource(R.string.version, BuildConfig.APPLICATION_ID, versionName)) - } + AboutText() } } } + @Composable + private fun AboutText() { + Column(modifier = Modifier.padding(16.dp)) { + Text(stringResource(R.string.version, BuildConfig.APPLICATION_ID, versionName)) + } + } + + @Composable + private fun BackButton() { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + } + } From b02f6eece8a270138cede3c563fb7f2964189458 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Tue, 7 May 2024 10:17:13 +0100 Subject: [PATCH 17/37] Remove about.xml layout. --- app/src/main/res/layout/about.xml | 42 ------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 app/src/main/res/layout/about.xml diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml deleted file mode 100644 index 76f4267..0000000 --- a/app/src/main/res/layout/about.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file From 54091696a0161f178517ab98db7b314a8fd8905c Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Tue, 7 May 2024 10:17:16 +0100 Subject: [PATCH 18/37] TODO --- TODO | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/TODO b/TODO index ce980c2..a9ca64d 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,12 @@ * Try out Jetpack Compose. - - deduplicate shared layout code. - - styling of onboarding doesn't match rest of app - - convert all app to Jetpack Compose? - - adjust styling? + - update to material 3 design + x onboarding + x about + - licences + - https://github.com/google/play-services-plugins/issues/248 + - https://stackoverflow.com/questions/76852715/how-to-use-open-source-notices-library-with-jetpack-compose + - https://github.com/jaredsburrows/gradle-license-plugin + - main * NDK crash reporting From 39ca679d6ec3fde41bd594f6f80dfb568920e095 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Tue, 7 May 2024 11:01:19 +0100 Subject: [PATCH 19/37] Remove unused imports. --- .../main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt index f56a391..43191ad 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/about/AboutActivity.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -23,7 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import uk.me.jeremygreen.merging.BuildConfig import uk.me.jeremygreen.merging.R -import uk.me.jeremygreen.merging.databinding.AboutBinding internal class AboutActivity: AppCompatActivity() { From c462aea19df8e634400275fa5b853376d93f8def Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Thu, 9 May 2024 16:42:51 +0100 Subject: [PATCH 20/37] TODO --- TODO | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO b/TODO index a9ca64d..f25e582 100644 --- a/TODO +++ b/TODO @@ -6,6 +6,10 @@ - https://github.com/google/play-services-plugins/issues/248 - https://stackoverflow.com/questions/76852715/how-to-use-open-source-notices-library-with-jetpack-compose - https://github.com/jaredsburrows/gradle-license-plugin + - tasks generate html, but aren't run automatically when build + - ./gradlew tasks breaks with AndroidSourceSet with name 'developDebugAndroidTest' not found. + => a pain to add dependencies + - - main * NDK crash reporting From f038f55b77d014b9f498885030d2187c6ec41c3a Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Thu, 9 May 2024 16:43:22 +0100 Subject: [PATCH 21/37] Partial attempt at using https://github.com/jaredsburrows/gradle-license-plugin. --- .gitignore | 3 +++ app/build.gradle | 7 +++++-- .../java/uk/me/jeremygreen/merging/main/MainActivity.kt | 7 ++----- build.gradle | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index ccbf754..6e38d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ .project .settings .classpath + +# https://github.com/jaredsburrows/gradle-license-plugin +open_source_licenses.html \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index bb9e9c0..5088874 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,11 @@ plugins { id 'com.android.application' - id 'com.google.android.gms.oss-licenses-plugin' id 'kotlin-android' id 'com.google.devtools.ksp' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'io.gitlab.arturbosch.detekt' + id 'com.jaredsburrows.license' } apply from: '../dependencies.gradle' @@ -98,6 +98,10 @@ android { namespace 'uk.me.jeremygreen.merging' } +licenseReport { + useVariantSpecificAssetDirs = true +} + detekt { buildUponDefaultConfig = true config.setFrom("../detekt.yml") @@ -135,7 +139,6 @@ dependencies { implementation 'com.google.firebase:firebase-crashlytics' // versioned with BOM implementation 'com.google.firebase:firebase-analytics' // versioned with BOM implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.4' - implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' implementation 'com.google.android.material:material:1.10.0' ksp "androidx.room:room-compiler:${room_version}" testImplementation "androidx.arch.core:core-testing:2.1.0" diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index c90508f..fec9f82 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -11,7 +11,6 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.viewpager2.widget.ViewPager2 -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig @@ -80,8 +79,7 @@ internal class MainActivity : AppCompatActivity() { this.appViewModel.allImages().observe(this) { images -> this.pagerAdapter.setImages(images) } - val licencesTitle = resources.getString(R.string.actionLicences) - OssLicensesMenuActivity.setActivityTitle(licencesTitle) + val licencesTitle = resources.getString(R.string.actionLicences) // } private fun screenView(screenName: String?) { @@ -118,8 +116,7 @@ internal class MainActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.actionLicences -> { - val intent = Intent(this, OssLicensesMenuActivity::class.java) - startActivity(intent) + // TODO true } R.id.actionAbout -> { diff --git a/build.gradle b/build.gradle index 7c9f460..625d765 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' + classpath 'com.jaredsburrows:gradle-license-plugin:0.9.7' classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' classpath 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.3' From bb0832c04369b64289ad6aee1a42eca424165abb Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Thu, 9 May 2024 16:43:53 +0100 Subject: [PATCH 22/37] Revert "Partial attempt at using https://github.com/jaredsburrows/gradle-license-plugin." This reverts commit f038f55b77d014b9f498885030d2187c6ec41c3a. --- .gitignore | 3 --- app/build.gradle | 7 ++----- .../java/uk/me/jeremygreen/merging/main/MainActivity.kt | 7 +++++-- build.gradle | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 6e38d7f..ccbf754 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,3 @@ .project .settings .classpath - -# https://github.com/jaredsburrows/gradle-license-plugin -open_source_licenses.html \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5088874..bb9e9c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,11 @@ plugins { id 'com.android.application' + id 'com.google.android.gms.oss-licenses-plugin' id 'kotlin-android' id 'com.google.devtools.ksp' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'io.gitlab.arturbosch.detekt' - id 'com.jaredsburrows.license' } apply from: '../dependencies.gradle' @@ -98,10 +98,6 @@ android { namespace 'uk.me.jeremygreen.merging' } -licenseReport { - useVariantSpecificAssetDirs = true -} - detekt { buildUponDefaultConfig = true config.setFrom("../detekt.yml") @@ -139,6 +135,7 @@ dependencies { implementation 'com.google.firebase:firebase-crashlytics' // versioned with BOM implementation 'com.google.firebase:firebase-analytics' // versioned with BOM implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.4' + implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' implementation 'com.google.android.material:material:1.10.0' ksp "androidx.room:room-compiler:${room_version}" testImplementation "androidx.arch.core:core-testing:2.1.0" diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index fec9f82..c90508f 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -11,6 +11,7 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.viewpager2.widget.ViewPager2 +import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig @@ -79,7 +80,8 @@ internal class MainActivity : AppCompatActivity() { this.appViewModel.allImages().observe(this) { images -> this.pagerAdapter.setImages(images) } - val licencesTitle = resources.getString(R.string.actionLicences) // + val licencesTitle = resources.getString(R.string.actionLicences) + OssLicensesMenuActivity.setActivityTitle(licencesTitle) } private fun screenView(screenName: String?) { @@ -116,7 +118,8 @@ internal class MainActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.actionLicences -> { - // TODO + val intent = Intent(this, OssLicensesMenuActivity::class.java) + startActivity(intent) true } R.id.actionAbout -> { diff --git a/build.gradle b/build.gradle index 625d765..7c9f460 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.jaredsburrows:gradle-license-plugin:0.9.7' + classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' classpath 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.3' From 025cbf6bf0750cb74deb74685813ff1bb6fe5e67 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 29 May 2024 11:24:38 +0100 Subject: [PATCH 23/37] Replace com.google.android.gms:play-services-oss-licenses with https://github.com/mikepenz/AboutLibraries. --- TODO | 10 +-- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 5 ++ .../merging/licences/LicencesActivity.kt | 72 +++++++++++++++++++ .../jeremygreen/merging/main/MainActivity.kt | 6 +- build.gradle | 2 +- dependencies.gradle | 1 + 7 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt diff --git a/TODO b/TODO index f25e582..51bdc82 100644 --- a/TODO +++ b/TODO @@ -2,15 +2,9 @@ - update to material 3 design x onboarding x about - - licences - - https://github.com/google/play-services-plugins/issues/248 - - https://stackoverflow.com/questions/76852715/how-to-use-open-source-notices-library-with-jetpack-compose - - https://github.com/jaredsburrows/gradle-license-plugin - - tasks generate html, but aren't run automatically when build - - ./gradlew tasks breaks with AndroidSourceSet with name 'developDebugAndroidTest' not found. - => a pain to add dependencies - - + X licences - main + - android:theme="@style/AppTheme.NoActionBar" required in manifest still? * NDK crash reporting diff --git a/app/build.gradle b/app/build.gradle index bb9e9c0..758b1b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ plugins { id 'com.android.application' - id 'com.google.android.gms.oss-licenses-plugin' + id 'com.mikepenz.aboutlibraries.plugin' id 'kotlin-android' id 'com.google.devtools.ksp' id 'com.google.gms.google-services' @@ -135,7 +135,7 @@ dependencies { implementation 'com.google.firebase:firebase-crashlytics' // versioned with BOM implementation 'com.google.firebase:firebase-analytics' // versioned with BOM implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.4' - implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' + implementation "com.mikepenz:aboutlibraries-compose-m3:${aboutLibsVersion}" implementation 'com.google.android.material:material:1.10.0' ksp "androidx.room:room-compiler:${room_version}" testImplementation "androidx.arch.core:core-testing:2.1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 606c4ef..8d37c95 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,11 @@ android:theme="@style/AppTheme.NoActionBar" android:exported="false"> + + + Column( + modifier = Modifier.padding(innerPadding), + ) { + LibrariesContainer( + Modifier.fillMaxSize() + ) + } + } + } + + @Composable + private fun BackButton() { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White + ) + } + } + +} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index c90508f..22a472f 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -11,13 +11,13 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.viewpager2.widget.ViewPager2 -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.about.AboutActivity import uk.me.jeremygreen.merging.databinding.MainBinding +import uk.me.jeremygreen.merging.licences.LicencesActivity import uk.me.jeremygreen.merging.model.AppViewModel import java.io.File import java.util.* @@ -80,8 +80,6 @@ internal class MainActivity : AppCompatActivity() { this.appViewModel.allImages().observe(this) { images -> this.pagerAdapter.setImages(images) } - val licencesTitle = resources.getString(R.string.actionLicences) - OssLicensesMenuActivity.setActivityTitle(licencesTitle) } private fun screenView(screenName: String?) { @@ -118,7 +116,7 @@ internal class MainActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.actionLicences -> { - val intent = Intent(this, OssLicensesMenuActivity::class.java) + val intent = Intent(this, LicencesActivity::class.java) startActivity(intent) true } diff --git a/build.gradle b/build.gradle index 7c9f460..8d76952 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6' + classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibsVersion" classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' classpath 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.3' diff --git a/dependencies.gradle b/dependencies.gradle index eccbfe4..0d1553c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -2,4 +2,5 @@ ext { kotlin_version = '1.9.21' // https://developer.android.com/jetpack/androidx/releases/compose-kotlin#pre-release_kotlin_compatibility kotlinCompilerExtensionVersion = '1.5.7' + aboutLibsVersion = '11.2.0' } From f49854021c01e94cd3a6ed7afb8c0d6d084f9f57 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 29 May 2024 12:51:12 +0100 Subject: [PATCH 24/37] TODO --- TODO | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO b/TODO index 51bdc82..975a3ad 100644 --- a/TODO +++ b/TODO @@ -89,6 +89,8 @@ * Don't show splashscreen if start up is fast enough - but if do show it, show it for at least 1.5s. +* Use ActivityResultContract not deprecated startActivityForResult/onActivityResult. + * Enable crashlytics and analytics in SplashActivity if previously enabled via OnboardingActivity. - currently, enable in MainActivity, which avoids need to persist state, but means early crashes not visible. From 444a03a2ef2019c4e9e0e3c2a0878e06fea54150 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 29 May 2024 12:56:56 +0100 Subject: [PATCH 25/37] Remove unused imports from LicencesActivity. --- .../java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt index 465d749..44ee1d7 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/licences/LicencesActivity.kt @@ -20,9 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer -import uk.me.jeremygreen.merging.BuildConfig import uk.me.jeremygreen.merging.R internal class LicencesActivity: AppCompatActivity() { From ea58ab37ac5813c5405d65dac4ece11443f01bdb Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Thu, 30 May 2024 15:09:54 +0100 Subject: [PATCH 26/37] Start to convert MainActivity to Jetpack Compose. --- TODO | 13 +- app/build.gradle | 1 - .../jeremygreen/merging/main/MainActivity.kt | 207 +++++++++++------- .../merging/main/PagerAdapterImpl.kt | 204 ++++++++--------- .../merging/main/ScreenFragmentFactory.kt | 17 -- .../merging/main/screen/AddImage.kt | 10 + .../merging/main/screen/AddImageFragment.kt | 28 --- .../merging/main/screen/ImageFragment.kt | 131 ----------- .../merging/main/screen/InputImage.kt | 120 ++++++++++ .../merging/main/screen/MergedImage.kt | 14 ++ .../main/screen/MergedImageFragment.kt | 28 --- app/src/main/res/layout/add_image_screen.xml | 8 - app/src/main/res/layout/image_screen.xml | 18 -- app/src/main/res/layout/main.xml | 45 ---- .../main/res/layout/merged_image_screen.xml | 20 -- app/src/main/res/menu/menu_main.xml | 14 -- app/src/main/res/values/dimens.xml | 1 - 17 files changed, 389 insertions(+), 490 deletions(-) delete mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/ScreenFragmentFactory.kt create mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImage.kt delete mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImageFragment.kt delete mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/ImageFragment.kt create mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt create mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt delete mode 100644 app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImageFragment.kt delete mode 100644 app/src/main/res/layout/add_image_screen.xml delete mode 100644 app/src/main/res/layout/image_screen.xml delete mode 100644 app/src/main/res/layout/main.xml delete mode 100644 app/src/main/res/layout/merged_image_screen.xml delete mode 100644 app/src/main/res/menu/menu_main.xml diff --git a/TODO b/TODO index 975a3ad..d56e731 100644 --- a/TODO +++ b/TODO @@ -4,7 +4,18 @@ x about X licences - main - - android:theme="@style/AppTheme.NoActionBar" required in manifest still? + X replace view binding with composables + x top bar + X menu + X make action button start camera app + - save camera image + - show images and faces + - splash + - Is AppTheme etc. required still? + - and AppCompatActivity as parent class + - remove old material library. + - remove viewBinding = true + - replace closeMenu function with Item Composable that also deduplicates DropdownMenuItem boilerplate * NDK crash reporting diff --git a/app/build.gradle b/app/build.gradle index 758b1b4..0048936 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,7 +136,6 @@ dependencies { implementation 'com.google.firebase:firebase-analytics' // versioned with BOM implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.4' implementation "com.mikepenz:aboutlibraries-compose-m3:${aboutLibsVersion}" - implementation 'com.google.android.material:material:1.10.0' ksp "androidx.room:room-compiler:${room_version}" testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.room:room-testing:${room_version}" diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 22a472f..787e37f 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -6,18 +6,44 @@ import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.util.Log -import android.view.Menu -import android.view.MenuItem +import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.core.content.FileProvider -import androidx.viewpager2.widget.ViewPager2 import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.about.AboutActivity -import uk.me.jeremygreen.merging.databinding.MainBinding import uk.me.jeremygreen.merging.licences.LicencesActivity +import uk.me.jeremygreen.merging.main.screen.AddImage +import uk.me.jeremygreen.merging.main.screen.InputImage +import uk.me.jeremygreen.merging.main.screen.MergedImage import uk.me.jeremygreen.merging.model.AppViewModel import java.io.File import java.util.* @@ -27,17 +53,12 @@ internal class MainActivity : AppCompatActivity() { companion object { private const val TAG = "MainActivity" private const val REQUEST_TAKE_PHOTO = 1 - private const val BUNDLE_KEY__FILE = "file" fun imagesDir(activity: Activity): File { return File(activity.filesDir, "photos") } } - private var file: File? = null - private lateinit var binding: MainBinding - private lateinit var pagerAdapter: PagerAdapterImpl - private lateinit var pageChangeCallback: ViewPager2.OnPageChangeCallback private lateinit var firebaseAnalytics: FirebaseAnalytics private val appViewModel by lazy { @@ -51,13 +72,6 @@ internal class MainActivity : AppCompatActivity() { // Activity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - this.binding = MainBinding.inflate(layoutInflater) - if (savedInstanceState != null) { - val fileString = savedInstanceState.getString(BUNDLE_KEY__FILE) - if (fileString != null) { - this.file = File(fileString) - } - } // Firebase Analytics and Crashlytics are only enabled after have agreed to their // use, which is done using OnboardingActivity. this.firebaseAnalytics = FirebaseAnalytics.getInstance(this) @@ -66,22 +80,109 @@ internal class MainActivity : AppCompatActivity() { val crashlytics = FirebaseCrashlytics.getInstance() crashlytics.setCrashlyticsCollectionEnabled(true) } - setContentView(this.binding.root) - setSupportActionBar(this.binding.toolbar) - this.pagerAdapter = PagerAdapterImpl(this) - this.binding.pager.adapter = this.pagerAdapter - this.binding.pager.offscreenPageLimit = 2 - this.pageChangeCallback = object: ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - val screenName: String? = this@MainActivity.pagerAdapter.screenName(binding.pager) - screenView(screenName) + setContent { Main() } +// this.appViewModel.allImages().observe(this) { images -> +// this.pagerAdapter.setImages(images) +// } + } + + @OptIn( + ExperimentalFoundationApi::class, + ExperimentalMaterial3Api::class + ) + @Composable + private fun Main() { + val context = this + val pagerState = rememberPagerState( + pageCount = { 2 } + ) + Scaffold( + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { Text(text = stringResource(R.string.appName)) }, + actions = { + OverflowMenu { closeMenu -> + DropdownMenuItem( + text = { Text(stringResource(R.string.actionAbout)) }, + onClick = { + closeMenu() + val intent = Intent(context, AboutActivity::class.java) + startActivity(intent) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.actionLicences)) }, + onClick = { + closeMenu() + val intent = Intent(context, LicencesActivity::class.java) + startActivity(intent) + } + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = ::handleTakePhoto, + ) { + Icon( + Icons.Default.Add, + contentDescription = "Add" + ) + } + } + + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding), + ) { + HorizontalPager( + state = pagerState, + beyondBoundsPageCount = 2 + ) { page -> + Pages(page, pagerState.pageCount) + } + } + } + } + + @Composable + private fun Pages(page: Int, pages: Int) { + when (page) { + 0 -> AddImage() // first + pages - 1 -> MergedImage() // last + else -> { + InputImage(page - 1) } } - this.appViewModel.allImages().observe(this) { images -> - this.pagerAdapter.setImages(images) + } + + @Composable + fun OverflowMenu(content: @Composable (closeMenu: () -> Unit) -> Unit) { + var showMenu by remember { mutableStateOf(false) } + val closeMenu = { showMenu = false} + IconButton(onClick = { + showMenu = !showMenu + }) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = null + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = closeMenu + ) { + content(closeMenu) } } + // TODO analytics private fun screenView(screenName: String?) { if (screenName == null) { return @@ -92,51 +193,7 @@ internal class MainActivity : AppCompatActivity() { this.firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, params) } - // Activity - override fun onResume() { - super.onResume() - binding.pager.registerOnPageChangeCallback(this.pageChangeCallback) - binding.fab.setOnClickListener { handleTakePhoto() } - } - - // Activity - override fun onPause() { - binding.fab.setOnClickListener(null) - binding.pager.unregisterOnPageChangeCallback(this.pageChangeCallback) - super.onPause() - } - - // Activity - override fun onCreateOptionsMenu(menu: Menu): Boolean { - this.menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - // Activity - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.actionLicences -> { - val intent = Intent(this, LicencesActivity::class.java) - startActivity(intent) - true - } - R.id.actionAbout -> { - val intent = Intent(this, AboutActivity::class.java) - startActivity(intent) - true - } - else -> super.onOptionsItemSelected(item) - } - } - - // Activity - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - val file = this.file - if (file != null) { - outState.putString(BUNDLE_KEY__FILE, file.path) - } - } + // TODO binding.fab.setOnClickListener { handleTakePhoto() } private fun handleTakePhoto() { val intent = createTakePhotoIntent() @@ -152,7 +209,6 @@ internal class MainActivity : AppCompatActivity() { BuildConfig.APPLICATION_ID + ".fileprovider", file ) - this.file = file intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri) return intent } @@ -160,9 +216,8 @@ internal class MainActivity : AppCompatActivity() { // Activity override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - val file = file - if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_TAKE_PHOTO && file != null) { - this.appViewModel.addImage(file.path) + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_TAKE_PHOTO) { + // TODO this.appViewModel.addImage(file.path) } } diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/PagerAdapterImpl.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/PagerAdapterImpl.kt index 21e0969..6264d27 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/PagerAdapterImpl.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/PagerAdapterImpl.kt @@ -1,105 +1,105 @@ package uk.me.jeremygreen.merging.main -import android.util.Log -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import uk.me.jeremygreen.merging.model.Image -import uk.me.jeremygreen.merging.main.screen.AddImageFragment -import uk.me.jeremygreen.merging.main.screen.ImageFragment -import uk.me.jeremygreen.merging.main.screen.MergedImageFragment +//import android.util.Log +//import androidx.fragment.app.Fragment +//import androidx.fragment.app.FragmentActivity +//import androidx.recyclerview.widget.DiffUtil +//import androidx.recyclerview.widget.RecyclerView +//import androidx.viewpager2.adapter.FragmentStateAdapter +//import androidx.viewpager2.widget.ViewPager2 +//import uk.me.jeremygreen.merging.model.Image +//import uk.me.jeremygreen.merging.main.screen.AddImage +//import uk.me.jeremygreen.merging.main.screen.InputImage +//import uk.me.jeremygreen.merging.main.screen.MergedImage -internal class PagerAdapterImpl( - fragmentActivity: FragmentActivity -) : FragmentStateAdapter(fragmentActivity) { - - companion object { - private const val TAG = "PagerAdapterImpl" - } - - private var ids: Set = setOf() - private var factories: List> = listOf() - - fun setImages(images: List) { - Log.v(TAG, "setImages(${images.size})") - val newFactories: MutableList> = mutableListOf() - if (images.isEmpty()) { - newFactories.add(AddImageFragment) - } - images.forEach {image -> - newFactories.add(ImageFragment.createFactory(image)) - } - if (images.size >= 2) { - newFactories.add(MergedImageFragment) - } - val newIds = newFactories.map{ factory -> factory.id }.toSet() - if (newIds.size != newFactories.size) { - throw AssertionError("ids: ${this.ids} factories: ${this.factories}") - } - if (RecyclerView.NO_ID in newIds) { - throw AssertionError("ids: ${this.ids} factories: ${this.factories}") - } - val changes = changes(this.factories, newFactories) - this.factories = newFactories - this.ids = newIds - changes.dispatchUpdatesTo(this) - } - - private fun changes( - old: List>, - new: List> - ): DiffUtil.DiffResult { - return DiffUtil.calculateDiff(object: DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return old[oldItemPosition].id == new[newItemPosition].id - } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return old[oldItemPosition].id == new[newItemPosition].id - } - override fun getNewListSize(): Int { - return new.size - } - override fun getOldListSize(): Int { - return old.size - } - }) - } - - /** - * Firebase analytics "screen name". - */ - fun screenName(viewPager2: ViewPager2): String? { - if (this.factories.isEmpty()) { - return null - } - val position = viewPager2.currentItem - return this.factories[position].screenName() - } - - // FragmentStateAdapter - override fun getItemId(position: Int): Long { - return this.factories[position].id // ...or RecyclerView.NO_ID? - } - - // FragmentStateAdapter - override fun containsItem(id: Long): Boolean { - return id in this.ids - } - - // FragmentStateAdapter - override fun getItemCount(): Int { - val count = this.ids.size - Log.v(TAG, "getItemCount() = ${count}") - return count - } - - // FragmentStateAdapter - override fun createFragment(position: Int): Fragment { - Log.v(TAG, "createFragment(${position})") - return this.factories[position].createInstance() - } - -} // FragmentStateAdapter +//internal class PagerAdapterImpl( +// fragmentActivity: FragmentActivity +//) : FragmentStateAdapter(fragmentActivity) { +// +// companion object { +// private const val TAG = "PagerAdapterImpl" +// } +// +// private var ids: Set = setOf() +// private var factories: List> = listOf() +// +// fun setImages(images: List) { +// Log.v(TAG, "setImages(${images.size})") +// val newFactories: MutableList> = mutableListOf() +// if (images.isEmpty()) { +// newFactories.add(AddImage) +// } +// images.forEach {image -> +// newFactories.add(InputImage.createFactory(image)) +// } +// if (images.size >= 2) { +// newFactories.add(MergedImage) +// } +// val newIds = newFactories.map{ factory -> factory.id }.toSet() +// if (newIds.size != newFactories.size) { +// throw AssertionError("ids: ${this.ids} factories: ${this.factories}") +// } +// if (RecyclerView.NO_ID in newIds) { +// throw AssertionError("ids: ${this.ids} factories: ${this.factories}") +// } +// val changes = changes(this.factories, newFactories) +// this.factories = newFactories +// this.ids = newIds +// changes.dispatchUpdatesTo(this) +// } +// +// private fun changes( +// old: List>, +// new: List> +// ): DiffUtil.DiffResult { +// return DiffUtil.calculateDiff(object: DiffUtil.Callback() { +// override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { +// return old[oldItemPosition].id == new[newItemPosition].id +// } +// override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { +// return old[oldItemPosition].id == new[newItemPosition].id +// } +// override fun getNewListSize(): Int { +// return new.size +// } +// override fun getOldListSize(): Int { +// return old.size +// } +// }) +// } +// +// /** +// * Firebase analytics "screen name". +// */ +// fun screenName(viewPager2: ViewPager2): String? { +// if (this.factories.isEmpty()) { +// return null +// } +// val position = viewPager2.currentItem +// return this.factories[position].screenName() +// } +// +// // FragmentStateAdapter +// override fun getItemId(position: Int): Long { +// return this.factories[position].id // ...or RecyclerView.NO_ID? +// } +// +// // FragmentStateAdapter +// override fun containsItem(id: Long): Boolean { +// return id in this.ids +// } +// +// // FragmentStateAdapter +// override fun getItemCount(): Int { +// val count = this.ids.size +// Log.v(TAG, "getItemCount() = ${count}") +// return count +// } +// +// // FragmentStateAdapter +// override fun createFragment(position: Int): Fragment { +// Log.v(TAG, "createFragment(${position})") +// return this.factories[position].createInstance() +// } +// +//} // FragmentStateAdapter diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/ScreenFragmentFactory.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/ScreenFragmentFactory.kt deleted file mode 100644 index 166fb9a..0000000 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/ScreenFragmentFactory.kt +++ /dev/null @@ -1,17 +0,0 @@ -package uk.me.jeremygreen.merging.main - -internal interface ScreenFragmentFactory { - - /** - * The ViewPager2 (RecyclerView) id. Images use non-negative Ints, and ViewPager2 uses -1. - */ - val id: Long - - fun createInstance(): T - - /** - * Firebase Analytics "screen name". - */ - fun screenName(): String - -} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImage.kt new file mode 100644 index 0000000..88ac6fd --- /dev/null +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImage.kt @@ -0,0 +1,10 @@ +package uk.me.jeremygreen.merging.main.screen + +import androidx.compose.runtime.Composable + +// id: Long = -2 +// screenName(): String = "AddImage" +@Composable +fun AddImage() { + // Empty - the UI is provided by MainActivity action button. +} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImageFragment.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImageFragment.kt deleted file mode 100644 index d0147b2..0000000 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/AddImageFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package uk.me.jeremygreen.merging.main.screen - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import uk.me.jeremygreen.merging.R -import uk.me.jeremygreen.merging.main.ScreenFragment -import uk.me.jeremygreen.merging.main.ScreenFragmentFactory - -internal class AddImageFragment : ScreenFragment() { - - companion object: ScreenFragmentFactory { - override val id: Long = -2 - override fun createInstance(): AddImageFragment { - return AddImageFragment() - } - override fun screenName(): String = "AddImage" - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.add_image_screen, container, false) - } - -} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/ImageFragment.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/ImageFragment.kt deleted file mode 100644 index cb3a582..0000000 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/ImageFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -package uk.me.jeremygreen.merging.main.screen - -import android.app.AlertDialog -import android.content.DialogInterface -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.google.mlkit.vision.face.FaceDetectorOptions -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import uk.me.jeremygreen.merging.R -import uk.me.jeremygreen.merging.databinding.ImageScreenBinding -import uk.me.jeremygreen.merging.main.ScreenFragment -import uk.me.jeremygreen.merging.main.ScreenFragmentFactory -import uk.me.jeremygreen.merging.model.Image -import uk.me.jeremygreen.merging.model.ProcessingStage - -internal class ImageFragment : ScreenFragment() { - - companion object { - - private const val TAG = "ImageFragment" - private const val BUNDLE_KEY__IMAGE_ID = "imageId" - - private const val BITMAP_WIDTH = 360 - private const val BITMAP_HEIGHT = 480 - - fun createFactory(image: Image): ScreenFragmentFactory { - require(image.id >= 0) { "might collide with non-image id: ${image.id}" } - return object: - ScreenFragmentFactory { - override val id: Long = image.id - override fun createInstance(): ImageFragment { - return ImageFragment().apply { - arguments = Bundle().apply { - putLong(BUNDLE_KEY__IMAGE_ID, image.id) - } - } - } - override fun screenName(): String = "Image" - } - } - - } - - private var _binding: ImageScreenBinding? = null - private val binding get() = _binding!! - - private val faceDetectorOptions by lazy { - FaceDetectorOptions.Builder() - .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE) - .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) - .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL) - .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) - .build() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = ImageScreenBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val bundle = arguments - val imageId: Long = bundle!!.getLong(BUNDLE_KEY__IMAGE_ID) - val facesView = binding.faces - appViewModel.faces(imageId).observe(viewLifecycleOwner) { faces -> - facesView.faces = faces - } - launch(Dispatchers.IO) { - val image = appViewModel.findById(imageId) - launch(Dispatchers.Main) { - facesView.setImage(image, ::handleLongClick) - } - val processingStage = appViewModel.getProcessingStage(imageId) - if (processingStage == ProcessingStage.unprocessed) { - processFaces(image, imageId) - } - } - } - - override fun onDestroyView() { - _binding = null - super.onDestroyView() - } - - private fun processFaces(image: Image, imageId: Long) { - // https//firebase.google.com/docs/ml-kit/android/detect-faces suggests size to use. - image.processBitmap(BITMAP_WIDTH, BITMAP_HEIGHT) { closeableReference -> - Log.i(TAG, "decoded bitmap for image id: ${imageId}") - // The IO thread has done it's work reading the Bitmap. Don't want to block this thread any more, - // so clone the reference and hand it to Dispatcher.Default coroutine to do the CPU-intensive - // face-detection work. - val clonedReference = closeableReference.clone() - launch(Dispatchers.Default) { - Log.i(TAG, "detecting faces for image id: ${imageId}") - image.findFaces(clonedReference, faceDetectorOptions, ::handleFaceDetectionError) { faces -> - Log.i(TAG, "detected ${faces.size} faces for image id: ${imageId}") - val processedImage = image.copy(processingStage = ProcessingStage.facesDetected) - appViewModel.addAll(processedImage, faces) - } - } - } - } - - private fun handleFaceDetectionError(e: Exception) { - Log.e(TAG, "face detection failed", e) - FirebaseCrashlytics.getInstance().recordException(e) - } - - private fun handleLongClick(image: Image) { - AlertDialog.Builder(requireContext()).apply { - setMessage(R.string.confirmDeleteImage) - setPositiveButton(R.string.ok) { _: DialogInterface, _: Int -> - binding.faces.setOnLongClickListener { false } - appViewModel.delete(image) - } - setNegativeButton(R.string.cancel) { _: DialogInterface, _: Int -> } - show() - } - } - -} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt new file mode 100644 index 0000000..c739045 --- /dev/null +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt @@ -0,0 +1,120 @@ +package uk.me.jeremygreen.merging.main.screen + +import android.app.AlertDialog +import android.content.DialogInterface +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.mlkit.vision.face.FaceDetectorOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import uk.me.jeremygreen.merging.R +import uk.me.jeremygreen.merging.model.Image +import uk.me.jeremygreen.merging.model.ProcessingStage + +private const val TAG = "ImageFragment" +private const val BUNDLE_KEY__IMAGE_ID = "imageId" + +private const val BITMAP_WIDTH = 360 +private const val BITMAP_HEIGHT = 480 + +private val faceDetectorOptions = + FaceDetectorOptions.Builder() + .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE) + .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL) + .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL) + .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) + .build() + +@Composable +fun InputImage(page: Int) { + Text("image: $page") + +// + + +} + +// fun createFactory(image: Image): ScreenFragmentFactory { +// require(image.id >= 0) { "might collide with non-image id: ${image.id}" } +// return object: +// ScreenFragmentFactory { +// override val id: Long = image.id +// override fun createInstance(): InputImage { +// return InputImage().apply { +// arguments = Bundle().apply { +// putLong(BUNDLE_KEY__IMAGE_ID, image.id) +// } +// } +// } +// override fun screenName(): String = "Image" +// } +// } +// +// } + +// private fun onViewCreated(view: View, savedInstanceState: Bundle?) { +// val bundle = arguments +// val imageId: Long = bundle!!.getLong(BUNDLE_KEY__IMAGE_ID) +// val facesView = binding.faces +// appViewModel.faces(imageId).observe(viewLifecycleOwner) { faces -> +// facesView.faces = faces +// } +// launch(Dispatchers.IO) { +// val image = appViewModel.findById(imageId) +// launch(Dispatchers.Main) { +// facesView.setImage(image, ::handleLongClick) +// } +// val processingStage = appViewModel.getProcessingStage(imageId) +// if (processingStage == ProcessingStage.unprocessed) { +// processFaces(image, imageId) +// } +// } +// } +// +// private fun processFaces(image: Image, imageId: Long) { +// // https//firebase.google.com/docs/ml-kit/android/detect-faces suggests size to use. +// image.processBitmap(BITMAP_WIDTH, BITMAP_HEIGHT) { closeableReference -> +// Log.i(TAG, "decoded bitmap for image id: ${imageId}") +// // The IO thread has done it's work reading the Bitmap. Don't want to block this thread any more, +// // so clone the reference and hand it to Dispatcher.Default coroutine to do the CPU-intensive +// // face-detection work. +// val clonedReference = closeableReference.clone() +// launch(Dispatchers.Default) { +// Log.i(TAG, "detecting faces for image id: ${imageId}") +// image.findFaces(clonedReference, faceDetectorOptions, ::handleFaceDetectionError) { faces -> +// Log.i(TAG, "detected ${faces.size} faces for image id: ${imageId}") +// val processedImage = image.copy(processingStage = ProcessingStage.facesDetected) +// appViewModel.addAll(processedImage, faces) +// } +// } +// } +// } +// +// private fun handleFaceDetectionError(e: Exception) { +// Log.e(TAG, "face detection failed", e) +// FirebaseCrashlytics.getInstance().recordException(e) +// } +// +// private fun handleLongClick(image: Image) { +// AlertDialog.Builder(requireContext()).apply { +// setMessage(R.string.confirmDeleteImage) +// setPositiveButton(R.string.ok) { _: DialogInterface, _: Int -> +// binding.faces.setOnLongClickListener { false } +// appViewModel.delete(image) +// } +// setNegativeButton(R.string.cancel) { _: DialogInterface, _: Int -> } +// show() +// } +// } + diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt new file mode 100644 index 0000000..5cc5ca3 --- /dev/null +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt @@ -0,0 +1,14 @@ +package uk.me.jeremygreen.merging.main.screen + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import uk.me.jeremygreen.merging.R + +// id: Long = -3 +// screenName(): String = "MergedImage" + +@Composable +fun MergedImage() { + Text(text = stringResource(R.string.merged_image)) +} diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImageFragment.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImageFragment.kt deleted file mode 100644 index 6827d2f..0000000 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImageFragment.kt +++ /dev/null @@ -1,28 +0,0 @@ -package uk.me.jeremygreen.merging.main.screen - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import uk.me.jeremygreen.merging.R -import uk.me.jeremygreen.merging.main.ScreenFragment -import uk.me.jeremygreen.merging.main.ScreenFragmentFactory - -internal class MergedImageFragment : ScreenFragment() { - - companion object: ScreenFragmentFactory { - override val id: Long = -3 - override fun createInstance(): MergedImageFragment { - return MergedImageFragment() - } - override fun screenName(): String = "MergedImage" - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.merged_image_screen, container, false) - } - -} diff --git a/app/src/main/res/layout/add_image_screen.xml b/app/src/main/res/layout/add_image_screen.xml deleted file mode 100644 index eeb13dd..0000000 --- a/app/src/main/res/layout/add_image_screen.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/image_screen.xml b/app/src/main/res/layout/image_screen.xml deleted file mode 100644 index 6afddb1..0000000 --- a/app/src/main/res/layout/image_screen.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/main.xml b/app/src/main/res/layout/main.xml deleted file mode 100644 index fb6e7c3..0000000 --- a/app/src/main/res/layout/main.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/merged_image_screen.xml b/app/src/main/res/layout/merged_image_screen.xml deleted file mode 100644 index e6834c8..0000000 --- a/app/src/main/res/layout/merged_image_screen.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml deleted file mode 100644 index 858a75e..0000000 --- a/app/src/main/res/menu/menu_main.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index ecdb70c..8542005 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,3 +1,2 @@ - 16dp From ebb6bda5873e69abedb5153da00fe52c59b9ce1c Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 31 May 2024 17:40:35 +0100 Subject: [PATCH 27/37] Show HorizontalPager pages based on images LiveData. No images or faces yet. --- .idea/detekt.xml | 7 +++ TODO | 6 +++ app/build.gradle | 1 + .../jeremygreen/merging/main/MainActivity.kt | 46 +++++++++++++++---- .../merging/main/screen/InputImage.kt | 12 +++-- 5 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 .idea/detekt.xml diff --git a/.idea/detekt.xml b/.idea/detekt.xml new file mode 100644 index 0000000..ee7289c --- /dev/null +++ b/.idea/detekt.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/TODO b/TODO index d56e731..b151071 100644 --- a/TODO +++ b/TODO @@ -8,9 +8,15 @@ x top bar X menu X make action button start camera app + - only show FAB if on add image page - save camera image - show images and faces - splash + - fresco is quite an old library and doesn't support jetpack compose directly + - coil? + - https://coil-kt.github.io/coil/migrating/#non-view-targets + - glide? + - picasso? - Is AppTheme etc. required still? - and AppCompatActivity as parent class - remove old material library. diff --git a/app/build.gradle b/app/build.gradle index 0048936..ea5c914 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,6 +112,7 @@ dependencies { // JetPack Compose implementation composeBom implementation 'androidx.compose.material3:material3' + implementation("androidx.compose.runtime:runtime-livedata:1.6.7") implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.activity:activity-compose' debugImplementation 'androidx.compose.ui:ui-tooling' diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 787e37f..3827978 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -29,6 +30,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -45,6 +47,7 @@ import uk.me.jeremygreen.merging.main.screen.AddImage import uk.me.jeremygreen.merging.main.screen.InputImage import uk.me.jeremygreen.merging.main.screen.MergedImage import uk.me.jeremygreen.merging.model.AppViewModel +import uk.me.jeremygreen.merging.model.Image import java.io.File import java.util.* @@ -93,8 +96,12 @@ internal class MainActivity : AppCompatActivity() { @Composable private fun Main() { val context = this + val images = this.appViewModel.allImages().observeAsState().value + //Log.i(TAG, "images: ${images?.size}") val pagerState = rememberPagerState( - pageCount = { 2 } + pageCount = { + pagerPageCount(images) + } ) Scaffold( topBar = { @@ -143,23 +150,42 @@ internal class MainActivity : AppCompatActivity() { ) { HorizontalPager( state = pagerState, - beyondBoundsPageCount = 2 + beyondBoundsPageCount = 2, + modifier = Modifier.fillMaxHeight() ) { page -> - Pages(page, pagerState.pageCount) + Pages(images, page) } } } } + private fun isMergedImageShown(images: List?) = !images.isNullOrEmpty() && images.size > 1 + + private fun pagerPageCount(images: List?): Int { + var pageCount = 1 // add image page + if (!images.isNullOrEmpty()) { + pageCount += images.size + } + if (isMergedImageShown(images)) { + pageCount += 1 + } + //Log.i(TAG, "pagerPageCount: ${pageCount}") + return pageCount + } + @Composable - private fun Pages(page: Int, pages: Int) { - when (page) { - 0 -> AddImage() // first - pages - 1 -> MergedImage() // last - else -> { - InputImage(page - 1) - } + private fun Pages(images: List?, page: Int) { + if (page == 0) { + AddImage() + return + } + val isLastPage = page == pagerPageCount(images) - 1 + if (isLastPage && isMergedImageShown(images)) { + MergedImage() + return } + val image = images?.get(page - 1) + InputImage(image) } @Composable diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt index c739045..cb725b9 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt @@ -5,8 +5,11 @@ import android.content.DialogInterface import android.os.Bundle import android.util.Log import android.view.View +import androidx.compose.foundation.Image import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.mlkit.vision.face.FaceDetectorOptions import kotlinx.coroutines.Dispatchers @@ -30,8 +33,10 @@ private val faceDetectorOptions = .build() @Composable -fun InputImage(page: Int) { - Text("image: $page") +internal fun InputImage(image: Image?) { + Text(image?.uri.toString()) + // TODO show FacesView +} // - -} - // fun createFactory(image: Image): ScreenFragmentFactory { // require(image.id >= 0) { "might collide with non-image id: ${image.id}" } // return object: From 17a08682a0edaaaadb4c5189c0ac3fef42bbae37 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 31 May 2024 17:56:14 +0100 Subject: [PATCH 28/37] Refactor MainActivity Floating Action Button code. --- .../jeremygreen/merging/main/MainActivity.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 3827978..1bdf5df 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -103,6 +103,16 @@ internal class MainActivity : AppCompatActivity() { pagerPageCount(images) } ) + val floatingActionButton = @Composable { + FloatingActionButton( + onClick = ::handleTakePhoto, + ) { + Icon( + Icons.Default.Add, + contentDescription = "Add" + ) + } + } Scaffold( topBar = { TopAppBar( @@ -133,17 +143,7 @@ internal class MainActivity : AppCompatActivity() { } ) }, - floatingActionButton = { - FloatingActionButton( - onClick = ::handleTakePhoto, - ) { - Icon( - Icons.Default.Add, - contentDescription = "Add" - ) - } - } - + floatingActionButton = floatingActionButton ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding), From 97a9652f8fddd45e8ac3bae3ed2649325c757bd9 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Fri, 31 May 2024 18:04:55 +0100 Subject: [PATCH 29/37] Only show AddImage page if there are no images. --- TODO | 1 - .../me/jeremygreen/merging/main/MainActivity.kt | 17 ++++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/TODO b/TODO index b151071..6026c1c 100644 --- a/TODO +++ b/TODO @@ -8,7 +8,6 @@ x top bar X menu X make action button start camera app - - only show FAB if on add image page - save camera image - show images and faces - splash diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 1bdf5df..4d549fa 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -162,20 +162,19 @@ internal class MainActivity : AppCompatActivity() { private fun isMergedImageShown(images: List?) = !images.isNullOrEmpty() && images.size > 1 private fun pagerPageCount(images: List?): Int { - var pageCount = 1 // add image page - if (!images.isNullOrEmpty()) { - pageCount += images.size + if (images.isNullOrEmpty()) { + return 1 // add image page } - if (isMergedImageShown(images)) { - pageCount += 1 + return if (isMergedImageShown(images)) { + images.size + 1 + } else { + images.size } - //Log.i(TAG, "pagerPageCount: ${pageCount}") - return pageCount } @Composable private fun Pages(images: List?, page: Int) { - if (page == 0) { + if (images.isNullOrEmpty()) { AddImage() return } @@ -184,7 +183,7 @@ internal class MainActivity : AppCompatActivity() { MergedImage() return } - val image = images?.get(page - 1) + val image = images?.get(page) InputImage(image) } From 23955367fd6e94bc3501ab81aa53819a2a57515a Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:44:33 +0100 Subject: [PATCH 30/37] Reimplement photo taking. --- .../jeremygreen/merging/main/MainActivity.kt | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 4d549fa..12f8563 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -4,9 +4,10 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle -import android.provider.MediaStore import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column @@ -33,6 +34,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -55,7 +57,6 @@ internal class MainActivity : AppCompatActivity() { companion object { private const val TAG = "MainActivity" - private const val REQUEST_TAKE_PHOTO = 1 fun imagesDir(activity: Activity): File { return File(activity.filesDir, "photos") @@ -97,15 +98,27 @@ internal class MainActivity : AppCompatActivity() { private fun Main() { val context = this val images = this.appViewModel.allImages().observeAsState().value - //Log.i(TAG, "images: ${images?.size}") val pagerState = rememberPagerState( pageCount = { pagerPageCount(images) } ) + var imageUri: Uri? by rememberSaveable { mutableStateOf(null) } + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + appViewModel.addImage(imageUri.toString()) + } + // TODO else analytics + } + ) val floatingActionButton = @Composable { FloatingActionButton( - onClick = ::handleTakePhoto, + onClick = { + imageUri = createTakeImageUri() + cameraLauncher.launch(imageUri) + }, ) { Icon( Icons.Default.Add, @@ -183,7 +196,7 @@ internal class MainActivity : AppCompatActivity() { MergedImage() return } - val image = images?.get(page) + val image = images[page] InputImage(image) } @@ -218,32 +231,15 @@ internal class MainActivity : AppCompatActivity() { this.firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, params) } - // TODO binding.fab.setOnClickListener { handleTakePhoto() } - private fun handleTakePhoto() { - val intent = createTakePhotoIntent() - startActivityForResult(intent, REQUEST_TAKE_PHOTO) - } - - private fun createTakePhotoIntent(): Intent { - val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - val uuid = UUID.randomUUID() - val file = File(imagesDir, "${uuid}.jpg") - val imageUri: Uri = FileProvider.getUriForFile( - baseContext, - BuildConfig.APPLICATION_ID + ".fileprovider", - file - ) - intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri) - return intent - } - - // Activity - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_TAKE_PHOTO) { - // TODO this.appViewModel.addImage(file.path) - } + private fun createTakeImageUri(): Uri { + val uuid = UUID.randomUUID() + val file = File(imagesDir, "${uuid}.jpg") + return FileProvider.getUriForFile( + baseContext, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) } } // MainActivity From 45ef5621f96f25e606cd62526717a8a120a8e80c Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:58:38 +0100 Subject: [PATCH 31/37] Refactor photo taking code into own Composable. --- .../jeremygreen/merging/main/MainActivity.kt | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 12f8563..0ef72a1 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -90,6 +90,24 @@ internal class MainActivity : AppCompatActivity() { // } } + @Composable + fun takePicture(): () -> Unit { + var imageUri: Uri? by rememberSaveable { mutableStateOf(null) } + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + appViewModel.addImage(imageUri.toString()) + } + // TODO else analytics + } + ) + return { + imageUri = createTakeImageUri() + cameraLauncher.launch(imageUri) + } + } + @OptIn( ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class @@ -103,22 +121,10 @@ internal class MainActivity : AppCompatActivity() { pagerPageCount(images) } ) - var imageUri: Uri? by rememberSaveable { mutableStateOf(null) } - val cameraLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.TakePicture(), - onResult = { success -> - if (success) { - appViewModel.addImage(imageUri.toString()) - } - // TODO else analytics - } - ) + val takePicture = takePicture() val floatingActionButton = @Composable { FloatingActionButton( - onClick = { - imageUri = createTakeImageUri() - cameraLauncher.launch(imageUri) - }, + onClick = { takePicture() }, ) { Icon( Icons.Default.Add, From d19243105bd1b787ceac6ce968c2e718b8bb0b2b Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:12:57 +0100 Subject: [PATCH 32/37] Need to create imagesDir. --- .../main/java/uk/me/jeremygreen/merging/main/MainActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 0ef72a1..a895051 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.core.content.FileProvider +import com.facebook.common.file.FileUtils import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig @@ -96,6 +97,7 @@ internal class MainActivity : AppCompatActivity() { val cameraLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.TakePicture(), onResult = { success -> + //Log.i(TAG, "takePicture: uri: $imageUri success: $success") if (success) { appViewModel.addImage(imageUri.toString()) } @@ -103,6 +105,7 @@ internal class MainActivity : AppCompatActivity() { } ) return { + FileUtils.mkdirs(imagesDir) imageUri = createTakeImageUri() cameraLauncher.launch(imageUri) } From 7d2550f12551f312a444f0e7ec58b59de118480c Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:13:23 +0100 Subject: [PATCH 33/37] Remove obsolete commented out code. --- .../main/java/uk/me/jeremygreen/merging/main/MainActivity.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index a895051..dead982 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -86,9 +86,6 @@ internal class MainActivity : AppCompatActivity() { crashlytics.setCrashlyticsCollectionEnabled(true) } setContent { Main() } -// this.appViewModel.allImages().observe(this) { images -> -// this.pagerAdapter.setImages(images) -// } } @Composable From d4c5725afa0c82b525d4b537e141f94a038837a7 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:21:36 +0100 Subject: [PATCH 34/37] Centre align merged image text. --- .../merging/main/screen/MergedImage.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt index 5cc5ca3..ea1322e 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/MergedImage.kt @@ -1,7 +1,12 @@ package uk.me.jeremygreen.merging.main.screen +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import uk.me.jeremygreen.merging.R @@ -10,5 +15,13 @@ import uk.me.jeremygreen.merging.R @Composable fun MergedImage() { - Text(text = stringResource(R.string.merged_image)) + Column ( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + Text( + text = stringResource(R.string.merged_image) + ) + } } From d53c08bc8aca0020b0834d107dc0d23ae3df4c26 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:26:26 +0100 Subject: [PATCH 35/37] Workaround for out-of-range page if add image when on merged image page if beyondBoundsPageCount set to 2 not 0. --- .../main/java/uk/me/jeremygreen/merging/main/MainActivity.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index dead982..199089c 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -193,6 +193,7 @@ internal class MainActivity : AppCompatActivity() { @Composable private fun Pages(images: List?, page: Int) { + Log.i(TAG, "Pages: page=$page images=${images?.size}") if (images.isNullOrEmpty()) { AddImage() return @@ -202,6 +203,10 @@ internal class MainActivity : AppCompatActivity() { MergedImage() return } + if (page < 0 || page >= images.size) { + // Non-zero beyondBoundsPageCount causes out-of-range page to be provided. + return + } val image = images[page] InputImage(image) } From 2f1d078a996e405f8e9d79d9586caf9911470f9b Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:35:01 +0100 Subject: [PATCH 36/37] Implement long-press image deletion (without confirmation dialog). --- .../me/jeremygreen/merging/main/MainActivity.kt | 2 +- .../jeremygreen/merging/main/screen/InputImage.kt | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index 199089c..fb70480 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -208,7 +208,7 @@ internal class MainActivity : AppCompatActivity() { return } val image = images[page] - InputImage(image) + InputImage(image, onLongClick = { appViewModel.delete(image) }) } @Composable diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt index cb725b9..5785156 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt @@ -5,9 +5,12 @@ import android.content.DialogInterface import android.os.Bundle import android.util.Log import android.view.View +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -32,9 +35,17 @@ private val faceDetectorOptions = .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_NONE) .build() +@OptIn(ExperimentalFoundationApi::class) @Composable -internal fun InputImage(image: Image?) { - Text(image?.uri.toString()) +internal fun InputImage(image: Image?, onLongClick : () -> Unit = {}) { + Text( + text = image?.uri.toString(), + modifier = Modifier.combinedClickable( + enabled = true, + onLongClick = { onLongClick() }, + onClick = {} + ) + ) // TODO show FacesView } From 38fc459248f4469840b6a253b854ffca5021b031 Mon Sep 17 00:00:00 2001 From: Jeremy Green <20728342+jg210@users.noreply.github.com> Date: Wed, 21 Aug 2024 16:31:02 +0100 Subject: [PATCH 37/37] Try out coil for image loading. Get weird UI glitches in combination with left-right swiping component. --- .idea/deploymentTargetDropDown.xml | 15 ++++++- README.md | 2 +- app/build.gradle | 2 + .../jeremygreen/merging/main/MainActivity.kt | 4 ++ .../merging/main/screen/InputImage.kt | 39 +++++++++---------- 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c338..59e2001 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -3,7 +3,20 @@ - + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 5c5186b..06dc4a7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It's using: * [ML Kit](https://developers.google.com/ml-kit/vision/face-detection) for face feature detection. * [Firebase Crashlytics](https://firebase.google.com/docs/crashlytics/) for crash reporting. * [Firebase Analytics](https://firebase.google.com/docs/analytics). -* [Fresco](https://developers.google.com/ml-kit/) for android Bitmap management. +* [Coil](https://coil-kt.github.io/coil/) for android Bitmap management. * [Circle CI](https://circleci.com/gh/jg210/merging) for automated build, test and continuous delivery [![CircleCI](https://circleci.com/gh/jg210/merging/tree/develop.svg?style=svg)](https://circleci.com/gh/jg210/merging) * [Fastlane](https://fastlane.tools/) to publish the app as a beta (early access) release for every commit on the release branch. [Get it on Google Play](https://play.google.com/store/apps/details?id=uk.me.jeremygreen.merging) * [Material Design](https://material.io/design/). diff --git a/app/build.gradle b/app/build.gradle index ea5c914..5467a5e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,8 @@ dependencies { implementation "androidx.room:room-ktx:${room_version}" implementation "androidx.room:room-runtime:${room_version}" implementation 'androidx.viewpager2:viewpager2:1.0.0' + implementation("io.coil-kt:coil:2.6.0") + implementation("io.coil-kt:coil-compose:2.6.0") implementation 'com.facebook.fresco:fresco:3.1.3' // https://github.com/facebook/fresco/issues/2598#issuecomment-1110860661 compileOnly 'com.facebook.infer.annotation:infer-annotation:0.18.0' diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt index fb70480..9d0f40c 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/MainActivity.kt @@ -10,8 +10,10 @@ import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -39,7 +41,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.core.content.FileProvider +import coil.compose.AsyncImage import com.facebook.common.file.FileUtils +import com.facebook.imagepipeline.request.ImageRequest import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics import uk.me.jeremygreen.merging.BuildConfig diff --git a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt index 5785156..d7e8e70 100644 --- a/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt +++ b/app/src/main/java/uk/me/jeremygreen/merging/main/screen/InputImage.kt @@ -1,25 +1,16 @@ package uk.me.jeremygreen.merging.main.screen -import android.app.AlertDialog -import android.content.DialogInterface -import android.os.Bundle -import android.util.Log -import android.view.View import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter -import com.google.firebase.crashlytics.FirebaseCrashlytics +import coil.compose.AsyncImage +import coil.request.ImageRequest import com.google.mlkit.vision.face.FaceDetectorOptions -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import uk.me.jeremygreen.merging.R import uk.me.jeremygreen.merging.model.Image -import uk.me.jeremygreen.merging.model.ProcessingStage +import java.io.File private const val TAG = "ImageFragment" private const val BUNDLE_KEY__IMAGE_ID = "imageId" @@ -38,13 +29,21 @@ private val faceDetectorOptions = @OptIn(ExperimentalFoundationApi::class) @Composable internal fun InputImage(image: Image?, onLongClick : () -> Unit = {}) { - Text( - text = image?.uri.toString(), - modifier = Modifier.combinedClickable( - enabled = true, - onLongClick = { onLongClick() }, - onClick = {} - ) + if (image == null) { + return + } + Text("${image.uri} exists: ${File(image.uri.path).exists()}") + AsyncImage( +// model = image.uri, + model = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Tabby_Kitten_on_Blue_Throw.jpg/1280px-Tabby_Kitten_on_Blue_Throw.jpg", + contentDescription = null, + modifier = Modifier. + fillMaxSize(). + combinedClickable( + enabled = true, + onLongClick = { onLongClick() }, + onClick = {} + ) ) // TODO show FacesView }