diff --git a/app/build.gradle b/app/build.gradle index d7f1aaeb21f3d69167971cd7000aa8ba585726e5..475f2d3c6ab5be325104a18a0d1c923b399a4b35 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/java/de/ccc/events/badge/card10/hatchery/AppDetailFragment.kt b/app/src/main/java/de/ccc/events/badge/card10/hatchery/AppDetailFragment.kt index 5e5e876f85443048c7c8e116e4535c94d0f1f15e..f1926c94d04ef244df6c07ecb89720976cdedf10 100644 --- a/app/src/main/java/de/ccc/events/badge/card10/hatchery/AppDetailFragment.kt +++ b/app/src/main/java/de/ccc/events/badge/card10/hatchery/AppDetailFragment.kt @@ -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 + } } } diff --git a/app/src/main/java/de/ccc/events/badge/card10/hatchery/HatcheryClient.kt b/app/src/main/java/de/ccc/events/badge/card10/hatchery/HatcheryClient.kt index 456e0731c44eb32e8b441fc5cea0ca146cde65ca..4dad8e53f7663a4ff6da9c32786b0efb82ad4015 100644 --- a/app/src/main/java/de/ccc/events/badge/card10/hatchery/HatcheryClient.kt +++ b/app/src/main/java/de/ccc/events/badge/card10/hatchery/HatcheryClient.kt @@ -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) + } } diff --git a/app/src/main/res/layout/app_detail_fragment.xml b/app/src/main/res/layout/app_detail_fragment.xml index de0afaf56976b8e6d9053dae0c584857ec6f39f6..1e9b0a6a1e4ce09b40e8c83faf5a7b39d18da614 100644 --- a/app/src/main/res/layout/app_detail_fragment.xml +++ b/app/src/main/res/layout/app_detail_fragment.xml @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 084ef4e1dcbf3ffa8ee9f1be01f4fa50e2c35670..ac827b1a52fe6f327cbc69176a6ce0cf2e1ec7fd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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>