From 2060ef72978a0bc54a9dc23027455edd6d73979b Mon Sep 17 00:00:00 2001 From: Evan Strat <5790137+evan10s@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:25:21 -0400 Subject: [PATCH] Android TLC: 2023 edition (with 2024 goodies) (#65) * Target SDK 33 * Update dependency versions * First pass at M3 update * add app launch todo * Remainder of M3 upgrade * Dependency updates * Cleanup M3 upgrade todos * Use android-build-box 1.25 for Java 17 support * Update detekt config to remove deprecated rules * Update detekt plugin version * Update comment * Style fixes * Use Java 17 * Update dependencies and finish M3 upgrade * Fix ruby lock file? * In-app update cleanup, update Hilt version, move from kapt to ksp * Final fixes * Remove unnecessary themes.xml files * Remove unused dependencies * change plugin repo order to optimize builds * Add back material dependency * Add CodeQL analysis for Ruby * Move Ruby analysis to separate workflow * Rename job for clarity * Remove redundant comment from Ruby CodeQL workflow * Trim trailing whitespace in README * Remove fade transitions from app navigations * Update release instructions * Refactor repeated update state code * Fix bug where optional update bottom sheet background didn't match dark theme * Fix copy spacing * Add debug buttons in settings to test update screens * revert version code change --------- Co-authored-by: Evan Strat Co-authored-by: Kristaps Berzinch --- .github/workflows/build.yml | 4 +- .github/workflows/internal-test-release.yml | 2 +- .github/workflows/ruby-codeql.yml | 30 +++ README.md | 66 ++--- app/build.gradle.kts | 28 +- .../apiary/ApiaryMobileApplication.kt | 4 +- .../org/robojackets/apiary/MainActivity.kt | 71 ++++-- .../apiary/di/MainActivityModule.kt | 11 +- .../apiary/network/UserAgentInterceptor.kt | 10 +- .../robojackets/apiary/ui/global/AppTopBar.kt | 30 ++- .../apiary/ui/settings/Settings.kt | 62 ++++- .../apiary/ui/settings/SettingsViewModel.kt | 36 ++- .../apiary/ui/update/UpdateAvailable.kt | 164 ++++++++---- .../apiary/ui/update/UpdateGate.kt | 226 +++++++--------- attendance/build.gradle.kts | 26 +- .../apiary/attendance/Attendance.kt | 11 +- .../model/AttendableTypeSelectionViewModel.kt | 15 +- .../attendance/model/AttendanceViewModel.kt | 27 +- .../attendance/ui/AttendableSelection.kt | 18 +- .../ui/AttendableTypeSelectionScreen.kt | 44 +++- .../src/main/res/values-night/themes.xml | 16 -- attendance/src/main/res/values/themes.xml | 16 -- auth/build.gradle.kts | 24 +- .../apiary/auth/ui/Authentication.kt | 241 ++++++++++-------- .../ui/permissions/InsufficientPermissions.kt | 39 ++- .../permissions/MissingHiddenTeamsCallout.kt | 13 +- auth/src/main/res/values-night/themes.xml | 16 -- auth/src/main/res/values/themes.xml | 16 -- base/build.gradle.kts | 28 +- .../apiary/base/ui/ActionPrompt.kt | 22 +- .../apiary/base/ui/IconWithText.kt | 2 +- .../apiary/base/ui/callout/Callout.kt | 21 +- .../apiary/base/ui/error/ErrorMessage.kt | 14 +- .../apiary/base/ui/icons/ExtraIcons.kt | 23 +- .../apiary/base/ui/nfc/BuzzCardPrompt.kt | 9 +- .../apiary/base/ui/nfc/NfcRequired.kt | 13 +- .../robojackets/apiary/base/ui/theme/Color.kt | 75 ++++++ .../robojackets/apiary/base/ui/theme/Shape.kt | 2 +- .../robojackets/apiary/base/ui/theme/Theme.kt | 135 ++++++++-- .../robojackets/apiary/base/ui/theme/Type.kt | 7 +- .../apiary/base/ui/util/LoadingSpinner.kt | 2 +- .../apiary/base/ui/util/MadeWithLove.kt | 2 +- build.gradle.kts | 32 +-- buildSrc/src/main/java/Dependencies.kt | 78 +++--- ci/detekt/detekt.yml | 57 ++--- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- navigation/build.gradle.kts | 18 +- .../apiary/navigation/NavigationDirections.kt | 6 +- settings.gradle.kts | 2 +- 50 files changed, 1073 insertions(+), 747 deletions(-) create mode 100644 .github/workflows/ruby-codeql.yml delete mode 100644 attendance/src/main/res/values-night/themes.xml delete mode 100644 attendance/src/main/res/values/themes.xml delete mode 100644 auth/src/main/res/values-night/themes.xml delete mode 100644 auth/src/main/res/values/themes.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8a15b8..c44948e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: '11' + java-version: '17' distribution: 'corretto' cache: gradle @@ -85,7 +85,7 @@ jobs: GOOGLE_PLAY_PRIVATE_KEY: ${{ secrets.GOOGLE_PLAY_PRIVATE_KEY }} IS_CI: true - - name: Perform CodeQL Analysis + - name: Perform CodeQL Analysis for Java/Kotlin uses: github/codeql-action/analyze@v3 with: category: "/language:java-kotlin" diff --git a/.github/workflows/internal-test-release.yml b/.github/workflows/internal-test-release.yml index 0f45d34..e449cb4 100644 --- a/.github/workflows/internal-test-release.yml +++ b/.github/workflows/internal-test-release.yml @@ -23,7 +23,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: '11' + java-version: '17' distribution: 'corretto' cache: gradle diff --git a/.github/workflows/ruby-codeql.yml b/.github/workflows/ruby-codeql.yml new file mode 100644 index 0000000..29291cc --- /dev/null +++ b/.github/workflows/ruby-codeql.yml @@ -0,0 +1,30 @@ +name: CodeQL Analysis + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + ruby: + + runs-on: ubuntu-latest + + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ruby + build-mode: none + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:ruby" diff --git a/README.md b/README.md index e87b90c..8073594 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ NFC functionality uses the NXP MIFARE TapLinx Android SDK. You must provide a l license key from the TapLinx Developer Center on https://www.mifare.net/en/products/tools/taplinx/. Additionally, important licensing information about the TapLinx library is included in the [`libs`](libs) -directory, including the [license](libs/LA_OPT_NXP_Software_License.txt) +directory, including the [license](libs/LA_OPT_NXP_Software_License.txt) and [Software Content Register](libs/Taplinx_Android_SDK_SCR.txt). **Note:** For RoboJackets developers, reach out in #apiary-mobile in Slack to obtain our keys. @@ -31,7 +31,7 @@ sentryDsn=SENTRY_DSN_HERE There are 5 modules encompassing features and utilities. - **app** - The main application module - + Note: To avoid circular dependencies, the below modules **must not** have the **app** module as a dependency. Place such code in the `base` (or a new) module. @@ -42,12 +42,12 @@ dependency. Place such code in the `base` (or a new) module. #### Dependency management -Dependency versions are managed centrally in +Dependency versions are managed centrally in [Dependencies.kt](buildSrc/src/main/java/Dependencies.kt) in the `buildSrc` module. If you change a version in `Dependencies.kt`, make sure to manually sync Gradle because Android Studio might not recognize that the change requires a Gradle sync. -After adding a dependency in `Dependencies.kt`, you must also add it to the appropriate Gradle +After adding a dependency in `Dependencies.kt`, you must also add it to the appropriate Gradle Script (take a look at a `build.gradle.kts` file for one of the modules for examples). ### Environment configuration @@ -73,12 +73,6 @@ is a helpful resource. Detekt is used for linting Kotlin code. The recommended command to run it is -_(Windows)_ -```bash -./gradlew detektAll -PdetektAutoFix=true -``` - -_(*nix)_ ```bash ./gradlew detektAll -PdetektAutoFix=true ``` @@ -87,7 +81,7 @@ The detektAutoFix parameter will automatically fix simple issues. ### Fastlane -We use Fastlane to automate steps of the Android release process, in combination with Concourse CI. +We use Fastlane to automate steps of the Android release process, in combination with Github Actions. To install Fastlane, you'll need Ruby with the development kit installed. On Windows, install the latest 64-bit version of Ruby+Devkit from https://rubyinstaller.org/downloads/. @@ -115,44 +109,24 @@ App releases don't have to perfectly coincide with PRs being merged, especially merged in close proximity. Our Concourse pipeline has jobs to automatically handle building, signing, and uploading production releases of the app. -1. After you've merged all PRs to be included in the release, ensure the `.update-priority` -file is set correctly according to the table below. Use priority 2 as the default. If you want to -use priority 4 or 5, post in #apiary-mobile first. - 1. Update priority affects if and how often users receive in-app update prompts to update the -app to the latest version. - -| Update priority | Description | Examples | Update timeline for users | -| --- | --- |--- | --- | -| 0/1/2 | Very low or low priority | UI touchups that don't impact functionality, releases with options to opt-in to beta features | No prompt initially. Optional prompt starting 14 days after release. Immediate update 21 days after release. | -| 3 | Medium priority | Medium-priority bug fixes, performance improvements, non-time-sensitive feature launches | No prompts for the first 3 days. Optional starting 4 days after release. Immediate update 21 days after release. | -| 4 | High priority or time-sensitive | High priority bug fixes, time-sensitive feature launches | Optional for the first 24 hours, then immediate. | -| 5 | Critical bug fixes | Crashes/bugs impacting major features, urgent vulnerabilities | Immediate update required. | - -2. Create a new release on `main` using a tag with a name like `v1.0.0`. Use semantic versioning -to determine how to increment the version number. - 1. Go to https://github.com/RoboJackets/apiary-mobile/releases - 2. Press the **Draft a new release** button. - 3. Decide on the new version tag; it **must** start with `v`. In general, you should increment -the previous release's version using [semantic versioning](https://semver.org/) guidelines (most -releases will be a 0.1.0 (minor) or 0.0.1 (patch) increment). Press **Choose a tag**, then enter the -new tag name to create it on publish. - 4. Leave **Release title** blank. Instead, press **Generate release notes**. The release title -and description should automatically fill in with the changes since the last release. - 1. In general, you shouldn't need to manually set the value of the `Previous tag` field, - unless the release notes seem incorrect. -3. When you publish the release (which creates a new tag), a Concourse job to create a draft Google -Play internal test release will begin shortly. - 1. If it doesn't start, a common reason is that it wasn't alphabetically the latest tag, so the - [`tagged-release`](https://concourse.sandbox.aws.robojackets.net/teams/information-technology/pipelines/apiary-mobile/resources/tagged-release) - resource didn't trigger a new build. We can disable old versions of the resource to trigger a new - build. -4. If the Concourse [build-release job](https://concourse.sandbox.aws.robojackets.net/teams/information-technology/pipelines/apiary-mobile/jobs/build-release) -finishes successfully, you'll find a new draft release on the Internal Test track in Google Play. +1. After you've merged all PRs to be included in the release, open the [Release to Internal Test](https://github.com/RoboJackets/apiary-mobile/actions/workflows/internal-test-release.yml) + Github Actions pipeline. +2. Find the `Run workflow` button. Use the table below to enter a value for the `update_priority`. + Update priority is an integer passed to Google Play, and it determines update nag behavior + (frequency/intensity) in the app. + +| Update priority | Description | Examples | +|-----------------|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| +| 1 | Optional update with prompts when the update is first available, then every 8 days. | UI touchups that don't impact functionality, releases with options to opt-in to beta features | +| 3 | Optional update with prompts when the update is first available, then every 4 days. | Medium-priority bug fixes, performance improvements, new features | +| 5 | Required update with immediate prompt and no option to decline. | Crashes/bugs impacting major features, urgent vulnerabilities | + +3. If the build runs successfully, you'll find a new draft release on the Internal Test track in Google Play. At this point, you should do some small QA efforts to verify the new build. Post in #apiary-mobile to have some people help you test. Note that access to the internal test track must be granted via the Google Play Console. 1. Internal testers may need to uninstall the app to see the update if it was recently published. -6. If no issues are found, it's time to release! Promote the build to the Production track in +4. If no issues are found, it's time to release! Promote the build to the Production track in Google Play, add release notes, and save the release. -7. Google Play typically spends a day or two reviewing the release, then makes it available. In +5. Google Play typically spends a day or two reviewing the release, then makes it available. In general, expect it to take at least ~24 hours for a production release to be available to users. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 253413f..a3dcadf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.application") kotlin("android") id("kotlin-android") - kotlin("kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") id("com.google.android.gms.oss-licenses-plugin") @@ -31,30 +31,33 @@ dependencies { implementation(AuthDependencies.appauth) - implementation(ComposeDependencies.accompanist_nav_material) + implementation(ComposeDependencies.compose_material_navigation) implementation(ComposeDependencies.compose_ui) implementation(ComposeDependencies.lifecycle_viewmodel_compose) implementation(ComposeDependencies.compose_ui_tooling) implementation(ComposeDependencies.compose_foundation) - implementation(ComposeDependencies.compose_material) + implementation(ComposeDependencies.compose_material3) implementation(ComposeDependencies.compose_material_icons_core) implementation(ComposeDependencies.compose_material_icons_extended) implementation(ComposeDependencies.compose_settings) - implementation(HiltDependencies.hilt) - kapt(HiltDependencies.hilt_android_compiler) + ksp(HiltDependencies.hilt_android_compiler) implementation(HiltDependencies.hilt_navigation_compose) + implementation(MaterialDependencies.material_android) + implementation(NetworkDependencies.moshi_converter_factory) implementation(NetworkDependencies.okhttp) implementation(platform(NetworkDependencies.okhttp_bom)) implementation(NetworkDependencies.okhttp_logging_interceptor) implementation(NetworkDependencies.retrofit) implementation(NetworkDependencies.sandwich) // yum yum + implementation(NetworkDependencies.sandwich_retrofit) + implementation(NetworkDependencies.sandwich_retrofit_serialization) implementation(platform(NfcDependencies.nfc_firebase_bom)) - implementation(NfcDependencies.nfc_firebase_core) // Firebase BoM and Core are required when including TapLinx (line below) manually + implementation(NfcDependencies.nfc_firebase_analytics) // Firebase BoM and Analytics (f/k/a Core) are required when including TapLinx (line below) manually implementation(files(NfcDependencies.nxp_nfc_android_aar_path)) // Test dependencies @@ -71,11 +74,11 @@ android { create("release") { } } - compileSdk = 32 + compileSdk = 35 defaultConfig { applicationId = "org.robojackets.apiary" minSdk = 21 - targetSdk = 32 + targetSdk = 35 versionCode = 12 versionName = "1.0.0" vectorDrawables { @@ -90,17 +93,18 @@ android { } buildFeatures { compose = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-beta03" + kotlinCompilerExtensionVersion = "1.5.1" } namespace = "org.robojackets.apiary" hilt { diff --git a/app/src/main/java/org/robojackets/apiary/ApiaryMobileApplication.kt b/app/src/main/java/org/robojackets/apiary/ApiaryMobileApplication.kt index 535df50..27fc2f8 100644 --- a/app/src/main/java/org/robojackets/apiary/ApiaryMobileApplication.kt +++ b/app/src/main/java/org/robojackets/apiary/ApiaryMobileApplication.kt @@ -8,8 +8,8 @@ import io.sentry.android.timber.SentryTimberIntegration import timber.log.Timber // Note: this class has to be in the same module as the @AndroidEntryPoint annotated class, which -// is MainActivity. In other words, you can't move this class to another module to solve -// dependency issues (use dependency injection instead!). +// is MainActivity. In other words, you can't move this class to another module to solve dependency +// issues (use dependency injection instead!). @HiltAndroidApp class ApiaryMobileApplication : Application() { override fun onCreate() { diff --git a/app/src/main/java/org/robojackets/apiary/MainActivity.kt b/app/src/main/java/org/robojackets/apiary/MainActivity.kt index ae3f25e..4abb8a3 100644 --- a/app/src/main/java/org/robojackets/apiary/MainActivity.kt +++ b/app/src/main/java/org/robojackets/apiary/MainActivity.kt @@ -6,29 +6,45 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.StringRes +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Contactless -import androidx.compose.runtime.* +import androidx.compose.material.navigation.ModalBottomSheetLayout +import androidx.compose.material.navigation.bottomSheet +import androidx.compose.material.navigation.rememberBottomSheetNavigator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.* import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi -import com.google.accompanist.navigation.material.ModalBottomSheetLayout -import com.google.accompanist.navigation.material.bottomSheet -import com.google.accompanist.navigation.material.rememberBottomSheetNavigator +import androidx.navigation.navArgument +import androidx.navigation.navigation +import androidx.navigation.plusAssign import com.nxp.nfclib.NxpNfcLib import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn @@ -55,6 +71,8 @@ import org.robojackets.apiary.ui.update.UpdateInProgress import timber.log.Timber import javax.inject.Inject +// TODO: see if we can make app launch screen match light/dark mode? + sealed class Screen( val navigationDestination: String, @StringRes val resourceId: Int, @@ -71,7 +89,7 @@ sealed class Screen( object Settings : Screen( - NavigationDestinations.settings, + NavigationDestinations.settingsSubgraph, R.string.settings, Icons.Filled.Settings, "settings" @@ -115,8 +133,6 @@ class MainActivity : ComponentActivity() { nfcLib.registerActivity(this, BuildConfig.taplinxKey, BuildConfig.taplinxOfflineKey) } - @OptIn(ExperimentalMaterialNavigationApi::class) - @ExperimentalMaterialApi @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -137,7 +153,7 @@ class MainActivity : ComponentActivity() { setContent { Apiary_MobileTheme { - window.statusBarColor = MaterialTheme.colors.primaryVariant.toArgb() + window.statusBarColor = MaterialTheme.colorScheme.secondary.toArgb() val navController = rememberNavController() val bottomSheetNavigator = rememberBottomSheetNavigator() navController.navigatorProvider += bottomSheetNavigator @@ -155,7 +171,7 @@ class MainActivity : ComponentActivity() { } // A surface container using the 'background' color from the theme - Surface(color = MaterialTheme.colors.background) { + Surface(color = MaterialTheme.colorScheme.background) { ModalBottomSheetLayout(bottomSheetNavigator) { UpdateGate( navReady = navReady, @@ -180,9 +196,9 @@ class MainActivity : ComponentActivity() { bottomBar = { val current = currentRoute(navController) if (shouldShowBottomNav(nfcEnabled, current)) { - BottomNavigation { + NavigationBar { navItems.forEach { screen -> - BottomNavigationItem( + NavigationBarItem( icon = { Icon( screen.icon, @@ -232,21 +248,24 @@ class MainActivity : ComponentActivity() { currentScreen != NavigationDestinations.updateInProgress @Suppress("LongMethod") - @OptIn(ExperimentalMaterialNavigationApi::class) - @ExperimentalMaterialApi @Composable private fun AppNavigation( navController: NavHostController, modifier: Modifier = Modifier, ) { val startDestination = - if (!authStateManager.current.isAuthorized) NavigationDestinations.authentication - else NavigationDestinations.attendanceSubgraph + if (!authStateManager.current.isAuthorized) { + NavigationDestinations.authentication + } else { + NavigationDestinations.attendanceSubgraph + } NavHost( navController = navController, startDestination = startDestination, modifier = modifier, + enterTransition = { EnterTransition.None }, + exitTransition = { ExitTransition.None }, ) { composable(NavigationDestinations.authentication) { AuthenticationScreen(hiltViewModel(), authManager) @@ -266,7 +285,7 @@ class MainActivity : ComponentActivity() { navArgument("attendableType") { type = NavType.StringType } ), ) { - val attendableType = it.arguments?.get("attendableType") + val attendableType = it.arguments?.getString("attendableType") AttendableSelectionScreen( hiltViewModel(), @@ -281,8 +300,8 @@ class MainActivity : ComponentActivity() { navArgument("attendableId") { type = NavType.IntType }, ) ) { - val attendableType = it.arguments?.get("attendableType") - val attendableId = it.arguments?.get("attendableId") + val attendableType = it.arguments?.getString("attendableType") + val attendableId = it.arguments?.getInt("attendableId") AttendanceScreen( hiltViewModel(), @@ -292,9 +311,15 @@ class MainActivity : ComponentActivity() { ) } } - composable(NavigationDestinations.settings) { - SettingsScreen(hiltViewModel()) + navigation( + startDestination = NavigationDestinations.settings, + route = NavigationDestinations.settingsSubgraph, + ) { + composable(NavigationDestinations.settings) { + SettingsScreen(hiltViewModel()) + } } + composable(NavigationDestinations.requiredUpdatePrompt) { RequiredUpdatePrompt() } diff --git a/app/src/main/java/org/robojackets/apiary/di/MainActivityModule.kt b/app/src/main/java/org/robojackets/apiary/di/MainActivityModule.kt index e63cb91..1acf3b5 100644 --- a/app/src/main/java/org/robojackets/apiary/di/MainActivityModule.kt +++ b/app/src/main/java/org/robojackets/apiary/di/MainActivityModule.kt @@ -2,7 +2,7 @@ package org.robojackets.apiary.di import android.content.Context import com.nxp.nfclib.NxpNfcLib -import com.skydoves.sandwich.coroutines.CoroutinesResponseCallAdapterFactory +import com.skydoves.sandwich.retrofit.adapters.ApiResponseCallAdapterFactory import com.squareup.moshi.Moshi import com.squareup.moshi.Types import dagger.Module @@ -45,8 +45,11 @@ object MainActivityModule { ): OkHttpClient { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.setLevel( - if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY - else HttpLoggingInterceptor.Level.BASIC + if (BuildConfig.DEBUG) { + HttpLoggingInterceptor.Level.BODY + } else { + HttpLoggingInterceptor.Level.BASIC + } ) // Only log detailed // network requests in debug builds loggingInterceptor.redactHeader("Authorization") // Redact access tokens in headers @@ -76,7 +79,7 @@ object MainActivityModule { ): Retrofit = Retrofit.Builder() .client(okHttpClient) .baseUrl(globalSettings.appEnv.apiBaseUrl.toString()) - .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() diff --git a/app/src/main/java/org/robojackets/apiary/network/UserAgentInterceptor.kt b/app/src/main/java/org/robojackets/apiary/network/UserAgentInterceptor.kt index dcd8d26..cafceb1 100644 --- a/app/src/main/java/org/robojackets/apiary/network/UserAgentInterceptor.kt +++ b/app/src/main/java/org/robojackets/apiary/network/UserAgentInterceptor.kt @@ -24,9 +24,13 @@ class UserAgentInterceptor(context: Context) : Interceptor { private fun getApplicationName(context: Context): String { val applicationInfo = context.applicationInfo val stringId = applicationInfo.labelRes - return if (stringId == 0) applicationInfo.nonLocalizedLabel.toString() else context.getString( + return if (stringId == 0) { + applicationInfo.nonLocalizedLabel.toString() + } else { + context.getString( stringId ) + } } private fun getDeviceName(): String { @@ -34,7 +38,9 @@ class UserAgentInterceptor(context: Context) : Interceptor { val model = Build.MODEL return if (model.startsWith(manufacturer)) { model.replaceFirstChar { it.titlecase() } - } else manufacturer.replaceFirstChar { it.titlecase() } + " " + model + } else { + manufacturer.replaceFirstChar { it.titlecase() } + " " + model + } } override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/org/robojackets/apiary/ui/global/AppTopBar.kt b/app/src/main/java/org/robojackets/apiary/ui/global/AppTopBar.kt index bf23577..6d75fdd 100644 --- a/app/src/main/java/org/robojackets/apiary/ui/global/AppTopBar.kt +++ b/app/src/main/java/org/robojackets/apiary/ui/global/AppTopBar.kt @@ -5,9 +5,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -17,14 +19,22 @@ import androidx.compose.ui.unit.dp import org.robojackets.apiary.base.ui.IconWithText import org.robojackets.apiary.base.ui.icons.WarningIcon +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppTopBar(isProdEnv: Boolean) { Column { TopAppBar( + colors = TopAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + scrolledContainerColor = Color.Unspecified, + navigationIconContentColor = Color.Unspecified, + titleContentColor = Color.Unspecified, + actionIconContentColor = Color.Unspecified, + ), title = { Text( text = "MyRoboJackets", - style = MaterialTheme.typography.h5, + style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.W800 ) }, @@ -34,17 +44,19 @@ fun AppTopBar(isProdEnv: Boolean) { Box( Modifier .fillMaxWidth() - .background(MaterialTheme.colors.error) + .background(MaterialTheme.colorScheme.error) .align(Alignment.CenterHorizontally) .padding(vertical = 4.dp) ) { IconWithText( - icon = { WarningIcon(tint = Color.White) }, - text = { Text( + icon = { WarningIcon(tint = MaterialTheme.colorScheme.onError) }, + text = { + Text( "Non-production server", modifier = Modifier.padding(start = 4.dp), - color = Color.White - ) } + color = MaterialTheme.colorScheme.onError + ) + } ) } } diff --git a/app/src/main/java/org/robojackets/apiary/ui/settings/Settings.kt b/app/src/main/java/org/robojackets/apiary/ui/settings/Settings.kt index 374875f..7bda8ca 100644 --- a/app/src/main/java/org/robojackets/apiary/ui/settings/Settings.kt +++ b/app/src/main/java/org/robojackets/apiary/ui/settings/Settings.kt @@ -8,11 +8,19 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* +import androidx.compose.material.icons.outlined.Build +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Logout +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.PrivacyTip +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material.icons.outlined.VerifiedUser +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -34,13 +42,16 @@ import org.robojackets.apiary.ui.update.UpdateStatus @Suppress("LongMethod", "LongParameterList") @Composable - private fun Settings( +private fun Settings( appEnv: AppEnvironment, user: UserInfo?, onLogout: () -> Unit, onOpenPrivacyPolicy: () -> Unit, onOpenMakeAWish: () -> Unit, onRefreshUser: () -> Unit, + onNavigateToOptionalUpdateBottomSheet: () -> Unit, + onNavigateToRequiredUpdatePrompt: () -> Unit, + onNavigateToUpdateInProgress: () -> Unit, ) { val context = LocalContext.current @@ -66,6 +77,21 @@ import org.robojackets.apiary.ui.update.UpdateStatus subtitle = { Text(text = user?.allPermissions?.joinToString(separator = ", ") ?: "None") }, onClick = { onRefreshUser() } ) + SettingsMenuLink( + icon = { Icon(Icons.Outlined.Update, contentDescription = "update") }, + title = { Text(text = "DEBUG: Open optional update bottom sheet") }, + onClick = { onNavigateToOptionalUpdateBottomSheet() } + ) + SettingsMenuLink( + icon = { Icon(Icons.Outlined.Update, contentDescription = "update") }, + title = { Text(text = "DEBUG: Open required update prompt") }, + onClick = { onNavigateToRequiredUpdatePrompt() } + ) + SettingsMenuLink( + icon = { Icon(Icons.Outlined.Update, contentDescription = "update") }, + title = { Text(text = "DEBUG: Open update in progress screen") }, + onClick = { onNavigateToUpdateInProgress() } + ) } SettingsMenuLink( icon = { Icon(Icons.Outlined.Logout, contentDescription = "logout") }, @@ -76,17 +102,21 @@ import org.robojackets.apiary.ui.update.UpdateStatus SettingsMenuLink( icon = { Icon(Icons.Outlined.Home, contentDescription = "home") }, title = { Text(text = "Server") }, - subtitle = { Text( + subtitle = { + Text( text = "${appEnv.name} (${appEnv.apiBaseUrl})" - ) }, + ) + }, onClick = {} ) SettingsMenuLink( icon = { Icon(Icons.Outlined.Build, contentDescription = "build") }, title = { Text(text = "Version") }, - subtitle = { Text( + subtitle = { + Text( text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" - ) }, + ) + }, onClick = {} ) SettingsMenuLink( @@ -143,7 +173,7 @@ fun SettingsScreen( } val state by viewModel.state.collectAsState() - val secondaryThemeColor = MaterialTheme.colors.background + val secondaryThemeColor = MaterialTheme.colorScheme.background ContentPadding { Settings( appEnv = viewModel.globalSettings.appEnv, @@ -161,6 +191,15 @@ fun SettingsScreen( }, onRefreshUser = { viewModel.getUser(forceRefresh = true) + }, + onNavigateToOptionalUpdateBottomSheet = { + viewModel.navigateToOptionalUpdateBottomSheet() + }, + onNavigateToRequiredUpdatePrompt = { + viewModel.navigateToRequiredUpdatePrompt() + }, + onNavigateToUpdateInProgress = { + viewModel.navigateToUpdateInProgress() } ) } @@ -177,5 +216,8 @@ private fun SettingsPreview() { onOpenPrivacyPolicy = {}, onOpenMakeAWish = {}, onRefreshUser = {}, + onNavigateToOptionalUpdateBottomSheet = {}, + onNavigateToRequiredUpdatePrompt = {}, + onNavigateToUpdateInProgress = {}, ) } diff --git a/app/src/main/java/org/robojackets/apiary/ui/settings/SettingsViewModel.kt b/app/src/main/java/org/robojackets/apiary/ui/settings/SettingsViewModel.kt index 8734317..d3202ea 100644 --- a/app/src/main/java/org/robojackets/apiary/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/org/robojackets/apiary/ui/settings/SettingsViewModel.kt @@ -7,12 +7,16 @@ import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.compose.ui.graphics.toArgb +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.skydoves.sandwich.getOrThrow import dagger.hilt.android.lifecycle.HiltViewModel import io.sentry.Sentry -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.robojackets.apiary.auth.AuthStateManager import org.robojackets.apiary.auth.model.UserInfo @@ -28,6 +32,7 @@ import io.sentry.protocol.User as SentryUser @HiltViewModel class SettingsViewModel @Inject constructor( + @Suppress("UnusedPrivateMember") private val savedStateHandle: SavedStateHandle, val globalSettings: GlobalSettings, val navigationManager: NavigationManager, val serverInfoRepository: ServerInfoRepository, @@ -35,9 +40,11 @@ class SettingsViewModel @Inject constructor( val authStateManager: AuthStateManager, ) : ViewModel() { val privacyPolicyUrl: Uri = Uri.withAppendedPath(globalSettings.appEnv.apiBaseUrl, "privacy") - val makeAWishUrl: Uri = Uri.parse("https://docs.google.com/forms/d/e/1FAIpQLSelERsYq3" + + val makeAWishUrl: Uri = Uri.parse( + "https://docs.google.com/forms/d/e/1FAIpQLSelERsYq3" + "gLmHbWvVCWha5iCU8z3r9VYC0hCN4ArLpMAiysaQ/viewform?entry.1338203640=MyRoboJackets%20" + - "Android") + "Android" + ) var customTabsClient: CustomTabsClient? = null val customTabsServiceConnection = object : CustomTabsServiceConnection() { @@ -67,11 +74,14 @@ class SettingsViewModel @Inject constructor( init { viewModelScope.launch { - combine(listOf( + combine( + listOf( user, - )) { - flows -> SettingsState( - flows[0] as UserInfo? + ) + ) { + flows -> + SettingsState( + flows[0] ) }.catch { throwable -> throw throwable } .collect { _state.value = it } @@ -88,6 +98,18 @@ class SettingsViewModel @Inject constructor( navigateToLogin() } + fun navigateToOptionalUpdateBottomSheet() { + navigationManager.navigate(NavigationActions.UpdatePrompts.anyScreenToOptionalUpdatePrompt()) + } + + fun navigateToRequiredUpdatePrompt() { + navigationManager.navigate(NavigationActions.UpdatePrompts.anyScreenToRequiredUpdatePrompt()) + } + + fun navigateToUpdateInProgress() { + navigationManager.navigate(NavigationActions.UpdatePrompts.anyScreenToUpdateInProgress()) + } + fun getCustomTabsIntent(toolbarColor: Int = webNavBarBackground.toArgb()): CustomTabsIntent { val customTabsBuilder = CustomTabsIntent.Builder() diff --git a/app/src/main/java/org/robojackets/apiary/ui/update/UpdateAvailable.kt b/app/src/main/java/org/robojackets/apiary/ui/update/UpdateAvailable.kt index d214674..e8afabd 100644 --- a/app/src/main/java/org/robojackets/apiary/ui/update/UpdateAvailable.kt +++ b/app/src/main/java/org/robojackets/apiary/ui/update/UpdateAvailable.kt @@ -1,34 +1,42 @@ package org.robojackets.apiary.ui.update -import android.app.Activity -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.android.play.core.ktx.AppUpdateResult +import kotlinx.coroutines.launch import org.robojackets.apiary.base.ui.icons.UpdateIcon -import org.robojackets.apiary.base.ui.util.getActivity -import se.warting.inappupdate.compose.rememberInAppUpdateState +import se.warting.inappupdate.compose.InAppUpdateState +import se.warting.inappupdate.compose.Mode import timber.log.Timber -// Just a random number so we can identify our update request later if necessary -const val UPDATE_REQUEST_CODE = 1999 - -fun triggerImmediateUpdate(appUpdateResult: AppUpdateResult.Available, activity: Activity) { - appUpdateResult.startImmediateUpdate(activity, UPDATE_REQUEST_CODE) -} - +@Suppress("LongMethod") @Composable fun InstallUpdateButton(onIgnoreUpdate: () -> Unit = {}) { - val updateState = rememberInAppUpdateState() + val inAppUpdateState = rememberInAppUpdateStateWithDefaults() var updateCanceled by remember { mutableStateOf(false) } - val context = LocalContext.current + val scope = rememberCoroutineScope() var updateError by remember { mutableStateOf(null) } when (updateCanceled) { @@ -37,19 +45,45 @@ fun InstallUpdateButton(onIgnoreUpdate: () -> Unit = {}) { } false -> { Button(onClick = { - val result = updateState.appUpdateResult - if (result is AppUpdateResult.Available) { - context.getActivity()?.let { triggerImmediateUpdate(result, it) } ?: run { - Timber.e("Context.getActivity() was null while trying to start immediate update") - updateError = "An update is available, but we were unable to start the update process." + when (inAppUpdateState) { + is InAppUpdateState.DownloadedUpdate -> { + scope.launch { + inAppUpdateState.appUpdateResult.completeUpdate() + } + } + + is InAppUpdateState.RequiredUpdate -> { + inAppUpdateState.onStartUpdate() + } + + is InAppUpdateState.OptionalUpdate -> { + inAppUpdateState.onStartUpdate(Mode.IMMEDIATE) } - } else { - Timber.e("User is in update flow but no update was available") - updateError = "Sorry! It seems there are no updates to install." + + is InAppUpdateState.InProgressUpdate -> { + Timber.w("UpdateAvailable: inAppUpdateState is InProgressUpdate") + updateError = "The update can't be started because an update is already in" + + " progress" + } + + is InAppUpdateState.Error -> { + Timber.e( + "UpdateAvailable: inAppUpdateState is Error: ${inAppUpdateState.exception}" + ) + updateError = + "An update is available, but an error occurred while trying to start" + + " it." + } + else -> { + updateError = "The update can't be started right now" + } + } + if (updateError?.isNotBlank() == true) { + Timber.e("Update error: $updateError") + updateCanceled = true } - updateCanceled = true }) { - Text("Download and install update") + Text("Update now") } } } @@ -67,10 +101,14 @@ fun InstallUpdateButton(onIgnoreUpdate: () -> Unit = {}) { }, title = { Text("Update failed") }, text = { - Text("${ - updateError ?: ("An unknown error occurred while starting the " + - "update.") - }\n\nPlease try again, or post in #it-helpdesk for assistance.") + Text( + "${ + updateError ?: ( + "An unknown error occurred while starting the " + + "update." + ) + }\n\nPlease try again, or post in #it-helpdesk for assistance." + ) } ) } @@ -84,11 +122,14 @@ fun RequiredUpdatePrompt() { verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight(), ) { - UpdateIcon(Modifier - .padding(bottom = 18.dp) - .size(96.dp)) - Text("Update to continue", style = MaterialTheme.typography.h4) - Text("To continue using MyRoboJackets, install the latest version. It'll only take a minute.", + UpdateIcon( + Modifier + .padding(bottom = 18.dp) + .size(96.dp) + ) + Text("Update to continue", style = MaterialTheme.typography.headlineMedium) + Text( + "To continue using MyRoboJackets, install the latest version. It'll only take a minute.", textAlign = TextAlign.Center, modifier = Modifier.padding(24.dp) ) @@ -102,23 +143,35 @@ fun RequiredUpdatePrompt() { fun OptionalUpdatePrompt( onIgnoreUpdate: () -> Unit ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxHeight(.5F), - ) { - UpdateIcon(Modifier - .padding(bottom = 9.dp) - .size(72.dp)) - Text("Update available", style = MaterialTheme.typography.h5) - Text("Install the latest version of MyRoboJackets for the latest features and " + - "bug fixes. It'll only take a minute.", - textAlign = TextAlign.Center, - modifier = Modifier.padding(20.dp) - ) - InstallUpdateButton(onIgnoreUpdate) - TextButton(onClick = onIgnoreUpdate) { - Text("Remind me later") + val inAppUpdateState = rememberInAppUpdateStateWithDefaults() + Surface { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxHeight(.5F), + ) { + UpdateIcon( + Modifier + .padding(bottom = 9.dp) + .size(72.dp) + ) + Text("Update available", style = MaterialTheme.typography.headlineSmall) + Text( + "Install the latest version of MyRoboJackets for the latest features and " + + "bug fixes. It'll only take a minute.", + textAlign = TextAlign.Center, + modifier = Modifier.padding(20.dp) + ) + InstallUpdateButton(onIgnoreUpdate) + TextButton(onClick = { + Timber.w("DEBUG: Optional update declined. $inAppUpdateState") + if (inAppUpdateState is InAppUpdateState.OptionalUpdate) { + inAppUpdateState.onDeclineUpdate() + } + onIgnoreUpdate() + }) { + Text("Not now") + } } } } @@ -133,8 +186,9 @@ fun UpdateInProgress() { modifier = Modifier.fillMaxHeight(), ) { CircularProgressIndicator(Modifier.padding(bottom = 28.dp)) - Text("Please wait...", style = MaterialTheme.typography.h5) - Text("We're finishing installing an update. It'll just be a minute or two!", + Text("Please wait...", style = MaterialTheme.typography.headlineSmall) + Text( + "We're finishing installing an update. It'll just be a minute or two!", textAlign = TextAlign.Center, modifier = Modifier.padding(20.dp) ) diff --git a/app/src/main/java/org/robojackets/apiary/ui/update/UpdateGate.kt b/app/src/main/java/org/robojackets/apiary/ui/update/UpdateGate.kt index 14a32d0..aa4978e 100644 --- a/app/src/main/java/org/robojackets/apiary/ui/update/UpdateGate.kt +++ b/app/src/main/java/org/robojackets/apiary/ui/update/UpdateGate.kt @@ -1,64 +1,21 @@ package org.robojackets.apiary.ui.update -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.Lifecycle -import com.google.android.play.core.install.model.AppUpdateType -import com.google.android.play.core.install.model.UpdateAvailability -import com.google.android.play.core.ktx.AppUpdateResult -import com.google.android.play.core.ktx.clientVersionStalenessDays -import com.google.android.play.core.ktx.updatePriority -import kotlinx.coroutines.launch -import org.robojackets.apiary.base.ui.util.OnLifecycleEvent -import org.robojackets.apiary.base.ui.util.getActivity +import se.warting.inappupdate.compose.InAppUpdateState import se.warting.inappupdate.compose.rememberInAppUpdateState import timber.log.Timber -const val UPDATE_PRIORITY_LOWEST = 0 -const val UPDATE_PRIORITY_LOWER = 1 -const val UPDATE_PRIORITY_LOW = 2 -const val UPDATE_PRIORITY_MEDIUM = 3 -const val UPDATE_PRIORITY_HIGH = 4 -const val UPDATE_PRIORITY_HIGHEST = 5 - -const val OPT_UPDATE_STALENESS_THRESHOLD_LOW_PRIORITY = 14 -const val OPT_UPDATE_STALENESS_THRESHOLD_MEDIUM_PRIORITY = 4 - -fun isImmediateUpdateOptional(priority: Int, staleness: Int): Boolean { - return when (priority) { - UPDATE_PRIORITY_LOWEST, - UPDATE_PRIORITY_LOWER, - UPDATE_PRIORITY_LOW -> staleness >= OPT_UPDATE_STALENESS_THRESHOLD_LOW_PRIORITY - UPDATE_PRIORITY_MEDIUM -> staleness >= OPT_UPDATE_STALENESS_THRESHOLD_MEDIUM_PRIORITY - UPDATE_PRIORITY_HIGH -> true - UPDATE_PRIORITY_HIGHEST -> true - else -> { - Timber.w("Unknown priority $priority when evaluating for flexible update") - false - } - } -} - -const val REQ_UPDATE_STALENESS_THRESHOLD_LOW_PRIORITY = 21 -const val REQ_UPDATE_STALENESS_THRESHOLD_MEDIUM_PRIORITY = 21 -const val REQ_UPDATE_STALENESS_THRESHOLD_HIGH_PRIORITY = 1 - -fun isImmediateUpdateRequired(priority: Int, staleness: Int): Boolean { - return when (priority) { - UPDATE_PRIORITY_LOWEST, - UPDATE_PRIORITY_LOWER, - UPDATE_PRIORITY_LOW -> staleness >= REQ_UPDATE_STALENESS_THRESHOLD_LOW_PRIORITY - UPDATE_PRIORITY_MEDIUM -> staleness >= REQ_UPDATE_STALENESS_THRESHOLD_MEDIUM_PRIORITY - UPDATE_PRIORITY_HIGH -> staleness >= REQ_UPDATE_STALENESS_THRESHOLD_HIGH_PRIORITY - UPDATE_PRIORITY_HIGHEST -> true - else -> { - Timber.w("Unknown priority $priority when evaluating for immediate update") - false - } - } +@Composable +fun rememberInAppUpdateStateWithDefaults(): InAppUpdateState { + return rememberInAppUpdateState( + highPrioritizeUpdates = 5, + mediumPrioritizeUpdates = 3, + promptIntervalHighPrioritizeUpdateInDays = 1, + promptIntervalMediumPrioritizeUpdateInDays = 4, + promptIntervalLowPrioritizeUpdateInDays = 8, + ) } @Suppress("ComplexMethod", "LongMethod") @@ -71,103 +28,112 @@ fun UpdateGate( content: @Composable () -> Unit, ) { content() - val updateState = rememberInAppUpdateState() - val scope = rememberCoroutineScope() - val result = updateState.appUpdateResult - val context = LocalContext.current - OnLifecycleEvent { owner, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - if (result is AppUpdateResult.Available) { - if (result.updateInfo.updateAvailability() - == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { - context.getActivity() - ?.let { result.startImmediateUpdate(it, UPDATE_REQUEST_CODE) } + when (val inAppUpdateState = rememberInAppUpdateStateWithDefaults()) { + is InAppUpdateState.DownloadedUpdate -> { + if (inAppUpdateState.isRequiredUpdate) { + LaunchedEffect(navReady) { + if (navReady) { + onShowRequiredUpdatePrompt() + } + } + } else { + LaunchedEffect(navReady) { + if (navReady) { + onShowOptionalUpdatePrompt() } } } - else -> Unit + content() } - } - - when (result) { - is AppUpdateResult.NotAvailable -> Unit - is AppUpdateResult.Available -> { - val priority = result.updateInfo.updatePriority - val staleness = result.updateInfo.clientVersionStalenessDays ?: -1 - - val immediateAllowed = - result.updateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) - val immediateRequired = immediateAllowed && - isImmediateUpdateRequired(priority, staleness) - val immediateOptional = immediateAllowed && - !immediateRequired && - isImmediateUpdateOptional(priority, staleness) - - when { - immediateRequired -> { - LaunchedEffect(navReady) { - if (navReady) { - onShowRequiredUpdatePrompt() - } - } - } - immediateOptional -> { - LaunchedEffect(navReady) { - if (navReady) { - onShowOptionalUpdatePrompt() - } - } + is InAppUpdateState.InProgressUpdate -> { + Timber.i("Update in progress: ${inAppUpdateState.installState.bytesDownloaded}") + LaunchedEffect(navReady) { + if (navReady) { + onShowUpdateInProgressScreen() } - else -> { - Timber.d("An update is available but no action is required currently") - content() + } + UpdateInProgress() + } + + InAppUpdateState.Loading -> { + Timber.i("In-app update - loading state") + content() + } + InAppUpdateState.NotAvailable -> { + content() + } + is InAppUpdateState.OptionalUpdate -> { + LaunchedEffect(navReady) { + if (navReady && inAppUpdateState.shouldPrompt) { + onShowOptionalUpdatePrompt() } } + content() } - is AppUpdateResult.InProgress -> Unit - is AppUpdateResult.Downloaded -> { - LaunchedEffect(result) { - if (navReady) { - onShowUpdateInProgressScreen() - scope.launch { - result.completeUpdate() - } + + is InAppUpdateState.RequiredUpdate -> { + LaunchedEffect(navReady) { + if (navReady && inAppUpdateState.shouldPrompt) { + onShowRequiredUpdatePrompt() } } + content() + } + + is InAppUpdateState.Error -> { + content() } } } @Composable fun UpdateStatus() { - val updateState = rememberInAppUpdateState() + when (val inAppUpdateState = rememberInAppUpdateStateWithDefaults()) { + is InAppUpdateState.DownloadedUpdate -> { + if (inAppUpdateState.isRequiredUpdate) { + Text("Downloaded > required") + } else { + Text("Downloaded > optional") + } + } - when (val result = updateState.appUpdateResult) { - is AppUpdateResult.NotAvailable -> Text("Up to date") - is AppUpdateResult.Available -> { - val priority = result.updateInfo.updatePriority - val staleness = result.updateInfo.clientVersionStalenessDays ?: -1 + is InAppUpdateState.InProgressUpdate -> { + Text( + "In progress (${inAppUpdateState.installState.bytesDownloaded}/" + + "${inAppUpdateState.installState.totalBytesToDownload}) bytes) " + + "| Install status: ${inAppUpdateState.installState.installStatus} " + + "| Error code: ${inAppUpdateState.installState.installErrorCode} " + + "| Package name: ${inAppUpdateState.installState.packageName}" + ) + } - val immediateAllowed = - result.updateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) + InAppUpdateState.Loading -> { + Text("Loading") + } + InAppUpdateState.NotAvailable -> { + Text("Not available") + } + is InAppUpdateState.OptionalUpdate -> { + Text( + "Optional update | Should prompt: ${inAppUpdateState.shouldPrompt} " + + " | Priority: ${inAppUpdateState.appUpdateInfo.priority}" + + " | Staleness: ${inAppUpdateState.appUpdateInfo.staleDays}" + + " | Version code: ${inAppUpdateState.appUpdateInfo.versionCode}" + ) + } - val immediateRequired = immediateAllowed && - isImmediateUpdateRequired(priority, staleness) - val immediateOptional = immediateAllowed && - !immediateRequired && - isImmediateUpdateOptional(priority, staleness) + is InAppUpdateState.RequiredUpdate -> { + Text( + "Required update | Should prompt: ${inAppUpdateState.shouldPrompt} " + + " | Priority: ${inAppUpdateState.appUpdateInfo.priority}" + + " | Staleness: ${inAppUpdateState.appUpdateInfo.staleDays}" + + " | Version code: ${inAppUpdateState.appUpdateInfo.versionCode}" + ) + } - when { - immediateRequired -> Text("Required update available (priority: " + - "$priority, staleness: $staleness)") - immediateOptional -> Text("Optional update available (priority: " + - "$priority, staleness: $staleness)") - else -> Text("Available") - } + is InAppUpdateState.Error -> { + Text("Error: ${inAppUpdateState.exception}") } - is AppUpdateResult.InProgress -> Text("Update in progress") - is AppUpdateResult.Downloaded -> Text("Update downloaded") } } diff --git a/attendance/build.gradle.kts b/attendance/build.gradle.kts index 4953318..2a6a0c5 100644 --- a/attendance/build.gradle.kts +++ b/attendance/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.library") kotlin("android") id("kotlin-android") - kotlin("kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } @@ -12,31 +12,31 @@ dependencies { implementation(project(mapOf("path" to ":base"))) implementation(project(mapOf("path" to ":navigation"))) implementation(project(mapOf("path" to ":auth"))) - implementation("androidx.navigation:navigation-common-ktx:2.3.5") - // Dependencies coreLibraryDesugaring(AndroidToolDependencies.android_tools_desugar_jdk) implementation(AndroidToolDependencies.timber) - implementation(ComposeDependencies.compose_material) + implementation(ComposeDependencies.compose_material3) implementation(ComposeDependencies.compose_ui) implementation(ComposeDependencies.lifecycle_viewmodel_compose) implementation(HiltDependencies.hilt) - kapt(HiltDependencies.hilt_android_compiler) + ksp(HiltDependencies.hilt_android_compiler) implementation(MaterialDependencies.material_android) implementation(NetworkDependencies.moshi) - kapt(NetworkDependencies.moshi_kotlin_codegen) + ksp(NetworkDependencies.moshi_kotlin_codegen) implementation(NetworkDependencies.okhttp) implementation(platform(NetworkDependencies.okhttp_bom)) implementation(NetworkDependencies.retrofit) implementation(NetworkDependencies.retrofuture) implementation(NetworkDependencies.sandwich) + implementation(NetworkDependencies.sandwich_retrofit) + implementation(NetworkDependencies.sandwich_retrofit_serialization) implementation(platform(NfcDependencies.nfc_firebase_bom)) - implementation(NfcDependencies.nfc_firebase_core) // Firebase BoM and Core are required when including TapLinx (line below) manually + implementation(NfcDependencies.nfc_firebase_analytics) // Firebase BoM and Analytics (f/k/a Core) are required when including TapLinx (line below) manually compileOnly(files(NfcDependencies.nxp_nfc_android_aar_path)) // Test dependencies @@ -46,10 +46,9 @@ dependencies { } android { - compileSdk = 32 + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 32 vectorDrawables { useSupportLibrary = true } @@ -61,17 +60,18 @@ android { } buildFeatures { compose = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-beta03" + kotlinCompilerExtensionVersion = "1.5.1" } namespace = "org.robojackets.apiary.attendance" hilt { diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/Attendance.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/Attendance.kt index ea6ac4c..ac53f75 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/Attendance.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/Attendance.kt @@ -1,8 +1,13 @@ package org.robojackets.apiary.attendance -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt index 844e487..de42a4c 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt @@ -3,7 +3,12 @@ package org.robojackets.apiary.attendance.model import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.skydoves.sandwich.* +import com.skydoves.sandwich.StatusCode +import com.skydoves.sandwich.message +import com.skydoves.sandwich.onError +import com.skydoves.sandwich.onException +import com.skydoves.sandwich.onSuccess +import com.skydoves.sandwich.retrofit.statusCode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -11,7 +16,8 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.robojackets.apiary.auth.model.Permission -import org.robojackets.apiary.auth.model.Permission.* +import org.robojackets.apiary.auth.model.Permission.CREATE_ATTENDANCE +import org.robojackets.apiary.auth.model.Permission.READ_USERS import org.robojackets.apiary.auth.model.UserInfo import org.robojackets.apiary.auth.network.UserRepository import org.robojackets.apiary.auth.util.getMissingPermissions @@ -88,13 +94,14 @@ class AttendableTypeSelectionViewModel @Inject constructor( "A server error occurred while checking if you have permission to " + "use this feature. Check your internet connection and try " + "again, or ask in #it-helpdesk for assistance." - else -> "An error occurred while checking if you have permission to use " + + else -> + "An error occurred while checking if you have permission to use " + "this feature. Check your internet connection and try again, or " + "ask in #it-helpdesk for assistance." } } .onException { - Timber.e(this.exception) + Timber.e(this.throwable) permissionsCheckError.value = "An error occurred while checking if you have " + "permission to use this feature. Check your internet connection and " + "try again, or ask in #it-helpdesk for assistance." diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendanceViewModel.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendanceViewModel.kt index f180a1a..7e88964 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendanceViewModel.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendanceViewModel.kt @@ -9,9 +9,13 @@ import com.skydoves.sandwich.onError import com.skydoves.sandwich.onException import com.skydoves.sandwich.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import org.robojackets.apiary.attendance.model.AttendanceScreenState.* +import org.robojackets.apiary.attendance.model.AttendanceScreenState.Loading +import org.robojackets.apiary.attendance.model.AttendanceScreenState.ReadyForTap import org.robojackets.apiary.attendance.network.AttendanceRepository import org.robojackets.apiary.base.model.Attendable import org.robojackets.apiary.base.model.AttendableType @@ -50,7 +54,8 @@ class AttendanceViewModel @Inject constructor( init { viewModelScope.launch { - combine(listOf( + combine( + listOf( lastAttendee, screenState, totalAttendees, @@ -61,8 +66,10 @@ class AttendanceViewModel @Inject constructor( selectedAttendable, error, missingHiddenTeams - )) { - flows -> AttendanceState( + ) + ) { + flows -> + AttendanceState( flows[0] as AttendanceStoreResult?, flows[1] as AttendanceScreenState, flows[2] as Int, @@ -125,7 +132,8 @@ class AttendanceViewModel @Inject constructor( loadingAttendables.value = true viewModelScope.launch { if (attendableType == AttendableType.Team && - (attendableTeams.value.isEmpty() || forceRefresh)) { + (attendableTeams.value.isEmpty() || forceRefresh) + ) { meetingsRepository.getTeams().onSuccess { attendableTeams.value = this.data.teams .filter { it.attendable } @@ -141,7 +149,8 @@ class AttendanceViewModel @Inject constructor( } } if (attendableType == AttendableType.Event && - (attendableEvents.value.isEmpty() || forceRefresh)) { + (attendableEvents.value.isEmpty() || forceRefresh) + ) { meetingsRepository.getEvents().onSuccess { attendableEvents.value = this.data.events }.onError { @@ -186,7 +195,7 @@ class AttendanceViewModel @Inject constructor( error.value = "Unable to fetch team info" }.onException { Timber.e( - this.exception, + this.throwable, "Exception occurred while fetching attendable teams" ) error.value = "Unable to fetch team info" @@ -204,7 +213,7 @@ class AttendanceViewModel @Inject constructor( error.value = "Unable to fetch event info" }.onException { Timber.e( - this.exception, + this.throwable, "Exception occurred while fetching attendable events" ) error.value = "Unable to fetch event info" diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableSelection.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableSelection.kt index bb59cb3..ad127f5 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableSelection.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableSelection.kt @@ -4,7 +4,12 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.* +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState @@ -21,7 +26,6 @@ import org.robojackets.apiary.base.ui.icons.WarningIcon import org.robojackets.apiary.base.ui.theme.danger import org.robojackets.apiary.base.ui.util.ContentPadding -@ExperimentalMaterialApi @Composable private fun AttendableList( attendables: List, @@ -36,12 +40,11 @@ private fun AttendableList( LazyColumn { itemsIndexed(attendables) { idx, attendable -> ListItem( + headlineContent = { attendableContent(attendable) }, Modifier.clickable { onAttendableSelected(attendable) } - ) { - attendableContent(attendable) - } + ) if (idx < attendables.size - 1) { Divider() } @@ -51,7 +54,6 @@ private fun AttendableList( } @Suppress("LongMethod") -@ExperimentalMaterialApi @Composable fun AttendableSelectionScreen( viewModel: AttendanceViewModel, @@ -102,7 +104,7 @@ fun AttendableSelectionScreen( onAttendableSelected = { viewModel.onAttendableSelected(it.toAttendable()) }, - title = { Text("Select a team", style = MaterialTheme.typography.h5) }, + title = { Text("Select a team", style = MaterialTheme.typography.headlineSmall) }, callout = { if (state.missingHiddenTeams == true) { Spacer(Modifier.height(4.dp)) @@ -121,7 +123,7 @@ fun AttendableSelectionScreen( onAttendableSelected = { viewModel.onAttendableSelected(it.toAttendable()) }, - title = { Text("Select an event", style = MaterialTheme.typography.h5) } + title = { Text("Select an event", style = MaterialTheme.typography.headlineSmall) } ) { Text(it.name) } diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt index 52b637c..0a644e6 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt @@ -1,8 +1,16 @@ package org.robojackets.apiary.attendance.ui import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Divider +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -18,7 +26,7 @@ import org.robojackets.apiary.base.ui.icons.GroupsIcon import org.robojackets.apiary.base.ui.util.ContentPadding import org.robojackets.apiary.base.ui.util.LoadingSpinner -@ExperimentalMaterialApi +@Suppress("LongMethod") @Composable fun AttendableTypeSelectionScreen( viewModel: AttendableTypeSelectionViewModel, @@ -38,7 +46,8 @@ fun AttendableTypeSelectionScreen( if (state.permissionsCheckError?.isNotEmpty() == true) { ErrorMessageWithRetry( message = state.permissionsCheckError ?: "An unknown error occurred", - onRetry = { viewModel.checkUserAttendanceAccess(forceRefresh = true) }) + onRetry = { viewModel.checkUserAttendanceAccess(forceRefresh = true) } + ) return@ContentPadding } @@ -57,27 +66,34 @@ fun AttendableTypeSelectionScreen( Column( Modifier .fillMaxWidth() - .fillMaxHeight()) { - Text("What do you want to take attendance for?", style = MaterialTheme.typography.h5) + .fillMaxHeight() + ) { + Text("What do you want to take attendance for?", style = MaterialTheme.typography.headlineSmall) Spacer(Modifier.defaultMinSize(minHeight = 16.dp)) Divider() ListItem( - icon = { GroupsIcon(Modifier.size(36.dp)) }, + leadingContent = { + GroupsIcon(Modifier.size(36.dp)) + }, + headlineContent = { + Text("Team", style = MaterialTheme.typography.titleLarge) + }, modifier = Modifier .defaultMinSize(minHeight = 80.dp) .clickable { viewModel.navigateToAttendableSelection(AttendableType.Team) } - ) { - Text("Team", style = MaterialTheme.typography.h6) - } + ) Divider() ListItem( - icon = { EventIcon(Modifier.size(36.dp)) }, + leadingContent = { + EventIcon(Modifier.size(36.dp)) + }, + headlineContent = { + Text("Event", style = MaterialTheme.typography.titleLarge) + }, modifier = Modifier .defaultMinSize(minHeight = 80.dp) .clickable { viewModel.navigateToAttendableSelection(AttendableType.Event) } - ) { - Text("Event", style = MaterialTheme.typography.h6) - } + ) Divider() } } diff --git a/attendance/src/main/res/values-night/themes.xml b/attendance/src/main/res/values-night/themes.xml deleted file mode 100644 index f864a73..0000000 --- a/attendance/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/attendance/src/main/res/values/themes.xml b/attendance/src/main/res/values/themes.xml deleted file mode 100644 index a17b26e..0000000 --- a/attendance/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index e6c79a8..ab545e9 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.library") kotlin("android") id("kotlin-android") - kotlin("kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } @@ -21,29 +21,31 @@ dependencies { implementation(AndroidToolDependencies.timber) implementation(AndroidXDependencies.androidx_activity_compose) - implementation(AndroidXDependencies.androidx_lifecycle_runtime_ktx) + implementation(AndroidXDependencies.androidx_lifecycle_runtime) implementation(AndroidXDependencies.androidx_navigation_compose) implementation(AuthDependencies.appauth) implementation(ComposeDependencies.accompanist_systemuicontroller) implementation(ComposeDependencies.compose_foundation) - implementation(ComposeDependencies.compose_material) + implementation(ComposeDependencies.compose_material3) implementation(ComposeDependencies.compose_ui) implementation(ComposeDependencies.compose_ui_tooling) implementation(HiltDependencies.hilt) - kapt(HiltDependencies.hilt_android_compiler) + ksp(HiltDependencies.hilt_android_compiler) implementation(MaterialDependencies.material_android) implementation(NetworkDependencies.moshi) - kapt(NetworkDependencies.moshi_kotlin_codegen) + ksp(NetworkDependencies.moshi_kotlin_codegen) implementation(NetworkDependencies.okhttp) implementation(platform(NetworkDependencies.okhttp_bom)) implementation(NetworkDependencies.retrofit) implementation(NetworkDependencies.retrofuture) implementation(NetworkDependencies.sandwich) + implementation(NetworkDependencies.sandwich_retrofit) + implementation(NetworkDependencies.sandwich_retrofit_serialization) // Test dependencies androidTestImplementation(ComposeDependencies.compose_ui_test) @@ -52,10 +54,9 @@ dependencies { } android { - compileSdk = 32 + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 32 vectorDrawables { useSupportLibrary = true @@ -69,17 +70,18 @@ android { } buildFeatures { compose = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-beta03" + kotlinCompilerExtensionVersion = "1.5.1" } namespace = "org.robojackets.apiary.auth" hilt { diff --git a/auth/src/main/java/org/robojackets/apiary/auth/ui/Authentication.kt b/auth/src/main/java/org/robojackets/apiary/auth/ui/Authentication.kt index fdb287b..1903a52 100644 --- a/auth/src/main/java/org/robojackets/apiary/auth/ui/Authentication.kt +++ b/auth/src/main/java/org/robojackets/apiary/auth/ui/Authentication.kt @@ -1,33 +1,53 @@ package org.robojackets.apiary.auth.ui -import android.app.Activity.* +import android.app.Activity.RESULT_OK import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.google.accompanist.systemuicontroller.rememberSystemUiController -import kotlinx.coroutines.launch import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationResponse import org.robojackets.apiary.auth.R import org.robojackets.apiary.auth.model.AuthenticationState import org.robojackets.apiary.auth.model.AuthenticationViewModel -import org.robojackets.apiary.auth.model.LoginStatus.* +import org.robojackets.apiary.auth.model.LoginStatus.COMPLETE +import org.robojackets.apiary.auth.model.LoginStatus.ERROR +import org.robojackets.apiary.auth.model.LoginStatus.NOT_STARTED import org.robojackets.apiary.auth.oauth2.AuthManager import org.robojackets.apiary.base.AppEnvironment -import org.robojackets.apiary.base.ui.theme.BottomSheetShape import org.robojackets.apiary.base.ui.util.MadeWithLove -@OptIn(ExperimentalMaterialApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Suppress("LongMethod", "MagicNumber") @Composable private fun Authentication( @@ -36,17 +56,8 @@ private fun Authentication( onAppEnvChange: (newEnv: AppEnvironment) -> Unit, viewModel: AuthenticationViewModel, ) { - // You have to `remember` two things here for some reason - // In any case, thanks to https://proandroiddev.com/getting-your-bottomsheetscaffold-working-on-jetpack-compose-beta-03-aa829b0c9b6c - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberBottomSheetState( - initialValue = BottomSheetValue.Collapsed - ) - ) - val coroutineScope = rememberCoroutineScope() - val systemUiController = rememberSystemUiController() - val backgroundColor = MaterialTheme.colors.background + val backgroundColor = MaterialTheme.colorScheme.background SideEffect { systemUiController.setSystemBarsColor(backgroundColor) } @@ -82,11 +93,13 @@ private fun Authentication( viewModel.navigateToAttendance() } } + ex != null -> viewModel.recordAuthError(ex) else -> viewModel.recordAuthError(null) } } } + authException != null -> viewModel.recordAuthError(authException) } } else { @@ -94,120 +107,130 @@ private fun Authentication( } } - BottomSheetScaffold( - scaffoldState = scaffoldState, - sheetShape = BottomSheetShape, - sheetPeekHeight = 0.dp, - sheetContent = { - Column( - Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly, - content = { - ChangeEnvironmentBottomSheetContent( - viewState, - onAppEnvChange, - scaffoldState - ) - } - ) - }) { - Surface( - color = MaterialTheme.colors.background, - modifier = Modifier.padding(8.dp) + var showChangeEnvBottomSheet by remember { mutableStateOf(false) } + + if (showChangeEnvBottomSheet) { + ChangeEnvBottomSheet( + onDismiss = { showChangeEnvBottomSheet = false }, + viewState = viewState, + onAppEnvChange = { + showChangeEnvBottomSheet = false + onAppEnvChange(it) + } + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Image( + painter = painterResource(id = R.drawable.ic_robobuzz_white_outline), + contentDescription = "RoboJackets logo", + modifier = Modifier + .fillMaxWidth(.45f) + .weight(1.0f) + ) + Column( + modifier = Modifier.weight(.5f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceAround ) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween + + Button( + onClick = { + val authRequest = authManager.getAuthRequest() + launcher.launch( + authManager.authService.getAuthorizationRequestIntent( + authRequest + ) + ) + }, ) { - Image( - painter = painterResource(id = R.drawable.ic_robobuzz_white_outline), - contentDescription = "RoboJackets logo", - modifier = Modifier - .fillMaxWidth(.45f) - .weight(1.0f) - ) - Column( - modifier = Modifier.weight(.5f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceAround - ) { + Text("Sign in with MyRoboJackets") + } + } - Button( - onClick = { - val authRequest = authManager.getAuthRequest() - launcher.launch( - authManager.authService.getAuthorizationRequestIntent( - authRequest - ) - ) - }, - ) { - Text("Sign in with MyRoboJackets") + if (viewState.loginStatus == ERROR) { + AlertDialog( + onDismissRequest = { + viewModel.setLoginStatus(NOT_STARTED) + }, + confirmButton = { + TextButton(onClick = { viewModel.setLoginStatus(NOT_STARTED) }) { + Text("Close") } - } - - if (viewState.loginStatus == ERROR) { - AlertDialog( - onDismissRequest = { - viewModel.setLoginStatus(NOT_STARTED) - }, - confirmButton = { - TextButton(onClick = { viewModel.setLoginStatus(NOT_STARTED) }) { - Text("Close") - } - }, - title = { Text("Login failed") }, - text = { - Text( - "${viewState.loginErrorMessage}\n\nTry logging in again. " + - "If that does not work, please post in #it-helpdesk in Slack." - ) - }, + }, + title = { Text("Login failed") }, + text = { + Text( + "${viewState.loginErrorMessage}\n\nTry logging in again. " + + "If that does not work, please post in #it-helpdesk in Slack." ) - } + }, + ) + } - TextButton(onClick = { - coroutineScope.launch { - scaffoldState.bottomSheetState.expand() - } - }) { - Text("Change server") - } - Text("Server: ${viewState.appEnv.name} (${viewState.appEnv.apiBaseUrl})") - MadeWithLove() - } + TextButton(onClick = { + showChangeEnvBottomSheet = true + }) { + Text("Change server") } + Text("Server: ${viewState.appEnv.name} (${viewState.appEnv.apiBaseUrl})") + MadeWithLove() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChangeEnvBottomSheet( + onDismiss: () -> Unit, + viewState: AuthenticationState, + onAppEnvChange: (newEnv: AppEnvironment) -> Unit +) { + val modalBottomSheetState = rememberModalBottomSheetState() + + ModalBottomSheet( + onDismissRequest = { onDismiss() }, + sheetState = modalBottomSheetState, + ) { + Column( + Modifier + .padding(bottom = 40.dp) + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly, + content = { + ChangeEnvironmentBottomSheetContent( + viewState, + onAppEnvChange, + ) + } + ) } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChangeEnvironmentBottomSheetContent( viewState: AuthenticationState, onAppEnvChange: (newEnv: AppEnvironment) -> Unit, - scaffoldState: BottomSheetScaffoldState ) { - val coroutineScope = rememberCoroutineScope() var unsavedAppEnvSelection by remember { mutableStateOf(viewState.appEnv) } val appEnvChoices = AppEnvironment.values() val saveNewAppEnvChoice: (() -> Unit) = { onAppEnvChange(unsavedAppEnvSelection) - coroutineScope.launch { - scaffoldState.bottomSheetState.collapse() - } } Text( "Change server", modifier = Modifier .padding(16.dp), - style = MaterialTheme.typography.h5 + style = MaterialTheme.typography.headlineSmall ) Column(Modifier.selectableGroup()) { @@ -230,7 +253,7 @@ private fun ChangeEnvironmentBottomSheetContent( ) Text( text = "${it.name} (${it.apiBaseUrl})", - style = MaterialTheme.typography.body1.merge(), + style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp) ) } @@ -248,7 +271,7 @@ private fun ChangeEnvironmentBottomSheetContent( @Composable fun AuthenticationScreen( viewModel: AuthenticationViewModel, - authManager: AuthManager + authManager: AuthManager, ) { val viewState by viewModel.state.collectAsState() Authentication( diff --git a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt index f904049..54faec2 100644 --- a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt +++ b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt @@ -1,18 +1,34 @@ package org.robojackets.apiary.auth.ui.permissions -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.CheckCircle -import androidx.compose.runtime.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat.* import org.robojackets.apiary.auth.model.Permission import org.robojackets.apiary.auth.model.Permission.* import org.robojackets.apiary.base.ui.error.GoToItHelpdesk @@ -40,7 +56,7 @@ fun InsufficientPermissions( ErrorIcon(Modifier.size(90.dp), tint = danger) Text( text = "$featureName unavailable", - style = MaterialTheme.typography.h4 + style = MaterialTheme.typography.headlineMedium, ) Text( text = "You don't have permission to use this feature. Please ask in #it-helpdesk for assistance.", @@ -88,8 +104,8 @@ fun PermissionDetailsDialog( ) { Text( text = "Required permissions", - style = MaterialTheme.typography.h5, - color = MaterialTheme.colors.onBackground, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(bottom = 20.dp) ) Divider() @@ -120,12 +136,10 @@ fun PermissionDetailsDialog( ) } -@OptIn(ExperimentalMaterialApi::class) @Composable fun PermissionsListItem(hasPermission: Boolean, permissionName: String) { - ListItem( - icon = { + leadingContent = { when (hasPermission) { true -> Icon( Icons.Outlined.CheckCircle, @@ -136,9 +150,8 @@ fun PermissionsListItem(hasPermission: Boolean, permissionName: String) { false -> ErrorIcon(Modifier.size(28.dp), tint = danger) } }, - ) { - Text(permissionName) - } + headlineContent = { Text(permissionName) }, + ) } @Composable diff --git a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/MissingHiddenTeamsCallout.kt b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/MissingHiddenTeamsCallout.kt index e1e107e..0869601 100644 --- a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/MissingHiddenTeamsCallout.kt +++ b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/MissingHiddenTeamsCallout.kt @@ -3,8 +3,8 @@ package org.robojackets.apiary.auth.ui.permissions import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -17,9 +17,12 @@ fun MissingHiddenTeamsCallout(onRefreshTeams: () -> Unit) { padding = PaddingValues(start = 12.dp, top = 10.dp, end = 12.dp, bottom = 7.dp) ) { Column { - Text("You don't have permission to view all teams, including " + - "training teams. Ask in #it-helpdesk for access.") - OutlinedButton(onClick = onRefreshTeams, Modifier.padding(top = 0.dp)) { + Text( + "You don't have permission to view all teams, including " + + "training teams. Ask in #it-helpdesk for access.", + Modifier.padding(top = 4.dp) + ) + OutlinedButton(onClick = onRefreshTeams, Modifier.padding(top = 4.dp)) { Text("Refresh teams") } } diff --git a/auth/src/main/res/values-night/themes.xml b/auth/src/main/res/values-night/themes.xml deleted file mode 100644 index f864a73..0000000 --- a/auth/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/auth/src/main/res/values/themes.xml b/auth/src/main/res/values/themes.xml deleted file mode 100644 index a17b26e..0000000 --- a/auth/src/main/res/values/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/base/build.gradle.kts b/base/build.gradle.kts index ac41121..81c6e58 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.library") kotlin("android") id("kotlin-android") - kotlin("kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } @@ -17,21 +17,25 @@ dependencies { implementation(AndroidToolDependencies.timber) implementation(ComposeDependencies.compose_foundation) - implementation(ComposeDependencies.compose_material) + implementation(ComposeDependencies.compose_material3) implementation(ComposeDependencies.compose_ui) implementation(ComposeDependencies.compose_ui_tooling) - implementation(ComposeDependencies.accompanist_nav_material) + implementation(ComposeDependencies.compose_material_navigation) implementation(HiltDependencies.hilt) - kapt(HiltDependencies.hilt_android_compiler) + ksp(HiltDependencies.hilt_android_compiler) + + implementation(MaterialDependencies.material_android) implementation(NetworkDependencies.moshi) - kapt(NetworkDependencies.moshi_kotlin_codegen) + ksp(NetworkDependencies.moshi_kotlin_codegen) implementation(NetworkDependencies.retrofit) implementation(NetworkDependencies.sandwich) + implementation(NetworkDependencies.sandwich_retrofit) + implementation(NetworkDependencies.sandwich_retrofit_serialization) implementation(platform(NfcDependencies.nfc_firebase_bom)) - implementation(NfcDependencies.nfc_firebase_core) // Firebase BoM and Core are required when including TapLinx (line below) manually + implementation(NfcDependencies.nfc_firebase_analytics) // Firebase BoM and Analytics (f/k/a Core) are required when including TapLinx (line below) manually compileOnly(files(NfcDependencies.nxp_nfc_android_aar_path)) // Test dependencies @@ -44,10 +48,9 @@ hilt { } android { - compileSdk = 32 + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 32 vectorDrawables { useSupportLibrary = true @@ -60,17 +63,18 @@ android { } buildFeatures { compose = true + buildConfig = true } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-beta03" + kotlinCompilerExtensionVersion = "1.5.1" } namespace = "org.robojackets.apiary.base" hilt { diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/ActionPrompt.kt b/base/src/main/java/org/robojackets/apiary/base/ui/ActionPrompt.kt index f4bd538..4ef5d93 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/ActionPrompt.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/ActionPrompt.kt @@ -1,8 +1,12 @@ package org.robojackets.apiary.base.ui -import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,12 +27,12 @@ fun ActionPrompt( Column( verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp, start = 4.dp, end = 4.dp) ) { icon() Text( text = title, - style = MaterialTheme.typography.h4, + style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(top = 6.dp), textAlign = TextAlign.Center, ) @@ -40,7 +44,7 @@ fun ActionPrompt( subtitle?.let { Text( text = subtitle, - style = MaterialTheme.typography.subtitle1, + style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center, ) } @@ -74,7 +78,9 @@ fun ActionPromptCardReadErrorWrongType() { title = "Card read error", subtitle = "Try tapping again" ) { - IconWithText(icon = { WarningIcon(tint = danger) }, - text = "We only support BuzzCards 😉") + IconWithText( + icon = { WarningIcon(tint = danger) }, + text = "We only support BuzzCards 😉" + ) } } diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/IconWithText.kt b/base/src/main/java/org/robojackets/apiary/base/ui/IconWithText.kt index 55ba14f..bc90d7d 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/IconWithText.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/IconWithText.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/callout/Callout.kt b/base/src/main/java/org/robojackets/apiary/base/ui/callout/Callout.kt index a48fcc4..04a7101 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/callout/Callout.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/callout/Callout.kt @@ -2,10 +2,14 @@ package org.robojackets.apiary.base.ui.callout import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.robojackets.apiary.base.ui.IconWithText import org.robojackets.apiary.base.ui.icons.ErrorIcon +import org.robojackets.apiary.base.ui.theme.isLight import org.robojackets.apiary.base.ui.theme.warningDarkSubtle import org.robojackets.apiary.base.ui.theme.warningLightMuted import org.robojackets.apiary.base.ui.theme.warningLightSubtle @@ -49,7 +54,7 @@ fun WarningCallout( padding: PaddingValues? = null, body: @Composable () -> Unit, ) { - val isLightTheme = MaterialTheme.colors.isLight + val isLightTheme = MaterialTheme.colorScheme.isLight() Callout( title = { @@ -57,7 +62,7 @@ fun WarningCallout( Text( titleText, modifier = Modifier.padding(start = 8.dp), - style = MaterialTheme.typography.h6 + style = MaterialTheme.typography.titleLarge ) } }, @@ -76,7 +81,9 @@ private fun WarningCalloutPreview() { WarningCallout( titleText = "Some teams are hidden", ) { - Text("Your account doesn't have permission to view all teams, including training " + - "teams. Ask in #it-helpdesk for access.") + Text( + "Your account doesn't have permission to view all teams, including training " + + "teams. Ask in #it-helpdesk for access." + ) } } diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt b/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt index 7354e7b..c8e9f2a 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt @@ -2,10 +2,16 @@ package org.robojackets.apiary.base.ui.error import android.content.Intent import android.net.Uri -import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.OutlinedButton -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/icons/ExtraIcons.kt b/base/src/main/java/org/robojackets/apiary/base/ui/icons/ExtraIcons.kt index 058fabd..baf992f 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/icons/ExtraIcons.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/icons/ExtraIcons.kt @@ -1,10 +1,9 @@ package org.robojackets.apiary.base.ui.icons -import androidx.compose.material.Icon -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -14,7 +13,7 @@ import org.robojackets.apiary.base.R @Composable fun ContactlessIcon( modifier: Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_outline_contactless_24dp), @@ -27,7 +26,7 @@ fun ContactlessIcon( @Composable fun WarningIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( Icons.Default.Warning, @@ -40,7 +39,7 @@ fun WarningIcon( @Composable fun ErrorIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_error_outline_24), @@ -53,7 +52,7 @@ fun ErrorIcon( @Composable fun CreditCardIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_credit_card_24), @@ -66,7 +65,7 @@ fun CreditCardIcon( @Composable fun PendingIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_outline_pending_24dp), @@ -79,7 +78,7 @@ fun PendingIcon( @Composable fun GroupsIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface, ) { Icon( painter = painterResource(id = R.drawable.ic_outline_groups_24dp), @@ -92,7 +91,7 @@ fun GroupsIcon( @Composable fun EventIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_outline_event_24dp), @@ -105,7 +104,7 @@ fun EventIcon( @Composable fun UpdateIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_update_24), @@ -118,7 +117,7 @@ fun UpdateIcon( @Composable fun TaskAltIcon( modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) + tint: Color = MaterialTheme.colorScheme.onSurface ) { Icon( painter = painterResource(id = R.drawable.ic_outline_task_alt_24dp), diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/nfc/BuzzCardPrompt.kt b/base/src/main/java/org/robojackets/apiary/base/ui/nfc/BuzzCardPrompt.kt index 6f034a9..1ca0477 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/nfc/BuzzCardPrompt.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/nfc/BuzzCardPrompt.kt @@ -5,7 +5,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier @@ -185,7 +190,7 @@ fun ManualGtidEntryPrompt( Text("Submit") } }, - title = { Text(text = "Manual GTID entry", style = MaterialTheme.typography.h5) }, + title = { Text(text = "Manual GTID entry", style = MaterialTheme.typography.headlineSmall) }, text = { Column { Text("Type the entire 9-digit GTID, starting with 90") diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/nfc/NfcRequired.kt b/base/src/main/java/org/robojackets/apiary/base/ui/nfc/NfcRequired.kt index 06f0df7..370b76c 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/nfc/NfcRequired.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/nfc/NfcRequired.kt @@ -6,9 +6,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Button +import androidx.compose.material3.Text +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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -26,7 +30,8 @@ fun NfcRequired(nfcEnabled: Boolean, gatedComposable: @Composable () -> Unit) { if (nfcEnabled) { gatedComposable() } else { - Column(Modifier.fillMaxHeight(), + Column( + Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Color.kt b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Color.kt index 19bfdb7..281d309 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Color.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Color.kt @@ -7,6 +7,8 @@ val Gold = Color(0xFFEEB211) val GoldLight = Color(0xFFFFE450) val GoldDark = Color(0xFFB78300) +val neutral = Color(0xFF949088) + val danger = Color(0xFFB00020) // Color inspiration from GitHub Primer CSS: https://primer.style/css/ @@ -26,3 +28,76 @@ val warningDarkMuted = Color(0x66BB8009) // outline val success = Color(0xFF4CAF50) val webNavBarBackground = Color(0xFF343A40) + +// Imported from Material theme generator +val primaryLight = Color(0xFFEEB211) // Gold +val onPrimaryLight = Color(0xFF000000) +val primaryContainerLight = Color(0xFFF7BA1E) +val onPrimaryContainerLight = Color(0xFF453100) +val secondaryLight = Color(0xFFB78300) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFFFE0A6) +val onSecondaryContainerLight = Color(0xFF5C450F) +val tertiaryLight = Color(0xFF526600) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFB3D04F) +val onTertiaryContainerLight = Color(0xFF2D3900) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF410002) +val backgroundLight = Color(0xFFFFF8F2) +val onBackgroundLight = Color(0xFF201B11) +val surfaceLight = Color(0xFFFFF8F2) +val onSurfaceLight = Color(0xFF201B11) +val surfaceVariantLight = Color(0xFFF0E0C7) +val onSurfaceVariantLight = Color(0xFF504533) +val outlineLight = Color(0xFF827661) +val outlineVariantLight = Color(0xFFD4C5AC) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF363025) +val inverseOnSurfaceLight = Color(0xFFFBEFDF) +val inversePrimaryLight = Color(0xFFFABD22) +val surfaceDimLight = Color(0xFFE4D8C9) +val surfaceBrightLight = Color(0xFFFFF8F2) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFFEF2E2) +val surfaceContainerLight = Color(0xFFF8ECDC) +val surfaceContainerHighLight = Color(0xFFF2E7D7) +val surfaceContainerHighestLight = Color(0xFFECE1D1) + +val primaryDark = Color(0xFFFFDB95) +val onPrimaryDark = Color(0xFF402D00) +val primaryContainerDark = Color(0xFFE7AC03) +val onPrimaryContainerDark = Color(0xFF382700) +val secondaryDark = Color(0xFFE4C281) +val onSecondaryDark = Color(0xFF402D00) +val secondaryContainerDark = Color(0xFF4F3904) +val onSecondaryContainerDark = Color(0xFFEECC89) +val tertiaryDark = Color(0xFFCDEC67) +val onTertiaryDark = Color(0xFF293500) +val tertiaryContainerDark = Color(0xFFA4C142) +val onTertiaryContainerDark = Color(0xFF232D00) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF17130A) +val onBackgroundDark = Color(0xFFECE1D1) +val surfaceDark = Color(0xFF17130A) +val onSurfaceDark = Color(0xFFECE1D1) +val surfaceVariantDark = Color(0xFF504533) +val onSurfaceVariantDark = Color(0xFFD4C5AC) +val outlineDark = Color(0xFF9C8F79) +val outlineVariantDark = Color(0xFF504533) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFECE1D1) +val inverseOnSurfaceDark = Color(0xFF363025) +val inversePrimaryDark = Color(0xFF7A5900) +val surfaceDimDark = Color(0xFF17130A) +val surfaceBrightDark = Color(0xFF3F382D) +val surfaceContainerLowestDark = Color(0xFF120E06) +val surfaceContainerLowDark = Color(0xFF201B11) +val surfaceContainerDark = Color(0xFF241F15) +val surfaceContainerHighDark = Color(0xFF2F291F) +val surfaceContainerHighestDark = Color(0xFF3A3429) diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Shape.kt b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Shape.kt index 20bcac2..ddad505 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Shape.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Shape.kt @@ -1,7 +1,7 @@ package org.robojackets.apiary.base.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Shapes +import androidx.compose.material3.Shapes import androidx.compose.ui.unit.dp val Shapes = Shapes( diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Theme.kt b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Theme.kt index 1f76d50..4d2aae7 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Theme.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Theme.kt @@ -1,48 +1,129 @@ package org.robojackets.apiary.base.ui.theme +import android.app.Activity +import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat -private val DarkColorPalette = darkColors( - primary = GoldLight, - primaryVariant = GoldDark, - secondary = Color.White, +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) -private val LightColorPalette = lightColors( - primary = Gold, - primaryVariant = GoldDark, - secondary = Color.Black, - - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) +// From https://stackoverflow.com/a/71594753 +@Suppress("MagicNumber") +fun ColorScheme.isLight() = this.background.luminance() > 0.5 + @Composable fun Apiary_MobileTheme( darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = false, content: @Composable() () -> Unit ) { - val colors = if (darkTheme) { - DarkColorPalette - } else { - LightColorPalette + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> darkScheme + else -> lightScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } } MaterialTheme( - colors = colors, + colorScheme = colorScheme, typography = Typography, - shapes = Shapes, content = content ) } diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Type.kt b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Type.kt index d67ef35..2398b76 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/theme/Type.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/theme/Type.kt @@ -1,6 +1,6 @@ package org.robojackets.apiary.base.ui.theme -import androidx.compose.material.Typography +import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -8,13 +8,14 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( - body1 = TextStyle( + bodyLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ), /* Other default text styles to override */ - button = TextStyle( + labelLarge = TextStyle( + fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, fontSize = 14.sp, // letterSpacing = 1.25.sp diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/util/LoadingSpinner.kt b/base/src/main/java/org/robojackets/apiary/base/ui/util/LoadingSpinner.kt index 7c49776..2ecc73a 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/util/LoadingSpinner.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/util/LoadingSpinner.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/util/MadeWithLove.kt b/base/src/main/java/org/robojackets/apiary/base/ui/util/MadeWithLove.kt index c6da7b1..4e7f57e 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/util/MadeWithLove.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/util/MadeWithLove.kt @@ -1,6 +1,6 @@ package org.robojackets.apiary.base.ui.util -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable @Composable diff --git a/build.gradle.kts b/build.gradle.kts index cb0f075..4e4923e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,17 @@ buildscript { repositories { - gradlePluginPortal() google() mavenCentral() + gradlePluginPortal() } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") - classpath("com.android.tools.build:gradle:7.4.2") - classpath("com.google.dagger:hilt-android-gradle-plugin:2.40.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("com.android.tools.build:gradle:8.5.1") + classpath("com.google.dagger:hilt-android-gradle-plugin:2.51.1") // This version needs to + // match the version for other Hilt dependencies defined in Dependencies.kt classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") - classpath("com.google.gms:google-services:4.3.10") - classpath("com.google.android.gms:oss-licenses-plugin:0.10.5") + classpath("com.google.gms:google-services:4.4.2") + classpath("com.google.android.gms:oss-licenses-plugin:0.10.6") } } @@ -29,19 +30,20 @@ allprojects { } plugins { - id("io.gitlab.arturbosch.detekt").version("1.20.0") - id("com.autonomousapps.dependency-analysis").version("1.4.0") - id("com.github.ben-manes.versions").version("0.42.0") + id("com.google.devtools.ksp").version("1.9.0-1.0.13").apply(false) + id("io.gitlab.arturbosch.detekt").version("1.23.0") + id("com.autonomousapps.dependency-analysis").version("1.21.0") + id("com.github.ben-manes.versions").version("0.46.0") } tasks.withType().configureEach { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } } tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } val projectSource = file(projectDir) @@ -64,14 +66,14 @@ tasks.register("detektAll", io.gitlab.arturbosch.detekt.Detekt::class) { include(kotlinFiles) exclude(resourceFiles, buildFiles) reports { - html.enabled = true - xml.enabled = false - txt.enabled = false + html.required.set(true) + xml.required.set(false) + txt.required.set(false) } } dependencies { - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.16.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.0") } // from https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/wiki/ABI-filtering diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index e14fabe..6ddc2fd 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -1,31 +1,30 @@ object ComposeDependencies { object Versions { - const val accompanist_version = "0.25.0" - const val compose_settings_version = "0.7.2" - const val compose_version = "1.1.1" - const val lifecycle_viewmodel_compose_version = "2.4.1" + const val accompanist_version = "0.34.0" + const val compose_settings_version = "2.4.0" + const val compose_version = "1.6.8" + const val lifecycle_viewmodel_compose_version = "2.8.2" + const val compose_material3_version = "1.2.1" + const val compose_material_navigation_version = "1.7.0-beta01" } const val accompanist_systemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:${Versions.accompanist_version}" - const val accompanist_nav_material = - "com.google.accompanist:accompanist-navigation-material:${Versions.accompanist_version}" const val compose_foundation = "androidx.compose.foundation:foundation:${Versions.compose_version}" - const val compose_material = "androidx.compose.material:material:${Versions.compose_version}" + const val compose_material3 = + "androidx.compose.material3:material3:${Versions.compose_material3_version}" const val compose_material_icons_core = "androidx.compose.material:material-icons-core:${Versions.compose_version}" const val compose_material_icons_extended = "androidx.compose.material:material-icons-extended:${Versions.compose_version}" - - const val compose_runtime = "androidx.compose.runtime:runtime:${Versions.compose_version}" - const val compose_runtime_livedata = - "androidx.compose.runtime:runtime-livedata:${Versions.compose_version}" + const val compose_material_navigation = + "androidx.compose.material:material-navigation:${Versions.compose_material_navigation_version}" const val compose_settings = - "com.github.alorma:compose-settings-ui:${Versions.compose_settings_version}" + "com.github.alorma.compose-settings:ui-tiles:${Versions.compose_settings_version}" const val compose_ui = "androidx.compose.ui:ui:${Versions.compose_version}" const val compose_ui_test = "androidx.compose.ui:ui-test-junit4:${Versions.compose_version}" @@ -46,11 +45,11 @@ object MaterialDependencies { object AndroidXDependencies { object Versions { - const val androidx_activity_compose_version = "1.4.0" - const val androidx_appcompat_version = "1.4.2" - const val androidx_browser_version = "1.4.0" - const val androidx_lifecycle_runtime_ktx_version = "2.4.1" - const val androidx_navigation_compose_version = "2.4.2" + const val androidx_activity_compose_version = "1.9.0" + const val androidx_appcompat_version = "1.7.0" + const val androidx_browser_version = "1.8.0" + const val androidx_lifecycle_runtime_version = "2.8.2" + const val androidx_navigation_compose_version = "2.7.7" } const val androidx_activity_compose = @@ -58,27 +57,24 @@ object AndroidXDependencies { const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat_version}" const val androidx_browser = "androidx.browser:browser:${Versions.androidx_browser_version}" - const val androidx_lifecycle_runtime_ktx = - "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidx_lifecycle_runtime_ktx_version}" + const val androidx_lifecycle_runtime = + "androidx.lifecycle:lifecycle-runtime:${Versions.androidx_lifecycle_runtime_version}" const val androidx_navigation_compose = "androidx.navigation:navigation-compose:${Versions.androidx_navigation_compose_version}" } object FirebaseDependencies { object Versions { - const val firebase_bom_version = "30.1.0" + const val firebase_bom_version = "33.1.0" } const val firebase_bom = "com.google.firebase:firebase-bom:${Versions.firebase_bom_version}" - const val firebase_core = "com.google.firebase:firebase-core" // No explicit version specified - // because of the inclusion of the Firebase BOM + const val firebase_analytics = "com.google.firebase:firebase-analytics-ktx" // versioned by BOM } object NfcDependencies { const val nfc_firebase_bom = FirebaseDependencies.firebase_bom - const val nfc_firebase_core = FirebaseDependencies.firebase_core // Duplicate definition - // with the goal of clarifying that the inclusion of Firebase Core here is specifically for - // the NXP NFC (TapLinx) library + const val nfc_firebase_analytics = FirebaseDependencies.firebase_analytics const val nxp_nfc_android_aar_path = "../libs/nxpnfcandroidlib-release.aar" } @@ -92,25 +88,27 @@ object AuthDependencies { object HiltDependencies { object Versions { - const val hilt_navigation_compose_version = "1.0.0" - const val hilt_version = "2.42" + const val hilt_navigation_compose_version = "1.2.0" + const val hilt_version = "2.51.1" // If you update this version, you also need to update + // the com.google.dagger:hilt-android-gradle-plugin version in build.gradle files } const val hilt = "com.google.dagger:hilt-android:${Versions.hilt_version}" const val hilt_android_compiler = "com.google.dagger:hilt-android-compiler:${Versions.hilt_version}" + const val dagger_producer = "com.google.dagger:dagger-producers:${Versions.hilt_version}" const val hilt_navigation_compose = "androidx.hilt:hilt-navigation-compose:${Versions.hilt_navigation_compose_version}" } object AndroidToolDependencies { object Versions { - const val android_tools_desugar_version = "1.1.5" + const val android_tools_desugar_version = "2.0.4" const val krate_version = "2.0.0" - const val gson_version = "2.9.0" - const val in_app_update_compose_version = "0.0.16" - const val open_source_licenses_version = "17.0.0" - const val sentry_version = "5.7.3" + const val gson_version = "2.11.0" + const val in_app_update_compose_version = "1.2.0" + const val open_source_licenses_version = "17.1.0" + const val sentry_version = "7.10.0" const val timber_version = "5.0.1" } @@ -118,7 +116,7 @@ object AndroidToolDependencies { "com.android.tools:desugar_jdk_libs:${Versions.android_tools_desugar_version}" const val gson = "com.google.code.gson:gson:${Versions.gson_version}" const val in_app_update_compose = - "se.warting.in-app-update:in-app-update-compose:${Versions.in_app_update_compose_version}" + "se.warting.in-app-update:in-app-update-compose-mui:${Versions.in_app_update_compose_version}" const val krate = "hu.autsoft:krate:${Versions.krate_version}" const val open_source_licenses = "com.google.android.gms:play-services-oss-licenses:${Versions.open_source_licenses_version}" @@ -129,12 +127,12 @@ object AndroidToolDependencies { object NetworkDependencies { object Versions { - const val moshi_version = "1.13.0" - const val moshi_converter_factory_version = "2.9.0" - const val okhttp_bom_version = "4.9.3" - const val retrofit_version = "2.9.0" + const val moshi_version = "1.15.1" + const val moshi_converter_factory_version = "2.11.0" + const val okhttp_bom_version = "4.12.0" + const val retrofit_version = "2.11.0" const val retrofuture_version = "1.7.4" - const val sandwich_version = "1.2.5" + const val sandwich_version = "2.0.8" } const val moshi = "com.squareup.moshi:moshi:${Versions.moshi_version}" @@ -147,6 +145,10 @@ object NetworkDependencies { const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit_version}" const val retrofuture = "net.sourceforge.streamsupport:android-retrofuture:${Versions.retrofuture_version}" const val sandwich = "com.github.skydoves:sandwich:${Versions.sandwich_version}" + const val sandwich_retrofit = + "com.github.skydoves:sandwich-retrofit:${Versions.sandwich_version}" + const val sandwich_retrofit_serialization = + "com.github.skydoves:sandwich-retrofit-serialization:${Versions.sandwich_version}" } object TestDependencies { diff --git a/ci/detekt/detekt.yml b/ci/detekt/detekt.yml index 418dc11..c2b990c 100644 --- a/ci/detekt/detekt.yml +++ b/ci/detekt/detekt.yml @@ -85,7 +85,7 @@ complexity: threshold: 10 includeStaticDeclarations: false includePrivateDeclarations: false - ComplexMethod: + CyclomaticComplexMethod: active: true threshold: 15 ignoreSingleWhenExpression: false @@ -302,7 +302,6 @@ formatting: active: false autoCorrect: true indentSize: 4 - continuationIndentSize: 4 MaximumLineLength: active: true maxLineLength: 120 @@ -411,7 +410,6 @@ naming: parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true EnumNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] @@ -433,7 +431,6 @@ naming: excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' excludeClassPattern: '$^' - ignoreOverridden: true ignoreAnnotated: - 'Composable' FunctionParameterNaming: @@ -441,7 +438,6 @@ naming: excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true InvalidPackageDeclaration: active: false excludes: ['**/*.kts'] @@ -487,7 +483,6 @@ naming: variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' - ignoreOverridden: true performance: active: true @@ -512,8 +507,6 @@ potential-bugs: active: false DoubleMutabilityForCollection: active: false - DuplicateCaseInWhenExpression: - active: true EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: @@ -526,7 +519,7 @@ potential-bugs: active: false IgnoredReturnValue: active: false - restrictToAnnotatedMethods: true + restrictToConfig: true returnValueAnnotations: - '*.CheckReturnValue' - '*.CheckResult' @@ -544,17 +537,12 @@ potential-bugs: LateinitUsage: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] - excludeAnnotatedProperties: [] + ignoreAnnotated: [] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: false - MissingWhenCase: - active: true - allowElseExpression: true NullableToStringCall: active: false - RedundantElseInWhen: - active: true UnconditionalJumpStatementInLoop: active: false UnnecessaryNotNullOperator: @@ -584,7 +572,8 @@ style: active: false DataClassContainsFunctions: active: false - conversionFunctionPrefix: 'to' + conversionFunctionPrefix: + - to DataClassShouldBeImmutable: active: false DestructuringDeclarationWithTooManyEntries: @@ -603,10 +592,11 @@ style: includeLineWrapping: false ForbiddenComment: active: true - values: - - 'TODO:' - - 'FIXME:' - - 'STOPSHIP:' + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' allowedPatterns: '' ForbiddenImport: active: false @@ -617,12 +607,6 @@ style: methods: - 'kotlin.io.println' - 'kotlin.io.print' - ForbiddenPublicDataClass: - active: true - excludes: ['**'] - ignorePackages: - - '*.internal' - - '*.internal.*' ForbiddenVoid: active: false ignoreOverridden: false @@ -631,15 +615,9 @@ style: active: true ignoreOverridableFunction: true ignoreActualFunction: true - excludedFunctions: '' - excludeAnnotatedFunction: + excludedFunctions: [] + ignoreAnnotated: - 'dagger.Provides' - LibraryCodeMustSpecifyReturnType: - active: true - excludes: ['**'] - LibraryEntitiesShouldNotBePublic: - active: true - excludes: ['**'] LoopWithTooManyJumpStatements: active: true maxJumpCount: 1 @@ -661,8 +639,6 @@ style: ignoreEnums: false ignoreRanges: false ignoreExtensionFunctions: true - MandatoryBracesIfStatements: - active: false MandatoryBracesLoops: active: false MaxLineLength: @@ -704,7 +680,8 @@ style: ReturnCount: active: true max: 2 - excludedFunctions: 'equals' + excludedFunctions: + - equals excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false @@ -722,10 +699,10 @@ style: active: false UnderscoresInNumericLiterals: active: false - acceptableDecimalLength: 5 + acceptableLength: 5 UnnecessaryAbstractClass: active: true - excludeAnnotatedClasses: + ignoreAnnotated: - 'dagger.Module' UnnecessaryAnnotationUseSiteTarget: active: false @@ -756,7 +733,7 @@ style: active: false UseDataClass: active: false - excludeAnnotatedClasses: [] + ignoreAnnotated: [] allowVars: false UseEmptyCounterpart: active: false diff --git a/gradle.properties b/gradle.properties index 1728bde..896ce41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,6 @@ org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" kotlin.code.style=official #Android -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +android.nonTransitiveRClass=true +android.nonFinalResIds=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffea1b6..e49c70f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Dec 15 17:03:27 EST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 643e28b..e466d60 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("com.android.library") kotlin("android") id("kotlin-android") - kotlin("kapt") + id("com.google.devtools.ksp") id("dagger.hilt.android.plugin") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") } @@ -22,19 +22,16 @@ dependencies { implementation(AndroidXDependencies.androidx_navigation_compose) implementation(HiltDependencies.hilt) - kapt(HiltDependencies.hilt_android_compiler) - - implementation(MaterialDependencies.material_android) + ksp(HiltDependencies.hilt_android_compiler) // Test dependencies androidTestImplementation(TestDependencies.junit) } android { - compileSdk = 32 + compileSdk = 35 defaultConfig { minSdk = 21 - targetSdk = 32 vectorDrawables { useSupportLibrary = true @@ -47,17 +44,18 @@ android { } buildFeatures { compose = true + buildConfig = true } compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" } composeOptions { - kotlinCompilerExtensionVersion = "1.2.0-beta03" + kotlinCompilerExtensionVersion = "1.5.1" } namespace = "org.robojackets.apiary.navigation" hilt { diff --git a/navigation/src/main/java/org/robojackets/apiary/navigation/NavigationDirections.kt b/navigation/src/main/java/org/robojackets/apiary/navigation/NavigationDirections.kt index 700a105..dd43110 100644 --- a/navigation/src/main/java/org/robojackets/apiary/navigation/NavigationDirections.kt +++ b/navigation/src/main/java/org/robojackets/apiary/navigation/NavigationDirections.kt @@ -5,7 +5,8 @@ import androidx.navigation.NavOptions // Based on https://proandroiddev.com/how-to-make-jetpack-compose-navigation-easier-and-testable-b4b19fd5f2e4 object NavigationDestinations { const val authentication = "authentication" - const val settings = "settings" + const val settingsSubgraph = "settings" + const val settings = "settingsMain" const val attendanceSubgraph = "attendance" const val attendableTypeSelection = "attendableTypeSelection" const val attendableSelection = "attendableSelection" @@ -20,7 +21,8 @@ object NavigationActions { fun anyScreenToAuthentication() = object : NavigationAction { override val destination = NavigationDestinations.authentication override val navOptions = NavOptions.Builder() - .setPopUpTo(0, true) +// .setPopUpTo(NavigationDestinations.authentication, true) + .setLaunchSingleTop(true) .build() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 826960a..e8742c2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,8 +1,8 @@ pluginManagement { repositories { google() - gradlePluginPortal() mavenCentral() + gradlePluginPortal() } }