package moe.yuuta.workmode import android.app.* import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.service.quicksettings.TileService import android.util.Log import android.util.LruCache import android.view.* import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.settings.widget.SwitchBar import com.crashlytics.android.Crashlytics import com.elvishew.xlog.Logger import com.elvishew.xlog.XLog import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.tabs.TabLayout import moe.yuuta.gplicense.LicenseCheckerCallback import moe.yuuta.gplicense.Policy import moe.yuuta.workmode.access.AccessorStarter import moe.yuuta.workmode.async.* import moe.yuuta.workmode.gpl.GPL import moe.yuuta.workmode.suspend.AsyncSuspender import moe.yuuta.workmode.suspend.SuspendTile import moe.yuuta.workmode.suspend.data.* import moe.yuuta.workmode.update.LifecycleUpdateChecker import moe.yuuta.workmode.utils.Utils import java.util.concurrent.CompletableFuture import java.util.stream.Collectors class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View.OnClickListener, LicenseCheckerCallback, moe.yuuta.workmode.update.Callback, LifecycleUIUpdateReceiver.Callback { private val logger: Logger = XLog.tag("MainActivity").build() companion object { const val RC_PICK = 1 } private lateinit var mAdapter: Adapter private lateinit var switchBar: SwitchBar private lateinit var progressBar: ProgressBar private lateinit var tabLayout: TabLayout private lateinit var welcomeTip: TextView private lateinit var mCheckUpdateObserver: LifecycleUpdateChecker private lateinit var mApplyFuture: CompletableFuture private var mSortDisplayStoppable: Stoppable? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) switchBar = findViewById(R.id.switch_bar) switchBar.show() welcomeTip = findViewById(R.id.welcome_tip) progressBar = findViewById(R.id.progress_apply) tabLayout = findViewById(R.id.tab) tabLayout.addTab(tabLayout.newTab().setTag("blacklist").setText(R.string.blacklist)) tabLayout.addTab(tabLayout.newTab().setTag("whitelist").setText(R.string.whitelist)) val fab: FloatingActionButton = findViewById(R.id.fab_add) fab.setOnClickListener(this) val recyclerView: RecyclerView = findViewById(R.id.recycler_apps) recyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) mAdapter = Adapter() recyclerView.adapter = mAdapter displayUI() mCheckUpdateObserver = LifecycleUpdateChecker(this, this) lifecycle.addObserver(LifecycleUIUpdateReceiver(this, this)) lifecycle.addObserver(mCheckUpdateObserver) lifecycle.addObserver(GPL(this, lifecycle, this)) setProgressUI(false) } override fun onSwitchChanged(switchView: Switch?, isChecked: Boolean) { logger.d("onSwitchChanged $isChecked") SuspendedStorage.get(this).setStatus(if (isChecked) Status.ON else Status.OFF) scheduleApply() } /** * Apply settings which are stored in SuspendedStorage to OS */ private fun scheduleApply() { if (::mApplyFuture.isInitialized && !mApplyFuture.isDone) { mApplyFuture.cancel(true) } mApplyFuture = AsyncSuspender(this) .applyFromSettings() setProgressUI(true) mApplyFuture .exceptionally { logger.e("Unable scheduleApply settings", it) if (Setup.FABRIC_ENABLE) Crashlytics.getInstance().core.logException(it) Toast.makeText(this@MainActivity, R.string.error_apply, Toast.LENGTH_LONG).show() } .thenRun { setProgressUI(false) displayUI() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { RC_PICK -> { if (resultCode == Activity.RESULT_OK && data != null && data.hasExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES)) { val newSet = data.getParcelableArrayListExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES)?.toSet() ?: setOf() logger.d("AR() $newSet") SuspendedStorage.get(this).setList(newSet.stream() .map { return@map it.trimToPersistable() } .collect(Collectors.toList()).toSet()) scheduleApply() } } } } private fun setProgressUI(showProgress: Boolean) { TileService.requestListeningState(this, ComponentName(this, SuspendTile::class.java)) Utils.setViewTreeEnable(findViewById(android.R.id.content), !showProgress) Utils.makeTabLayoutDisable(tabLayout, !showProgress) progressBar.visibility = if (showProgress) View.VISIBLE else View.GONE } override fun onDestroy() { if (::mApplyFuture.isInitialized && !mApplyFuture.isDone) { mApplyFuture.cancel(true) } mAdapter.destroy() if (mSortDisplayStoppable != null) (mSortDisplayStoppable as Stoppable).stop() super.onDestroy() } /** * Display the data from SuspendedStorage to UI */ private fun displayUI() { switchBar.removeOnSwitchChangeListener(this) val checked = SuspendedStorage.get(this).getStatus() == Status.ON // logger.d("code $code B-C") if (checked != switchBar.isChecked) switchBar.isChecked = checked // logger.d("code $code C") switchBar.addOnSwitchChangeListener(this) tabLayout.removeOnTabSelectedListener(mSwitchListModeListener) tabLayout.getTabAt( when (SuspendedStorage.get(this).getListMode()) { ListMode.BLACKLIST -> 0 ListMode.WHITELIST -> 1 } )!!.select() tabLayout.addOnTabSelectedListener(mSwitchListModeListener) if (mSortDisplayStoppable != null) { val stoppable = mSortDisplayStoppable as Stoppable stoppable.stop() mSortDisplayStoppable = null } mSortDisplayStoppable = Async.beginTask(object : Runnable> { override fun run(): List? { val sCollator = java.text.Collator.getInstance() return SuspendedStorage.get(this@MainActivity).getList() .stream() .sorted { o1, o2 -> // TODO val canSafelyLoadAppInfoForO1 = Utils.canSafelyLoadAppInfo(o1, this@MainActivity) val canSafelyLoadAppInfoForO2 = Utils.canSafelyLoadAppInfo(o2, this@MainActivity) if (!canSafelyLoadAppInfoForO1 || !canSafelyLoadAppInfoForO2) { if (o1.userId > o2.userId) { return@sorted 1 } else if (o1.userId < o2.userId) { return@sorted -1 } else { return@sorted 0 } } if ((canSafelyLoadAppInfoForO1 && !canSafelyLoadAppInfoForO2) || (!canSafelyLoadAppInfoForO1 && canSafelyLoadAppInfoForO2)) { // A unsafe app is comparing to a safe app, just put the unsafe one in under the safe one. return@sorted 1 } return@sorted sCollator.compare(packageManager.getApplicationLabel(packageManager.getApplicationInfo(o1.packageName, 0)).toString() , packageManager.getApplicationLabel(packageManager.getApplicationInfo(o2.packageName, 0))) } .collect(Collectors.toList()) } }, object : Callback> { override fun onStop(success: Boolean, result: List?, e: Throwable?) { if (result != null) { 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) if (result.isEmpty()) { welcomeTip.setText(when (SuspendedStorage.get(this@MainActivity).getListMode()) { ListMode.BLACKLIST -> R.string.blacklist_welcome ListMode.WHITELIST -> R.string.whitelist_welcome }) welcomeTip.visibility = View.VISIBLE } else { welcomeTip.visibility = View.GONE } } else { if (e == null) logger.e("Unable to sort data") else logger.e("Unable to sort data with error", e) } } }) } override fun updateUI(intent: Intent) { when (intent.action) { AccessorStarter.ACTION_UPDATE_UI_STATE -> { displayUI() } AccessorStarter.ACTION_UPDATE_UI_PROGRESS -> { logger.d("Updating progress from receiver") setProgressUI(intent.getBooleanExtra(AccessorStarter.EXTRA_SHOW_PROGRESS, false)) } } } private val mSwitchListModeListener = object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { } override fun onTabUnselected(tab: TabLayout.Tab?) { } override fun onTabSelected(tab: TabLayout.Tab) { when (tab.tag) { "blacklist" -> { AlertDialog.Builder(this@MainActivity) .setTitle(R.string.blacklist_toggle_title) .setMessage(R.string.blacklist_toggle_information) .setCancelable(false) .setNegativeButton(android.R.string.cancel) { _, _ -> tabLayout.removeOnTabSelectedListener(this) tabLayout.getTabAt(1)?.select() tabLayout.addOnTabSelectedListener(this) } .setPositiveButton(android.R.string.ok) { _, _ -> SuspendedStorage.get(this@MainActivity).setListMode(ListMode.BLACKLIST) scheduleApply() } .show() } "whitelist" -> { AlertDialog.Builder(this@MainActivity) .setTitle(R.string.whitelist_toggle_title) .setMessage(R.string.whitelist_toggle_information) .setCancelable(false) .setNegativeButton(android.R.string.cancel) { _, _ -> tabLayout.removeOnTabSelectedListener(this) tabLayout.getTabAt(0)?.select() tabLayout.addOnTabSelectedListener(this) } .setPositiveButton(android.R.string.ok) { _, _ -> SuspendedStorage.get(this@MainActivity).setListMode(ListMode.WHITELIST) scheduleApply() } .show() } } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_get_logs -> { return try { startActivity(Intent.createChooser(Setup.buildShareLogsIntent(this), getString(R.string.get_logs))) true } catch (e: Exception) { try { logger.e("Share logs", e) } catch (ignored: Exception) {} System.err.println("Unable to share logs, ${Log.getStackTraceString(e)}") true } } R.id.action_feedback -> { val intent = Intent(Intent.ACTION_SENDTO) .setData(Uri.parse("mailto:")) intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.feedback_subject)) intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("android-apps@yuuta.moe")) startActivity(Intent.createChooser(intent, getString(R.string.feedback))) return true } R.id.action_about -> { AboutFragment().show(supportFragmentManager, "About") return true } R.id.action_check_update -> { mCheckUpdateObserver.start() return true } R.id.action_oss -> { startActivity(Intent(this, OssLicensesMenuActivity::class.java)) return true } else -> return super.onOptionsItemSelected(item) } } override fun onClick(v: View?) { if (v == null) return when (v.id) { R.id.fab_add -> { val selected = arrayListOf() selected.addAll(SuspendedStorage.get(this).getList().stream() .map { return@map it.copyToSimpleTransferableInfo() } .collect(Collectors.toList())) startActivityForResult(Intent(this, ApplicationPickerActivity::class.java) .putExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGES, selected), RC_PICK) } } } override fun allow(reason: Int) = proceedLicensing(reason) override fun dontAllow(reason: Int) = proceedLicensing(reason) override fun applicationError(errorCode: Int) { logger.e("StatusException: $errorCode") SuspendedStorage.get(this).reportCrack("a_e", "co: $errorCode") } private fun proceedLicensing(reason: Int) { logger.d("Status: ${ when (reason) { Policy.LICENSED -> "OK" Policy.NOT_LICENSED -> "Fail" Policy.RETRY -> "Unknown" else -> "? $reason" } }") when (reason) { Policy.LICENSED -> SuspendedStorage.get(this).removeCrack("g_p_l") Policy.NOT_LICENSED -> SuspendedStorage.get(this).reportCrack("g_p_l", "n_p") Policy.RETRY -> SuspendedStorage.get(this).reportCrack("g_p_l", "rt") } } override fun showToast(name: String, url: String) { logger.d("Update available: $name ($url)") val manager = getSystemService(NotificationManager::class.java) manager.createNotificationChannel(NotificationChannel("update", getString(R.string.notification_channel_update), NotificationManager.IMPORTANCE_HIGH)) manager.cancel(0) manager.notify(0, Notification.Builder(this, "update") .setContentTitle(getString(R.string.notification_title_update, name)) .setContentText(getString(R.string.notification_text_update_summary)) .setContentIntent(PendingIntent.getActivity(this, 1, Intent(Intent.ACTION_VIEW, Uri.parse(url)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_UPDATE_CURRENT)) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_work_24dp) .setShowWhen(true) .setStyle(Notification.BigTextStyle() .bigText(getString(R.string.notification_text_update))) .build()) } } private class Adapter : RecyclerView.Adapter() { private val mIconMemoryCaches: LruCache = LruCache(Runtime.getRuntime().maxMemory().toInt() / 5) private val mStoppableGroup: StoppableGroup = StoppableGroup() internal var data: List = listOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH = VH(LayoutInflater.from(parent.context).inflate(R.layout.item_application, parent, false)) override fun getItemCount(): Int = data.size override fun onBindViewHolder(holder: VH, position: Int) { val context = holder.itemView.context val packageInfo = data[position] // TODO if (Utils.canSafelyLoadAppInfo(packageInfo, context)) { val icon = getIconFromMemoryCache(packageInfo.packageName) if (icon != null) { holder.icon.setImageDrawable(icon) } else { loadIcon(packageInfo.packageName, holder.itemView.context, holder.icon) } } holder.title.text = if (Utils.canSafelyLoadAppInfo(packageInfo, context)) context .packageManager .getApplicationLabel( context.packageManager.getApplicationInfo(packageInfo.packageName, 0) ) else packageInfo.packageName holder.summary.text = context.getString(R.string.app_list_user_template, packageInfo.userId) } 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) } 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() } } class AboutFragment : DialogFragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.dialog_about, container, false) view.findViewById(R.id.text_version).text = BuildConfig.VERSION_NAME return view } }