From 3f1b741d9dc61bea4272188339f4cefc828107e7 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:06:11 +0800 Subject: [PATCH] Refactor progress bar handling to BaseActivity --- .../java/com/v2ray/ang/ui/BackupActivity.kt | 8 +- .../java/com/v2ray/ang/ui/BaseActivity.kt | 122 +++++++++++++++++- .../com/v2ray/ang/ui/CheckUpdateActivity.kt | 4 + .../java/com/v2ray/ang/ui/MainActivity.kt | 30 ++--- .../com/v2ray/ang/ui/PerAppProxyActivity.kt | 8 +- .../com/v2ray/ang/ui/SubSettingActivity.kt | 4 +- .../com/v2ray/ang/ui/UserAssetActivity.kt | 4 +- .../src/main/res/layout/activity_backup.xml | 8 -- .../app/src/main/res/layout/activity_base.xml | 13 +- .../main/res/layout/activity_bypass_list.xml | 8 -- .../app/src/main/res/layout/activity_main.xml | 2 +- .../main/res/layout/activity_sub_setting.xml | 8 -- .../main/res/layout/activity_user_asset.xml | 8 -- 13 files changed, 161 insertions(+), 66 deletions(-) diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt index 703b3c78..ebcb2d1a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt @@ -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() } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt index eabd69ae..9bb80229 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt @@ -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(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(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(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(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 + } + } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt index 411a3b40..fbd3739c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/CheckUpdateActivity.kt @@ -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() + } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt index 63a77121..4b7fb0d3 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainActivity.kt @@ -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() } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt index 4b9c5982..05193944 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/PerAppProxyActivity.kt @@ -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() } } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt index 06c787da..1230a18c 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/SubSettingActivity.kt @@ -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() } } diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt index b8eff1df..626567f0 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/UserAssetActivity.kt @@ -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() } } } diff --git a/V2rayNG/app/src/main/res/layout/activity_backup.xml b/V2rayNG/app/src/main/res/layout/activity_backup.xml index 797752ee..b3c2ce0b 100644 --- a/V2rayNG/app/src/main/res/layout/activity_backup.xml +++ b/V2rayNG/app/src/main/res/layout/activity_backup.xml @@ -9,14 +9,6 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - + + + app:layout_constraintEnd_toEndOf="parent"> + + diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml index a5e4d835..caf9c83b 100644 --- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -8,14 +8,6 @@ android:orientation="vertical" tools:context=".ui.PerAppProxyActivity"> - - - - - -