Refactor progress bar handling to BaseActivity

This commit is contained in:
2dust
2026-01-11 17:06:11 +08:00
parent b71614a9cd
commit 3f1b741d9d
13 changed files with 161 additions and 66 deletions
@@ -246,7 +246,7 @@ class BackupActivity : BaseActivity() {
return
}
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
var tempFile: File? = null
@@ -283,7 +283,7 @@ class BackupActivity : BaseActivity() {
} catch (_: Exception) {
}
withContext(Dispatchers.Main) {
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -296,7 +296,7 @@ class BackupActivity : BaseActivity() {
return
}
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
var target: File? = null
@@ -328,7 +328,7 @@ class BackupActivity : BaseActivity() {
} catch (_: Exception) {
}
withContext(Dispatchers.Main) {
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -14,6 +14,7 @@ import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.v2ray.ang.R
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.helper.CustomDividerItemDecoration
@@ -21,7 +22,21 @@ import com.v2ray.ang.util.MyContextWrapper
import com.v2ray.ang.util.Utils
/**
* BaseActivity provides common helpers and UI wiring used across the app's activities.
*
* Responsibilities:
* - Inflate a shared base layout that contains a toolbar and a content container.
* - Provide convenient overloads of `setContentViewWithToolbar` to attach child layouts or
* view-binding roots into the base container and initialize the toolbar.
* - Expose a global in-layout `ProgressBar` (cached) with `showLoading()` / `hideLoading()` helpers.
* - Provide a helper to add a custom divider to RecyclerViews.
* - Wrap base context according to user locale settings.
*/
abstract class BaseActivity : AppCompatActivity() {
// Progress indicator that sits at the bottom of the toolbar
private var progressBar: LinearProgressIndicator? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@@ -32,6 +47,15 @@ abstract class BaseActivity : AppCompatActivity() {
}
}
/**
* Handle action bar item selections.
*
* Currently this handles the home/up button by delegating to the activity's
* onBackPressedDispatcher to provide consistent back navigation behavior.
*
* @param item the selected menu item
* @return true if the event was handled, otherwise delegates to the superclass
*/
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> {
// Handles the home button press by delegating to the onBackPressedDispatcher.
@@ -43,17 +67,29 @@ abstract class BaseActivity : AppCompatActivity() {
else -> super.onOptionsItemSelected(item)
}
/**
* Wrap the base context with the user's locale settings.
*
* This ensures resources are loaded using the configured locale.
*
* @param newBase the original base context to wrap
*/
override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(MyContextWrapper.wrap(newBase ?: return, SettingsManager.getLocale()))
}
/**
* Adds a custom divider to a RecyclerView.
* Adds a custom divider drawable to the provided RecyclerView.
*
* @param recyclerView The target RecyclerView to which the divider will be added.
* @param context The context used to access resources.
* @param drawableResId The resource ID of the drawable to be used as the divider.
* @param orientation The orientation of the divider (DividerItemDecoration.VERTICAL or DividerItemDecoration.HORIZONTAL).
* This is a convenience helper that constructs a [CustomDividerItemDecoration]
* using the given drawable resource id and adds it to the RecyclerView.
*
* @param recyclerView the target RecyclerView
* @param context the context used to resolve resources (may be activity or application context)
* @param drawableResId the drawable resource id to use as the divider
* @param orientation one of [DividerItemDecoration.VERTICAL] or [DividerItemDecoration.HORIZONTAL]
*
* @throws IllegalArgumentException if the drawable resource cannot be found
*/
protected fun addCustomDividerToRecyclerView(recyclerView: RecyclerView, context: Context?, drawableResId: Int, orientation: Int = DividerItemDecoration.VERTICAL) {
// Get the drawable from resources
@@ -67,6 +103,17 @@ abstract class BaseActivity : AppCompatActivity() {
recyclerView.addItemDecoration(dividerItemDecoration)
}
/**
* Configure the toolbar instance using the default toolbar id if null is passed.
*
* This helper will set the toolbar as the action bar and configure the up button
* visibility plus optional title.
*
* @param toolbar the toolbar instance to configure (may be null, in which case the view
* with id R.id.toolbar in the activity content will be used)
* @param showHomeAsUp whether the home/up affordance should be shown (default true)
* @param title optional title to set on the activity
*/
protected fun setupToolbar(toolbar: Toolbar?, showHomeAsUp: Boolean = true, title: CharSequence? = null) {
val tb = toolbar ?: findViewById<Toolbar?>(R.id.toolbar)
tb?.let {
@@ -74,24 +121,56 @@ abstract class BaseActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
title?.let { t -> this.title = t }
}
progressBar = findViewById(R.id.progress_bar)
}
/**
* Inflate the shared base layout, attach the child layout resource into the base
* content container, cache the in-layout ProgressBar and configure the toolbar.
*
* Typical usage in subclasses:
* setContentViewWithToolbar(R.layout.activity_settings, showHomeAsUp = true, title = "Settings")
*
* @param layoutResId child layout resource to inflate into the base content container
* @param showHomeAsUp whether to show the up/home affordance on the toolbar (default true)
* @param title optional activity title to set on the toolbar
*/
protected fun setContentViewWithToolbar(layoutResId: Int, showHomeAsUp: Boolean = true, title: CharSequence? = null) {
val base = LayoutInflater.from(this).inflate(R.layout.activity_base, null)
val container = base.findViewById<FrameLayout>(R.id.content_container)
LayoutInflater.from(this).inflate(layoutResId, container, true)
progressBar = base.findViewById(R.id.progress_bar)
super.setContentView(base)
setupToolbar(base, showHomeAsUp, title)
}
/**
* Inflate the shared base layout, attach the provided child view (commonly a view-binding root)
* into the base content container, cache the in-layout ProgressBar and configure the toolbar.
*
* Typical usage with view binding:
* setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = "...")
*
* @param childView the already-inflated child view to add to the base content container
* @param showHomeAsUp whether to show the up/home affordance on the toolbar (default true)
* @param title optional activity title to set on the toolbar
*/
protected fun setContentViewWithToolbar(childView: View, showHomeAsUp: Boolean = true, title: CharSequence? = null) {
val base = LayoutInflater.from(this).inflate(R.layout.activity_base, null)
val container = base.findViewById<FrameLayout>(R.id.content_container)
container.addView(childView, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
progressBar = base.findViewById(R.id.progress_bar)
super.setContentView(base)
setupToolbar(base, showHomeAsUp, title)
}
/**
* Internal helper that configures the MaterialToolbar found in the inflated base root.
*
* @param baseRoot the root view of the inflated base layout
* @param showHomeAsUp whether to show the up/home affordance
* @param title optional title to set on the support action bar
*/
private fun setupToolbar(baseRoot: View, showHomeAsUp: Boolean, title: CharSequence?) {
val toolbar = baseRoot.findViewById<MaterialToolbar>(R.id.toolbar)
toolbar?.let {
@@ -101,4 +180,37 @@ abstract class BaseActivity : AppCompatActivity() {
}
}
/**
* Show the base layout's ProgressBar.
*
* This method is safe to call from background threads; the visibility change will
* be posted to the UI thread via [runOnUiThread]. If the base layout was not set yet
* (progressBar == null) the call is a no-op.
*/
protected fun showLoading() {
runOnUiThread {
progressBar?.visibility = View.VISIBLE
}
}
/**
* Hide the base layout's ProgressBar.
*
* Safe to call from background threads. No-op if the progress bar hasn't been cached.
*/
protected fun hideLoading() {
runOnUiThread {
progressBar?.visibility = View.GONE
}
}
/**
* Returns true when the base ProgressBar is currently visible.
*
* @return true if the progress bar exists and its visibility is VISIBLE
*/
protected fun isLoadingVisible(): Boolean {
return progressBar?.visibility == View.VISIBLE
}
}
@@ -45,6 +45,7 @@ class CheckUpdateActivity : BaseActivity() {
private fun checkForUpdates(includePreRelease: Boolean) {
toast(R.string.update_checking_for_update)
showLoading()
lifecycleScope.launch {
try {
@@ -58,6 +59,9 @@ class CheckUpdateActivity : BaseActivity() {
Log.e(AppConfig.TAG, "Failed to check for updates: ${e.message}")
toastError(e.message ?: getString(R.string.toast_failure))
}
finally {
hideLoading()
}
}
}
@@ -422,7 +422,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
private fun importBatchConfig(server: String?) {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
try {
@@ -438,12 +438,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
countSub > 0 -> setupGroupTab()
else -> toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
hideLoading()
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
hideLoading()
}
Log.e(AppConfig.TAG, "Failed to import batch config", e)
}
@@ -468,7 +468,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
* import config from sub
*/
private fun importConfigViaSub(): Boolean {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val count = mainViewModel.updateConfigViaSubAll()
@@ -480,14 +480,14 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else {
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
hideLoading()
}
}
return true
}
private fun exportAll() {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.exportAllServer()
launch(Dispatchers.Main) {
@@ -495,7 +495,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
toast(getString(R.string.title_export_config_count, ret))
else
toastError(R.string.toast_failure)
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -503,13 +503,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private fun delAllConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeAllServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -522,13 +522,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private fun delDuplicateConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeDuplicateServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_duplicate_config_count, ret))
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -541,13 +541,13 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private fun delInvalidConfig() {
AlertDialog.Builder(this).setMessage(R.string.del_invalid_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val ret = mainViewModel.removeInvalidServer()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
toast(getString(R.string.title_del_config_count, ret))
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -558,12 +558,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
private fun sortByTestResults() {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
mainViewModel.sortByTestResults()
launch(Dispatchers.Main) {
mainViewModel.reloadServerList()
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -60,7 +60,7 @@ class PerAppProxyActivity : BaseActivity() {
}
private fun initList() {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch {
try {
@@ -98,7 +98,7 @@ class PerAppProxyActivity : BaseActivity() {
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "Error loading apps", e)
} finally {
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -179,7 +179,7 @@ class PerAppProxyActivity : BaseActivity() {
private fun selectProxyAppAuto() {
toast(R.string.msg_downloading_content)
binding.pbWaiting.show()
showLoading()
val url = AppConfig.ANDROID_PACKAGE_NAME_LIST_URL
lifecycleScope.launch(Dispatchers.IO) {
@@ -192,7 +192,7 @@ class PerAppProxyActivity : BaseActivity() {
//Log.i(AppConfig.TAG, content)
selectProxyApp(content, true)
toastSuccess(R.string.toast_success)
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -58,7 +58,7 @@ class SubSettingActivity : BaseActivity() {
}
R.id.sub_update -> {
binding.pbWaiting.show()
showLoading()
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll()
@@ -70,7 +70,7 @@ class SubSettingActivity : BaseActivity() {
} else {
toastError(R.string.toast_failure)
}
binding.pbWaiting.hide()
hideLoading()
}
}
@@ -219,7 +219,7 @@ class UserAssetActivity : BaseActivity() {
}
private fun downloadGeoFiles() {
binding.pbWaiting.show()
showLoading()
toast(R.string.msg_downloading_content)
val httpPort = SettingsManager.getHttpPort()
@@ -247,7 +247,7 @@ class UserAssetActivity : BaseActivity() {
} else {
toast(getString(R.string.toast_failure))
}
binding.pbWaiting.hide()
hideLoading()
}
}
}
@@ -9,14 +9,6 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:indicatorColor="@color/color_fab_active" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -14,6 +14,15 @@
app:layout_constraintEnd_toEndOf="parent"
app:titleTextAppearance="@style/TextAppearance.AppCompat.Title" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/color_fab_active" />
<FrameLayout
android:id="@+id/content_container"
android:layout_width="0dp"
@@ -21,6 +30,8 @@
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent">
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -8,14 +8,6 @@
android:orientation="vertical"
tools:context=".ui.PerAppProxyActivity">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:indicatorColor="@color/color_fab_active" />
<LinearLayout
android:id="@+id/header_view"
android:layout_width="match_parent"
@@ -37,7 +37,7 @@
android:orientation="vertical">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
@@ -7,14 +7,6 @@
android:fitsSystemWindows="true"
tools:context=".ui.SubSettingActivity">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:indicatorColor="@color/color_fab_active" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
@@ -7,14 +7,6 @@
android:fitsSystemWindows="true"
tools:context=".ui.UserAssetActivity">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_waiting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:indicatorColor="@color/color_fab_active" />
<androidx.core.widget.NestedScrollView
android:id="@+id/main_content"
android:layout_width="match_parent"