Skip to content

Features implementation

Andrea Severi edited this page Dec 8, 2025 · 3 revisions
Hide keys from version control (Git)

The secrets-gradle-plugin is used to hide API keys, urls, etc. from version control. It lets you access these keys inside:

  • kotlin: using BuildConfig, like BuildConfig.myKey
  • AndroidManifest.xml: with this syntax ${myKey} (⚠️ keys inside manifest are still visible when decompiling the apk, so make sure to apply restrictions to those APIs)

You can also create buildtype-based or flavor based properties files, like debug.properties for a debug variant.

Encrypted DataStore
  1. Copy KeyStoreManager.kt
  2. Change the DataStore serializer, like this:
class UserPreferencesSerializer @Inject constructor(
    @Dispatcher(Io) private val ioDispatcher: CoroutineDispatcher,
    private val keyStoreManager: KeyStoreManager = KeyStoreManager("user_preferences")
) : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        // 1st change
        val decryptedBytes = keyStoreManager.decrypt(input)
        
        return try {
            withContext(ioDispatcher) {
                ProtoBuf.decodeFromByteArray<UserPreferences>(bytes = decryptedBytes)
            }
        } catch (exception: SerializationException) {
            throw CorruptionException("Cannot read proto. $exception")
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        withContext(ioDispatcher) {
            // 2nd change
            keyStoreManager.encrypt(
                bytes = ProtoBuf.encodeToByteArray(t),
                outputStream = output
            )
        }
    }
}
Get app info
@Composable
fun MyScreen() {
    val context = LocalContext.current
    val appVersion = context.getAppVersion()
}

data class AppVersion(
    /**
     * App string version e.g. 1.0.0, 1.1.0, ...
     */
    val versionName: String,

    /**
     * App code version e.g. 1, 2, 3, ...
     */
    val versionCode: Long
)

fun Context.getAppVersion(): AppVersion? {
    return try {
        val packageManager = this.packageManager
        val packageInfo = packageManager.getPackageInfoCompat(this.packageName)
        AppVersion(
            versionName = packageInfo.versionName,
            versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
        )
    } catch (e: PackageManager.NameNotFoundException) {
        // Timber.e(e)
        null
    }
}

private fun PackageManager.getPackageInfoCompat(packageName: String): PackageInfo {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        this.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
    } else {
        this.getPackageInfo(packageName, 0)
    }
}
Open email app
@Composable
fun MyScreen() {
    val context = LocalContext.current
    val onSendEmail = { context.sendEmail(Email(to = listOf(EmailAddress("example@gmail.com")))) }
}

fun Context.sendEmail(email: Email) {
    val actionSendType = when {
        email.attachments.isEmpty() -> Intent.ACTION_SENDTO
        email.attachments.size == 1 -> Intent.ACTION_SEND
        else -> Intent.ACTION_SEND_MULTIPLE
    }

    val intent = Intent(actionSendType).apply {
        data = Uri.parse("mailto:") // Only email apps handle this.

        // to
        putExtra(Intent.EXTRA_EMAIL, email.to.map { it.value }.toTypedArray())

        // cc
        if (email.cc.isNotEmpty()) {
            putExtra(Intent.EXTRA_CC, email.cc.map { it.value }.toTypedArray())
        }

        // bcc
        if (email.bcc.isNotEmpty()) {
            putExtra(Intent.EXTRA_BCC, email.cc.map { it.value }.toTypedArray())
        }

        // subject
        email.subject?.let {
            putExtra(Intent.EXTRA_SUBJECT, it)
        }

        // body
        email.body?.let {
            putExtra(Intent.EXTRA_TEXT, it)
        }

        // attachments
        if (email.attachments.isNotEmpty()) {
            putExtra(Intent.EXTRA_STREAM, email.attachments.toTypedArray())
        }
    }
    if (intent.resolveActivity(this.packageManager) != null) {
        startActivity(intent)
    }
}

data class Email(
    val to: List<EmailAddress>,
    val cc: List<EmailAddress> = emptyList(),
    val bcc: List<EmailAddress> = emptyList(),
    val subject: String? = null,
    val body: String? = null,
    val attachments: List<Uri> = emptyList()
)

@JvmInline
value class EmailAddress(val value: String)
Get app locale from @Composable
@Composable
fun MyScreen() {
    val context = LocalContext.current
    val locale = getLocale()
}

@Composable
@ReadOnlyComposable
fun getLocale(): Locale {
    val configuration = LocalConfiguration.current
    return ConfigurationCompat.getLocales(configuration).get(0)
        ?: LocaleListCompat.getDefault()[0]!!
}
Open PlayStore app

Get util functions from this link (omitted for brevity)

For more complicated features, see here or take a look at other util functions here

Clone this wiki locally