Skip to content

Commit

Permalink
Improve upgrade flow
Browse files Browse the repository at this point in the history
* Upgrade dependencies
* Extra screen for FOSS and GPLAY build
* Add support for subscription
  • Loading branch information
d4rken committed Sep 2, 2024
1 parent d5a4a3d commit e285410
Show file tree
Hide file tree
Showing 60 changed files with 1,618 additions and 685 deletions.
60 changes: 60 additions & 0 deletions app-common/src/main/java/eu/darken/octi/common/ca/CaString.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package eu.darken.octi.common.ca

import android.content.Context

/**
* Context aware string
*/
interface CaString {
fun get(context: Context): String

fun isEmpty(context: Context): Boolean = this == EMPTY || get(context).isEmpty()

companion object {
val EMPTY: CaString = object : CaString {
override fun get(context: Context): String = ""
}
}
}

internal class CachedCaString(val resolv: (Context) -> String) : CaString {

private lateinit var cache: String

override fun get(context: Context): String {
if (::cache.isInitialized) return cache
synchronized(this) {
if (!::cache.isInitialized) cache = resolv(context)
return cache
}
}

override fun toString(): String = if (::cache.isInitialized) {
"CachedCaString(\"$cache\")"
} else {
"CachedCaString(${Integer.toHexString(hashCode())})"
}
}

fun caString(provider: Context.(Context) -> String): CaString = object : CaString {
override fun get(context: Context): String = provider(context, context)
}

fun caString(direct: String): CaString = object : CaString {
override fun get(context: Context): String = direct
override fun toString(): String = "CaString(\"$direct\")"
}

fun CaString.cache(): CaString = CachedCaString { this.get(it) }

fun String.toCaString(): CaString = caString(this)

fun Int.toCaString(): CaString = caString { getString(this@toCaString) }.cache()

fun Int.toCaString(vararg args: Any): CaString = caString { it.getString(this@toCaString, *args) }.cache()

fun Pair<Int, Array<out Any?>>.toCaString() = caString {
val (res, args) = this@toCaString
it.getString(res, *args)
}.cache()

Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
package eu.darken.octi.common.error

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import eu.darken.octi.common.R
import eu.darken.octi.common.ca.CaString
import eu.darken.octi.common.ca.caString

interface HasLocalizedError {
fun getLocalizedError(context: Context): LocalizedError
fun getLocalizedError(): LocalizedError
}

data class LocalizedError(
val throwable: Throwable,
val label: String,
val description: String
val label: CaString,
val description: CaString,
val fixActionLabel: CaString? = null,
val fixAction: ((Activity) -> Unit)? = null,
val infoAction: ((Activity) -> Unit)? = null,
) {
fun asText() = "$label:\n$description"
fun asText() = caString { "${label.get(it)}:\n${description.get(it)}" }
}

fun Throwable.localized(c: Context): LocalizedError = when {
this is HasLocalizedError -> this.getLocalizedError(c)
this is HasLocalizedError -> this.getLocalizedError()
this is ActivityNotFoundException -> LocalizedError(
throwable = this,
label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" },
description = caString {
"${it.getString(R.string.general_error_no_compatible_app_found_msg)}\n$localizedMessage"
}
)

localizedMessage != null -> LocalizedError(
throwable = this,
label = "${c.getString(R.string.general_error_label)}: ${this::class.simpleName!!}",
description = localizedMessage ?: getStackTracePeek()
label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" },
description = caString { localizedMessage ?: getStackTracePeek() }
)

else -> LocalizedError(
throwable = this,
label = "${c.getString(R.string.general_error_label)}: ${this::class.simpleName!!}",
description = getStackTracePeek()
label = caString { "${c.getString(R.string.general_error_label)} - ${this@localized::class.simpleName!!}" },
description = caString { getStackTracePeek() }
)
}

Expand Down
9 changes: 9 additions & 0 deletions app-common/src/main/res/drawable/ic_arrow_back.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="?colorOnPrimarySurface"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
21 changes: 21 additions & 0 deletions app-common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
<resources>
<string name="app_name">Octi</string>
<string name="general_error_label">Error</string>
<string name="general_error_no_compatible_app_found_msg">No compatible app found to handle this request.</string>
<string name="general_share_action">Share</string>
<string name="general_done_action">Done</string>
<string name="general_copy_action">Copy</string>
<string name="general_remove_action">Remove</string>
<string name="general_cancel_action">Cancel</string>
<string name="general_thanks_action">Thanks</string>
<string name="general_gotit_action">Got it</string>
<string name="general_na_label">N/A</string>
<string name="general_empty_label">Empty</string>
<string name="general_dismiss_action">Dismiss</string>
<string name="general_maybe_later_action">Maybe later</string>
<string name="general_manage_action">Manage</string>
<string name="general_quota_label">Quota</string>
<string name="general_upgrade_action">Upgrade</string>
<string name="general_donate_action">Donate</string>
<string name="general_check_action">Check</string>
<string name="general_internal_not_available_msg">Internet connection unavailable</string>
<string name="general_refresh_action">Refresh</string>
<string name="general_continue">Continue</string>
<string name="general_show_details_action">Show details</string>
</resources>
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ dependencies {

implementation("com.journeyapps:zxing-android-embedded:4.3.0")

"gplayImplementation"("com.android.billingclient:billing:4.0.0")
"gplayImplementation"("com.android.billingclient:billing:7.0.0")
"gplayImplementation"("com.android.billingclient:billing-ktx:7.0.0")

implementation("io.coil-kt:coil:2.0.0-rc02")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import eu.darken.octi.common.upgrade.core.UpgradeControlFoss
import eu.darken.octi.common.upgrade.core.UpgradeRepoFoss
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
abstract class UpgradeModule {
@Binds
@Singleton
abstract fun control(foss: UpgradeControlFoss): UpgradeRepo
abstract fun control(foss: UpgradeRepoFoss): UpgradeRepo

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import java.time.Instant

@JsonClass(generateAdapter = true)
data class FossUpgrade(
val upgradedAt: Instant,
val reason: Reason
@Json(name = "upgradedAt") val upgradedAt: Instant,
@Json(name = "reason") val upgradeType: Type,
) {
@JsonClass(generateAdapter = false)
enum class Reason {
@Json(name = "foss.upgrade.reason.donated") DONATED,
@Json(name = "foss.upgrade.reason.alreadydonated") ALREADY_DONATED,
@Json(name = "foss.upgrade.reason.nomoney") NO_MONEY;
enum class Type {
@Json(name = "GITHUB_SPONSORS") GITHUB_SPONSORS,
@Json(name = "foss.upgrade.reason.donated") LEGACY_DONATED,
@Json(name = "foss.upgrade.reason.alreadydonated") LEGACY_ALREADY_DONATED,
@Json(name = "foss.upgrade.reason.nomoney") LEGACY_NO_MONEY,
;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package eu.darken.octi.common.upgrade.core

import eu.darken.octi.common.WebpageTool
import eu.darken.octi.common.coroutine.AppScope
import eu.darken.octi.common.datastore.valueBlocking
import eu.darken.octi.common.debug.logging.log
import eu.darken.octi.common.debug.logging.logTag
import eu.darken.octi.common.flow.setupCommonEventHandlers
import eu.darken.octi.common.upgrade.UpgradeRepo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import java.time.Instant
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class UpgradeRepoFoss @Inject constructor(
@AppScope private val appScope: CoroutineScope,
private val fossCache: FossCache,
private val webpageTool: WebpageTool,
) : UpgradeRepo {
override val mainWebsite: String = SITE

private val refreshTrigger = MutableStateFlow(UUID.randomUUID())

override val upgradeInfo: Flow<UpgradeRepo.Info> = combine(
fossCache.upgrade.flow,
refreshTrigger
) { data, _ ->
if (data == null) {
Info()
} else {
Info(
isPro = true,
upgradedAt = data.upgradedAt,
fossUpgradeType = data.upgradeType,
)
}
}
.setupCommonEventHandlers(TAG) { "upgradeInfo" }

fun launchGithubSponsorsUpgrade() = appScope.launch {
log(TAG) { "launchGithubSponsorsUpgrade()" }
fossCache.upgrade.valueBlocking = FossUpgrade(
upgradedAt = Instant.now(),
upgradeType = FossUpgrade.Type.GITHUB_SPONSORS
)
webpageTool.open(mainWebsite)
}

override suspend fun refresh() {
log(TAG) { "refresh()" }
refreshTrigger.value = UUID.randomUUID()
}

data class Info(
override val isPro: Boolean = false,
override val upgradedAt: Instant? = null,
val fossUpgradeType: FossUpgrade.Type? = null,
) : UpgradeRepo.Info {
override val type: UpgradeRepo.Type = UpgradeRepo.Type.FOSS
}

companion object {
private const val SITE = "https://github.com/sponsors/d4rken"
private val TAG = logTag("Upgrade", "Foss", "Repo")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package eu.darken.octi.common.upgrade.ui

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.octi.R
import eu.darken.octi.common.uix.Fragment3
import eu.darken.octi.common.viewbinding.viewBinding
import eu.darken.octi.databinding.UpgradeFragmentBinding

@AndroidEntryPoint
class UpgradeFragment : Fragment3(R.layout.upgrade_fragment) {

override val vm: UpgradeViewModel by viewModels()
override val ui: UpgradeFragmentBinding by viewBinding()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ui.toolbar.setupWithNavController(findNavController())

ui.upgradeGithubSponsorsAction.setOnClickListener {
Toast.makeText(
requireContext(),
R.string.upgrade_screen_thanks_toast,
Toast.LENGTH_LONG,
).show()
vm.goGithubSponsors()
}

super.onViewCreated(view, savedInstanceState)
}

}
Loading

0 comments on commit e285410

Please sign in to comment.