package moe.yuuta.workmode import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable 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.suspend.AsyncSuspender import moe.yuuta.workmode.suspend.data.TransferableSuspendedApp import java.util.* import java.util.concurrent.CompletableFuture import java.util.stream.Collectors class ApplicationPickerActivity : AppCompatActivity() { companion object { const val EXTRA_SELECTED_PACKAGES = "moe.yuuta.workmode.ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES" } private val logger: Logger = XLog.tag("ApplicationPickerActivity").build() private lateinit var mAdapter: Adapter private lateinit var mLoadAppsFuture: CompletableFuture> private lateinit var mProgressBar: ProgressBar private lateinit var fab: FloatingActionButton private fun setResultAndFinish(packages: List?) { val selected = arrayListOf() selected.addAll(packages ?: listOf()) selected.stream() .forEach { it.trimData() } setResult(if (packages == null) Activity.RESULT_CANCELED else Activity.RESULT_OK, Intent().putExtra(EXTRA_SELECTED_PACKAGES, selected)) 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.data.stream() .filter { return@filter it.selected } .map { return@map it.packageInfo } .collect(Collectors.toList())) } load() } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { onBackPressed() true } else -> super.onOptionsItemSelected(item) } } private fun load() { if (::mLoadAppsFuture.isInitialized && !mLoadAppsFuture.isDone) { mLoadAppsFuture.cancel(true) } mLoadAppsFuture = AsyncSuspender(this).getInstalledApplicationsAcrossUser(0) mProgressBar.visibility = View.VISIBLE fab.visibility = View.GONE mLoadAppsFuture .handle { result, e -> runOnUiThread { mProgressBar.visibility = View.GONE fab.visibility = View.VISIBLE if (e == null && result != null) { val selected = intent.getParcelableArrayListExtra(EXTRA_SELECTED_PACKAGES) ?: listOf() logger.d("Selected: $selected") if (BuildConfig.DEBUG) logger.d("Installed: $result") display(result.stream() .map { return@map SelectedApp(it, selected.any { a -> return@any a.essentiallyEqual(it) }) } .collect(Collectors.toList())) } 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) { 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.toMutableList() diff.dispatchUpdatesTo(mAdapter) } override fun onDestroy() { if (::mLoadAppsFuture.isInitialized && !mLoadAppsFuture.isDone) { mLoadAppsFuture.cancel(true) } mAdapter.destroy() super.onDestroy() } private class Adapter : RecyclerView.Adapter() { private val mIconMemoryCaches: LruCache = LruCache(Runtime.getRuntime().maxMemory().toInt() / 5) private val mStoppableGroup: StoppableGroup = StoppableGroup() internal var data: MutableList = mutableListOf() 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 = if (packageInfo.packageInfo.icon != null) BitmapDrawable(context.resources, packageInfo.packageInfo.icon) else getIconFromMemoryCache(packageInfo.packageInfo.packageName) if (icon != null) { holder.icon.setImageDrawable(icon) } else { loadIcon(packageInfo.packageInfo.packageName, holder.itemView.context, holder.icon) } holder.title.text = if (packageInfo.packageInfo.label != null) packageInfo.packageInfo.label else context .packageManager .getApplicationLabel( context.packageManager.getApplicationInfo(packageInfo.packageInfo.packageName, 0) ) holder.summary.text = context.getString(R.string.app_list_user_template, packageInfo.packageInfo.userId) holder.checkBox.isChecked = packageInfo.selected holder.checkBox.setOnClickListener { packageInfo.selected = holder.checkBox.isChecked Collections.replaceAll(data, data.stream() .filter { return@filter it.packageInfo == packageInfo.packageInfo } .findFirst() .get(), packageInfo) } } 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 summary: TextView = itemView.findViewById(android.R.id.summary) 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 { 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 { 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 packageInfo: TransferableSuspendedApp, var selected: Boolean )