diff options
Diffstat (limited to 'app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt')
-rw-r--r-- | app/src/main/java/moe/yuuta/workmode/ApplicationPickerActivity.kt | 222 |
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 |