aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt')
-rw-r--r--app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt222
1 files changed, 222 insertions, 0 deletions
diff --git a/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt
new file mode 100644
index 0000000..a5e3d0c
--- /dev/null
+++ b/app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt
@@ -0,0 +1,222 @@
+package moe.yuuta.workmode
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.util.LruCache
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.elvishew.xlog.Logger
+import com.elvishew.xlog.XLog
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import moe.yuuta.workmode.async.Async
+import moe.yuuta.workmode.async.Callback
+import moe.yuuta.workmode.async.Runnable
+import moe.yuuta.workmode.async.StoppableGroup
+import moe.yuuta.workmode.utils.Utils
+import java.util.stream.Collectors
+
+class ApplicationPickerActivity : AppCompatActivity() {
+ companion object {
+ const val EXTRA_SELECTED_PACKAGE_NAME = "moe.yuuta.workmode.ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME"
+ }
+ private val logger: Logger = XLog.tag("ApplicationPickerActivity").build()
+
+ private lateinit var mAdapter: Adapter
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ private lateinit var mProgressBar: ProgressBar
+ private lateinit var fab: FloatingActionButton
+
+ private fun setResultAndFinish(packageNames: Array<String>?) {
+ setResult(if (packageNames == null) Activity.RESULT_CANCELED else Activity.RESULT_OK,
+ Intent().putExtra(EXTRA_SELECTED_PACKAGE_NAME, packageNames ?: arrayOf()))
+ finish()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_application_picker)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ val recyclerView: RecyclerView = findViewById(R.id.recycler_apps)
+ fab = findViewById(R.id.fab_ok)
+ mProgressBar = findViewById(R.id.progress_load)
+ recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
+ mAdapter = Adapter()
+ recyclerView.adapter = mAdapter
+ fab.setOnClickListener {
+ setResultAndFinish(mAdapter.checked.toTypedArray())
+ }
+ load()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ android.R.id.home -> {
+ onBackPressed()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun load() {
+ mStoppableGroup.add(Async.beginTask(object : Runnable<List<SelectedApp>> {
+ override fun run(): List<SelectedApp> {
+ val selected = intent.getStringArrayExtra(EXTRA_SELECTED_PACKAGE_NAME) ?: arrayOf()
+ return packageManager.getInstalledApplications(0)
+ .stream()
+ .filter(Utils.buildGeneralApplicationInfoFilter(this@ApplicationPickerActivity))
+ .sorted(ApplicationInfo.DisplayNameComparator(packageManager))
+ .map {
+ return@map SelectedApp(it.packageName, selected.contains(it.packageName))
+ }
+ .collect(Collectors.toList())
+ }
+ }, object : Callback<List<SelectedApp>> {
+ override fun onStart() {
+ mProgressBar.visibility = View.VISIBLE
+ fab.visibility = View.GONE
+ }
+
+ override fun onStop(success: Boolean, result: List<SelectedApp>?, e: Throwable?) {
+ mProgressBar.visibility = View.GONE
+ fab.visibility = View.VISIBLE
+ if (success && result != null) {
+ display(result)
+ } else {
+ Toast.makeText(this@ApplicationPickerActivity,
+ R.string.error_load_applications, Toast.LENGTH_LONG)
+ .show()
+ if (e != null) {
+ logger.e("Load applications", e)
+ } else {
+ logger.e("Cannot load applications (no stacktrace)")
+ }
+ // Not sure if the toast will dismiss immediately
+ // setResultAndFinish()
+ }
+ }
+ }))
+ }
+
+ private fun display(result: List<SelectedApp>) {
+ val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = mAdapter.itemCount
+
+ override fun getNewListSize(): Int = result.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition]::class.java.name ==
+ result[newItemPosition]::class.java.name
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ mAdapter.data[oldItemPosition] ==
+ result[newItemPosition]
+ })
+ mAdapter.data = result
+ diff.dispatchUpdatesTo(mAdapter)
+ }
+
+ override fun onDestroy() {
+ mStoppableGroup.stop()
+ mAdapter.destroy()
+ super.onDestroy()
+ }
+
+ private class Adapter : RecyclerView.Adapter<Adapter.VH>() {
+ private val mIconMemoryCaches: LruCache<String, Drawable> =
+ LruCache(Runtime.getRuntime().maxMemory().toInt() / 5)
+ private val mStoppableGroup: StoppableGroup = StoppableGroup()
+ internal var data: List<SelectedApp> = listOf()
+ internal val checked: MutableSet<String> = mutableSetOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH =
+ VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application_select, parent, false))
+
+ override fun getItemCount(): Int = data.size
+
+ override fun onBindViewHolder(holder: VH, position: Int) {
+ val context = holder.itemView.context
+ val packageInfo = data[position]
+ val icon = getIconFromMemoryCache(packageInfo.packageName)
+ if (icon != null) {
+ holder.icon.setImageDrawable(icon)
+ } else {
+ loadIcon(packageInfo.packageName, holder.itemView.context, holder.icon)
+ }
+ holder.title.text = context
+ .packageManager
+ .getApplicationLabel(
+ context.packageManager.getApplicationInfo(packageInfo.packageName, 0)
+ )
+ if (packageInfo.selected) checked.add(packageInfo.packageName)
+ else checked.remove(packageInfo.packageName)
+ holder.checkBox.isChecked = checked.contains(packageInfo.packageName)
+ holder.checkBox.setOnClickListener {
+ val selected = holder.checkBox.isChecked
+ if (selected) checked.add(packageInfo.packageName)
+ else checked.remove(packageInfo.packageName)
+ }
+ }
+
+ class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ internal val icon: ImageView = itemView.findViewById(android.R.id.icon)
+ internal val title: TextView = itemView.findViewById(android.R.id.title)
+ internal val checkBox: CheckBox = itemView.findViewById(android.R.id.checkbox)
+ }
+
+ private fun addDrawableToMemoryCache(pkg: String?, icon: Drawable) {
+ if (getIconFromMemoryCache(pkg) == null) {
+ mIconMemoryCaches.put(pkg ?: "", icon)
+ }
+ }
+
+ private fun getIconFromMemoryCache(pkg: String?): Drawable? {
+ return mIconMemoryCaches.get(pkg ?: "")
+ }
+
+ private fun loadIcon(pkg: String, context: Context, imageView: ImageView) {
+ mStoppableGroup.add(Async.beginTask(object : Runnable<Drawable> {
+ override fun run(): Drawable {
+ var icon: Drawable?
+ try {
+ icon = context.packageManager
+ .getApplicationIcon(pkg)
+ } catch (ignore: PackageManager.NameNotFoundException) {
+ icon = null
+ }
+ if (icon == null) {
+ icon = ContextCompat.getDrawable(context, android.R.mipmap.sym_def_app_icon)!!
+ }
+ addDrawableToMemoryCache(pkg, icon)
+ return icon
+ }
+ }, object : Callback<Drawable> {
+ override fun onStop(success: Boolean, result: Drawable?, e: Throwable?) {
+ if (success && result != null) imageView.setImageDrawable(result)
+ }
+ }))
+ }
+
+ fun destroy() {
+ mStoppableGroup.stop()
+ }
+ }
+}
+
+private data class SelectedApp(
+ val packageName: String,
+ val selected: Boolean
+) \ No newline at end of file