diff --git a/.github/whatsnew/whatsnew-en-US b/.github/whatsnew/whatsnew-en-US index ad2cb72..71e52a6 100644 --- a/.github/whatsnew/whatsnew-en-US +++ b/.github/whatsnew/whatsnew-en-US @@ -1,4 +1,3 @@ -Android U (34) support -Predictive back gesture support -Themed app icon updated +Android 15 (35) and 16 (36) support +Removed Android L (21) and L_MR1 (22) support Bug fixes and performance improvements \ No newline at end of file diff --git a/.github/workflows/android-master.yml b/.github/workflows/android-master.yml index af7dc5c..522491d 100644 --- a/.github/workflows/android-master.yml +++ b/.github/workflows/android-master.yml @@ -14,9 +14,9 @@ jobs: needs: [ tests ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Cache gradle - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -24,10 +24,10 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} restore-keys: | ${{ runner.os }}-gradle- - - name: set up JDK 17 - uses: actions/setup-java@v3 + - name: set up JDK 21 + uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: 'gradle' - name: Grant execute permission for gradlew @@ -44,7 +44,7 @@ jobs: alias: ${{ secrets.KEYSTORE_ALIAS }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: - BUILD_TOOLS_VERSION: "34.0.0" + BUILD_TOOLS_VERSION: "36.0.0" - name: Deploy Bundle to Play Store uses: r0adkll/upload-google-play@v1 with: diff --git a/.github/workflows/github-releases.yml b/.github/workflows/github-releases.yml index d8c8092..8246cbd 100644 --- a/.github/workflows/github-releases.yml +++ b/.github/workflows/github-releases.yml @@ -12,7 +12,7 @@ jobs: contents: write if: ${{ github.ref == 'refs/heads/master' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - uses: ncipollo/release-action@v1 with: allowUpdates: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b0215d..43d1bc0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,9 @@ jobs: name: Run unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Cache gradle - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -18,11 +18,12 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} restore-keys: | ${{ runner.os }}-gradle- - - name: set up JDK 17 - uses: actions/setup-java@v3 + - name: set up JDK 21 + uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'adopt' + cache: 'gradle' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Unit tests @@ -36,9 +37,9 @@ jobs: contents: 'read' id-token: 'write' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Cache gradle - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.gradle/caches @@ -46,10 +47,10 @@ jobs: key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} restore-keys: | ${{ runner.os }}-gradle- - - name: set up JDK 17 - uses: actions/setup-java@v3 + - name: set up JDK 21 + uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'adopt' cache: 'gradle' - name: Grant execute permission for gradlew @@ -58,11 +59,11 @@ jobs: run: ./gradlew bundleDebug assembleDebugAndroidTest --stacktrace - id: auth name: Authenticate to Google Cloud - uses: google-github-actions/auth@v1 + uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GC_PROVIDER_NAME }} service_account: ${{ secrets.GC_SA_EMAIL }} - name: Set up Google Cloud SDK - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 - name: Instrumentation Tests run: 'gcloud firebase test android run --type instrumentation --app app/build/outputs/bundle/debug/app-debug.aab --test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk --device model=panther,version=33 --timeout 30m --no-auto-google-login --num-flaky-test-attempts=1' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 681a41b..8c83db1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,10 @@ captures/ .idea/androidTestResultsUserPreferences.xml .idea/migrations.xml .idea/runConfigurations/ +.idea/deploymentTargetSelector.xml +.idea/AndroidProjectSystem.xml +.idea/deviceManager.xml +.kotlin/ *.iml .run/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/README.md b/README.md index ce18dd3..0fa5fc5 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ This is a repository for the Device Info Android app that displays specific info ## Build Tools -* Android Studio Hedgehog (Gradle 8.2) +* Android Studio Narwhal Feature Drop (Gradle 4.0-8.12) * AndroidX/Jetpack -* Android 5.0 (API 21) or above (built against 14/API 34) -* Kotlin 1.9 +* Android 6.0 (API 23) or above (built against 16/API 36) +* Kotlin 2.2 ## Android Permissions diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a257be..bc00255 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Properties plugins { @@ -6,16 +7,17 @@ plugins { id("com.google.devtools.ksp") id("kotlin-parcelize") id("dagger.hilt.android.plugin") + alias(libs.plugins.compose.compiler) } android { namespace = "com.cwlarson.deviceid" - compileSdk = 34 + compileSdk = 36 defaultConfig { - minSdk = 21 - targetSdk = 34 - versionCode = 18 - versionName = "1.5.1" + minSdk = 23 + targetSdk = 36 + versionCode = 19 + versionName = "1.6.0" vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "com.cwlarson.deviceid.CustomTestRunner" } @@ -50,14 +52,14 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - composeOptions.kotlinCompilerExtensionVersion = "1.5.5" + kotlin.compilerOptions { + jvmTarget = JvmTarget.fromTarget("17") + optIn.add("kotlinx.coroutines.ExperimentalCoroutinesApi") + } testOptions { animationsDisabled = true unitTests.isIncludeAndroidResources = true unitTests.all { it.jvmArgs("-Xmx2g") } - kotlinOptions.freeCompilerArgs += - listOf("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi") } packagingOptions.resources.merges.addAll( listOf("META-INF/LICENSE.md", "META-INF/LICENSE-notice.md") @@ -65,87 +67,79 @@ android { testBuildType = "debug" } -val coroutinesBom: Dependency = dependencies.platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3") -val hiltVersion: String by rootProject.extra -val lifecycleVersion = "2.6.2" -val composeBom: Dependency = dependencies.platform("androidx.compose:compose-bom:2023.10.01") -val composeAccompanistVersion = "0.32.0" -val datastoreVersion = "1.0.0" -val mockkVersion = "1.13.8" dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - implementation(coroutinesBom) - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android") - implementation("com.google.android.material:material:1.10.0") - implementation("androidx.webkit:webkit:1.9.0") - implementation("androidx.datastore:datastore:$datastoreVersion") - implementation("androidx.datastore:datastore-preferences:$datastoreVersion") - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.core:core-splashscreen:1.0.1") + implementation(platform(libs.kotlinx.coroutines.bom)) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.material) + implementation(libs.androidx.webkit) + implementation(libs.androidx.datastore) + implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.core.splashscreen) // Compose - implementation(composeBom) - implementation("androidx.activity:activity-compose:1.8.1") - implementation("androidx.compose.ui:ui") + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.ui) // Tooling support (Previews, etc.) - debugImplementation("androidx.compose.ui:ui-tooling") - implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation(libs.androidx.ui.tooling) + implementation(libs.androidx.ui.tooling.preview) // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) - implementation("androidx.compose.foundation:foundation") + implementation(libs.androidx.foundation) // Material Design - implementation("androidx.compose.material:material") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.material3:material3-window-size-class") - implementation("androidx.compose.ui:ui-text-google-fonts") + implementation(libs.androidx.material) + implementation(libs.androidx.material3) + implementation(libs.androidx.material3.window.size) + implementation(libs.androidx.ui.google.fonts) // Material design icons - implementation("androidx.compose.material:material-icons-core") - implementation("androidx.compose.material:material-icons-extended") + implementation(libs.androidx.material.icons.core) + implementation(libs.androidx.material.icons.extended) // Compose Accompanist - implementation("com.google.accompanist:accompanist-systemuicontroller:$composeAccompanistVersion") - implementation("com.google.accompanist:accompanist-permissions:$composeAccompanistVersion") + implementation(libs.accompanist.permissions) // Lifecycle - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") - implementation("androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion") + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.lifecycle.common.java8) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) //Navigation - implementation("androidx.navigation:navigation-compose:2.7.5") + implementation(libs.androidx.navigation.compose) // Google Play App Updates - implementation("com.google.android.play:app-update-ktx:2.1.0") + implementation(libs.app.update.ktx) // Timber - implementation("com.jakewharton.timber:timber:5.0.1") + implementation(libs.timber) // Hilt - implementation("com.google.dagger:hilt-android:$hiltVersion") - implementation("androidx.hilt:hilt-navigation-compose:1.1.0") - ksp("com.google.dagger:hilt-android-compiler:$hiltVersion") - androidTestImplementation("com.google.dagger:hilt-android-testing:$hiltVersion") - kspAndroidTest("com.google.dagger:hilt-android-compiler:$hiltVersion") + implementation(libs.hilt.android) + implementation(libs.androidx.hilt.navigation.compose) + ksp(libs.hilt.compiler) + androidTestImplementation(libs.hilt.android.testing) + kspAndroidTest(libs.hilt.android.compiler) // Instrumentation Testing - androidTestImplementation("androidx.test:core-ktx:1.5.0") - androidTestImplementation("androidx.test:runner:1.5.2") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") - androidTestImplementation(composeBom) - androidTestImplementation("androidx.compose.ui:ui-test-junit4") - debugImplementation("androidx.compose.ui:ui-test-manifest") - androidTestImplementation("io.mockk:mockk-android:$mockkVersion") - androidTestImplementation("io.mockk:mockk-agent:$mockkVersion") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1") - androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") + androidTestImplementation(libs.core.ktx) + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(libs.jetbrains.kotlinx.coroutines.test) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.test.manifest) + androidTestImplementation(libs.mockk.android) + androidTestImplementation(libs.mockk.agent) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.espresso.intents) + androidTestImplementation(libs.androidx.uiautomator) // Unit Testing - testImplementation("junit:junit:4.13.2") - testImplementation("io.mockk:mockk-android:$mockkVersion") - testImplementation("io.mockk:mockk-agent:$mockkVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") - testImplementation("app.cash.turbine:turbine:1.0.0") - testImplementation(composeBom) - testImplementation("androidx.compose.ui:ui-test-junit4") + testImplementation(libs.junit) + testImplementation(libs.mockk.android) + testImplementation(libs.mockk.agent) + testImplementation(libs.jetbrains.kotlinx.coroutines.test) + testImplementation(libs.turbine) + testImplementation(platform(libs.androidx.compose.bom)) + testImplementation(libs.androidx.ui.test.junit4) // Robolectric Testing - testImplementation("org.robolectric:robolectric:4.11") - testImplementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("androidx.test:rules:1.5.0") + testImplementation(libs.robolectric) + testImplementation(libs.androidx.junit.ktx) + testImplementation(libs.androidx.rules) // LeakCanary - debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + debugImplementation(libs.leakcanary.android) } \ No newline at end of file diff --git a/app/src/androidTest/java/com/cwlarson/deviceid/MainActivityTest.kt b/app/src/androidTest/java/com/cwlarson/deviceid/MainActivityTest.kt index eb4d82a..36de413 100644 --- a/app/src/androidTest/java/com/cwlarson/deviceid/MainActivityTest.kt +++ b/app/src/androidTest/java/com/cwlarson/deviceid/MainActivityTest.kt @@ -2,15 +2,41 @@ package com.cwlarson.deviceid import android.content.Intent import androidx.compose.ui.semantics.Role -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.getUnclippedBoundsInRoot +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasTextExactly +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.junit4.createEmptyComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onChildren +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.compose.ui.unit.height import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.cwlarson.deviceid.androidtestutils.hasRole -import com.cwlarson.deviceid.data.* +import com.cwlarson.deviceid.data.AllRepository +import com.cwlarson.deviceid.data.DeviceRepository +import com.cwlarson.deviceid.data.HardwareRepository +import com.cwlarson.deviceid.data.NetworkRepository +import com.cwlarson.deviceid.data.SoftwareRepository +import com.cwlarson.deviceid.data.TabDataStatus import com.cwlarson.deviceid.settings.PreferenceManager import com.cwlarson.deviceid.settings.UserPreferences import com.cwlarson.deviceid.tabs.Item @@ -24,15 +50,23 @@ import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.* +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest -import org.junit.* +import org.junit.After import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test import javax.inject.Inject @HiltAndroidTest @@ -85,7 +119,12 @@ class MainActivityTest { every { appUpdateUtils.updateState } returns updateState coEvery { appUpdateUtils.awaitIsFlexibleUpdateDownloaded() } returns false every { preferenceManager.searchHistory } returns flowOf(false) - every { preferenceManager.getSearchHistoryItems(any()) } returns flowOf(listOf("history1", "history2")) + every { preferenceManager.getSearchHistoryItems(any()) } returns flowOf( + listOf( + "history1", + "history2" + ) + ) every { preferenceManager.darkTheme } returns flowOf(false) every { preferenceManager.autoRefreshRate } returns flowOf(0) every { preferenceManager.userPreferencesFlow } returns flowOf(UserPreferences()) @@ -100,9 +139,10 @@ class MainActivityTest { private fun launchScenario(hasSearchIntent: Boolean = false) { scenario = if (hasSearchIntent) - ActivityScenario.launch(Intent( - ApplicationProvider.getApplicationContext(), MainActivity::class.java - ).apply { action = Intent.ACTION_SEARCH }) + ActivityScenario.launch( + Intent( + ApplicationProvider.getApplicationContext(), MainActivity::class.java + ).apply { action = Intent.ACTION_SEARCH }) else ActivityScenario.launch(MainActivity::class.java) } @@ -169,13 +209,11 @@ class MainActivityTest { fun test_SearchView_nameFade_singlePane_initial() = runTest(dispatcher) { launchScenario() isScreenSw600dp(false) - composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() - .filterToOne(hasTextExactly("Device Info")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Device Info")).assertIsDisplayed() composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() .filterToOne(hasTextExactly("Search all")).assertDoesNotExist() dispatcher.scheduler.advanceUntilIdle() - composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() - .filterToOne(hasTextExactly("Device Info")).assertDoesNotExist() + composeTestRule.onNode(hasTextExactly("Device Info")).assertDoesNotExist() composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() .filterToOne(hasTextExactly("Search all")).assertIsDisplayed() } @@ -184,14 +222,12 @@ class MainActivityTest { fun test_SearchView_nameFade_singlePane_recreate() = runTest(dispatcher) { launchScenario() isScreenSw600dp(false) - composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() - .filterToOne(hasTextExactly("Device Info")).assertIsDisplayed() + composeTestRule.onNode(hasTextExactly("Device Info")).assertIsDisplayed() composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() .filterToOne(hasTextExactly("Search all")).assertDoesNotExist() dispatcher.scheduler.advanceUntilIdle() scenario.recreate() - composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() - .filterToOne(hasTextExactly("Device Info")).assertDoesNotExist() + composeTestRule.onNode(hasTextExactly("Device Info")).assertDoesNotExist() composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() .filterToOne(hasTextExactly("Search all")).assertIsDisplayed() } @@ -200,8 +236,7 @@ class MainActivityTest { fun test_SearchView_nameFade_dualPane() = runTest(dispatcher) { launchScenario() isScreenSw600dp() - composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() - .filterToOne(hasTextExactly("Device Info")).assertDoesNotExist() + composeTestRule.onNode(hasTextExactly("Device Info")).assertDoesNotExist() composeTestRule.onNodeWithTag(MAIN_ACTIVITY_TEST_TAG_SEARCH).onChildren() .filterToOne(hasTextExactly("Search all")).assertIsDisplayed() composeTestRule.onNode( @@ -484,7 +519,8 @@ class MainActivityTest { launchScenario() val outsideX = 0 val outsideY = with(composeTestRule.density) { - composeTestRule.onAllNodes(isRoot()).onFirst().getUnclippedBoundsInRoot().height.roundToPx() / 2 + composeTestRule.onAllNodes(isRoot()).onFirst() + .getUnclippedBoundsInRoot().height.roundToPx() / 2 } UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).click(outsideX, outsideY) composeTestRule.awaitIdle() diff --git a/app/src/main/java/com/cwlarson/deviceid/MainActivity.kt b/app/src/main/java/com/cwlarson/deviceid/MainActivity.kt index 0ca91f4..3df74a4 100644 --- a/app/src/main/java/com/cwlarson/deviceid/MainActivity.kt +++ b/app/src/main/java/com/cwlarson/deviceid/MainActivity.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting @@ -14,33 +15,80 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* +import androidx.compose.material.icons.outlined.Android +import androidx.compose.material.icons.outlined.DeveloperBoard +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.PermDeviceInformation +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.SettingsEthernet +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -51,8 +99,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.compose.ui.window.PopupProperties import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -73,8 +121,10 @@ import com.cwlarson.deviceid.tabs.TabScreen import com.cwlarson.deviceid.tabsdetail.TabDetailScreen import com.cwlarson.deviceid.ui.theme.AppTheme import com.cwlarson.deviceid.ui.util.IntentHandler -import com.cwlarson.deviceid.util.* -import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.cwlarson.deviceid.util.AppUpdateUtils +import com.cwlarson.deviceid.util.DispatcherProvider +import com.cwlarson.deviceid.util.InstallState +import com.cwlarson.deviceid.util.UpdateState import com.google.android.play.core.install.model.InstallStatus import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -151,8 +201,8 @@ private sealed class Screen( } @OptIn( - ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, - ExperimentalMaterial3Api::class, ExperimentalMaterial3WindowSizeClassApi::class + ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class, + ExperimentalMaterial3WindowSizeClassApi::class ) @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -170,7 +220,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() if (savedInstanceState == null) { onNewIntent(intent) lifecycleScope.launch(dispatcherProvider.Main) { appUpdateUtils.checkForFlexibleUpdate() } @@ -203,17 +253,7 @@ class MainActivity : ComponentActivity() { ) AppTheme(darkTheme = darkTheme ?: isSystemInDarkTheme()) { var appBarVisible by rememberSaveable { mutableStateOf(false) } - val statusBarColor = - if (appBarVisible) MaterialTheme.colorScheme.surface else Color.Transparent - val systemUiController = rememberSystemUiController() - val useDarkIcons = !isSystemInDarkTheme() val keyboardController = LocalSoftwareKeyboardController.current - SideEffect { - systemUiController.setNavigationBarColor( - Color.Transparent, darkIcons = useDarkIcons - ) - systemUiController.setStatusBarColor(statusBarColor, darkIcons = useDarkIcons) - } var isSideNavVisible by rememberSaveable { mutableStateOf(true) } var searchBarQuery by rememberSaveable { mutableStateOf("") } var isSearchOpen by rememberSaveable { mutableStateOf(false) } @@ -243,7 +283,7 @@ class MainActivity : ComponentActivity() { ), onClick = { navController.navigateUp() }) { Icon( - imageVector = Icons.Outlined.ArrowBack, + imageVector = Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = stringResource(R.string.menu_back) ) } @@ -479,13 +519,12 @@ class MainActivity : ComponentActivity() { }) } - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) intentHandler.onNewIntent(dispatcherProvider = dispatcherProvider, intent = intent) } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun SearchView( modifier: Modifier = Modifier, navController: NavController, @@ -533,7 +572,7 @@ private fun SearchView( onSearchOpen(false) }) { Icon( - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onSurfaceVariant, contentDescription = stringResource(R.string.nav_app_bar_navigate_up_description) ) diff --git a/app/src/main/java/com/cwlarson/deviceid/data/HardwareRepository.kt b/app/src/main/java/com/cwlarson/deviceid/data/HardwareRepository.kt index c19df32..b05b360 100644 --- a/app/src/main/java/com/cwlarson/deviceid/data/HardwareRepository.kt +++ b/app/src/main/java/com/cwlarson/deviceid/data/HardwareRepository.kt @@ -6,7 +6,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.hardware.display.DisplayManager -import android.os.* +import android.os.BatteryManager +import android.os.Build +import android.os.Environment +import android.os.Handler +import android.os.Looper +import android.os.StatFs import android.text.format.Formatter import android.util.DisplayMetrics import android.view.Display @@ -17,7 +22,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.BatteryStd import androidx.compose.material.icons.outlined.Memory import androidx.compose.material.icons.outlined.Storage -import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.os.EnvironmentCompat import com.cwlarson.deviceid.R @@ -29,9 +33,16 @@ import com.cwlarson.deviceid.tabs.ItemType import com.cwlarson.deviceid.util.DispatcherProvider import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import timber.log.Timber -import java.util.* +import java.util.StringJoiner import javax.inject.Inject class HardwareRepository @Inject constructor( @@ -130,7 +141,7 @@ class HardwareRepository @Inject constructor( title = R.string.hardware_title_external_storage, itemType = ItemType.HARDWARE, subtitle = try { // Mounted and not emulated, most likely a real SD Card - val appsDir = ContextCompat.getExternalFilesDirs(context, null).filter { + val appsDir = context.getExternalFilesDirs(null).filter { it != null && EnvironmentCompat.getStorageState(it) == Environment.MEDIA_MOUNTED && !Environment.isExternalStorageEmulated(it) diff --git a/app/src/main/java/com/cwlarson/deviceid/data/NetworkRepository.kt b/app/src/main/java/com/cwlarson/deviceid/data/NetworkRepository.kt index 800f490..3902a7b 100644 --- a/app/src/main/java/com/cwlarson/deviceid/data/NetworkRepository.kt +++ b/app/src/main/java/com/cwlarson/deviceid/data/NetworkRepository.kt @@ -4,7 +4,11 @@ import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothManager import android.content.Context -import android.net.* +import android.net.ConnectivityManager +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.os.Build @@ -23,7 +27,12 @@ import com.cwlarson.deviceid.util.AppPermission import com.cwlarson.deviceid.util.DispatcherProvider import com.cwlarson.deviceid.util.isGranted import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import timber.log.Timber import java.net.InetAddress import javax.inject.Inject @@ -76,16 +85,9 @@ class NetworkRepository @Inject constructor( * http://developer.android.com/about/versions/marshmallow/android-6.0-changes.html#behavior-hardware-id */ @SuppressLint("HardwareIds", "MissingPermission") - private fun wifiMac(wifiInfo: WifiInfo?) = Item( + private fun wifiMac() = Item( title = R.string.network_title_wifi_mac, itemType = ItemType.NETWORK, - subtitle = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - try { - wifiInfo?.let { ItemSubtitle.Text(it.macAddress) } ?: ItemSubtitle.Error - } catch (e: Throwable) { - Timber.w(e) - ItemSubtitle.Error - } - } else ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.M) + subtitle = ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.M) ) private fun wifiBSSID(wifiInfo: WifiInfo?) = Item( @@ -278,7 +280,7 @@ class NetworkRepository @Inject constructor( } override fun onUnavailable() { - map[0] = wifiMac(null) + map[0] = wifiMac() map[1] = wifiBSSID(null) map[2] = wifiSSID(null) map[3] = wifiFrequency(null) @@ -303,7 +305,7 @@ class NetworkRepository @Inject constructor( val ipAddresses = connectivityManager?.getLinkProperties(network)?.linkAddresses?.map { it.address } ?: emptyList() - map[0] = wifiMac(wifiInfo) + map[0] = wifiMac() map[1] = wifiBSSID(wifiInfo) map[2] = wifiSSID(wifiInfo) map[3] = wifiFrequency(wifiInfo) @@ -342,7 +344,7 @@ class NetworkRepository @Inject constructor( val wifiInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) networkCapabilities.transportInfo as? WifiInfo else @Suppress("DEPRECATION") wifiManager?.connectionInfo - map[0] = wifiMac(wifiInfo) + map[0] = wifiMac() map[1] = wifiBSSID(wifiInfo) map[2] = wifiSSID(wifiInfo) map[3] = wifiFrequency(wifiInfo) @@ -373,13 +375,7 @@ class NetworkRepository @Inject constructor( private fun bluetoothMac() = flowOf(Item(title = R.string.network_title_bluetooth_mac, itemType = ItemType.NETWORK, subtitle = try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - bluetoothManager?.let { - ItemSubtitle.Text(it.adapter.address) - } ?: ItemSubtitle.Error - } else { - ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.M) - } + ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.M) } catch (e: Throwable) { Timber.w(e) ItemSubtitle.Error @@ -409,7 +405,10 @@ class NetworkRepository @Inject constructor( private fun manufacturerCode() = flowOf(Item(title = R.string.network_title_manufacturer_code, itemType = ItemType.NETWORK, subtitle = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) telephonyManager?.let { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.BAKLAVA) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) telephonyManager?.let { + @Suppress("DEPRECATION") ItemSubtitle.Text(it.manufacturerCode) } ?: ItemSubtitle.Error else ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.Q) @@ -447,10 +446,9 @@ class NetworkRepository @Inject constructor( "${it.activeModemCount}" ) } ?: ItemSubtitle.Error - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> telephonyManager?.let { + else -> telephonyManager?.let { @Suppress("DEPRECATION") ItemSubtitle.Text("${it.phoneCount}") } ?: ItemSubtitle.Error - else -> ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) } } catch (e: Throwable) { Timber.w(e) @@ -661,10 +659,9 @@ class NetworkRepository @Inject constructor( flowOf(Item(title = R.string.network_title_hearing_aid_supported, itemType = ItemType.NETWORK, subtitle = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) telephonyManager?.let { + telephonyManager?.let { ItemSubtitle.Text("${it.isHearingAidCompatibilitySupported}") } ?: ItemSubtitle.Error - else ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) } catch (e: Throwable) { Timber.w(e) ItemSubtitle.Error @@ -710,7 +707,14 @@ class NetworkRepository @Inject constructor( private fun isSmsCapable() = flowOf(Item(title = R.string.network_title_sms_capable, itemType = ItemType.NETWORK, subtitle = try { - telephonyManager?.let { ItemSubtitle.Text("${it.isSmsCapable}") } ?: ItemSubtitle.Error + telephonyManager?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + ItemSubtitle.Text("${it.isDeviceSmsCapable}") + else { + @Suppress("DEPRECATION") + ItemSubtitle.Text("${it.isSmsCapable}") + } + } ?: ItemSubtitle.Error } catch (e: Throwable) { Timber.w(e) ItemSubtitle.Error @@ -719,10 +723,14 @@ class NetworkRepository @Inject constructor( private fun isVoiceCapable() = flowOf(Item(title = R.string.network_title_voice_capable, itemType = ItemType.NETWORK, subtitle = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) telephonyManager?.let { - ItemSubtitle.Text("${it.isVoiceCapable}") + telephonyManager?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) + ItemSubtitle.Text("${it.isDeviceVoiceCapable}") + else { + @Suppress("DEPRECATION") + ItemSubtitle.Text("${it.isVoiceCapable}") + } } ?: ItemSubtitle.Error - else ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.LOLLIPOP_MR1) } catch (e: Throwable) { Timber.w(e) ItemSubtitle.Error diff --git a/app/src/main/java/com/cwlarson/deviceid/data/SoftwareRepository.kt b/app/src/main/java/com/cwlarson/deviceid/data/SoftwareRepository.kt index 13b2f3c..5a696f8 100644 --- a/app/src/main/java/com/cwlarson/deviceid/data/SoftwareRepository.kt +++ b/app/src/main/java/com/cwlarson/deviceid/data/SoftwareRepository.kt @@ -64,6 +64,8 @@ fun Int.sdkToVersion(): String { Build.VERSION_CODES.S_V2 -> "12.1" Build.VERSION_CODES.TIRAMISU -> "13.0" Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> "14.0" + Build.VERSION_CODES.VANILLA_ICE_CREAM -> "15.0" + Build.VERSION_CODES.BAKLAVA -> "16.0" else -> "" } } @@ -126,21 +128,19 @@ class SoftwareRepository @Inject constructor( private fun patchLevel() = Item( title = R.string.software_title_patch_level, itemType = ItemType.SOFTWARE, subtitle = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - ItemSubtitle.Text( - try { - val patchDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).run { - parse(Build.VERSION.SECURITY_PATCH) - } - DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d, yyyy").run { - "${DateFormat.format(this, patchDate)}" - } - } catch (e: ParseException) { - Timber.w(e) - Build.VERSION.SECURITY_PATCH + ItemSubtitle.Text( + try { + val patchDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).run { + parse(Build.VERSION.SECURITY_PATCH) } - ) - else ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) + DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d, yyyy").run { + "${DateFormat.format(this, patchDate)}" + } + } catch (e: ParseException) { + Timber.w(e) + Build.VERSION.SECURITY_PATCH + } + ) } catch (e: Throwable) { Timber.w(e) ItemSubtitle.Error diff --git a/app/src/main/java/com/cwlarson/deviceid/data/TabData.kt b/app/src/main/java/com/cwlarson/deviceid/data/TabData.kt index c97df9a..3dca10d 100644 --- a/app/src/main/java/com/cwlarson/deviceid/data/TabData.kt +++ b/app/src/main/java/com/cwlarson/deviceid/data/TabData.kt @@ -4,7 +4,11 @@ import android.content.Context import com.cwlarson.deviceid.settings.PreferenceManager import com.cwlarson.deviceid.tabs.Item import com.cwlarson.deviceid.util.DispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOn sealed class TabDataStatus { data object Loading : TabDataStatus() @@ -36,7 +40,7 @@ abstract class TabData( else true }.sortedBy { item -> item.getFormattedString(context) } trySend(TabDataStatus.Success(result)) - } catch (e: Throwable) { + } catch (_: Throwable) { trySend(TabDataStatus.Error) } } @@ -54,7 +58,7 @@ abstract class TabData( } ?: true } trySend(TabDetailStatus.Success(result)) - } catch (e: Throwable) { + } catch (_: Throwable) { trySend(TabDetailStatus.Error) } } @@ -78,7 +82,7 @@ abstract class TabData( } else false }.sortedBy { item -> item.getFormattedString(context) } trySend(TabDataStatus.Success(result)) - } catch (e: Throwable) { + } catch (_: Throwable) { trySend(TabDataStatus.Error) } } diff --git a/app/src/main/java/com/cwlarson/deviceid/search/SearchScreen.kt b/app/src/main/java/com/cwlarson/deviceid/search/SearchScreen.kt index a0b4e2e..19f65fe 100644 --- a/app/src/main/java/com/cwlarson/deviceid/search/SearchScreen.kt +++ b/app/src/main/java/com/cwlarson/deviceid/search/SearchScreen.kt @@ -3,13 +3,35 @@ package com.cwlarson.deviceid.search import androidx.annotation.VisibleForTesting import androidx.compose.animation.Crossfade 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.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHostState +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.graphics.ColorFilter @@ -93,7 +115,7 @@ private fun MainContent( when (status) { is TabDataStatus.Success -> items(status.list) { item -> ItemListItem(item = item) { clickedItem = null; clickedItem = item } - Divider(modifier = Modifier.testTag(SEARCH_TEST_TAG_DIVIDER)) + HorizontalDivider(modifier = Modifier.testTag(SEARCH_TEST_TAG_DIVIDER)) } else -> items(emptyList()) { } } diff --git a/app/src/main/java/com/cwlarson/deviceid/settings/PreferenceManager.kt b/app/src/main/java/com/cwlarson/deviceid/settings/PreferenceManager.kt index 6c4f982..5971c09 100644 --- a/app/src/main/java/com/cwlarson/deviceid/settings/PreferenceManager.kt +++ b/app/src/main/java/com/cwlarson/deviceid/settings/PreferenceManager.kt @@ -4,10 +4,24 @@ import android.content.Context import android.os.Build import androidx.appcompat.app.AppCompatDelegate import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import com.cwlarson.deviceid.R import com.cwlarson.deviceid.util.DispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import org.json.JSONArray import timber.log.Timber import java.io.IOException @@ -142,7 +156,7 @@ class PreferenceManager @Inject constructor( JSONArray(items ?: "[]").toList().filter { item -> filter?.let { item.contains(it, ignoreCase = true) } ?: true } - } catch (e: Throwable) { + } catch (_: Throwable) { emptyList() } }.flowOn(dispatcherProvider.IO) diff --git a/app/src/main/java/com/cwlarson/deviceid/tabs/TabScreen.kt b/app/src/main/java/com/cwlarson/deviceid/tabs/TabScreen.kt index 4756673..77a84d3 100644 --- a/app/src/main/java/com/cwlarson/deviceid/tabs/TabScreen.kt +++ b/app/src/main/java/com/cwlarson/deviceid/tabs/TabScreen.kt @@ -3,7 +3,23 @@ package com.cwlarson.deviceid.tabs import androidx.annotation.VisibleForTesting import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -18,7 +34,11 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.runtime.* +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.graphics.ColorFilter @@ -104,7 +124,6 @@ private fun MainContent( snackbarHostState: SnackbarHostState, status: TabDataStatus, onForceRefresh: () -> Unit, onItemClick: (item: Item) -> Unit ) { - //FIXME: Bug with pullRefresh staying on screen, will be fixed with Compose 1.4.0-alpha03+ val swipeRefreshState = rememberPullRefreshState( refreshing = status is TabDataStatus.Loading, onRefresh = onForceRefresh ) diff --git a/app/src/main/java/com/cwlarson/deviceid/tabsdetail/TabDetailScreen.kt b/app/src/main/java/com/cwlarson/deviceid/tabsdetail/TabDetailScreen.kt index 4e554b0..32bbcdc 100644 --- a/app/src/main/java/com/cwlarson/deviceid/tabsdetail/TabDetailScreen.kt +++ b/app/src/main/java/com/cwlarson/deviceid/tabsdetail/TabDetailScreen.kt @@ -2,12 +2,28 @@ package com.cwlarson.deviceid.tabsdetail import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Share -import androidx.compose.material3.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -79,7 +95,7 @@ private fun ResultsScreen(item: Item) { text = item.subtitle.getSubTitleText() ?: "", style = MaterialTheme.typography.bodyMedium ) - Divider(modifier = Modifier.padding(vertical = 16.dp)) + HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) Row(modifier = Modifier.fillMaxWidth()) { TextButton( onClick = item.share(), diff --git a/app/src/main/java/com/cwlarson/deviceid/ui/util/ComposeUtils.kt b/app/src/main/java/com/cwlarson/deviceid/ui/util/ComposeUtils.kt index a3a1c3f..47621c7 100644 --- a/app/src/main/java/com/cwlarson/deviceid/ui/util/ComposeUtils.kt +++ b/app/src/main/java/com/cwlarson/deviceid/ui/util/ComposeUtils.kt @@ -1,30 +1,58 @@ package com.cwlarson.deviceid.ui.util import android.annotation.SuppressLint +import android.content.ClipData import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Android -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.* +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.cwlarson.deviceid.R import com.cwlarson.deviceid.tabs.Item import com.cwlarson.deviceid.tabs.ItemSubtitle @@ -46,7 +74,7 @@ fun AppPermission.loadPermissionLabel(context: Context = LocalContext.current): try { context.packageManager.getPermissionInfo(permissionName, 0) .loadLabel(context.packageManager) - } catch (e: Throwable) { + } catch (_: Throwable) { stringResource(id = R.string.general_error) } @@ -106,12 +134,15 @@ class IntentHandler(private val activity: ComponentActivity) : DefaultLifecycleO @Composable fun Item.copyItemToClipboard(): (() -> Unit)? { + val scope = rememberCoroutineScope() val context = LocalContext.current - val clipboardManager = LocalClipboardManager.current + val clipboardManager = LocalClipboard.current val string = stringResource(R.string.copy_to_clipboard, getFormattedString()) return subtitle.getSubTitleText()?.let { if (it.isBlank()) null else ({ - clipboardManager.setText(AnnotatedString(it)) + scope.launch { + clipboardManager.setClipEntry(ClipEntry(ClipData.newPlainText(it, it))) + } Toast.makeText(context, string, Toast.LENGTH_SHORT).show() }) } @@ -310,7 +341,7 @@ fun ListItem( .fillMaxWidth() .testTag(LIST_ITEM_TEST_TAG_PROGRESS), color = MaterialTheme.colorScheme.secondary, trackColor = Color.Transparent, - progress = chartPercentage + progress = { chartPercentage } ) if (secondaryText != null) Text( modifier = Modifier.paddingFromBaseline(top = secondaryPadding), diff --git a/app/src/main/java/com/cwlarson/deviceid/util/SystemUtils.kt b/app/src/main/java/com/cwlarson/deviceid/util/SystemUtils.kt index efa2358..511dc5d 100644 --- a/app/src/main/java/com/cwlarson/deviceid/util/SystemUtils.kt +++ b/app/src/main/java/com/cwlarson/deviceid/util/SystemUtils.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.Build -import androidx.compose.runtime.* import timber.log.Timber /** @@ -27,7 +26,7 @@ inline val Context.gmsPackageInfo: PackageInfo? packageManager.getPackageInfo(name, PackageManager.PackageInfoFlags.of(0)) else -> packageManager.getPackageInfo(name, 0) } - } catch (e: Throwable) { + } catch (_: Throwable) { null } @@ -39,6 +38,6 @@ fun Context.systemProperty(key: String): String? = try { val systemProperties = classLoader.loadClass("android.os.SystemProperties") val methodGet = systemProperties.getMethod("get", String::class.java) methodGet(systemProperties, key) as String -} catch (e: Throwable) { +} catch (_: Throwable) { null } \ No newline at end of file diff --git a/app/src/test/java/com/cwlarson/deviceid/data/AllRepositoryTest.kt b/app/src/test/java/com/cwlarson/deviceid/data/AllRepositoryTest.kt index ff82546..9d4db0e 100644 --- a/app/src/test/java/com/cwlarson/deviceid/data/AllRepositoryTest.kt +++ b/app/src/test/java/com/cwlarson/deviceid/data/AllRepositoryTest.kt @@ -49,7 +49,7 @@ class AllRepositoryTest { fun `Verify item list is from all repositories`() = runTest { every { preferencesManager.autoRefreshRateMillis } returns flowOf(0) repository.items().test { - val item = awaitItem() + val item = expectMostRecentItem() assertNotNull(item.itemFromList(R.string.device_title_android_id)) assertNotNull(item.itemFromList(R.string.network_title_phone_number)) assertNotNull(item.itemFromList(R.string.software_title_android_version)) diff --git a/app/src/test/java/com/cwlarson/deviceid/data/NetworkRepositoryTest.kt b/app/src/test/java/com/cwlarson/deviceid/data/NetworkRepositoryTest.kt index 87f918b..ab3433a 100644 --- a/app/src/test/java/com/cwlarson/deviceid/data/NetworkRepositoryTest.kt +++ b/app/src/test/java/com/cwlarson/deviceid/data/NetworkRepositoryTest.kt @@ -21,7 +21,15 @@ import com.cwlarson.deviceid.tabs.ItemSubtitle import com.cwlarson.deviceid.tabs.ItemType import com.cwlarson.deviceid.testutils.CoroutineTestRule import com.cwlarson.deviceid.testutils.awaitItemFromList -import com.cwlarson.deviceid.testutils.shadows.* +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowBluetoothAdapter +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowContextImpl +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowEuiccManager +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowSubscriptionManager +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowTelephonyManager +import com.cwlarson.deviceid.testutils.shadows.ExceptionShadowWifiInfo +import com.cwlarson.deviceid.testutils.shadows.MyShadowEuiccManager +import com.cwlarson.deviceid.testutils.shadows.MyShadowTelephonyManager +import com.cwlarson.deviceid.testutils.shadows.MyShadowWifiInfo import com.cwlarson.deviceid.util.AppPermission import com.cwlarson.deviceid.util.DispatcherProvider import io.mockk.mockk @@ -1234,26 +1242,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns text when bluetooth mac is below android M`() = runTest { - shadowOf((context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter) - .setAddress("00:00:00:00:00:00") - repository.items().test { - performWifiInfoCallback() - assertEquals( - Item( - title = R.string.network_title_bluetooth_mac, - itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.Text("00:00:00:00:00:00") - ), awaitItemFromList(R.string.network_title_bluetooth_mac) - ) - cancelAndConsumeRemainingEvents() - } - } - - @Test - @Config(sdk = [Build.VERSION_CODES.O]) - fun `Returns not possible when bluetooth mac is above android N`() = runTest { + fun `Returns not possible when bluetooth mac`() = runTest { shadowOf((context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter) .setAddress("00:00:00:00:00:00") repository.items().test { @@ -1269,44 +1258,6 @@ class NetworkRepositoryTest { } } - @Test - @Config( - sdk = [Build.VERSION_CODES.LOLLIPOP_MR1], shadows = [ExceptionShadowBluetoothAdapter::class] - ) - fun `Returns error when bluetooth mac with an exception and is below M`() = runTest { - shadowOf((context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter) - .setAddress("00:00:00:00:00:00") - repository.items().test { - performWifiInfoCallback() - assertEquals( - Item( - title = R.string.network_title_bluetooth_mac, - itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.Error - ), awaitItemFromList(R.string.network_title_bluetooth_mac) - ) - cancelAndConsumeRemainingEvents() - } - } - - @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns error when bluetooth mac with a null system service and is below M`() = - runTest { - shadowOf(context).removeSystemService(Context.BLUETOOTH_SERVICE) - repository.items().test { - performWifiInfoCallback() - assertEquals( - Item( - title = R.string.network_title_bluetooth_mac, - itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.Error - ), awaitItemFromList(R.string.network_title_bluetooth_mac) - ) - cancelAndConsumeRemainingEvents() - } - } - @Test fun `Returns text when bluetooth hostname is available with permission granted and above Android S`() = runTest { @@ -1391,6 +1342,22 @@ class NetworkRepositoryTest { } } + @Test + @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) + fun `Returns not possible when manufacturer code is above VIC`() = runTest { + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_manufacturer_code, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.NoLongerPossible(Build.VERSION_CODES.BAKLAVA) + ), awaitItemFromList(R.string.network_title_manufacturer_code) + ) + cancelAndConsumeRemainingEvents() + } + } + @Test @Config(sdk = [Build.VERSION_CODES.Q], shadows = [MyShadowTelephonyManager::class]) fun `Returns text when manufacturer code is above android P`() = runTest { @@ -1499,7 +1466,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.O]) fun `Returns not possible when nai is below android P`() = runTest { repository.items().test { performWifiInfoCallback() @@ -1621,7 +1588,7 @@ class NetworkRepositoryTest { @Test @Config(sdk = [Build.VERSION_CODES.O]) - fun `Returns text when phone count is available and is above android N`() = runTest { + fun `Returns text when phone count is available and is below R`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) .setPhoneCount(5) repository.items().test { @@ -1639,7 +1606,7 @@ class NetworkRepositoryTest { @Test @Config(sdk = [Build.VERSION_CODES.O], shadows = [ExceptionShadowTelephonyManager::class]) - fun `Returns error when phone count with exception and is above android N`() = runTest { + fun `Returns error when phone count with exception and is below R`() = runTest { repository.items().test { performWifiInfoCallback() assertEquals( @@ -1655,7 +1622,7 @@ class NetworkRepositoryTest { @Test @Config(sdk = [Build.VERSION_CODES.O]) - fun `Returns error when phone count with a null system service and is above android N`() = + fun `Returns error when phone count with a null system service and is below R`() = runTest { shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) repository.items().test { @@ -1671,22 +1638,6 @@ class NetworkRepositoryTest { } } - @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns not possible when phone count is below android M`() = runTest { - repository.items().test { - performWifiInfoCallback() - assertEquals( - Item( - title = R.string.network_title_phone_count, - itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) - ), awaitItemFromList(R.string.network_title_phone_count) - ) - cancelAndConsumeRemainingEvents() - } - } - @Test @Config(sdk = [Build.VERSION_CODES.P]) fun `Returns text when sim serial is available and is below android Q`() = runTest { @@ -4335,8 +4286,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.M]) - fun `Returns text when hearing aid supported is available and is android M+`() = runTest { + fun `Returns text when hearing aid supported is available`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) .setHearingAidCompatibilitySupported(true) repository.items().test { @@ -4353,26 +4303,8 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns not available when hearing aid supported is below android M`() = runTest { - shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) - .setHearingAidCompatibilitySupported(true) - repository.items().test { - performWifiInfoCallback() - assertEquals( - Item( - title = R.string.network_title_hearing_aid_supported, - itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) - ), awaitItemFromList(R.string.network_title_hearing_aid_supported) - ) - cancelAndConsumeRemainingEvents() - } - } - - @Test - @Config(sdk = [Build.VERSION_CODES.M], shadows = [ExceptionShadowTelephonyManager::class]) - fun `Returns error when hearing aid supported with exception and is android M+`() = + @Config(shadows = [ExceptionShadowTelephonyManager::class]) + fun `Returns error when hearing aid supported with exception`() = runTest { repository.items().test { performWifiInfoCallback() @@ -4388,8 +4320,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.M]) - fun `Returns error when hearing aid supported with a null system service and is android M+`() = + fun `Returns error when hearing aid supported with a null system service`() = runTest { shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) repository.items().test { @@ -4503,7 +4434,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1], shadows = [MyShadowTelephonyManager::class]) + @Config(sdk = [Build.VERSION_CODES.P], shadows = [MyShadowTelephonyManager::class]) fun `Returns not available when multi sim supported is below android Q`() = runTest { extract(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) .setIsMultiSimSupported(TelephonyManager.MULTISIM_ALLOWED) @@ -4576,7 +4507,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.P]) fun `Returns not possible when rtt supported is available and is below android Q`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) @@ -4629,7 +4560,8 @@ class NetworkRepositoryTest { } @Test - fun `Returns text when sms supported is available`() = runTest { + @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) + fun `Returns text when sms capable is available and is below VIC`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) .setIsSmsCapable(true) repository.items().test { @@ -4646,8 +4578,29 @@ class NetworkRepositoryTest { } @Test - @Config(shadows = [ExceptionShadowTelephonyManager::class]) - fun `Returns error when sms supported with exception`() = runTest { + @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) + fun `Returns text when sms capable is available and is Baklava+`() = runTest { + shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) + .setDeviceSmsCapable(true) + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_sms_capable, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.Text("true") + ), awaitItemFromList(R.string.network_title_sms_capable) + ) + cancelAndConsumeRemainingEvents() + } + } + + @Test + @Config( + sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE], + shadows = [ExceptionShadowTelephonyManager::class] + ) + fun `Returns error when sms capable with exception and is below VIC`() = runTest { repository.items().test { performWifiInfoCallback() assertEquals( @@ -4662,7 +4615,27 @@ class NetworkRepositoryTest { } @Test - fun `Returns error when sms supported with a null system service`() = runTest { + @Config( + sdk = [Build.VERSION_CODES.BAKLAVA], + shadows = [ExceptionShadowTelephonyManager::class] + ) + fun `Returns error when sms capable with exception and is Baklava+`() = runTest { + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_sms_capable, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.Error + ), awaitItemFromList(R.string.network_title_sms_capable) + ) + cancelAndConsumeRemainingEvents() + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) + fun `Returns error when sms capable with a null system service and is below VIC`() = runTest { shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) repository.items().test { performWifiInfoCallback() @@ -4678,8 +4651,25 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns text when voice capable is available and is android L_MR1+`() = runTest { + @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) + fun `Returns error when sms capable with a null system service and is Baklava+`() = runTest { + shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_sms_capable, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.Error + ), awaitItemFromList(R.string.network_title_sms_capable) + ) + cancelAndConsumeRemainingEvents() + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) + fun `Returns text when voice capable is available and is below VIC`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) .setVoiceCapable(true) repository.items().test { @@ -4696,18 +4686,18 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP]) - fun `Returns not possible when voice capable is available and is below android L_MR1`() = + @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) + fun `Returns not possible when voice capable is available and is Baklava+`() = runTest { shadowOf(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager) - .setVoiceCapable(true) + .setDeviceVoiceCapable(true) repository.items().test { performWifiInfoCallback() assertEquals( Item( title = R.string.network_title_voice_capable, itemType = ItemType.NETWORK, - subtitle = ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.LOLLIPOP_MR1) + subtitle = ItemSubtitle.Text("true") ), awaitItemFromList(R.string.network_title_voice_capable) ) cancelAndConsumeRemainingEvents() @@ -4716,10 +4706,10 @@ class NetworkRepositoryTest { @Test @Config( - sdk = [Build.VERSION_CODES.LOLLIPOP_MR1], + sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE], shadows = [ExceptionShadowTelephonyManager::class] ) - fun `Returns error when sms supported with exception and is android L_MR1+`() = runTest { + fun `Returns error when voice capable with exception and is below VIC`() = runTest { repository.items().test { performWifiInfoCallback() assertEquals( @@ -4734,8 +4724,45 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns error when sms supported with a null system service and is android L_MR1+`() = + @Config( + sdk = [Build.VERSION_CODES.BAKLAVA], + shadows = [ExceptionShadowTelephonyManager::class] + ) + fun `Returns error when voice capable with exception and is Baklava+`() = runTest { + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_voice_capable, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.Error + ), awaitItemFromList(R.string.network_title_voice_capable) + ) + cancelAndConsumeRemainingEvents() + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE]) + fun `Returns error when voice capable with a null system service and is below VIC`() = + runTest { + shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) + repository.items().test { + performWifiInfoCallback() + assertEquals( + Item( + title = R.string.network_title_voice_capable, + itemType = ItemType.NETWORK, + subtitle = ItemSubtitle.Error + ), awaitItemFromList(R.string.network_title_voice_capable) + ) + cancelAndConsumeRemainingEvents() + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.BAKLAVA]) + fun `Returns error when voice capable with a null system service and is Baklava+`() = runTest { shadowOf(context).removeSystemService(Context.TELEPHONY_SERVICE) repository.items().test { @@ -4770,7 +4797,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.O]) fun `Returns not possible when esim id available and is below android P`() = runTest { repository.items().test { performWifiInfoCallback() @@ -4838,7 +4865,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.O]) fun `Returns not possible when esim enabled available and is below android P`() = runTest { repository.items().test { performWifiInfoCallback() @@ -4924,7 +4951,7 @@ class NetworkRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.O]) fun `Returns not possible when esim os version available and is below android P`() = runTest { repository.items().test { diff --git a/app/src/test/java/com/cwlarson/deviceid/data/SoftwareRepositoryTest.kt b/app/src/test/java/com/cwlarson/deviceid/data/SoftwareRepositoryTest.kt index 9cfdc66..c3bfe79 100644 --- a/app/src/test/java/com/cwlarson/deviceid/data/SoftwareRepositoryTest.kt +++ b/app/src/test/java/com/cwlarson/deviceid/data/SoftwareRepositoryTest.kt @@ -36,7 +36,8 @@ import org.robolectric.shadows.ShadowBuild import org.robolectric.shadows.ShadowSystemProperties import org.robolectric.shadows.ShadowWebView import java.text.SimpleDateFormat -import java.util.* +import java.util.Calendar +import java.util.TimeZone @RunWith(AndroidJUnit4::class) class SoftwareRepositoryTest { @@ -125,7 +126,9 @@ class SoftwareRepositoryTest { assertEquals("12.1", 32.sdkToVersion()) assertEquals("13.0", 33.sdkToVersion()) assertEquals("14.0", 34.sdkToVersion()) - assertEquals("", 35.sdkToVersion()) + assertEquals("15.0", 35.sdkToVersion()) + assertEquals("16.0", 36.sdkToVersion()) + assertEquals("", 37.sdkToVersion()) } @Test @@ -162,6 +165,12 @@ class SoftwareRepositoryTest { assertEquals("", 29.getCodename()) assertEquals("", 30.getCodename()) assertEquals("", 31.getCodename()) + assertEquals("", 32.getCodename()) + assertEquals("", 33.getCodename()) + assertEquals("", 34.getCodename()) + assertEquals("", 35.getCodename()) + assertEquals("", 36.getCodename()) + assertEquals("", 37.getCodename()) } @Test @@ -216,21 +225,6 @@ class SoftwareRepositoryTest { } } - @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) - fun `Returns not possible when patch level is below android M`() = runTest { - repository.items().test { - assertEquals( - Item( - title = R.string.software_title_patch_level, - itemType = ItemType.SOFTWARE, - subtitle = ItemSubtitle.NotPossibleYet(Build.VERSION_CODES.M) - ), awaitItemFromList(R.string.software_title_patch_level) - ) - awaitComplete() - } - } - @Ignore("Not possible?") @Test fun `Returns error when patch level with an exception`() = runTest { } @@ -252,7 +246,7 @@ class SoftwareRepositoryTest { } @Test - @Config(sdk = [Build.VERSION_CODES.LOLLIPOP_MR1]) + @Config(sdk = [Build.VERSION_CODES.M]) fun `Returns not possible when preview sdk int is below android N`() = runTest { repository.items().test { assertEquals( diff --git a/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/ExceptionShadowTelephonyManager.kt b/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/ExceptionShadowTelephonyManager.kt index 273561f..27fd3c8 100644 --- a/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/ExceptionShadowTelephonyManager.kt +++ b/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/ExceptionShadowTelephonyManager.kt @@ -89,7 +89,7 @@ class ExceptionShadowTelephonyManager: ShadowTelephonyManager() { } @Implementation - fun isDataRoamingEnabled(): Boolean { + override fun isDataRoamingEnabled(): Boolean { throw NullPointerException() } @@ -117,4 +117,14 @@ class ExceptionShadowTelephonyManager: ShadowTelephonyManager() { override fun isVoiceCapable(): Boolean { throw NullPointerException() } + + @Implementation + override fun isDeviceSmsCapable(): Boolean { + throw NullPointerException() + } + + @Implementation + override fun isDeviceVoiceCapable(): Boolean { + throw NullPointerException() + } } \ No newline at end of file diff --git a/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/MyShadowTelephonyManager.kt b/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/MyShadowTelephonyManager.kt index 5001a5e..1ec3ec6 100644 --- a/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/MyShadowTelephonyManager.kt +++ b/app/src/test/java/com/cwlarson/deviceid/testutils/shadows/MyShadowTelephonyManager.kt @@ -33,7 +33,7 @@ class MyShadowTelephonyManager : ShadowTelephonyManager() { } @Implementation - fun isDataRoamingEnabled() = isDataRoamingEnabled + override fun isDataRoamingEnabled() = isDataRoamingEnabled fun setIsMultiSimSupported(value: Int) { isMultiSimSupported = value diff --git a/app/src/test/java/com/cwlarson/deviceid/util/ComposeUtilsTest.kt b/app/src/test/java/com/cwlarson/deviceid/util/ComposeUtilsTest.kt index 2d47556..25a1963 100644 --- a/app/src/test/java/com/cwlarson/deviceid/util/ComposeUtilsTest.kt +++ b/app/src/test/java/com/cwlarson/deviceid/util/ComposeUtilsTest.kt @@ -14,7 +14,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText @@ -27,10 +27,26 @@ import com.cwlarson.deviceid.tabs.Item import com.cwlarson.deviceid.tabs.ItemSubtitle import com.cwlarson.deviceid.tabs.ItemType import com.cwlarson.deviceid.testutils.CoroutineTestRule -import com.cwlarson.deviceid.ui.util.* -import io.mockk.* +import com.cwlarson.deviceid.ui.util.IntentHandler +import com.cwlarson.deviceid.ui.util.click +import com.cwlarson.deviceid.ui.util.copyItemToClipboard +import com.cwlarson.deviceid.ui.util.loadPermissionLabel +import com.cwlarson.deviceid.ui.util.share +import io.mockk.Called +import io.mockk.EqMatcher +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.mockkStatic +import io.mockk.spyk +import io.mockk.verify import kotlinx.coroutines.test.runTest -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -157,10 +173,10 @@ class ComposeUtilsTest { @Test fun test_copyItemToClipboard_click_null() { composeTestRule.setContent { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = LocalClipboard.current Item(R.string.app_name, ItemType.DEVICE, ItemSubtitle.Error) .copyItemToClipboard()?.invoke() - assertNull(clipboardManager.getText()?.text) + runTest { assertNull(clipboardManager.getClipEntry()) } } assertTrue(ShadowToast.shownToastCount() == 0) } @@ -168,10 +184,10 @@ class ComposeUtilsTest { @Test fun test_copyItemToClipboard_click_blank() { composeTestRule.setContent { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = LocalClipboard.current Item(R.string.app_name, ItemType.DEVICE, ItemSubtitle.Text("")) .copyItemToClipboard()?.invoke() - assertNull(clipboardManager.getText()?.text) + runTest { assertNull(clipboardManager.getClipEntry()) } } assertTrue(ShadowToast.shownToastCount() == 0) } @@ -179,10 +195,10 @@ class ComposeUtilsTest { @Test fun test_copyItemToClipboard_click_nonnull() { composeTestRule.setContent { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = LocalClipboard.current Item(R.string.app_name, ItemType.DEVICE, ItemSubtitle.Text("Name")) .copyItemToClipboard()?.invoke() - assertEquals("Name", clipboardManager.getText()?.text) + runTest { assertEquals("Name", clipboardManager.getClipEntry()?.clipData?.getItemAt(0)?.text) } } assertTrue(ShadowToast.showedToast("Copied Device Info to clipboard!")) } @@ -381,7 +397,6 @@ class ComposeUtilsTest { verify { clickedDetails(item) } } - @Suppress("TestFunctionName") @Composable private fun ComposableUnderTest( item: Item, clickedRefresh: (() -> Unit), clickedDetails: ((Item) -> Unit) diff --git a/build.gradle.kts b/build.gradle.kts index 01dca67..f5ce9de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,16 @@ plugins { - id("com.google.devtools.ksp") version "1.9.20-1.0.14" apply false + alias(libs.plugins.ksp) apply false } buildscript { - val hiltVersion by rootProject.extra { "2.49" } repositories { google() mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.2.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") - classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion") + classpath(libs.gradle) + classpath(libs.kotlin.gradle.plugin) + classpath(libs.hilt.android.gradle.plugin) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -25,5 +24,5 @@ allprojects { } tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) + delete(layout.buildDirectory) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..a41c6fe --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,92 @@ +[versions] +accompanist = "0.37.3" +activityCompose = "1.10.1" +appUpdateKtx = "2.1.0" +composeBom = "2025.08.00" +coreKtx = "1.17.0" +coreKtxVersion = "1.7.0" +coreSplashscreen = "1.0.1" +datastore = "1.1.7" +espressoCore = "3.7.0" +gradle = "8.12.1" +hiltNavigationCompose = "1.2.0" +hiltVersion = "2.57.1" +junit = "4.13.2" +junitKtx = "1.3.0" +kotlin = "2.2.10" +kotlinx = "1.10.2" +ksp = "2.2.10-2.0.2" +leakcanaryAndroid = "2.14" +lifecycle = "2.9.2" +material = "1.12.0" # Needed until MainActivity inherits from ComponentActivity instead +mockk = "1.14.5" +navigationCompose = "2.9.3" +robolectric = "4.16-beta-1" +rules = "1.7.0" +runner = "1.7.0" +timber = "5.0.1" +turbine = "1.2.1" +uiautomator = "2.3.0" +webkit = "1.14.0" + +[libraries] +accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } +androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoCore" } +androidx-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } +androidx-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" } +androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" } +androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } +androidx-material = { module = "androidx.compose.material:material" } +androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } +androidx-material3 = { module = "androidx.compose.material3:material3" } +androidx-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } +androidx-rules = { module = "androidx.test:rules", version.ref = "rules" } +androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } +androidx-ui = { module = "androidx.compose.ui:ui" } +androidx-ui-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts" } +androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } +app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } +core-ktx = { module = "androidx.test:core-ktx", version.ref = "coreKtxVersion" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltVersion" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltVersion" } +hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hiltVersion" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hiltVersion" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltVersion" } +jetbrains-kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" } +material = { module = "com.google.android.material:material", version.ref = "material" } +mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockk" } +mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } + + +[plugins] +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 289c4f8..d63af2a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Mar 17 10:24:06 CDT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME