Commit 60f1b86c authored by Anon's avatar Anon Committed by Andreas Schildbach
Browse files

Implement Hatchery UI for downloading a list of apps.

parent 8a3c18a0
......@@ -37,3 +37,5 @@ val LIGHT_SENSOR_CHARACTERISTIC_UUID = UUID.fromString("422302f0-2342-2342-2342-
val TIME_CHARACTERISTIC_UUID = UUID.fromString("42230201-2342-2342-2342-234223422342")
const val UPDATE_CLOCK_FREQUENCY_MINS = 5
const val HATCHERY_BASE_URL = "https://badge.team"
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.common
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import de.ccc.events.badge.card10.R
class LoadingDialog : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.loading_dialog, container, false)
init {
this.isCancelable = false
}
}
\ No newline at end of file
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.os.Parcel
import android.os.Parcelable
data class App(
val name: String,
val slug: String,
val description: String,
val download_counter: Int,
val status: String,
val revision: String,
val size_of_zip: Int,
val size_of_content: Int,
val category: String
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readString()!!,
parcel.readInt(),
parcel.readString()!!,
parcel.readString()!!,
parcel.readInt(),
parcel.readInt(),
parcel.readString()!!
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeString(slug)
parcel.writeString(description)
parcel.writeInt(download_counter)
parcel.writeString(status)
parcel.writeString(revision)
parcel.writeInt(size_of_zip)
parcel.writeInt(size_of_content)
parcel.writeString(category)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<App> {
override fun createFromParcel(parcel: Parcel): App {
return App(parcel)
}
override fun newArray(size: Int): Array<App?> {
return arrayOfNulls(size)
}
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import de.ccc.events.badge.card10.R
import kotlinx.android.synthetic.main.app_detail_fragment.*
import java.lang.IllegalStateException
class AppDetailFragment : Fragment() {
private lateinit var app: App
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = arguments ?: throw IllegalStateException()
app = bundle.getParcelable<App>("app") ?: throw IllegalStateException()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.app_detail_fragment, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
label_name.text = app.name
label_download_count.text = getString(R.string.app_detail_downloads, app.download_counter)
label_content_size.text = getString(R.string.app_detail_content_size, app.size_of_content)
label_description.text = app.description
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class AppListAdapter(val clickListener: (App) -> Unit) : RecyclerView.Adapter<AppViewHolder>() {
private var data = listOf<App>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
val inflater = LayoutInflater.from(parent.context)
return AppViewHolder(inflater, parent)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
holder.bind(data[position], clickListener)
}
fun update(items: List<App>) {
data = items
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.content.Context
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import de.ccc.events.badge.card10.R
import de.ccc.events.badge.card10.common.LoadingDialog
import kotlinx.android.synthetic.main.app_list_fragment.*
import java.lang.ref.WeakReference
class AppListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var listAdapter: AppListAdapter
private lateinit var layoutManager: RecyclerView.LayoutManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.app_list_fragment, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
listAdapter = AppListAdapter { app: App -> onAppClicked(app) }
layoutManager = LinearLayoutManager(activity)
recyclerView = list
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = listAdapter
val loadingDialog = LoadingDialog()
loadingDialog.show(fragmentManager, "loading")
DownloadTask(activity as Context, listAdapter, loadingDialog).execute()
}
private fun onAppClicked(app: App) {
val bundle = Bundle()
bundle.putParcelable("app", app)
val fragment = AppDetailFragment()
fragment.arguments = bundle
val fragmentManager = activity?.supportFragmentManager ?: throw IllegalStateException()
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
}
private class DownloadTask(
context: Context,
private val adapter: AppListAdapter,
private val loadingDialog: LoadingDialog
) : AsyncTask<Void, Void, List<App>?>() {
private val context = WeakReference<Context>(context)
override fun doInBackground(vararg p0: Void?): List<App>? {
val client = HatcheryClient()
return try {
client.getAppList()
} catch (e: HatcheryClientException) {
null
}
}
override fun onPostExecute(result: List<App>?) {
if (result == null) {
loadingDialog.dismiss()
val ctx = context.get() ?: throw IllegalStateException()
AlertDialog.Builder(ctx).setMessage(R.string.hatchery_error_generic)
return
}
adapter.update(result)
adapter.notifyDataSetChanged()
loadingDialog.dismiss()
}
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import de.ccc.events.badge.card10.R
class AppViewHolder(inflater: LayoutInflater, parent: ViewGroup) :
RecyclerView.ViewHolder(inflater.inflate(R.layout.app_list_item, parent, false)) {
fun bind(app: App, clickListener: (App) -> Unit) {
val nameLabel = itemView.findViewById<TextView>(R.id.label_name)
nameLabel.text = app.name
itemView.setOnClickListener {
clickListener(app)
}
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import android.util.Log
import de.ccc.events.badge.card10.HATCHERY_BASE_URL
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONException
private const val TAG = "HatcheryClient"
class HatcheryClient {
fun getAppList(): List<App> {
val client = OkHttpClient()
val request = Request.Builder()
.url(HATCHERY_BASE_URL + "/eggs/list/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() ?: ""
val resultList = mutableListOf<App>()
try {
val responseJson = JSONArray(body)
for (i in 0 until responseJson.length()) {
val item = responseJson.getJSONObject(i)
resultList.add(
App(
name = item.getString("name"),
slug = item.getString("slug"),
description = item.getString("description"),
download_counter = item.getInt("download_counter"),
status = item.getString("status"),
revision = item.getString("revision"),
size_of_zip = item.getInt("size_of_zip"),
size_of_content = item.getInt("size_of_content"),
category = item.getString("category")
)
)
}
} catch (e: JSONException) {
Log.e(TAG, "Error parsing JSON: ${e.message}")
throw HatcheryClientException(0)
}
return resultList
}
}
/*
* Copyright by the original author or authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.ccc.events.badge.card10.hatchery
import java.lang.Exception
class HatcheryClientException(val httpCode: Int) : Exception()
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_padding">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/container_header">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textStyle="bold"
android:id="@+id/label_name"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_name"
android:id="@+id/label_download_count"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_download_count"
android:id="@+id/label_content_size"/>
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/container_header"
android:layout_marginTop="@dimen/app_detail_description_margin"
android:id="@+id/label_description"/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list"/>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/default_list_item_height">
<TextView
android:id="@+id/label_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:paddingLeft="@dimen/default_list_item_margin_side"
android:textStyle="bold"/>
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/loading_dialog_padding">
<ProgressBar android:layout_width="@dimen/loading_dialog_progress"
android:layout_height="@dimen/loading_dialog_progress"
android:layout_gravity="center"
android:indeterminate="true"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/loading_dialog_text_margin"
android:text="@string/loading_dialog_loading"/>