package com.michatec.store.screen import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Parcel import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout import android.widget.Toolbar import androidx.activity.OnBackPressedCallback import androidx.core.os.BundleCompat import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import com.michatec.store.BuildConfig import com.michatec.store.R import com.michatec.store.content.Cache import com.michatec.store.content.Preferences import com.michatec.store.database.CursorOwner import com.michatec.store.utility.KParcelable import com.michatec.store.utility.Utils import com.michatec.store.utility.extension.android.* import com.michatec.store.utility.extension.resources.* import com.michatec.store.utility.extension.text.* import org.woheller69.freeDroidWarn.FreeDroidWarn abstract class ScreenActivity: FragmentActivity() { companion object { private const val STATE_FRAGMENT_STACK = "fragmentStack" } sealed class SpecialIntent { object Updates: SpecialIntent() class Install(val packageName: String?, val cacheFileName: String?): SpecialIntent() } private class FragmentStackItem(val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?): KParcelable { override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(className) dest.writeByte(if (arguments != null) 1 else 0) arguments?.writeToParcel(dest, flags) dest.writeByte(if (savedState != null) 1 else 0) savedState?.writeToParcel(dest, flags) } companion object { @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { val className = it.readString()!! val arguments = if (it.readByte().toInt() == 0) null else Bundle.CREATOR.createFromParcel(it) arguments?.classLoader = ScreenActivity::class.java.classLoader val savedState = if (it.readByte().toInt() == 0) null else Fragment.SavedState.CREATOR.createFromParcel(it) FragmentStackItem(className, arguments, savedState) } } } lateinit var cursorOwner: CursorOwner private set private val fragmentStack = mutableListOf() private val currentFragment: Fragment? get() { supportFragmentManager.executePendingTransactions() return supportFragmentManager.findFragmentById(R.id.main_content) } override fun attachBaseContext(base: Context) { super.attachBaseContext(Utils.configureLocale(base)) } override fun onCreate(savedInstanceState: Bundle?) { setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration)) super.onCreate(savedInstanceState) FreeDroidWarn.showWarningOnUpgrade(this, BuildConfig.VERSION_CODE) WindowCompat.setDecorFitsSystemWindows(window, false) addContentView(FrameLayout(this).apply { id = R.id.main_content }, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) if (savedInstanceState == null) { cursorOwner = CursorOwner() supportFragmentManager.beginTransaction() .add(cursorOwner, CursorOwner::class.java.name) .commit() } else { cursorOwner = supportFragmentManager .findFragmentByTag(CursorOwner::class.java.name) as CursorOwner } savedInstanceState?.let { BundleCompat.getParcelableArrayList(it, STATE_FRAGMENT_STACK, FragmentStackItem::class.java) ?.let { list -> fragmentStack += list } } if (savedInstanceState == null) { replaceFragment(TabsFragment(), null) if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { handleIntent(intent) } } onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { val currentFragment = currentFragment if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) { hideKeyboard() if (!popFragment()) { isEnabled = false onBackPressedDispatcher.onBackPressed() isEnabled = true } } } }) supportFragmentManager.addFragmentOnAttachListener { _, _ -> hideKeyboard() } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack)) } private fun replaceFragment(fragment: Fragment, open: Boolean?) { if (open != null) { currentFragment?.view?.translationZ = (if (open) -1f else 1f) } supportFragmentManager .beginTransaction() .apply { if (open != null) { setCustomAnimations(if (open) R.animator.slide_in else 0, if (open) R.animator.slide_in_keep else R.animator.slide_out) } } .replace(R.id.main_content, fragment) .commit() } private fun pushFragment(fragment: Fragment) { currentFragment?.let { fragmentStack.add(FragmentStackItem(it::class.java.name, it.arguments, supportFragmentManager.saveFragmentInstanceState(it))) } replaceFragment(fragment, true) } private fun popFragment(): Boolean { return fragmentStack.isNotEmpty() && run { val stackItem = fragmentStack.removeAt(fragmentStack.size - 1) val fragment = supportFragmentManager.fragmentFactory.instantiate(classLoader, stackItem.className) stackItem.arguments?.let(fragment::setArguments) stackItem.savedState?.let(fragment::setInitialSavedState) replaceFragment(fragment, false) true } } private fun hideKeyboard() { (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager) ?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0) } internal fun onToolbarCreated(toolbar: Toolbar) { if (fragmentStack.isNotEmpty()) { toolbar.navigationIcon = toolbar.context.getDrawableFromAttr(android.R.attr.homeAsUpIndicator) toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } } } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleIntent(intent) } protected val Intent.packageName: String? get() { val uri = data return when { uri?.scheme == "package" || uri?.scheme == "fdroid.app" -> { uri.schemeSpecificPart?.nullIfEmpty() } uri?.scheme == "market" && uri.host == "details" -> { uri.getQueryParameter("id")?.nullIfEmpty() } uri != null && uri.scheme in setOf("http", "https") -> { val host = uri.host.orEmpty() if (host == "f-droid.org" || host.endsWith(".f-droid.org")) { uri.lastPathSegment?.nullIfEmpty() } else { null } } else -> { null } } } protected fun handleSpecialIntent(specialIntent: SpecialIntent) { when (specialIntent) { is SpecialIntent.Updates -> { if (currentFragment !is TabsFragment) { fragmentStack.clear() replaceFragment(TabsFragment(), true) } val tabsFragment = currentFragment as TabsFragment tabsFragment.selectUpdates() } is SpecialIntent.Install -> { val packageName = specialIntent.packageName if (!packageName.isNullOrEmpty()) { val fragment = currentFragment if (fragment !is ProductFragment || fragment.packageName != packageName) { pushFragment(ProductFragment(packageName)) } specialIntent.cacheFileName?.let(::startPackageInstaller) } Unit } }::class } open fun handleIntent(intent: Intent?) { when (intent?.action) { Intent.ACTION_VIEW -> { val packageName = intent.packageName if (!packageName.isNullOrEmpty()) { val fragment = currentFragment if (fragment !is ProductFragment || fragment.packageName != packageName) { pushFragment(ProductFragment(packageName)) } } } } } internal fun startPackageInstaller(cacheFileName: String) { val (uri, flags) = if (Android.sdk(24)) { Pair(Cache.getReleaseUri(this, cacheFileName), Intent.FLAG_GRANT_READ_URI_PERMISSION) } else { Pair(Uri.fromFile(Cache.getReleaseFile(this, cacheFileName)), 0) } startActivity(Intent(Intent.ACTION_VIEW) .setDataAndType(uri, "application/vnd.android.package-archive") .addFlags(flags or Intent.FLAG_ACTIVITY_NEW_TASK)) } internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName)) internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) internal fun navigatePreferences() = pushFragment(PreferencesFragment()) internal fun navigateAddRepository() = pushFragment(EditRepositoryFragment(null)) internal fun navigateRepository(repositoryId: Long) = pushFragment(RepositoryFragment(repositoryId)) internal fun navigateEditRepository(repositoryId: Long) = pushFragment(EditRepositoryFragment(repositoryId)) }