-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Build distribution add initial API (#279)
* Build distribution add initial API * Build distribution add initial API
- Loading branch information
Showing
10 changed files
with
437 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 73 additions & 1 deletion
74
distribution/distribution/src/main/kotlin/com/emergetools/distribution/Distribution.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,77 @@ | ||
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 | ||
|
||
/** | ||
* The public Android SDK for Emerge Tools Build Distribution. | ||
*/ | ||
object Distribution { | ||
// Left intentionally blank. | ||
/** | ||
* Initialize build distribution. This should be called once in each process. | ||
* This method may be called from any thread with a Looper. It is safe to | ||
* call this from the main thread. Options may be passed if you want to override the default values. | ||
* @param context Android context | ||
* @param options Override distribution settings | ||
*/ | ||
fun init(context: Context, options: DistributionOptions? = null) { | ||
DistributionInternal.init(context, options ?: DistributionOptions()) | ||
} | ||
|
||
/** | ||
* | ||
*/ | ||
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) | ||
} | ||
} | ||
|
||
/** | ||
* Optional settings for build distribution. | ||
*/ | ||
data class DistributionOptions( | ||
/** | ||
* Pass an existing OkHttpClient. If null Distribution will create it's own OkHttpClient. | ||
* 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 info: UpdateInfo) : UpdateStatus() | ||
object UpToDate : UpdateStatus() | ||
} |
16 changes: 16 additions & 0 deletions
16
...tion/distribution/src/main/kotlin/com/emergetools/distribution/DistributionInitializer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.emergetools.distribution | ||
|
||
import android.content.Context | ||
import androidx.startup.Initializer | ||
import androidx.work.WorkManagerInitializer | ||
|
||
class DistributionInitializer : Initializer<Distribution> { | ||
override fun create(context: Context): Distribution { | ||
Distribution.init(context) | ||
return Distribution | ||
} | ||
|
||
override fun dependencies(): List<Class<out Initializer<*>>> { | ||
return listOf(WorkManagerInitializer::class.java) | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
distribution/distribution/src/main/kotlin/com/emergetools/distribution/internal/Constants.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package com.emergetools.distribution.internal | ||
|
||
internal const val TAG = "Distribution" |
223 changes: 223 additions & 0 deletions
223
...istribution/src/main/kotlin/com/emergetools/distribution/internal/DistributionInternal.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
// We catch very generic exceptions on purpose. We need to avoid crashing the app. | ||
@file:Suppress("TooGenericExceptionCaught") | ||
|
||
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 | ||
import kotlinx.coroutines.suspendCancellableCoroutine | ||
import kotlinx.coroutines.withContext | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.SerializationException | ||
import kotlinx.serialization.json.Json | ||
import okhttp3.Call | ||
import okhttp3.Callback | ||
import okhttp3.HttpUrl | ||
import okhttp3.OkHttpClient | ||
import okhttp3.Request | ||
import okhttp3.Response | ||
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) | ||
} catch (_: SerializationException) { | ||
null | ||
} catch (_: IllegalArgumentException) { | ||
null | ||
} | ||
} | ||
|
||
private fun decodeResult(body: String?): UpdateStatus { | ||
if (body == null) { | ||
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) | ||
} | ||
|
||
return UpdateStatus.Error("Unexpected response $body") | ||
} | ||
|
||
private class State( | ||
val handler: Handler, | ||
val tag: String, | ||
val apiKey: String?, | ||
private var okHttpClient: OkHttpClient? | ||
) { | ||
|
||
@Synchronized | ||
fun getOkHttpClient(): OkHttpClient { | ||
var client = okHttpClient | ||
if (client == null) { | ||
client = OkHttpClient() | ||
okHttpClient = client | ||
} | ||
return client | ||
} | ||
} | ||
|
||
object DistributionInternal { | ||
private var state: State? = null | ||
|
||
@Synchronized | ||
fun init(context: Context, options: DistributionOptions) { | ||
if (state != null) { | ||
Log.e(TAG, "Distribution already initialized, ignoring Distribution.init().") | ||
return | ||
} | ||
|
||
val looper = Looper.myLooper() | ||
if (looper == null) { | ||
Log.e(TAG, "Distribution.init() must be called from a thread with a Looper.") | ||
return | ||
} | ||
|
||
val handler = Handler(looper) | ||
|
||
// 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? { | ||
var theState: State? | ||
synchronized(this) { | ||
theState = state | ||
} | ||
return theState | ||
} | ||
|
||
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") | ||
} | ||
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): 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("tag", state.tag) | ||
addQueryParameter("appId", applicationId) | ||
addQueryParameter("platform", "android") | ||
}.build() | ||
|
||
val request = Request.Builder().apply { | ||
url(url) | ||
}.build() | ||
|
||
val client = state.getOkHttpClient() | ||
val call = client.newCall(request) | ||
executeAsync(call).use { response -> | ||
return withContext(Dispatchers.IO) { | ||
val body = response.body?.string() | ||
println(body) | ||
return@withContext decodeResult(body) | ||
} | ||
} | ||
} | ||
|
||
@ExperimentalCoroutinesApi // resume with a resource cleanup. | ||
suspend fun executeAsync(call: Call): Response = | ||
suspendCancellableCoroutine { continuation -> | ||
continuation.invokeOnCancellation { | ||
call.cancel() | ||
} | ||
call.enqueue( | ||
object : Callback { | ||
override fun onFailure( | ||
call: Call, | ||
e: IOException, | ||
) { | ||
continuation.resumeWithException(e) | ||
} | ||
|
||
override fun onResponse( | ||
call: Call, | ||
response: Response, | ||
) { | ||
continuation.resume(response) { | ||
response.closeQuietly() | ||
} | ||
} | ||
}, | ||
) | ||
} |
Oops, something went wrong.