Skip to content

Commit

Permalink
Build distribution add initial API
Browse files Browse the repository at this point in the history
  • Loading branch information
chromy committed Oct 17, 2024
1 parent 92ef4ac commit 1f13be3
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 20 deletions.
4 changes: 4 additions & 0 deletions distribution/distribution/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application>
<meta-data
android:name="com.emergetools.distribution.API_KEY"
android:value="${emerge.distribution.apiKey}" />

<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.emergetools.distribution

import android.content.Context
import android.content.Intent
import android.net.Uri
import com.emergetools.distribution.internal.DistributionInternal
import kotlinx.serialization.Serializable
import okhttp3.OkHttpClient

/**
Expand All @@ -22,8 +25,23 @@ object Distribution {
/**
*
*/
suspend fun checkForUpdate(context: Context, apiKey: String? = null): UpdateStatus {
return DistributionInternal.checkForUpdate(context, apiKey)
fun isEnabled(context: Context): Boolean {
return DistributionInternal.isEnabled(context)
}

/**
* Check to see if an updated version of the current app exists.
*/
suspend fun checkForUpdate(context: Context): UpdateStatus {
return DistributionInternal.checkForUpdate(context)
}

/**
* Download the provided update.
*/
fun downloadUpdate(context: Context, info: UpdateInfo) {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(info.downloadUrl))
context.startActivity(browserIntent)
}
}

Expand All @@ -36,16 +54,24 @@ data class DistributionOptions(
* This allows reuse of existing OkHttpClient thread pools etc.
*/
val okHttpClient: OkHttpClient? = null,

/**
* Emerge Tools build tag used to find the desired next build.
*/
val tag: String? = null,
)

@Serializable
data class UpdateInfo(
val id: String,
val tag: String,
val version: String,
val appId: String,
val downloadUrl: String,
)

sealed class UpdateStatus {
class Error(val message: String) : UpdateStatus()
class NewRelease(
val id: String,
val tag: String,
val version: String,
val appId: String,
val downloadUrl: String
) : UpdateStatus()
class NewRelease(val info: UpdateInfo) : UpdateStatus()
object UpToDate : UpdateStatus()
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package com.emergetools.distribution.internal

import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.emergetools.distribution.DistributionOptions
import com.emergetools.distribution.UpdateInfo
import com.emergetools.distribution.UpdateStatus
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -26,11 +29,26 @@ import okhttp3.internal.closeQuietly
import java.io.IOException
import kotlin.coroutines.resumeWithException

private const val MANIFEST_TAG_API_KEY = "com.emergetools.distribution.API_KEY"

internal fun getApiKey(metadata: Bundle): String? {
val apiKey = metadata.getString(MANIFEST_TAG_API_KEY, null)
if (apiKey == "") {
return null
}
return apiKey
}

@Serializable
internal data class CheckForUpdatesMessageResult(
val message: String
)

@Serializable
internal data class CheckForUpdatesSuccessResult(
val updateInfo: UpdateInfo
)

private inline fun <reified T> tryDecode(s: String): T? {
return try {
Json.decodeFromString<T>(s)
Expand All @@ -46,6 +64,11 @@ private fun decodeResult(body: String?): UpdateStatus {
return UpdateStatus.Error("Empty response from server")
}

val success = tryDecode<CheckForUpdatesSuccessResult>(body)
if (success !== null) {
return UpdateStatus.NewRelease(success.updateInfo)
}

val message = tryDecode<CheckForUpdatesMessageResult>(body)
if (message !== null) {
return UpdateStatus.Error(message.message)
Expand All @@ -54,7 +77,12 @@ private fun decodeResult(body: String?): UpdateStatus {
return UpdateStatus.Error("Unexpected response $body")
}

private class State(val handler: Handler, private var okHttpClient: OkHttpClient?) {
private class State(
val handler: Handler,
val tag: String,
val apiKey: String?,
private var okHttpClient: OkHttpClient?
) {

@Synchronized
fun getOkHttpClient(): OkHttpClient {
Expand All @@ -71,7 +99,6 @@ object DistributionInternal {
private var state: State? = null

@Synchronized
@Suppress("unused")
fun init(context: Context, options: DistributionOptions) {
if (state != null) {
Log.e(TAG, "Distribution already initialized, ignoring Distribution.init().")
Expand All @@ -85,7 +112,17 @@ object DistributionInternal {
}

val handler = Handler(looper)
state = State(handler, options.okHttpClient)

// apiKey may be null if
val metaData = context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA
).metaData
val apiKey = getApiKey(metaData)

val tag = options.tag ?: "release"

state = State(handler, tag, apiKey, options.okHttpClient)
}

private fun getState(): State? {
Expand All @@ -96,37 +133,49 @@ object DistributionInternal {
return theState
}

suspend fun checkForUpdate(context: Context, apiKey: String?): UpdateStatus {
suspend fun checkForUpdate(context: Context): UpdateStatus {
try {
val state = getState()
if (state == null) {
Log.e(TAG, "Build distribution not initialized")
return UpdateStatus.Error("Build distribution not initialized")
}
if (apiKey == null) {
Log.e(TAG, "No API key available")
return UpdateStatus.Error("No API key available")
}
return doCheckForUpdate(context, state, apiKey)
return doCheckForUpdate(context, state)
} catch (e: Exception) {
Log.e(TAG, "Error: $e")
return UpdateStatus.Error("Error: $e")
}
}

@Suppress("unused")
fun isEnabled(context: Context): Boolean {
try {
val state = getState()
return state?.apiKey != null
} catch (e: Exception) {
Log.e(TAG, "Error: $e")
return false
}
}
}

@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun doCheckForUpdate(context: Context, state: State, apiKey: String): UpdateStatus {
private suspend fun doCheckForUpdate(context: Context, state: State): UpdateStatus {
// Despite the name context.packageName is the actually the application id.
val applicationId = context.packageName
val apiKey = state.apiKey
if (apiKey == null) {
Log.e(TAG, "No API key available")
return UpdateStatus.Error("No API key available")
}

val url = HttpUrl.Builder().apply {
scheme("https")
host("api.emergetools.com")
addPathSegment("distribution")
addPathSegment("checkForUpdates")
addQueryParameter("apiKey", apiKey)
addQueryParameter("binaryIdentifier", "polar bears")
addQueryParameter("tag", state.tag)
addQueryParameter("appId", applicationId)
addQueryParameter("platform", "android")
}.build()
Expand Down

0 comments on commit 1f13be3

Please sign in to comment.