Commit 8e7a0fba authored by genofire's avatar genofire
Browse files

Merge branch 'hatchery-download' into 'master'

Hatchery app download

See merge request card10/companion-app-android!6
parents 757410b6 0318c9ab
......@@ -39,4 +39,5 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.1.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.work:work-runtime-ktx:2.2.0'
implementation 'org.apache.commons:commons-compress:1.18'
}
......@@ -22,14 +22,22 @@
package de.ccc.events.badge.card10.hatchery
import android.os.AsyncTask
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import de.ccc.events.badge.card10.R
import de.ccc.events.badge.card10.common.LoadingDialog
import kotlinx.android.synthetic.main.app_detail_fragment.*
import java.lang.IllegalStateException
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
import java.io.File
private const val TAG = "AppDetailFragment"
class AppDetailFragment : Fragment() {
......@@ -51,5 +59,93 @@ class AppDetailFragment : Fragment() {
label_content_size.text = getString(R.string.app_detail_content_size, app.size_of_content)
label_description.text = app.description
button_download.setOnClickListener {
val ctx = activity ?: throw java.lang.IllegalStateException()
val loadingDialog = LoadingDialog()
loadingDialog.show(fragmentManager, "loading")
val errorDialog =
AlertDialog.Builder(ctx).setMessage(R.string.hatchery_error_generic)
.setPositiveButton(
R.string.dialog_action_ok
) { dialog, _ -> dialog.dismiss() }
.create()
ReleaseDownload(app, ctx.cacheDir, loadingDialog, errorDialog).execute()
}
}
private class ReleaseDownload(
private val app: App,
private val cacheDir: File,
private val loadingDialog: LoadingDialog,
private val errorDialog: AlertDialog
) : AsyncTask<Void, Void, List<String>?>() {
override fun doInBackground(vararg p0: Void?): List<String>? {
return try {
cacheDir.deleteRecursively()
cacheDir.mkdir()
val appDir = File(cacheDir.absolutePath + "/apps").mkdirs()
val inputStream = HatcheryClient().openDownloadStream(app)
val file = File.createTempFile(app.slug, ".tar.gz", cacheDir)
val outputStream = file.outputStream()
inputStream.copyTo(outputStream)
val appFiles = mutableListOf<String>()
val tarStream = TarArchiveInputStream(GzipCompressorInputStream(file.inputStream()))
while (true) {
val entry = tarStream.nextTarEntry ?: break
if (entry.isDirectory) {
continue
}
// TODO: A bit hacky. Maybe there is a better way?
val targetFile = File(cacheDir, "apps/${entry.name}")
targetFile.parentFile?.mkdirs()
targetFile.createNewFile()
Log.d(TAG, "Extracting ${entry.name} to ${targetFile.absolutePath}")
tarStream.copyTo(targetFile.outputStream())
appFiles.add("apps/${entry.name}")
}
val launcher = createLauncher(app.slug, cacheDir)
appFiles.add(launcher)
appFiles
} catch (e: Exception) {
null
}
}
override fun onPostExecute(result: List<String>?) {
if (result == null) {
loadingDialog.dismiss()
errorDialog.show()
return
}
loadingDialog.dismiss()
}
fun createLauncher(slug: String, cacheDir: File): String {
val fileName = "$slug.py"
val file = File(cacheDir, fileName)
file.createNewFile()
val src = """
# Launcher script for $slug
import os
os.exec("apps/$slug/__init__.py")
""".trimIndent()
file.writeText(src)
return fileName
}
}
}
......@@ -29,14 +29,18 @@ import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.InputStream
private const val TAG = "HatcheryClient"
class HatcheryClient {
fun getAppList(): List<App> {
val client = OkHttpClient()
// TODO: Filter by category
val request = Request.Builder()
.url(HATCHERY_BASE_URL + "/eggs/list/json")
.url("$HATCHERY_BASE_URL/eggs/list/json")
.build()
val response: Response
......@@ -79,4 +83,57 @@ class HatcheryClient {
return resultList
}
fun getReleaseUrl(app: App): String {
val client = OkHttpClient()
val request = Request.Builder()
.url("$HATCHERY_BASE_URL/eggs/get/${app.slug}/json")
.build()
val response: Response
try {
response = client.newCall(request).execute()
} catch (e: Exception) {
throw HatcheryClientException(0)
}
if (response.code != 200) {
throw HatcheryClientException(response.code)
}
val body = response.body?.string() ?: ""
try {
val responseJson = JSONObject(body)
val version = responseJson.getJSONObject("info").getString("version")
return responseJson.getJSONObject("releases")
.getJSONArray(version).getJSONObject(0).getString("url")
} catch (e: JSONException) {
Log.e(TAG, "Error parsing JSON: ${e.message}")
throw HatcheryClientException(0)
}
}
fun openDownloadStream(app: App): InputStream {
val releaseUrl = getReleaseUrl(app)
val client = OkHttpClient()
val request = Request.Builder()
.url(releaseUrl)
.build()
val response: Response
try {
response = client.newCall(request).execute()
} catch (e: Exception) {
throw HatcheryClientException(0)
}
if (response.code != 200) {
throw HatcheryClientException(response.code)
}
return response.body?.byteStream() ?: throw HatcheryClientException(0)
}
}
......@@ -35,8 +35,7 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/button_download"
android:text="@string/app_detail_button_download"
android:enabled="false"/>
android:text="@string/app_detail_button_download"/>
</androidx.constraintlayout.widget.ConstraintLayout>
......
......@@ -16,6 +16,8 @@
<string name="file_transfer_label_selected_file">Selected file:</string>
<string name="loading_dialog_loading">Loading</string>
<string name="dialog_action_ok">OK</string>
<string name="dialog_action_cancel">Cancel</string>
<string name="hatchery_error_generic">Something went wrong</string>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment