From ea80e658cf25283035faaab5c4618bddf7809e61 Mon Sep 17 00:00:00 2001 From: machiav3lli Date: Thu, 23 Dec 2021 01:41:03 +0100 Subject: [PATCH] Add: The main activity [new UI] --- .../looker/droidify/database/CursorOwner.kt | 2 +- .../droidify/ui/activities/MainActivityX.kt | 308 ++++++++++++++++++ .../ui/viewmodels/MainActivityViewModelX.kt | 10 + src/main/res/layout/activity_main_x.xml | 8 +- 4 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt create mode 100644 src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt diff --git a/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt index 24928f8e..818721bf 100644 --- a/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt +++ b/src/main/kotlin/com/looker/droidify/database/CursorOwner.kt @@ -45,7 +45,7 @@ class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks { fun onCursorData(request: Request, cursor: Cursor?) } - private data class ActiveRequest( + data class ActiveRequest( val request: Request, val callback: Callback?, val cursor: Cursor?, diff --git a/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt new file mode 100644 index 00000000..66c4f6dc --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/activities/MainActivityX.kt @@ -0,0 +1,308 @@ +package com.looker.droidify.ui.activities + +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.os.Bundle +import android.view.* +import android.view.inputmethod.InputMethodManager +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.appbar.MaterialToolbar +import com.looker.droidify.BuildConfig +import com.looker.droidify.ContextWrapperX +import com.looker.droidify.R +import com.looker.droidify.content.Preferences +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.Database +import com.looker.droidify.database.QueryLoader +import com.looker.droidify.databinding.ActivityMainXBinding +import com.looker.droidify.installer.AppInstaller +import com.looker.droidify.screen.* +import com.looker.droidify.service.Connection +import com.looker.droidify.service.SyncService +import com.looker.droidify.ui.fragments.MainNavFragmentX +import com.looker.droidify.ui.fragments.Source +import com.looker.droidify.ui.viewmodels.MainActivityViewModelX +import com.looker.droidify.utility.extension.android.Android +import com.looker.droidify.utility.extension.text.nullIfEmpty +import kotlinx.coroutines.launch + +class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks { + companion object { + const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES" + const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL" + const val EXTRA_CACHE_FILE_NAME = + "${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME" + } + + sealed class SpecialIntent { + object Updates : SpecialIntent() + class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent() + } + + lateinit var binding: ActivityMainXBinding + lateinit var toolbar: MaterialToolbar + lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var navController: NavController + private val viewModel: MainActivityViewModelX by viewModels() + + private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ -> + navController.currentDestination?.let { + val source = Source.values()[when (it.id) { + R.id.latestTab -> 1 + R.id.installedTab -> 2 + else -> 0 // R.id.exploreTab + }] + updateUpdateNotificationBlocker(source) + } + }) + + lateinit var cursorOwner: CursorOwner + private set + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration)) + super.onCreate(savedInstanceState) + + binding = ActivityMainXBinding.inflate(layoutInflater) + binding.lifecycleOwner = this + toolbar = binding.toolbar + + if (savedInstanceState == null && (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) { + handleIntent(intent) + } + setContentView(binding.root) + + if (savedInstanceState == null) { + cursorOwner = CursorOwner() + supportFragmentManager.beginTransaction() + .add(cursorOwner, CursorOwner::class.java.name) + .commit() + } else { + cursorOwner = supportFragmentManager + .findFragmentByTag(CursorOwner::class.java.name) as CursorOwner + } + + setSupportActionBar(toolbar) + + if (Android.sdk(28) && !Android.Device.isHuaweiEmui) { + toolbar.menu.setGroupDividerEnabled(true) + } + + toolbar.isFocusableInTouchMode = true + binding.collapsingToolbar.title = getString(R.string.application_name) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_content) as NavHostFragment + navController = navHostFragment.navController + binding.bottomNavigation.setupWithNavController(navController) + + appBarConfiguration = AppBarConfiguration( + setOf(R.id.exploreTab, R.id.latestTab, R.id.installedTab) + ) + setupActionBarWithNavController(navController, appBarConfiguration) + + supportFragmentManager.addFragmentOnAttachListener { _, _ -> + hideKeyboard() + } + } + + override fun onStart() { + super.onStart() + syncConnection.bind(this) + } + + override fun onSupportNavigateUp(): Boolean { + return navController.navigateUp(appBarConfiguration) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_main, menu) + return super.onCreateOptionsMenu(menu) + } + + private fun hideKeyboard() { + (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager) + ?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0) + } + + fun syncManual(item: MenuItem) { + syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL) + } + + fun navigateSettings(item: MenuItem) { + navigateSettings() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private 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 + } + } + } + + private fun handleSpecialIntent(specialIntent: SpecialIntent) { + when (specialIntent) { + is SpecialIntent.Updates -> navController.navigate(R.id.installedTab) + is SpecialIntent.Install -> { + val packageName = specialIntent.packageName + if (!packageName.isNullOrEmpty()) { + lifecycleScope.launch { + specialIntent.cacheFileName?.let { + AppInstaller.getInstance(this@MainActivityX) + ?.defaultInstaller?.install(packageName, it) + } + } + } + Unit + } + }::class + } + + private fun handleIntent(intent: Intent?) { + when (intent?.action) { + Intent.ACTION_VIEW -> { + val packageName = intent.packageName + if (!packageName.isNullOrEmpty()) { + navigateProduct(packageName) + } + } + ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates) + ACTION_INSTALL -> handleSpecialIntent( + SpecialIntent.Install( + intent.packageName, + intent.getStringExtra(EXTRA_CACHE_FILE_NAME) + ) + ) + } + } + + override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(ContextWrapperX.wrap(newBase)) + } + + internal fun navigateProduct(packageName: String) { + // TODO + } + + private fun navigateSettings() { + // TODO + } + + private fun updateUpdateNotificationBlocker(activeSource: Source) { + val blockerFragment = if (activeSource == Source.UPDATES) { + supportFragmentManager.fragments.asSequence().mapNotNull { it as? MainNavFragmentX } + .find { it.source == activeSource } + } else { + null + } + syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment) + } + + fun attachCursorOwner(callback: CursorOwner.Callback, request: CursorOwner.Request) { + val oldActiveRequest = viewModel.activeRequests[request.id] + if (oldActiveRequest?.callback != null && + oldActiveRequest.callback != callback && oldActiveRequest.cursor != null + ) { + oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null) + } + val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) { + callback.onCursorData(request, oldActiveRequest.cursor) + oldActiveRequest.cursor + } else { + null + } + viewModel.activeRequests[request.id] = CursorOwner.ActiveRequest(request, callback, cursor) + if (cursor == null) { + LoaderManager.getInstance(this).restartLoader(request.id, null, this) + } + } + + + fun detachCursorOwner(callback: CursorOwner.Callback) { + for (id in viewModel.activeRequests.keys) { + val activeRequest = viewModel.activeRequests[id]!! + if (activeRequest.callback == callback) { + viewModel.activeRequests[id] = activeRequest.copy(callback = null) + } + } + } + + override fun onCreateLoader(id: Int, args: Bundle?): Loader { + val request = viewModel.activeRequests[id]!!.request + return QueryLoader(this) { + when (request) { + is CursorOwner.Request.ProductsAvailable -> Database.ProductAdapter + .query( + installed = false, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order, + signal = it + ) + is CursorOwner.Request.ProductsInstalled -> Database.ProductAdapter + .query( + installed = true, + updates = false, + searchQuery = request.searchQuery, + section = request.section, + order = request.order, + signal = it + ) + is CursorOwner.Request.ProductsUpdates -> Database.ProductAdapter + .query( + installed = true, + updates = true, + searchQuery = request.searchQuery, + section = request.section, + order = request.order, + signal = it + ) + is CursorOwner.Request.Repositories -> Database.RepositoryAdapter.query(it) + } + } + } + + override fun onLoadFinished(loader: Loader, data: Cursor?) { + val activeRequest = viewModel.activeRequests[loader.id] + if (activeRequest != null) { + viewModel.activeRequests[loader.id] = activeRequest.copy(cursor = data) + activeRequest.callback?.onCursorData(activeRequest.request, data) + } + } + + override fun onLoaderReset(loader: Loader) = onLoadFinished(loader, null) +} diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt new file mode 100644 index 00000000..bc3fb11c --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainActivityViewModelX.kt @@ -0,0 +1,10 @@ +package com.looker.droidify.ui.viewmodels + +import androidx.lifecycle.ViewModel +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.ui.activities.MainActivityX + +class MainActivityViewModelX() : ViewModel() { + + val activeRequests = mutableMapOf() +} \ No newline at end of file diff --git a/src/main/res/layout/activity_main_x.xml b/src/main/res/layout/activity_main_x.xml index 8849cdba..fc4c1cad 100644 --- a/src/main/res/layout/activity_main_x.xml +++ b/src/main/res/layout/activity_main_x.xml @@ -37,17 +37,21 @@ android:id="@+id/toolbar" style="?attr/toolbarStyle" android:layout_width="match_parent" - android:layout_height="?actionBarSize" /> + android:layout_height="?actionBarSize" + app:menu="@menu/menu_main" /> + app:defaultNavHost="true" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:navGraph="@navigation/navigation_graph_main" />