package moe.yuuta.workmode import android.app.Activity import android.content.* 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.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.snackbar.Snackbar 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.ListMode import moe.yuuta.workmode.suspend.data.Status import moe.yuuta.workmode.suspend.data.SuspendedStorage import moe.yuuta.workmode.update.Update import moe.yuuta.workmode.update.UpdateChecker import moe.yuuta.workmode.utils.Utils import java.util.stream.Collectors class MainActivity : AppCompatActivity(), SwitchBar.OnSwitchChangeListener, View.OnClickListener, LicenseCheckerCallback { 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 val mStoppableGroup: StoppableGroup = StoppableGroup() 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() val filter = IntentFilter(AccessorStarter.ACTION_UPDATE_UI_STATE) filter.addAction(AccessorStarter.ACTION_UPDATE_UI_PROGRESS) registerReceiver(mUIUpdateReceiver, filter) scheduleUpdateChecking() lifecycle.addObserver(GPL(this, lifecycle, this)) setProgressUI(false) } override fun onSwitchChanged(switchView: Switch?, isChecked: Boolean) { SuspendedStorage(this).setStatus(if (isChecked) Status.ON else Status.OFF) scheduleApply() } /** * Apply settings which are stored in SuspendedStorage to OS */ private fun scheduleApply() { mStoppableGroup.add(AsyncSuspender(this).applyFromSettings(object : Callback { override fun onStart() { setProgressUI(true) } override fun onStop(success: Boolean, result: Unit?, e: Throwable?) { setProgressUI(false) displayUI() if (!success) { logger.e("Unable scheduleApply settings", e) if (Setup.FABRIC_ENABLE) Crashlytics.getInstance().core.logException(e) Toast.makeText(this@MainActivity, R.string.error_apply, Toast.LENGTH_LONG).show() } } })) } 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_PACKAGE_NAME)) { val newSet = data.getStringArrayExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME).toSet() logger.d("AR() $newSet") SuspendedStorage(this).setList(newSet) 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() { mStoppableGroup.stop() mAdapter.destroy() if (mSortDisplayStoppable != null) (mSortDisplayStoppable as Stoppable).stop() unregisterReceiver(mUIUpdateReceiver) super.onDestroy() } /** * Display the data from SuspendedStorage to UI */ private fun displayUI() { switchBar.removeOnSwitchChangeListener(this) switchBar.isChecked = SuspendedStorage(this).getStatus() == Status.ON switchBar.addOnSwitchChangeListener(this) tabLayout.removeOnTabSelectedListener(mSwitchListModeListener) tabLayout.getTabAt( when (SuspendedStorage(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(this@MainActivity).getList() .stream() .sorted { o1, o2 -> return@sorted sCollator.compare(packageManager.getApplicationLabel(packageManager.getApplicationInfo(o1, 0)).toString() , packageManager.getApplicationLabel(packageManager.getApplicationInfo(o2, 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(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) } } }) } private val mUIUpdateReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent == null) return 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 fun scheduleUpdateChecking() { mStoppableGroup.add(Async.beginTask(UpdateChecker(), object : Callback { override fun onStop(success: Boolean, result: Update?, e: Throwable?) { if (result == null) return if (result.version <= BuildConfig.VERSION_CODE) return if (!shouldOpenGooglePlay() && !result.altUrlEnabled && !result.altUrlForce) return Snackbar.make(findViewById(android.R.id.content), getString(R.string.update_available, result.name), Snackbar.LENGTH_LONG) .setAction(R.string.view) { val url = if (shouldOpenGooglePlay() && !result.altUrlForce) "https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}" else result.altUrl try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } catch (ignored: ActivityNotFoundException) {} } .show() } private fun shouldOpenGooglePlay(): Boolean = "com.android.vending" == this@MainActivity.packageManager.getInstallerPackageName(BuildConfig.APPLICATION_ID) })) } 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(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(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_check_update -> { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=${BuildConfig.APPLICATION_ID}"))) 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 -> { startActivityForResult(Intent(this, ApplicationPickerActivity::class.java) .putExtra(ApplicationPickerActivity.EXTRA_SELECTED_PACKAGE_NAME, SuspendedStorage(this).getList().toTypedArray()), 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(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(this).removeCrack("g_p_l") Policy.NOT_LICENSED -> SuspendedStorage(this).reportCrack("g_p_l", "n_p") Policy.RETRY -> SuspendedStorage(this).reportCrack("g_p_l", "rt") } } } 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 packageName = data[position] val icon = getIconFromMemoryCache(packageName) if (icon != null) { holder.icon.setImageDrawable(icon) } else { loadIcon(packageName, holder.itemView.context, holder.icon) } holder.title.text = context .packageManager .getApplicationLabel( context.packageManager.getApplicationInfo(packageName, 0) ) } 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) } 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() } }