diff --git a/.github/workflows/build_app.yml b/.github/workflows/build_app.yml index 01cfa3dd8..e60fbd105 100644 --- a/.github/workflows/build_app.yml +++ b/.github/workflows/build_app.yml @@ -41,4 +41,4 @@ jobs: # Run Build Project - name: Build gradle project - run: ./gradlew build + run: ./gradlew assembleFossRelease assemblePlayRelease diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2bfe229c..e5c9bab1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,45 +37,20 @@ jobs: echo "${{ secrets.RELEASE_KEYSTORE_BASE64 }}" | base64 --decode > app/kahani-release.jks - name: Build Release APK - run: ./gradlew assembleRelease + run: ./gradlew assemblePlayRelease env: RELEASE_STORE_FILE: kahani-release.jks RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_PASSWORD }} RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} - - name: Build Changelog - id: build_changelog - uses: mikepenz/release-changelog-builder-action@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - configuration: | - { - "categories": [ - { - "title": "## 🚀 Features", - "labels": ["feature", "enhancement"] - }, - { - "title": "## 🐛 Bug Fixes", - "labels": ["fix", "bug"] - }, - { - "title": "## 📦 Dependencies & Maintenance", - "labels": ["chore", "deps", "dependency", "refactor"] - } - ], - "template": "{{CHANGELOG}}" - } - - name: Create Release uses: softprops/action-gh-release@v2 with: - files: app/build/outputs/apk/release/app-release.apk + files: app/build/outputs/apk/play/release/app-play-release.apk tag_name: v${{ steps.get_version.outputs.version }} name: Kahani v${{ steps.get_version.outputs.version }} - body: ${{ steps.build_changelog.outputs.changelog }} + body: "Update the Changelog" draft: false prerelease: false env: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8fc8607d9..8838aad5b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,8 +62,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - buildConfigField("String", "CLARITY_PROJECT_ID", "\"vc8bgk8nk9\"") - signingConfigs { create("release") { val envKeyStore = System.getenv("RELEASE_STORE_FILE") @@ -85,6 +83,23 @@ android { } } + flavorDimensions += "distribution" + + productFlavors { + create("foss") { + dimension = "distribution" + applicationIdSuffix = ".foss" + buildConfigField("String", "APP_NAME_SUFFIX", "\" (FOSS)\"") + buildConfigField("String", "DISTRIBUTION", "\"foss\"") + } + create("play") { + dimension = "distribution" + buildConfigField("String", "APP_NAME_SUFFIX", "\"\"") + buildConfigField("String", "DISTRIBUTION", "\"play\"") + buildConfigField("String", "CLARITY_PROJECT_ID", "\"vc8bgk8nk9\"") + } + } + buildTypes { release { val releaseSigningConfig = signingConfigs.getByName("release") @@ -127,6 +142,15 @@ android { } +// Disable Google Services and Crashlytics for FOSS flavor tasks +tasks.configureEach { + if (name.contains("Foss", ignoreCase = true)) { + if (name.contains("GoogleServices") || name.contains("Crashlytics") || name.contains("UploadCrashlyticsMappingFile")) { + enabled = false + } + } +} + dependencies { implementation(project(":lib")) @@ -188,11 +212,11 @@ dependencies { implementation(libs.moshi) implementation(libs.moshi.kotlin) - implementation(libs.microsoft.clarity) + "playImplementation"(libs.microsoft.clarity) - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.crashlytics) - implementation(libs.firebase.analytics) + "playImplementation"(platform(libs.firebase.bom)) + "playImplementation"(libs.firebase.crashlytics) + "playImplementation"(libs.firebase.analytics) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) diff --git a/app/src/main/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt b/app/src/foss/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt similarity index 100% rename from app/src/main/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt rename to app/src/foss/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt diff --git a/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt b/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt new file mode 100644 index 000000000..e372d910f --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt @@ -0,0 +1,14 @@ +package org.grakovne.lissen.analytics + +import org.grakovne.lissen.common.RunningComponent +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClarityComponent + @Inject + constructor() : RunningComponent { + override fun onCreate() {} + + fun updateConsent(accepted: Boolean) {} + } diff --git a/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt b/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt new file mode 100644 index 000000000..64fd22820 --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt @@ -0,0 +1,18 @@ +package org.grakovne.lissen.analytics + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ClarityTracker + @Inject + constructor() { + fun setUser(userId: String) {} + + fun trackEvent(eventName: String) {} + + fun trackEvent( + eventName: String, + value: String, + ) {} + } diff --git a/app/src/foss/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt b/app/src/foss/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt new file mode 100644 index 000000000..1c867c696 --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt @@ -0,0 +1,5 @@ +package org.grakovne.lissen.common + +import androidx.compose.ui.Modifier + +fun Modifier.maskForAnalytics(): Modifier = this diff --git a/app/src/foss/kotlin/org/grakovne/lissen/common/CrashHandler.kt b/app/src/foss/kotlin/org/grakovne/lissen/common/CrashHandler.kt new file mode 100644 index 000000000..987ff5fc7 --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/common/CrashHandler.kt @@ -0,0 +1,45 @@ +package org.grakovne.lissen.common + +import android.content.Context +import android.os.Looper +import android.widget.Toast +import org.grakovne.lissen.R + +class CrashHandler( + context: Context, + private val defaultHandler: Thread.UncaughtExceptionHandler?, +) : Thread.UncaughtExceptionHandler { + private val context = context.applicationContext + + override fun uncaughtException( + thread: Thread, + ex: Throwable, + ) { + try { + object : Thread() { + override fun run() { + Looper.prepare() + Toast.makeText(context, R.string.app_crash_toast, Toast.LENGTH_LONG).show() + + // Schedule the looper to quit so the thread doesn't block indefinitely + android.os.Handler(Looper.myLooper()!!).postDelayed( + { Looper.myLooper()?.quit() }, + TOAST_TIMEOUT + 500, + ) + + Looper.loop() + } + }.start() + + Thread.sleep(TOAST_TIMEOUT) + } catch (e: Exception) { + // Ignore errors in the crash handler itself + } finally { + defaultHandler?.uncaughtException(thread, ex) + } + } + + companion object { + private const val TOAST_TIMEOUT = 1000L + } +} diff --git a/app/src/foss/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt b/app/src/foss/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt new file mode 100644 index 000000000..1f908118c --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt @@ -0,0 +1,13 @@ +package org.grakovne.lissen.common + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface CrashReporterModule { + @Binds + fun bindCrashReporter(reporter: NoOpCrashReporter): CrashReporter +} diff --git a/app/src/foss/kotlin/org/grakovne/lissen/common/NoOpCrashReporter.kt b/app/src/foss/kotlin/org/grakovne/lissen/common/NoOpCrashReporter.kt new file mode 100644 index 000000000..8fae2bb3b --- /dev/null +++ b/app/src/foss/kotlin/org/grakovne/lissen/common/NoOpCrashReporter.kt @@ -0,0 +1,18 @@ +package org.grakovne.lissen.common + +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NoOpCrashReporter + @Inject + constructor() : CrashReporter { + override fun setCollectionEnabled(enabled: Boolean) { + // Do nothing in FOSS + } + + override fun recordException(ex: Throwable) { + Timber.e(ex, "Crash reported (FOSS mode - no remote reporting)") + } + } diff --git a/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt b/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt index 149c8c266..6f03a241c 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/LissenApplication.kt @@ -15,6 +15,9 @@ class LissenApplication : Application() { @Inject lateinit var runningComponents: Set<@JvmSuppressWildcards RunningComponent> + @Inject + lateinit var crashReporter: org.grakovne.lissen.common.CrashReporter + override fun onCreate() { super.onCreate() appContext = applicationContext @@ -26,9 +29,7 @@ class LissenApplication : Application() { } val isCrashReportingEnabled = preferences.getCrashReportingEnabled() - com.google.firebase.crashlytics.FirebaseCrashlytics - .getInstance() - .setCrashlyticsCollectionEnabled(isCrashReportingEnabled) + crashReporter.setCollectionEnabled(isCrashReportingEnabled) val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() @@ -47,9 +48,7 @@ class LissenApplication : Application() { it.onCreate() } catch (ex: Exception) { Timber.e(ex, "Unable to register Running component: ${ex.message}") - com.google.firebase.crashlytics.FirebaseCrashlytics - .getInstance() - .recordException(ex) + crashReporter.recordException(ex) } } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/common/CrashReporter.kt b/app/src/main/kotlin/org/grakovne/lissen/common/CrashReporter.kt new file mode 100644 index 000000000..7c8c3e06c --- /dev/null +++ b/app/src/main/kotlin/org/grakovne/lissen/common/CrashReporter.kt @@ -0,0 +1,7 @@ +package org.grakovne.lissen.common + +interface CrashReporter { + fun setCollectionEnabled(enabled: Boolean) + + fun recordException(ex: Throwable) +} diff --git a/app/src/main/kotlin/org/grakovne/lissen/content/AuthRepository.kt b/app/src/main/kotlin/org/grakovne/lissen/content/AuthRepository.kt index db56421c7..7d79a6460 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/content/AuthRepository.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/content/AuthRepository.kt @@ -66,7 +66,7 @@ class AuthRepository ) clarityTracker.trackEvent("login_success") } catch (e: Exception) { - Timber.e(e, "Failed to send login analytics to Clarity") + Timber.e(e, "Failed to send login analytics") } // Trigger library fetch diff --git a/app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/ContentCachingManager.kt b/app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/ContentCachingManager.kt index 2c992dbd9..c884a7179 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/ContentCachingManager.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/content/cache/persistent/ContentCachingManager.kt @@ -1,8 +1,6 @@ package org.grakovne.lissen.content.cache.persistent import android.content.Context -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -13,6 +11,7 @@ import org.grakovne.lissen.analytics.ClarityTracker import org.grakovne.lissen.channel.audiobookshelf.common.api.RequestHeadersProvider import org.grakovne.lissen.channel.common.MediaChannel import org.grakovne.lissen.channel.common.createOkHttpClient +import org.grakovne.lissen.common.CrashReporter import org.grakovne.lissen.content.cache.common.findRelatedFiles import org.grakovne.lissen.content.cache.common.findRelatedFilesByStartTimes import org.grakovne.lissen.content.cache.common.withBlur @@ -43,6 +42,7 @@ class ContentCachingManager private val preferences: LissenSharedPreferences, private val clarityTracker: ClarityTracker, private val localCacheRepository: LocalCacheRepository, + private val crashReporter: CrashReporter, ) { fun cacheMediaItem( mediaItem: DetailedItem, @@ -270,7 +270,7 @@ class ContentCachingManager } } } catch (ex: Exception) { - Firebase.crashlytics.recordException(ex) + crashReporter.recordException(ex) return@withContext CacheState(CacheStatus.Error) } } @@ -300,7 +300,7 @@ class ContentCachingManager .withBlur(context, width = 300) // Trigger thumbnail transformation .writeToFile(thumbFile) } catch (ex: Exception) { - Firebase.crashlytics.recordException(ex) + crashReporter.recordException(ex) return@fold CacheState(CacheStatus.Error) } }, diff --git a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt index b9fd884d4..7eb063b06 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/persistence/preferences/LissenSharedPreferences.kt @@ -6,8 +6,6 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import androidx.core.content.edit -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase import com.squareup.moshi.Types import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose @@ -16,6 +14,7 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import org.grakovne.lissen.channel.common.ChannelCode import org.grakovne.lissen.common.ColorScheme +import org.grakovne.lissen.common.CrashReporter import org.grakovne.lissen.common.LibraryOrderingConfiguration import org.grakovne.lissen.common.NetworkTypeAutoCache import org.grakovne.lissen.common.PlaybackVolumeBoost @@ -48,6 +47,7 @@ class LissenSharedPreferences @Inject constructor( @ApplicationContext context: Context, + private val crashReporter: CrashReporter, ) { private val sharedPreferences: SharedPreferences = context.getSharedPreferences("secure_prefs", Context.MODE_PRIVATE) @@ -79,20 +79,6 @@ class LissenSharedPreferences remove(KEY_TOKEN) remove(KEY_ACCESS_TOKEN) remove(KEY_REFRESH_TOKEN) - - remove(KEY_SERVER_VERSION) - - remove(CACHE_FORCE_ENABLED) - - remove(KEY_PREFERRED_LIBRARY_ID) - remove(KEY_PREFERRED_LIBRARY_NAME) - remove(KEY_PREFERRED_LIBRARY_TYPE) - - remove(KEY_CUSTOM_HEADERS) - remove(KEY_BYPASS_SSL) - remove(KEY_LOCAL_URLS) - - remove(KEY_PLAYING_BOOK) } } @@ -650,6 +636,22 @@ class LissenSharedPreferences awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } }.distinctUntilChanged() + private fun decrypt(data: String): String? = + try { + val decodedData = Base64.decode(data, Base64.DEFAULT) + val iv = decodedData.sliceArray(0 until 12) + val cipherText = decodedData.sliceArray(12 until decodedData.size) + + val cipher = Cipher.getInstance(TRANSFORMATION) + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) + + String(cipher.doFinal(cipherText)) + } catch (ex: Exception) { + crashReporter.recordException(ex) + null + } + companion object { private const val KEY_ALIAS = "secure_key_alias" private const val KEY_HOST = "host" @@ -729,22 +731,5 @@ class LissenSharedPreferences return Base64.encodeToString(ivAndCipherText, Base64.DEFAULT) } - - private fun decrypt(data: String): String? { - val decodedData = Base64.decode(data, Base64.DEFAULT) - val iv = decodedData.sliceArray(0 until 12) - val cipherText = decodedData.sliceArray(12 until decodedData.size) - - val cipher = Cipher.getInstance(TRANSFORMATION) - val spec = GCMParameterSpec(128, iv) - cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) - - return try { - String(cipher.doFinal(cipherText)) - } catch (ex: Exception) { - Firebase.crashlytics.recordException(ex) - null - } - } } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/playback/MediaRepository.kt b/app/src/main/kotlin/org/grakovne/lissen/playback/MediaRepository.kt index a21a24cd3..91097d7eb 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/playback/MediaRepository.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/playback/MediaRepository.kt @@ -22,8 +22,6 @@ import androidx.media3.session.SessionToken import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.MoreExecutors -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -34,6 +32,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.grakovne.lissen.analytics.ClarityTracker +import org.grakovne.lissen.common.CrashReporter import org.grakovne.lissen.content.LissenMediaProvider import org.grakovne.lissen.lib.domain.CurrentEpisodeTimerOption import org.grakovne.lissen.lib.domain.DetailedItem @@ -68,6 +67,7 @@ class MediaRepository private val mediaChannel: LissenMediaProvider, private val shakeDetector: ShakeDetector, private val clarityTracker: ClarityTracker, + private val crashReporter: CrashReporter, ) { fun getBookFlow(bookId: String): Flow = mediaChannel.fetchBookFlow(bookId) @@ -186,7 +186,7 @@ class MediaRepository override fun onFailure(t: Throwable) { Timber.e("Unable to add callback to player") - Firebase.crashlytics.recordException(t) + crashReporter.recordException(t) clarityTracker.trackEvent("playback_error", t.message ?: "Unknown error") } }, @@ -320,7 +320,7 @@ class MediaRepository toneGen.release() }, 200) } catch (e: Exception) { - Firebase.crashlytics.recordException(e) + crashReporter.recordException(e) Timber.e(e, "Failed to play reset sound") } } @@ -360,7 +360,7 @@ class MediaRepository seekTo(chapterStartsAt) } catch (ex: Exception) { - Firebase.crashlytics.recordException(ex) + crashReporter.recordException(ex) return } } @@ -389,7 +389,7 @@ class MediaRepository seekTo(absolutePosition) } catch (ex: Exception) { - Firebase.crashlytics.recordException(ex) + crashReporter.recordException(ex) return } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/components/VerticalScrollbar.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/components/VerticalScrollbar.kt index 49465019d..382e7fbab 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/components/VerticalScrollbar.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/components/VerticalScrollbar.kt @@ -44,8 +44,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.google.firebase.crashlytics.ktx.crashlytics -import com.google.firebase.ktx.Firebase import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import timber.log.Timber @@ -97,7 +95,6 @@ fun Modifier.withScrollbar( } } catch (ex: Exception) { Timber.w("Unable to apply scrollbar due to ${ex.message}") - Firebase.crashlytics.recordException(ex) return this } } diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt index c960fdf65..b23a2c5de 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -59,6 +60,7 @@ import coil3.ImageLoader import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch +import org.grakovne.lissen.BuildConfig import org.grakovne.lissen.R import org.grakovne.lissen.common.LibraryOrderingConfiguration import org.grakovne.lissen.common.NetworkService @@ -107,6 +109,7 @@ fun LibraryScreen( var pullRefreshing by remember { mutableStateOf(false) } val searchRequested by libraryViewModel.searchRequested.observeAsState(false) + val isInitializing by libraryViewModel.isInitializing.collectAsState(true) val preparingError by playerViewModel.preparingError.observeAsState(false) val preferredLibrary by settingsViewModel.preferredLibrary.observeAsState() @@ -160,7 +163,7 @@ fun LibraryScreen( } val hasContent = library.itemCount > 0 || recentBooks.isNotEmpty() - val isLoading = pullRefreshing || library.loadState.refresh is LoadState.Loading + val isLoading = isInitializing || pullRefreshing || library.loadState.refresh is LoadState.Loading isLoading && !hasContent } @@ -498,7 +501,7 @@ fun LibraryScreen( ) } - if (analyticsConsent == null) { + if (BuildConfig.DISTRIBUTION == "play" && analyticsConsent == null) { AnalyticsConsentBottomSheet( onAccept = { settingsViewModel.updateAnalyticsConsent(true) }, onDecline = { settingsViewModel.updateAnalyticsConsent(false) }, diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/composables/LibrarySearchActionComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/composables/LibrarySearchActionComposable.kt index 54a3377c3..2f78d1d69 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/composables/LibrarySearchActionComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/library/composables/LibrarySearchActionComposable.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import com.microsoft.clarity.modifiers.clarityMask import org.grakovne.lissen.R +import org.grakovne.lissen.common.maskForAnalytics @Composable fun LibrarySearchActionComposable( @@ -80,7 +80,7 @@ fun LibrarySearchActionComposable( modifier = Modifier .weight(1f) - .clarityMask() + .maskForAnalytics() .focusRequester(focusRequester), textStyle = typography.bodyLarge.copy(color = colorScheme.onBackground), singleLine = true, diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/login/LoginScreen.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/login/LoginScreen.kt index 5bfe15d62..e66834ba2 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/login/LoginScreen.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/login/LoginScreen.kt @@ -56,12 +56,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import com.microsoft.clarity.modifiers.clarityMask import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import org.grakovne.lissen.R import org.grakovne.lissen.channel.common.AuthMethod import org.grakovne.lissen.channel.common.makeText +import org.grakovne.lissen.common.maskForAnalytics import org.grakovne.lissen.ui.extensions.withMinimumTime import org.grakovne.lissen.ui.navigation.AppNavigationService import org.grakovne.lissen.viewmodel.LoginViewModel @@ -157,7 +157,7 @@ fun LoginScreen( Modifier .fillMaxWidth() .padding(vertical = 4.dp) - .clarityMask() + .maskForAnalytics() .testTag("hostInput"), ) @@ -171,7 +171,7 @@ fun LoginScreen( Modifier .fillMaxWidth() .padding(vertical = 12.dp) - .clarityMask() + .maskForAnalytics() .testTag("usernameInput"), ) @@ -197,7 +197,7 @@ fun LoginScreen( Modifier .fillMaxWidth() .padding(vertical = 4.dp) - .clarityMask() + .maskForAnalytics() .testTag("passwordInput"), ) diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/ChapterSearchActionComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/ChapterSearchActionComposable.kt index 921f94631..eb856e570 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/ChapterSearchActionComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/player/ChapterSearchActionComposable.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import com.microsoft.clarity.modifiers.clarityMask import org.grakovne.lissen.R +import org.grakovne.lissen.common.maskForAnalytics @Composable fun ChapterSearchActionComposable(onSearchRequested: (String) -> Unit) { @@ -64,7 +64,7 @@ fun ChapterSearchActionComposable(onSearchRequested: (String) -> Unit) { modifier = Modifier .weight(1f) - .clarityMask() + .maskForAnalytics() .focusRequester(focusRequester), textStyle = typography.bodyLarge.copy(color = colorScheme.onBackground), singleLine = true, diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt index 1ac34f5b3..2fb4ed014 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/AdvancedSettingsComposable.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import kotlinx.coroutines.launch +import org.grakovne.lissen.BuildConfig import org.grakovne.lissen.R import org.grakovne.lissen.ui.navigation.AppNavigationService import org.grakovne.lissen.ui.screens.settings.composable.SettingsToggleItem @@ -110,11 +111,13 @@ fun AdvancedSettingsComposable( onclick = { navController.showLocalUrlSettings() }, ) - SettingsToggleItem( - title = stringResource(R.string.settings_screen_crash_report_title), - description = stringResource(R.string.settings_screen_crash_report_description), - initialState = crashReporting, - ) { viewModel.preferCrashReporting(it) } + if (BuildConfig.DISTRIBUTION == "play") { + SettingsToggleItem( + title = stringResource(R.string.settings_screen_crash_report_title), + description = stringResource(R.string.settings_screen_crash_report_description), + initialState = crashReporting, + ) { viewModel.preferCrashReporting(it) } + } } } }, diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/CustomHeaderComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/CustomHeaderComposable.kt index de4b0233c..ebc48e1fa 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/CustomHeaderComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/CustomHeaderComposable.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.microsoft.clarity.modifiers.clarityMask import org.grakovne.lissen.R +import org.grakovne.lissen.common.maskForAnalytics import org.grakovne.lissen.lib.domain.connection.ServerRequestHeader import org.grakovne.lissen.lib.domain.connection.ServerRequestHeader.Companion.clean @@ -54,7 +54,7 @@ fun CustomHeaderComposable( label = { Text(stringResource(R.string.custom_header_hint_name)) }, singleLine = true, shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clarityMask(), + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).maskForAnalytics(), ) OutlinedTextField( @@ -63,7 +63,7 @@ fun CustomHeaderComposable( label = { Text(stringResource(R.string.custom_header_hint_value)) }, singleLine = true, shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth().clarityMask(), + modifier = Modifier.fillMaxWidth().maskForAnalytics(), ) } diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlComposable.kt index 1329e9f46..b189b96a7 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/advanced/LocalUrlComposable.kt @@ -22,8 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import com.microsoft.clarity.modifiers.clarityMask import org.grakovne.lissen.R +import org.grakovne.lissen.common.maskForAnalytics import org.grakovne.lissen.lib.domain.connection.LocalUrl @Composable @@ -62,7 +62,7 @@ fun LocalUrlComposable( Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clarityMask(), + .maskForAnalytics(), ) OutlinedTextField( @@ -73,7 +73,7 @@ fun LocalUrlComposable( label = { Text(stringResource(R.string.hint_server_url_input)) }, singleLine = true, shape = RoundedCornerShape(16.dp), - modifier = Modifier.fillMaxWidth().clarityMask(), + modifier = Modifier.fillMaxWidth().maskForAnalytics(), ) } diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/LicenseFooterComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/LicenseFooterComposable.kt index c4c5218c2..4aedd3fb1 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/LicenseFooterComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/LicenseFooterComposable.kt @@ -62,8 +62,8 @@ fun LicenseFooterComposable() { text = stringResource( R.string.settings_screen_footer_app_name_pattern, - stringResource(R.string.branding_name), - BuildConfig.VERSION_NAME, + stringResource(R.string.branding_name) + BuildConfig.APP_NAME_SUFFIX, + "v${BuildConfig.VERSION_NAME}", ), style = TextStyle( diff --git a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/ServerSettingsComposable.kt b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/ServerSettingsComposable.kt index 96aa66fe6..1e5722c07 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/ServerSettingsComposable.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/ui/screens/settings/composable/ServerSettingsComposable.kt @@ -37,9 +37,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.microsoft.clarity.modifiers.clarityMask import org.grakovne.lissen.R import org.grakovne.lissen.channel.audiobookshelf.HostType +import org.grakovne.lissen.common.maskForAnalytics import org.grakovne.lissen.ui.navigation.AppNavigationService import org.grakovne.lissen.ui.theme.Spacing import org.grakovne.lissen.viewmodel.SettingsViewModel @@ -88,7 +88,7 @@ fun ServerSettingsComposable( color = colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.clarityMask(), + modifier = Modifier.maskForAnalytics(), ) } } @@ -129,7 +129,7 @@ fun ServerSettingsComposable( InfoRow( label = stringResource(R.string.settings_screen_connected_as_title), value = it, - modifier = Modifier.clarityMask(), + modifier = Modifier.maskForAnalytics(), ) HorizontalDivider() diff --git a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/LibraryViewModel.kt b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/LibraryViewModel.kt index 910c65f61..d303f8074 100644 --- a/app/src/main/kotlin/org/grakovne/lissen/viewmodel/LibraryViewModel.kt +++ b/app/src/main/kotlin/org/grakovne/lissen/viewmodel/LibraryViewModel.kt @@ -61,6 +61,9 @@ class LibraryViewModel private val _totalCount = MutableLiveData() val totalCount: LiveData = _totalCount + private val _isInitializing = MutableStateFlow(true) + val isInitializing: StateFlow = _isInitializing + private val pageConfig = PagingConfig( pageSize = PAGE_SIZE, @@ -100,6 +103,8 @@ class LibraryViewModel currentLibraryId = preferences.getPreferredLibrary()?.id ?: "" currentOrdering = preferences.getLibraryOrdering() localCacheUpdatedAt = latestLocalUpdate ?: 0L + } else { + _isInitializing.value = false } } @@ -232,34 +237,38 @@ class LibraryViewModel fun refreshLibrary(forceRefresh: Boolean = false) { viewModelScope.launch { - withContext(Dispatchers.IO) { - val isAvailable = - if (forceRefresh) { - networkService.refreshServerAvailabilitySync() - } else { - networkService.isServerAvailable.value - } - - val shouldSync = (forceRefresh || isAvailable) && !preferences.isForceCache() - - if (shouldSync) { - val libraryId = preferences.getPreferredLibrary()?.id + try { + withContext(Dispatchers.IO) { + val isAvailable = + if (forceRefresh) { + networkService.refreshServerAvailabilitySync() + } else { + networkService.isServerAvailable.value + } + + val shouldSync = (forceRefresh || isAvailable) && !preferences.isForceCache() + + if (shouldSync) { + val libraryId = preferences.getPreferredLibrary()?.id + + if (libraryId != null) { + bookRepository.syncLibraryPage( + libraryId = libraryId, + pageSize = PAGE_SIZE, + pageNumber = 0, + ) + } - if (libraryId != null) { - bookRepository.syncLibraryPage( - libraryId = libraryId, - pageSize = PAGE_SIZE, - pageNumber = 0, - ) + bookRepository.syncRepositories() } - bookRepository.syncRepositories() - } - - when (searchRequested.value) { - true -> searchPagingSource?.invalidate() - else -> defaultPagingSource.value?.invalidate() + when (searchRequested.value) { + true -> searchPagingSource?.invalidate() + else -> defaultPagingSource.value?.invalidate() + } } + } finally { + _isInitializing.value = false } } } diff --git a/app/src/play/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt b/app/src/play/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt new file mode 100644 index 000000000..0a948ab75 --- /dev/null +++ b/app/src/play/kotlin/org/grakovne/lissen/analytics/AnalyticsModule.kt @@ -0,0 +1,16 @@ +package org.grakovne.lissen.analytics + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import org.grakovne.lissen.common.RunningComponent + +@Module +@InstallIn(SingletonComponent::class) +interface AnalyticsModule { + @Binds + @IntoSet + fun bindClarityComponent(component: ClarityComponent): RunningComponent +} diff --git a/app/src/main/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt b/app/src/play/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt similarity index 100% rename from app/src/main/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt rename to app/src/play/kotlin/org/grakovne/lissen/analytics/ClarityComponent.kt diff --git a/app/src/main/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt b/app/src/play/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt similarity index 100% rename from app/src/main/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt rename to app/src/play/kotlin/org/grakovne/lissen/analytics/ClarityTracker.kt diff --git a/app/src/play/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt b/app/src/play/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt new file mode 100644 index 000000000..472435d3a --- /dev/null +++ b/app/src/play/kotlin/org/grakovne/lissen/common/AnalyticsExtensions.kt @@ -0,0 +1,6 @@ +package org.grakovne.lissen.common + +import androidx.compose.ui.Modifier +import com.microsoft.clarity.modifiers.clarityMask + +fun Modifier.maskForAnalytics(): Modifier = this.clarityMask() diff --git a/app/src/main/kotlin/org/grakovne/lissen/common/CrashHandler.kt b/app/src/play/kotlin/org/grakovne/lissen/common/CrashHandler.kt similarity index 100% rename from app/src/main/kotlin/org/grakovne/lissen/common/CrashHandler.kt rename to app/src/play/kotlin/org/grakovne/lissen/common/CrashHandler.kt diff --git a/app/src/play/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt b/app/src/play/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt new file mode 100644 index 000000000..04b36ff09 --- /dev/null +++ b/app/src/play/kotlin/org/grakovne/lissen/common/CrashReporterModule.kt @@ -0,0 +1,13 @@ +package org.grakovne.lissen.common + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface CrashReporterModule { + @Binds + fun bindCrashReporter(reporter: FirebaseCrashReporter): CrashReporter +} diff --git a/app/src/play/kotlin/org/grakovne/lissen/common/FirebaseCrashReporter.kt b/app/src/play/kotlin/org/grakovne/lissen/common/FirebaseCrashReporter.kt new file mode 100644 index 000000000..b8165ec64 --- /dev/null +++ b/app/src/play/kotlin/org/grakovne/lissen/common/FirebaseCrashReporter.kt @@ -0,0 +1,18 @@ +package org.grakovne.lissen.common + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseCrashReporter + @Inject + constructor() : CrashReporter { + override fun setCollectionEnabled(enabled: Boolean) { + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled) + } + + override fun recordException(ex: Throwable) { + FirebaseCrashlytics.getInstance().recordException(ex) + } + } diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt index 5c6283f0f..34cb5f818 100644 --- a/metadata/en-US/full_description.txt +++ b/metadata/en-US/full_description.txt @@ -1,3 +1,19 @@ -Listen to your favorite audiobooks through a minimalist app designed for one task +Kahani (meaning "story") is a modern, offline-first client for Audiobookshelf, designed specifically for a premium audiobook and podcast experience on Android. -Sync your listened books between devices and other Audiobookshelf players and download books to listen to them without the Internet +Built with Material Design 3 and full Dynamic Color support, Kahani feels like a native part of your device while providing powerful features for power listeners. + +✨ KEY FEATURES + +• Offline-First: Download entire books or specific chapters to listen anywhere, anytime. +• Seamless Sync: Your listening progress syncs automatically across all your devices and the server. +• Visual Excellence: Stunning Material You interface with smooth transitions and adaptive themes. +• Advanced Playback: Variable playback speed, skip silence, and customizable seek intervals. +• Chapter Support: Easily browse and navigate through chapters with precise progress tracking. +• Podcast Ready: Full support for both your audiobook collection and your favorite podcasts. +• Sleep Timer: Fall asleep without worry using the flexible sleep timer (time-based or end-of-chapter). +• Home Widget: Control your current story directly from your home screen. +• Network Aware: Intelligently manages offline/online states and server availability. + +Kahani is focused on simplicity, privacy, and the joy of a good story. + +Note: An Audiobookshelf server instance is required to use this application. diff --git a/metadata/en-US/images/featureGraphic.png b/metadata/en-US/images/featureGraphic.png deleted file mode 100644 index 450a5f96f..000000000 Binary files a/metadata/en-US/images/featureGraphic.png and /dev/null differ diff --git a/metadata/en-US/images/icon.png b/metadata/en-US/images/icon.png index a6f2b3127..017098239 100644 Binary files a/metadata/en-US/images/icon.png and b/metadata/en-US/images/icon.png differ diff --git a/metadata/en-US/short_description.txt b/metadata/en-US/short_description.txt index 601599e12..e01fd8b24 100644 --- a/metadata/en-US/short_description.txt +++ b/metadata/en-US/short_description.txt @@ -1 +1 @@ -Clean Audiobookshelf Player +Modern, offline-first audiobook & podcast client for Audiobookshelf. diff --git a/metadata/en-US/title.txt b/metadata/en-US/title.txt index 9075fd0d1..00ad61bb8 100644 --- a/metadata/en-US/title.txt +++ b/metadata/en-US/title.txt @@ -1 +1 @@ -Lissen: Audiobookshelf client +Kahani: Audiobookshelf client