diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e795263..9fc8edf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -35,15 +35,28 @@ android {
buildTypes {
debug {
+ isDebuggable = true
enableUnitTestCoverage = true
enableAndroidTestCoverage = true
+ // versionNameSuffix = "-release" // Optional: Adds a suffix to the version name for differentiation
+ buildConfigField("Boolean", "IS_DEVELOPER_MODE_ENABLED", "true")
+ }
+ create("benchmark") {
+ initWith(buildTypes.getByName("release"))
+ signingConfig = signingConfigs.getByName("debug")
+ matchingFallbacks += listOf("release")
}
release {
- isMinifyEnabled = false
+ isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
+ // Configures the signing with the debug key for now so that we can run the release build
+ signingConfig = signingConfigs.getByName("debug")
+ isDebuggable = false // Disables debugging for security reasons
+ isShrinkResources = true // Further reduces the size of the APK by removing unused resources
+ buildConfigField("Boolean", "IS_DEVELOPER_MODE_ENABLED", "false")
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6c22f6d..f334dd8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/app/books/tanga/TangaApp.kt b/app/src/main/java/app/books/tanga/TangaApp.kt
index fe820a5..7afbafc 100644
--- a/app/src/main/java/app/books/tanga/TangaApp.kt
+++ b/app/src/main/java/app/books/tanga/TangaApp.kt
@@ -1,6 +1,7 @@
package app.books.tanga
import android.app.Application
+import android.os.StrictMode
import app.books.tanga.di.TimberTrees
import app.books.tanga.di.plantAll
import app.books.tanga.errors.TangaErrorTracker
@@ -22,5 +23,30 @@ class TangaApp : Application() {
// Initialize the error tracker
errorTracker.init()
+
+ configureStrictMode()
+ }
+
+ private fun configureStrictMode() {
+ if (BuildConfig.DEBUG) {
+ StrictMode.setThreadPolicy(
+ StrictMode.ThreadPolicy.Builder()
+ .detectDiskReads()
+ .detectDiskWrites()
+ .detectNetwork()
+ .penaltyLog()
+ .penaltyFlashScreen()
+ .build()
+ )
+
+ StrictMode.setVmPolicy(
+ StrictMode.VmPolicy.Builder()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .detectActivityLeaks()
+ .penaltyLog()
+ .build()
+ )
+ }
}
}
diff --git a/app/src/main/java/app/books/tanga/common/urls/DownloadUrlGenerator.kt b/app/src/main/java/app/books/tanga/common/urls/DownloadUrlGenerator.kt
index b9d8ad8..6ab0916 100644
--- a/app/src/main/java/app/books/tanga/common/urls/DownloadUrlGenerator.kt
+++ b/app/src/main/java/app/books/tanga/common/urls/DownloadUrlGenerator.kt
@@ -1,5 +1,6 @@
package app.books.tanga.common.urls
+import app.books.tanga.di.IoDispatcher
import app.books.tanga.entity.SummaryId
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.FirebaseStorage
@@ -11,7 +12,10 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.withContext
import timber.log.Timber
/**
@@ -35,7 +39,8 @@ interface DownloadUrlGenerator {
* using Firebase Storage.
*/
class StorageDownloadUrlGenerator @Inject constructor(
- private val storage: FirebaseStorage
+ private val storage: FirebaseStorage,
+ @IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : DownloadUrlGenerator {
/**
@@ -44,10 +49,12 @@ class StorageDownloadUrlGenerator @Inject constructor(
*/
override suspend fun generateCoverDownloadUrl(
summaryId: SummaryId
- ): String? = SummaryCoverUrlCache.get(summaryId) ?: getSummaryReference(summaryId).generateDownloadUrl(
- path = SummaryFormatType.COVER.filename
- ).also { url ->
- url?.let { SummaryCoverUrlCache.put(summaryId, it) }
+ ): String? = withContext(ioDispatcher) {
+ SummaryCoverUrlCache.get(summaryId) ?: getSummaryReference(summaryId).generateDownloadUrl(
+ path = SummaryFormatType.COVER.filename
+ ).also { url ->
+ url?.let { SummaryCoverUrlCache.put(summaryId, it) }
+ }
}
override suspend fun generateTextDownloadUrl(summaryId: SummaryId): String? = getSummaryReference(
@@ -86,7 +93,7 @@ class StorageDownloadUrlGenerator @Inject constructor(
* Singleton instance of [StorageDownloadUrlGenerator] for convenience in places where when can't use Hilt DI.
*/
val instance: StorageDownloadUrlGenerator
- get() = StorageDownloadUrlGenerator(storage = Firebase.storage)
+ get() = StorageDownloadUrlGenerator(storage = Firebase.storage, ioDispatcher = Dispatchers.IO)
}
}
diff --git a/app/src/main/java/app/books/tanga/di/GoogleSignInModule.kt b/app/src/main/java/app/books/tanga/di/GoogleSignInModule.kt
index 3f0797d..335a5cb 100644
--- a/app/src/main/java/app/books/tanga/di/GoogleSignInModule.kt
+++ b/app/src/main/java/app/books/tanga/di/GoogleSignInModule.kt
@@ -38,7 +38,7 @@ class GoogleSignInModule {
.builder()
.setSupported(true)
.setServerClientId(clientId)
- .setFilterByAuthorizedAccounts(true)
+ .setFilterByAuthorizedAccounts(false)
.build()
).setAutoSelectEnabled(true)
.build()
@@ -55,7 +55,7 @@ class GoogleSignInModule {
.builder()
.setSupported(true)
.setServerClientId(clientId)
- .setFilterByAuthorizedAccounts(true)
+ .setFilterByAuthorizedAccounts(false)
.build()
).build()
diff --git a/app/src/main/java/app/books/tanga/feature/auth/AnonymousAuthService.kt b/app/src/main/java/app/books/tanga/feature/auth/AnonymousAuthService.kt
index 9d928a0..784104c 100644
--- a/app/src/main/java/app/books/tanga/feature/auth/AnonymousAuthService.kt
+++ b/app/src/main/java/app/books/tanga/feature/auth/AnonymousAuthService.kt
@@ -29,7 +29,7 @@ class AnonymousAuthServiceImpl @Inject constructor(
@Suppress("TooGenericExceptionThrown")
override suspend fun signInAnonymously(): AuthResult {
val result = auth.signInAnonymously().await()
- val firebaseUser = result.user ?: throw Throwable("Failed to sign in anonymously")
+ val firebaseUser = result.user ?: throw Throwable("Anonymous sign in returned null user")
val user = firebaseUser.toAnonymousUser()
diff --git a/app/src/main/java/app/books/tanga/feature/auth/AuthenticationInteractor.kt b/app/src/main/java/app/books/tanga/feature/auth/AuthenticationInteractor.kt
index 97908e6..83a25f7 100644
--- a/app/src/main/java/app/books/tanga/feature/auth/AuthenticationInteractor.kt
+++ b/app/src/main/java/app/books/tanga/feature/auth/AuthenticationInteractor.kt
@@ -33,7 +33,7 @@ class AuthenticationInteractor @Inject constructor(
val authResult = anonymousAuthService.signInAnonymously()
authResult.user
}.onFailure {
- Timber.e("Anonymous sign in failed", it)
+ Timber.e(it, "Anonymous sign in failed")
return Result.failure(DomainError.AuthenticationError(it))
}
@@ -65,6 +65,7 @@ class AuthenticationInteractor @Inject constructor(
sessionManager.openSession(sessionId)
user
}.onFailure {
+ Timber.e(it, "Complete Google sign in failed")
return Result.failure(DomainError.UnableToSignInWithGoogleError(it))
}
diff --git a/app/src/main/java/app/books/tanga/feature/onboarding/OnboardingScreen.kt b/app/src/main/java/app/books/tanga/feature/onboarding/OnboardingScreen.kt
index 59fb445..16bdfeb 100644
--- a/app/src/main/java/app/books/tanga/feature/onboarding/OnboardingScreen.kt
+++ b/app/src/main/java/app/books/tanga/feature/onboarding/OnboardingScreen.kt
@@ -13,6 +13,7 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -57,6 +58,15 @@ fun OnboardingScreen(
val pagerState = rememberPagerState()
val scope = rememberCoroutineScope()
+ val onNextClick: () -> Unit = remember {
+ {
+ scope.launch {
+ if (pagerState.currentPage < MAX_PAGER_INDEX) {
+ pagerState.animateScrollToPage(pagerState.currentPage + 1)
+ }
+ }
+ }
+ }
Column(
modifier = modifier
@@ -85,11 +95,7 @@ fun OnboardingScreen(
FinishOnboardingButton(
modifier = Modifier.weight(1f),
pagerState = pagerState,
- onNextClick = {
- scope.launch {
- pagerState.animateScrollToPage(pagerState.currentPage.inc())
- }
- },
+ onNextClick = onNextClick,
onFinishClick = {
onboardingViewModel.onOnboardingCompleted()
navController.popBackStack()
diff --git a/app/src/test/java/app/books/tanga/StorageDownloadUrlGeneratorTest.kt b/app/src/test/java/app/books/tanga/StorageDownloadUrlGeneratorTest.kt
index a493d45..cba4c20 100644
--- a/app/src/test/java/app/books/tanga/StorageDownloadUrlGeneratorTest.kt
+++ b/app/src/test/java/app/books/tanga/StorageDownloadUrlGeneratorTest.kt
@@ -12,13 +12,16 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.tasks.await
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+@OptIn(ExperimentalCoroutinesApi::class)
class StorageDownloadUrlGeneratorTest {
private lateinit var generator: StorageDownloadUrlGenerator
@@ -28,6 +31,8 @@ class StorageDownloadUrlGeneratorTest {
private val mockkStorageReferenceSummary = mockk()
private val mockkStorageReferenceSummaryCover = mockk()
+ private val testDispatcher = UnconfinedTestDispatcher()
+
@BeforeEach
fun setUp() {
mockkStatic("kotlinx.coroutines.tasks.TasksKt")
@@ -37,7 +42,7 @@ class StorageDownloadUrlGeneratorTest {
every { mockkStorageReferenceSummary.child(any()) } returns mockkStorageReferenceSummaryCover
coEvery { mockkStorageReferenceSummaryCover.downloadUrl.await().toString() } returns mockUrl
- generator = StorageDownloadUrlGenerator(storage = mockStorage)
+ generator = StorageDownloadUrlGenerator(storage = mockStorage, testDispatcher)
}
@Test
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
new file mode 100644
index 0000000..9d65744
--- /dev/null
+++ b/benchmark/build.gradle.kts
@@ -0,0 +1,52 @@
+plugins {
+ alias(libs.plugins.androidTest)
+ alias(libs.plugins.kotlin.android)
+}
+
+android {
+ namespace = "app.books.tanga.benchmark"
+ compileSdk = 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ defaultConfig {
+ minSdk = 24
+ targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ // This benchmark buildType is used for benchmarking, and should function like your
+ // release build (for example, with minification on). It"s signed with a debug key
+ // for easy local/CI testing.
+ create("benchmark") {
+ isDebuggable = false
+ signingConfig = getByName("debug").signingConfig
+ matchingFallbacks += listOf("release")
+ }
+ }
+
+ targetProjectPath = ":app"
+ experimentalProperties["android.experimental.self-instrumenting"] = true
+}
+
+dependencies {
+ implementation(libs.android.test.junit)
+ implementation(libs.android.espresso.core)
+ implementation(libs.uiautomator)
+ implementation(libs.benchmark.macro.junit4)
+}
+
+androidComponents {
+ beforeVariants(selector().all()) {
+ it.enable = it.buildType == "benchmark"
+ }
+}
diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..227314e
--- /dev/null
+++ b/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/benchmark/src/main/java/app/books/tanga/benchmark/ExampleStartupBenchmark.kt b/benchmark/src/main/java/app/books/tanga/benchmark/ExampleStartupBenchmark.kt
new file mode 100644
index 0000000..a4ad103
--- /dev/null
+++ b/benchmark/src/main/java/app/books/tanga/benchmark/ExampleStartupBenchmark.kt
@@ -0,0 +1,38 @@
+package app.books.tanga.benchmark
+
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This is an example startup benchmark.
+ *
+ * It navigates to the device's home screen, and launches the default activity.
+ *
+ * Before running this benchmark:
+ * 1) switch your app's active build variant in the Studio (affects Studio runs only)
+ * 2) add `` to your app's manifest, within the `` tag
+ *
+ * Run this benchmark from Studio to see startup measurements, and captured system traces
+ * for investigating your app's performance.
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleStartupBenchmark {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureRepeated(
+ packageName = "app.books.tanga",
+ metrics = listOf(StartupTimingMetric()),
+ iterations = 5,
+ startupMode = StartupMode.COLD
+ ) {
+ pressHome()
+ startActivityAndWait()
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 14fc373..ca33060 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -24,6 +24,7 @@ plugins {
alias(libs.plugins.detekt) apply false
alias(libs.plugins.junit5) apply false
alias(libs.plugins.kover) apply false
+ alias(libs.plugins.androidTest) apply false
}
subprojects {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 2c5ebac..0b2d110 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -40,7 +40,7 @@ hilt-version = "2.47"
# Firebase and Data
# https://firebase.google.com/support/release-notes/android
-firebase-bom-version = "31.2.0"
+firebase-bom-version = "32.7.0"
# https://developer.android.com/jetpack/androidx/releases/datastore
datastore-version = "1.0.0"
@@ -67,6 +67,11 @@ mockk-version = "1.13.8"
mockito-android-version = "5.7.0"
mockito-kotlin-version = "5.1.0"
+# Performance Testing
+uiautomator = "2.2.0"
+benchmark-macro-junit4 = "1.2.2"
+
+
# Google Services
# google-services-version = "4.3.15"
@@ -167,6 +172,8 @@ android-test-junit = { module = "androidx.test.ext:junit", version.ref = "androi
android-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-version" }
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-version" }
navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navigation-testing-version" }
+uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "uiautomator" }
+benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "benchmark-macro-junit4" }
# Lint Checks
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint-api" }
@@ -189,3 +196,4 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-version"
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt-version" }
junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "junit5-version" }
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover-version" }
+androidTest = { id = "com.android.test", version.ref = "android-plugin-gradle-version" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b6f5b7e..39329b4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,3 +17,4 @@ rootProject.name = "Tanga"
include(":app")
include(":core-ui")
include(":code-checks")
+include(":benchmark")