From e5dfb50e18e5bc106fbd4f2470988bee93ddd870 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 02:37:22 -0400 Subject: [PATCH 01/33] gradle: targetSdk v35 --- app/src/main/AndroidManifest.xml | 1 + .../main/res/xml/data_extraction_rules.xml | 19 +++++++++++++++++++ build.gradle | 4 ++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/xml/data_extraction_rules.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c5b54aea..f9d6b5a9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,7 @@ android:allowBackup="${allowBackup}" android:enableOnBackInvokedCallback="true" android:fullBackupContent="@xml/backup_rules" + android:dataExtractionRules="@xml/data_extraction_rules" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 00000000..c80e9e71 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 63ce9bf5..fc0b8159 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { compileSdkVersion = 35 minSdkVersion = 23 - targetSdkVersion = 34 + targetSdkVersion = 35 kotlin_version = '2.1.20' kotlinx_version = '1.10.2' @@ -68,7 +68,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.10.0' + classpath 'com.android.tools.build:gradle:8.10.1' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath 'com.google.gms:google-services:4.4.2' classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3' From a5e80e35ed6246dfd7f26acebd042d42f68bbcf6 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 02:40:50 -0400 Subject: [PATCH 02/33] SimpleWeather: edge to edge fixes * Fix edge to edge on LocationSearchActivity * Replace search view container with SearchBar * Replace custom search toolbar with SearchView --- .../activities/LocationSearchActivity.kt | 255 +++--------------- .../activities/WindowColorActivity.kt | 49 ++++ .../controls/CustomSearchView.kt | 27 ++ .../controls/LocationSearchView.kt | 150 +++++++++++ .../simpleweather/main/MainActivity.kt | 20 +- .../simpleweather/setup/SetupActivity.kt | 40 +++ .../setup/SetupLocationFragment.kt | 7 +- .../res/layout/activity_location_search.xml | 32 +-- .../res/layout/fragment_setup_location.xml | 10 +- .../main/res/layout/location_search_bar.xml | 52 ---- .../thewizrd/common/utils/ActivityUtils.kt | 36 +-- 11 files changed, 325 insertions(+), 353 deletions(-) create mode 100644 app/src/main/java/com/thewizrd/simpleweather/activities/WindowColorActivity.kt create mode 100644 app/src/main/java/com/thewizrd/simpleweather/controls/CustomSearchView.kt create mode 100644 app/src/main/java/com/thewizrd/simpleweather/controls/LocationSearchView.kt delete mode 100644 app/src/main/res/layout/location_search_bar.xml diff --git a/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt b/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt index 8626320b..67df9af3 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt @@ -4,33 +4,26 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent -import android.content.res.ColorStateList import android.content.res.Configuration import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.Window import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.activity.result.contract.ActivityResultContract import androidx.activity.viewModels -import androidx.core.view.GestureDetectorCompat import androidx.core.view.ViewCompat import androidx.core.view.ViewGroupCompat -import androidx.core.view.isVisible +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePaddingRelative import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.search.SearchView import com.google.android.material.snackbar.Snackbar import com.google.android.material.transition.platform.MaterialContainerTransform import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback -import com.thewizrd.common.helpers.SimpleGestureListener import com.thewizrd.common.utils.ActivityUtils.setFullScreen import com.thewizrd.common.utils.ActivityUtils.setTransparentWindow import com.thewizrd.common.utils.ErrorMessage @@ -48,16 +41,14 @@ import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.UserThemeMode import com.thewizrd.simpleweather.R -import com.thewizrd.simpleweather.adapters.LocationQueryAdapter -import com.thewizrd.simpleweather.adapters.LocationQueryFooterAdapter import com.thewizrd.simpleweather.databinding.ActivityLocationSearchBinding -import com.thewizrd.simpleweather.databinding.SearchActionBarBinding -import com.thewizrd.simpleweather.helpers.WindowColorManager -import com.thewizrd.simpleweather.locale.UserLocaleActivity import com.thewizrd.simpleweather.snackbar.SnackbarWindowAdjustCallback -import kotlinx.coroutines.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.collectLatest -import timber.log.Timber +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope class LocationSearch : ActivityResultContract() { override fun createIntent(context: Context, input: Void?): Intent { @@ -79,11 +70,9 @@ class LocationSearch : ActivityResultContract() { } } -class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChangeListener, - WindowColorManager { +class LocationSearchActivity : WindowColorActivity() { companion object { private const val TAG = "LocSearchFragment" - private const val KEY_SEARCHTEXT = "search_text" const val RESULT_SUCCESS = Activity.RESULT_OK const val RESULT_CANCELED = Activity.RESULT_CANCELED @@ -91,11 +80,6 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange } private lateinit var binding: ActivityLocationSearchBinding - private lateinit var searchBarBinding: SearchActionBarBinding - private lateinit var mAdapter: ConcatAdapter - private lateinit var mLocationAdapter: LocationQueryAdapter - private lateinit var mFooterAdapter: LocationQueryFooterAdapter - private lateinit var mLayoutManager: RecyclerView.LayoutManager private val locationSearchViewModel: LocationSearchViewModel by viewModels() @@ -132,26 +116,34 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange // Inflate the layout for this fragment binding = ActivityLocationSearchBinding.inflate(layoutInflater) - setContentView(binding.root) - binding.lifecycleOwner = this - searchBarBinding = binding.searchBar - searchBarBinding.lifecycleOwner = this + setContentView(binding.root) ViewCompat.setTransitionName(binding.root, null) ViewGroupCompat.setTransitionGroup((binding.root as ViewGroup), true) - // Initialize - searchBarBinding.searchBackButton.setOnClickListener { - setResult(RESULT_CANCELED) - onBackPressedDispatcher.onBackPressed() - } - searchBarBinding.searchCloseButton.setOnClickListener { - searchBarBinding.searchView.setText("") + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> + val sysBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + + binding.root.updatePaddingRelative( + start = sysBarInsets.left, + end = sysBarInsets.right, + top = sysBarInsets.top, + bottom = sysBarInsets.bottom + ) + + WindowInsetsCompat.CONSUMED } - searchBarBinding.searchCloseButton.visibility = View.GONE - searchBarBinding.searchView.addTextChangedListener(object : TextWatcher { + // Initialize + binding.searchView.setVisible(true) + binding.searchView.addTransitionListener { _, _, newState -> + if (newState == SearchView.TransitionState.HIDING || newState == SearchView.TransitionState.HIDDEN) { + setResult(RESULT_CANCELED) + onBackPressedDispatcher.onBackPressed() + } + } + binding.searchView.editText.addTextChangedListener(object : TextWatcher { private var textChangedJob: Job? = null override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { @@ -180,63 +172,18 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange private fun runSearchOp(e: Editable) { val newText = e.toString() - searchBarBinding.searchCloseButton.isVisible = newText.isNotEmpty() fetchLocations(newText) } }) - searchBarBinding.searchView.onFocusChangeListener = - View.OnFocusChangeListener { v, hasFocus -> - if (hasFocus) { - showInputMethod(v.findFocus()) - } else { - hideInputMethod(v) - } - } - searchBarBinding.searchView.setOnEditorActionListener(TextView.OnEditorActionListener { v, actionId, _ -> + binding.searchView.editText.setOnEditorActionListener(TextView.OnEditorActionListener { v, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { fetchLocations(v.text.toString()) - hideInputMethod(v) + binding.searchView.clearFocusAndHideKeyboard() return@OnEditorActionListener true } false }) - - // use this setting to improve performance if you know that changes - // in content do not change the layout size of the RecyclerView - binding.recyclerView.setHasFixedSize(true) - - // use a linear layout manager - mLayoutManager = LinearLayoutManager(this) - binding.recyclerView.layoutManager = mLayoutManager - - // specify an adapter (see also next example) - mLocationAdapter = LocationQueryAdapter() - mLocationAdapter.setOnClickListener(recyclerClickListener) - binding.recyclerView.adapter = ConcatAdapter(mLocationAdapter).also { mAdapter = it } - mFooterAdapter = LocationQueryFooterAdapter() - - /* - * Capture touch events on RecyclerView - * We're not using ADJUST_RESIZE so hide the keyboard when necessary - * Hide the keyboard if we're scrolling to the bottom (so the bottom items behind the keyboard are visible) - * Leave the keyboard up if we're scrolling to the top (items at the top are already visible) - */ - val gestureDetector = GestureDetectorCompat(this, gestureListener) - - binding.recyclerView.setOnTouchListener { _, event -> - runCatching { - gestureDetector.onTouchEvent(event) - }.onFailure { - Timber.tag(TAG).w(it) - }.getOrElse { false } - } - - if (savedInstanceState != null) { - val text = savedInstanceState.getString(KEY_SEARCHTEXT) - if (!text.isNullOrBlank()) { - searchBarBinding.searchView.setText(text, TextView.BufferType.EDITABLE) - } - } + binding.searchView.onItemClickListener = recyclerClickListener val color = getAttrColor(R.attr.colorPrimarySurface) window.setTransparentWindow(color) @@ -253,19 +200,13 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange lifecycleScope.launch { locationSearchViewModel.isLoading.collect { loading -> - showLoading(loading) + binding.searchView.showLoading(loading) } } lifecycleScope.launch { locationSearchViewModel.locations.collectLatest { - if (it.isNotEmpty()) { - mLocationAdapter.submitList(it) - mAdapter.addAdapter(mFooterAdapter) - } else { - mLocationAdapter.submitList(it) - mAdapter.removeAdapter(mFooterAdapter) - } + binding.searchView.submitList(it) } } @@ -293,29 +234,16 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange } } - override fun onStart() { - super.onStart() - } - override fun onResume() { super.onResume() - searchBarBinding.searchView.requestFocus() + binding.searchView.requestFocusAndShowKeyboard() } override fun onPause() { - hideInputMethod(searchBarBinding.searchView) - searchBarBinding.searchView.clearFocus() + binding.searchView.clearFocusAndHideKeyboard() super.onPause() } - override fun onStop() { - super.onStop() - } - - override fun onDestroy() { - super.onDestroy() - } - private val recyclerClickListener = object : ListAdapterOnClickInterface { override fun onClick(view: View, item: LocationQuery) { if (item != LocationQuery.EMPTY) { @@ -324,75 +252,6 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange } } - private fun showLoading(show: Boolean) { - binding.searchProgressBar.isIndeterminate = show - binding.recyclerView.isEnabled = !show - } - - private val gestureListener = object : SimpleGestureListener() { - private var mY = 0 - private var shouldCloseKeyboard = false - - override fun onDown(e: MotionEvent?): Boolean { - e?.run { - mY = y.toInt() - } - return super.onDown(e) - } - - override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { - setResult(RESULT_CANCELED) - onBackPressedDispatcher.onBackPressed() - return super.onSingleTapConfirmed(e) - } - - override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent?, - distanceX: Float, - distanceY: Float - ): Boolean { - e2?.run { - val newY = y.toInt() - val dY = mY - newY - mY = newY - // Set flag to hide the keyboard if we're scrolling down - // So we can see what's behind the keyboard - shouldCloseKeyboard = dY > 0 - } - - if (shouldCloseKeyboard) { - hideInputMethod(binding.recyclerView) - shouldCloseKeyboard = false - } - - return super.onScroll(e1, e2, distanceX, distanceY) - } - - override fun onFling( - e1: MotionEvent?, - e2: MotionEvent?, - velocityX: Float, - velocityY: Float - ): Boolean { - e2?.run { - val newY = y.toInt() - val dY = mY - newY - mY = newY - // Set flag to hide the keyboard if we're scrolling down - // So we can see what's behind the keyboard - shouldCloseKeyboard = dY > 0 - } - - if (shouldCloseKeyboard) { - hideInputMethod(binding.recyclerView) - shouldCloseKeyboard = false - } - - return super.onFling(e1, e2, velocityX, velocityY) - } - } - override fun onThemeChanged(mode: UserThemeMode) { updateWindowColors(mode) } @@ -402,20 +261,13 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange } private fun updateWindowColors(mode: UserThemeMode) { - var backgroundColor = getAttrColor(android.R.attr.colorBackground) - var navBarColor = getAttrColor(R.attr.colorSurface) + var backgroundColor = getAttrColor(R.attr.colorSurfaceContainerHigh) if (mode == UserThemeMode.AMOLED_DARK) { backgroundColor = Colors.BLACK - navBarColor = Colors.BLACK } binding.root.setBackgroundColor(backgroundColor) - if (binding.appBar.background is MaterialShapeDrawable) { - val materialShapeDrawable = binding.appBar.background as MaterialShapeDrawable - materialShapeDrawable.fillColor = ColorStateList.valueOf(navBarColor) - } else { - binding.appBar.setBackgroundColor(navBarColor) - } + binding.searchView.setBackgroundOverlayColor(backgroundColor) window.setTransparentWindow( backgroundColor, Colors.TRANSPARENT, @@ -457,33 +309,4 @@ class LocationSearchActivity : UserLocaleActivity(), UserThemeMode.OnThemeChange locationSearchViewModel.setErrorMessageShown(error) } - - private fun showInputMethod(view: View?) { - view?.let { - val imm = - it.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - ?: return - imm.showSoftInput(it, 0) - } - } - - private fun hideInputMethod(view: View?) { - view?.let { - val imm = - it.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - ?: return - imm.hideSoftInputFromWindow(it.windowToken, 0) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putString(KEY_SEARCHTEXT, - if (!searchBarBinding.searchView.text.isNullOrBlank()) { - searchBarBinding.searchView.text.toString() - } else { - "" - }) - - super.onSaveInstanceState(outState) - } } \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/activities/WindowColorActivity.kt b/app/src/main/java/com/thewizrd/simpleweather/activities/WindowColorActivity.kt new file mode 100644 index 00000000..f7b0676e --- /dev/null +++ b/app/src/main/java/com/thewizrd/simpleweather/activities/WindowColorActivity.kt @@ -0,0 +1,49 @@ +package com.thewizrd.simpleweather.activities + +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.os.Bundle +import androidx.annotation.CallSuper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.thewizrd.shared_resources.utils.UserThemeMode +import com.thewizrd.shared_resources.utils.UserThemeMode.OnThemeChangeListener +import com.thewizrd.simpleweather.helpers.WindowColorManager +import com.thewizrd.simpleweather.locale.UserLocaleActivity +import kotlinx.coroutines.launch + +abstract class WindowColorActivity : UserLocaleActivity(), OnThemeChangeListener, + WindowColorManager { + private var prevConfig: Configuration? = null + + @CallSuper + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + updateWindowColors() + } + } + } + + abstract override fun onThemeChanged(mode: UserThemeMode) + abstract override fun updateWindowColors() + + @CallSuper + override fun onStart() { + super.onStart() + prevConfig = Configuration(this.resources.configuration) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + if (prevConfig == null || (newConfig.diff(prevConfig) and ActivityInfo.CONFIG_ORIENTATION) != 0) { + updateWindowColors() + } + + prevConfig = Configuration(newConfig) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/controls/CustomSearchView.kt b/app/src/main/java/com/thewizrd/simpleweather/controls/CustomSearchView.kt new file mode 100644 index 00000000..0bc31366 --- /dev/null +++ b/app/src/main/java/com/thewizrd/simpleweather/controls/CustomSearchView.kt @@ -0,0 +1,27 @@ +package com.thewizrd.simpleweather.controls + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.annotation.ColorInt +import com.google.android.material.elevation.ElevationOverlayProvider +import com.google.android.material.search.SearchView +import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx +import com.thewizrd.simpleweather.R + +open class CustomSearchView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : SearchView(context, attrs) { + private val elevationOverlayProvider: ElevationOverlayProvider = + ElevationOverlayProvider(context) + + fun setBackgroundOverlayColor(@ColorInt backgroundColor: Int) { + // 6dp + val elevation = context.dpToPx(6f) + val backgroundColorWithOverlay = + elevationOverlayProvider.compositeOverlayIfNeeded(backgroundColor, elevation) + findViewById(R.id.open_search_view_background)?.setBackgroundColor( + backgroundColorWithOverlay + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/controls/LocationSearchView.kt b/app/src/main/java/com/thewizrd/simpleweather/controls/LocationSearchView.kt new file mode 100644 index 00000000..8552c9a4 --- /dev/null +++ b/app/src/main/java/com/thewizrd/simpleweather/controls/LocationSearchView.kt @@ -0,0 +1,150 @@ +package com.thewizrd.simpleweather.controls + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.Gravity +import android.view.MotionEvent +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.progressindicator.LinearProgressIndicator +import com.thewizrd.common.helpers.SimpleGestureListener +import com.thewizrd.shared_resources.helpers.ListAdapterOnClickInterface +import com.thewizrd.shared_resources.locationdata.LocationQuery +import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx +import com.thewizrd.simpleweather.R +import com.thewizrd.simpleweather.adapters.LocationQueryAdapter +import com.thewizrd.simpleweather.adapters.LocationQueryFooterAdapter + +@SuppressLint("ClickableViewAccessibility") +class LocationSearchView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : CustomSearchView(context, attrs) { + private val locationAdapter = LocationQueryAdapter() + private val footerAdapter = LocationQueryFooterAdapter() + private val adapter = ConcatAdapter(locationAdapter) + + val recyclerView = RecyclerView(context, attrs).apply { + layoutParams = generateDefaultLayoutParams() + clipToPadding = false + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + adapter = this@LocationSearchView.adapter + } + + private val searchProgressBar: LinearProgressIndicator + + private val gestureListener = object : SimpleGestureListener() { + private var mY = 0 + private var shouldCloseKeyboard = false + + override fun onDown(e: MotionEvent?): Boolean { + e?.run { + mY = y.toInt() + } + return super.onDown(e) + } + + override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { + hide() + return super.onSingleTapConfirmed(e) + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent?, + distanceX: Float, + distanceY: Float + ): Boolean { + e2?.run { + val newY = y.toInt() + val dY = mY - newY + mY = newY + // Set flag to hide the keyboard if we're scrolling down + // So we can see what's behind the keyboard + shouldCloseKeyboard = dY > 0 + } + + if (shouldCloseKeyboard) { + clearFocusAndHideKeyboard() + shouldCloseKeyboard = false + } + + return super.onScroll(e1, e2, distanceX, distanceY) + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + e2?.run { + val newY = y.toInt() + val dY = mY - newY + mY = newY + // Set flag to hide the keyboard if we're scrolling down + // So we can see what's behind the keyboard + shouldCloseKeyboard = dY > 0 + } + + if (shouldCloseKeyboard) { + clearFocusAndHideKeyboard() + shouldCloseKeyboard = false + } + + return super.onFling(e1, e2, velocityX, velocityY) + } + } + + private val gestureDetector = GestureDetector(context, gestureListener) + + var onItemClickListener: ListAdapterOnClickInterface? = null + set(value) { + field = value + locationAdapter.setOnClickListener(value) + } + + init { + recyclerView.setOnTouchListener { _, event -> + runCatching { + gestureDetector.onTouchEvent(event) + }.getOrElse { false } + } + addView(recyclerView) + + // Add progress bar + searchProgressBar = LinearProgressIndicator(context).apply { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + gravity = Gravity.BOTTOM + } + isIndeterminate = false + trackThickness = context.dpToPx(1f).toInt() + } + findViewById(R.id.open_search_view_toolbar_container)?.addView( + searchProgressBar + ) + } + + fun showLoading(show: Boolean) { + searchProgressBar.isIndeterminate = show + recyclerView.isEnabled = !show + } + + fun submitList(list: List) { + locationAdapter.submitList(list) + + if (list.isNotEmpty()) { + adapter.addAdapter(footerAdapter) + } else { + adapter.removeAdapter(footerAdapter) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/main/MainActivity.kt b/app/src/main/java/com/thewizrd/simpleweather/main/MainActivity.kt index 3e5c0c69..5831e506 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/main/MainActivity.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/main/MainActivity.kt @@ -2,7 +2,6 @@ package com.thewizrd.simpleweather.main import android.annotation.SuppressLint import android.content.Intent -import android.content.pm.ActivityInfo import android.content.res.ColorStateList import android.content.res.Configuration import android.graphics.Outline @@ -44,12 +43,10 @@ import com.thewizrd.shared_resources.utils.ContextUtils.getOrientation import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.UserThemeMode -import com.thewizrd.shared_resources.utils.UserThemeMode.OnThemeChangeListener import com.thewizrd.simpleweather.NavGraphDirections import com.thewizrd.simpleweather.R +import com.thewizrd.simpleweather.activities.WindowColorActivity import com.thewizrd.simpleweather.databinding.ActivityMainBinding -import com.thewizrd.simpleweather.helpers.WindowColorManager -import com.thewizrd.simpleweather.locale.UserLocaleActivity import com.thewizrd.simpleweather.notifications.WeatherAlertNotificationService import com.thewizrd.simpleweather.preferences.SettingsFragment import com.thewizrd.simpleweather.services.UpdaterUtils @@ -58,7 +55,7 @@ import com.thewizrd.simpleweather.updates.InAppUpdateManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class MainActivity : UserLocaleActivity(), OnThemeChangeListener, WindowColorManager { +class MainActivity : WindowColorActivity() { companion object { private const val TAG = "MainActivity" private const val INSTALL_REQUESTCODE = 168 @@ -67,8 +64,6 @@ class MainActivity : UserLocaleActivity(), OnThemeChangeListener, WindowColorMan private lateinit var binding: ActivityMainBinding private var mNavController: NavController? = null - private var prevConfig: Configuration? = null - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private var appUpdateManager: InAppUpdateManager? = null @@ -204,8 +199,6 @@ class MainActivity : UserLocaleActivity(), OnThemeChangeListener, WindowColorMan override fun onStart() { super.onStart() - prevConfig = Configuration(this.resources.configuration) - AnalyticsLogger.logEvent("$TAG: onStart") val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) @@ -430,13 +423,4 @@ class MainActivity : UserLocaleActivity(), OnThemeChangeListener, WindowColorMan ) } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - if (prevConfig == null || (newConfig.diff(prevConfig) and ActivityInfo.CONFIG_ORIENTATION) != 0) { - updateWindowColors(settingsManager.getUserThemeMode()) - } - - prevConfig = Configuration(newConfig) - } } \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupActivity.kt b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupActivity.kt index ab94f120..1511982f 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupActivity.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.view.Window +import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.annotation.IdRes import androidx.core.view.ViewCompat @@ -41,6 +42,8 @@ class SetupActivity : UserLocaleActivity() { private var mNavController: NavController? = null private var isWeatherLoaded = false + private lateinit var onBackPressedCallback: OnBackPressedCallback + // Widget id for ConfigurationActivity private var mAppWidgetId: Int = AppWidgetManager.INVALID_APPWIDGET_ID @@ -133,6 +136,24 @@ class SetupActivity : UserLocaleActivity() { setupBottomNavBar() initializeNavController() } + + onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val destination = mNavController?.currentDestination + + if (destination != null) { + if (!canGoBack(destination.id)) { + finish() + } else { + mNavController?.navigateUp() + } + } else { + mNavController?.navigateUp() + } + } + } + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) } override fun onStart() { @@ -155,6 +176,7 @@ class SetupActivity : UserLocaleActivity() { mNavController!!.addOnDestinationChangedListener { _, destination, _ -> Timber.d("Destination: $destination") updateBottomNavigationBarForDestination(destination.id) + onBackPressedCallback.isEnabled = !canGoBack(destination.id) } } } @@ -291,6 +313,24 @@ class SetupActivity : UserLocaleActivity() { } } + private fun canGoBack(@IdRes destinationId: Int): Boolean { + return when (destinationId) { + R.id.setupLocationFragment -> { + BuildConfig.IS_NONGMS + } + + R.id.setupSettingsFragment -> { + false + } + + R.id.mainActivity -> { + false + } + + else -> true + } + } + private fun updateBottomNavigationBarForDestination(@IdRes destinationId: Int) { binding.bottomNavBar.setSelectedItem(getPosition(destinationId)) if (destinationId == R.id.setupLocationFragment) { diff --git a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupLocationFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupLocationFragment.kt index 14583cd9..0232d0ba 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupLocationFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupLocationFragment.kt @@ -114,7 +114,7 @@ class SetupLocationFragment : CustomFragment() { binding = FragmentSetupLocationBinding.inflate(inflater, container, false) /* Event Listeners */ - binding.searchBar.searchViewContainer.setOnClickListener { + binding.searchBar.setOnClickListener { locationSearchLauncher.launch( ActivityOptionsCompat.makeSceneTransitionAnimation( requireActivity(), @@ -123,10 +123,7 @@ class SetupLocationFragment : CustomFragment() { ) ) } - ViewCompat.setTransitionName( - binding.searchBar.searchViewContainer, - Constants.SHARED_ELEMENT - ) + ViewCompat.setTransitionName(binding.searchBar, Constants.SHARED_ELEMENT) binding.gpsFollow.setOnClickListener { fetchGeoLocation() } diff --git a/app/src/main/res/layout/activity_location_search.xml b/app/src/main/res/layout/activity_location_search.xml index 522cd139..df48cb5c 100644 --- a/app/src/main/res/layout/activity_location_search.xml +++ b/app/src/main/res/layout/activity_location_search.xml @@ -15,7 +15,6 @@ @@ -23,37 +22,14 @@ android:id="@+id/root_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?android:attr/colorBackground" + android:background="?colorSurfaceContainerHigh" android:transitionName="shared_element_container"> - - - - - - - - - + android:hint="@string/location_search_hint" /> diff --git a/app/src/main/res/layout/fragment_setup_location.xml b/app/src/main/res/layout/fragment_setup_location.xml index ba3f4c13..15d750fa 100644 --- a/app/src/main/res/layout/fragment_setup_location.xml +++ b/app/src/main/res/layout/fragment_setup_location.xml @@ -43,19 +43,15 @@ android:id="@+id/divider" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:layout_constraintGuide_percent="0.475" + app:layout_constraintGuide_percent="0.465" android:orientation="horizontal" /> - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/common/src/main/java/com/thewizrd/common/utils/ActivityUtils.kt b/common/src/main/java/com/thewizrd/common/utils/ActivityUtils.kt index 3351d310..2fa49c1d 100644 --- a/common/src/main/java/com/thewizrd/common/utils/ActivityUtils.kt +++ b/common/src/main/java/com/thewizrd/common/utils/ActivityUtils.kt @@ -7,10 +7,10 @@ import android.view.Window import android.widget.Toast import androidx.activity.ComponentActivity import androidx.annotation.ColorInt -import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.core.graphics.ColorUtils import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,6 +28,7 @@ object ActivityUtils { setTransparentWindow(backgroundColor, statusBarColor, navBarColor, true) } + @Suppress("DEPRECATION") fun Window.setTransparentWindow( @ColorInt backgroundColor: Int, @ColorInt statusBarColor: Int, @@ -56,12 +57,8 @@ object ActivityUtils { ) < 4.5f val statBarProtected = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || !isLightStatusBar - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setLightStatusBar(setColors && isLightStatusBar) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setLightNavBar(setColors && isLightNavBar) - } + setLightStatusBar(setColors && isLightStatusBar) + setLightNavBar(setColors && isLightNavBar) this.statusBarColor = if (setColors) { if (statBarProtected) statusBarColor else ColorUtils.blendARGB( @@ -94,6 +91,7 @@ object ActivityUtils { setStatusBarColor(backgroundColor, statusBarColor, true) } + @Suppress("DEPRECATION") private fun Window.setStatusBarColor( @ColorInt backgroundColor: Int, @ColorInt statusBarColor: Int, @@ -129,30 +127,14 @@ object ActivityUtils { } } - @RequiresApi(api = Build.VERSION_CODES.M) fun Window.setLightStatusBar(setLight: Boolean) { - if (setLight) { - decorView.systemUiVisibility = ( - decorView.systemUiVisibility - or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) - } else { - decorView.systemUiVisibility = ( - decorView.systemUiVisibility - and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()) - } + WindowInsetsControllerCompat(this, decorView) + .isAppearanceLightStatusBars = setLight } - @RequiresApi(api = Build.VERSION_CODES.O) fun Window.setLightNavBar(setLight: Boolean) { - if (setLight) { - decorView.systemUiVisibility = ( - decorView.systemUiVisibility - or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) - } else { - decorView.systemUiVisibility = ( - decorView.systemUiVisibility - and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()) - } + WindowInsetsControllerCompat(this, decorView) + .isAppearanceLightNavigationBars = setLight } fun ComponentActivity.showToast(@StringRes resId: Int, duration: Int) { From 4f1b4d0a704c9e2ccfbe1e06418250ca092e5af2 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 03:34:47 -0400 Subject: [PATCH 03/33] workers: add retry weather retrieval cause --- .../services/WeatherUpdaterWorker.kt | 79 +++++++++-------- .../services/WidgetUpdaterWorker.kt | 38 +++++++-- .../common/weatherdata/WeatherDataLoader.kt | 4 + .../services/WeatherUpdaterWorker.kt | 84 +++++++++++-------- .../services/WidgetUpdaterWorker.kt | 77 +++++++++++------ 5 files changed, 178 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt index 191c5a39..6d944f7d 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt @@ -19,13 +19,13 @@ import com.thewizrd.common.utils.ErrorMessage import com.thewizrd.common.utils.LiveDataUtils.awaitWithTimeout import com.thewizrd.common.weatherdata.WeatherDataLoader import com.thewizrd.common.weatherdata.WeatherRequest +import com.thewizrd.common.weatherdata.WeatherResult import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.remoteconfig.remoteConfigService import com.thewizrd.shared_resources.utils.* import com.thewizrd.shared_resources.utils.Logger -import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.notifications.PoPChanceNotificationHelper import com.thewizrd.simpleweather.notifications.WeatherNotificationWorker @@ -154,15 +154,13 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C override suspend fun doWork(): Result { Logger.writeLine(Log.INFO, "%s: Work started", TAG) - - if (!WeatherUpdaterHelper.executeWork(applicationContext)) - return Result.failure() - - return Result.success() + return WeatherUpdaterHelper.executeWork(applicationContext) } private object WeatherUpdaterHelper { - suspend fun executeWork(context: Context): Boolean { + suspend fun executeWork(context: Context): Result { + var result = Result.success() + val wm = weatherModule.weatherManager var locationChanged = false @@ -174,12 +172,12 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C if (settingsManager.isWeatherLoaded()) { if (settingsManager.useFollowGPS()) { try { - val result = updateLocation() + val locationResult = updateLocation() - when (result) { + when (locationResult) { is LocationResult.Changed -> { locationChanged = true - settingsManager.saveLastGPSLocData(result.data) + settingsManager.saveLastGPSLocData(locationResult.data) } else -> { // no-op @@ -197,7 +195,7 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C // Refresh weather data for widgets preloadWeather() - val weather = getWeather() + val weatherResult = getWeather() if (WidgetUpdaterHelper.widgetsExist()) { WidgetUpdaterHelper.refreshWidgets(context) @@ -215,11 +213,11 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C ShortcutCreatorWorker.updateShortcuts(context) } - if (weather != null) { + if (weatherResult is WeatherResult.Success || weatherResult is WeatherResult.WeatherWithError) { if (settingsManager.useAlerts() && wm.supportsAlerts()) { WeatherAlertHandler.postAlerts( settingsManager.getHomeData()!!, - weather.weatherAlerts + weatherResult.data?.weatherAlerts ) } @@ -233,35 +231,48 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C } LocalBroadcastManager.getInstance(context) .sendBroadcast(Intent(CommonActions.ACTION_WEATHER_SENDWEATHERUPDATE)) - } else { - Timber.tag(TAG).i("Work failed...") - return false + } + + result = when (weatherResult) { + is WeatherResult.Success -> Result.success() + is WeatherResult.NoWeather -> Result.failure() + is WeatherResult.Error, + is WeatherResult.WeatherWithError -> Result.retry() } } - Timber.tag(TAG).i("Work completed successfully...") - return true + when (result) { + Result.success() -> { + Timber.tag(TAG).i("Work completed successfully...") + } + + Result.retry() -> { + Timber.tag(TAG).w("Work failed. Will retry...") + } + + Result.failure() -> { + Timber.tag(TAG).e("Work failed...") + } + } + + return result } // Re-schedule alarm at selected interval from now - private suspend fun getWeather(): Weather? = withContext(Dispatchers.IO) { + private suspend fun getWeather(): WeatherResult = withContext(Dispatchers.IO) { Timber.tag(TAG).d("Getting weather data for home...") - val weather = try { - val locData = settingsManager.getHomeData() ?: return@withContext null - WeatherDataLoader(locData) - .loadWeatherData( - WeatherRequest.Builder() - .forceRefresh(false) - .loadAlerts() - .loadForecasts() - .build() - ) - } catch (ex: Exception) { - Logger.writeLine(Log.ERROR, ex, "%s: getWeather error", TAG) - null - } - weather + val locData = + settingsManager.getHomeData() ?: return@withContext WeatherResult.NoWeather() + + WeatherDataLoader(locData) + .loadWeatherResult( + WeatherRequest.Builder() + .forceRefresh(false) + .loadAlerts() + .loadForecasts() + .build() + ) } private suspend fun preloadWeather() = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt index 491c48a9..b0b4a724 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt @@ -125,25 +125,35 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co } override suspend fun doWork(): Result { - WidgetUpdaterWork.executeWork(applicationContext) - return Result.success() + return WidgetUpdaterWork.executeWork(applicationContext) } private object WidgetUpdaterWork { - suspend fun executeWork(context: Context) { + suspend fun executeWork(context: Context): Result { + var result = Result.success() + val settingsManager = SettingsManager(context.applicationContext) if (settingsManager.isWeatherLoaded()) { // If saved data DNE (for current location), refresh weather - val result = loadWeather() - if (result !is WeatherResult.Success && result !is WeatherResult.WeatherWithError) { - if (loadWeather(true).let { it is WeatherResult.Success && !it.isSavedData }) { + var weatherResult = loadWeather() + if (weatherResult !is WeatherResult.Success && weatherResult !is WeatherResult.WeatherWithError) { + weatherResult = loadWeather(true) + + if (weatherResult.let { it is WeatherResult.Success && !it.isSavedData }) { localBroadcastManager.sendBroadcast( Intent(CommonActions.ACTION_WEATHER_SENDWEATHERUPDATE).apply { putExtra(WearableSettings.KEY_PARTIAL_WEATHER_UPDATE, true) } ) } + + result = when (weatherResult) { + is WeatherResult.Success -> Result.success() + is WeatherResult.NoWeather -> Result.failure() + is WeatherResult.Error, + is WeatherResult.WeatherWithError -> Result.retry() + } } if (WidgetUpdaterHelper.widgetsExist()) { @@ -163,7 +173,21 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co } } - Timber.tag(TAG).i("Work completed successfully...") + when (result) { + Result.success() -> { + Timber.tag(TAG).i("Work completed successfully...") + } + + Result.retry() -> { + Timber.tag(TAG).w("Work failed. Will retry...") + } + + Result.failure() -> { + Timber.tag(TAG).e("Work failed...") + } + } + + return result } private suspend fun loadWeather(forceRefresh: Boolean = false): WeatherResult = diff --git a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt index fdadbb3d..c2b115ea 100644 --- a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt +++ b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt @@ -18,6 +18,7 @@ import com.thewizrd.weather_api.weatherModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext +import java.io.IOException import java.time.Duration import java.time.ZoneOffset import java.time.ZonedDateTime @@ -180,6 +181,9 @@ class WeatherDataLoader { } catch (weatherEx: WeatherException) { wEx = weatherEx weather = null + } catch (ioEx: IOException) { + wEx = WeatherException(ErrorStatus.NETWORKERROR, ioEx) + weather = null } catch (ex: Exception) { Logger.writeLine(Log.ERROR, ex, "WeatherDataLoader: error getting weather data") weather = null diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt index 1ca13737..7c2ce068 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt @@ -12,13 +12,13 @@ import com.thewizrd.common.utils.ErrorMessage import com.thewizrd.common.utils.LiveDataUtils.awaitWithTimeout import com.thewizrd.common.weatherdata.WeatherDataLoader import com.thewizrd.common.weatherdata.WeatherRequest +import com.thewizrd.common.weatherdata.WeatherResult import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.remoteconfig.remoteConfigService import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.wearable.WearableDataSync -import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.services.ServiceNotificationHelper.JOB_ID import com.thewizrd.simpleweather.services.ServiceNotificationHelper.getForegroundNotification @@ -124,15 +124,13 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C override suspend fun doWork(): Result { Logger.writeLine(Log.INFO, "%s: Work started", TAG) - - if (!WeatherUpdaterHelper.executeWork(applicationContext)) - return Result.failure() - - return Result.success() + return WeatherUpdaterHelper.executeWork(applicationContext) } private object WeatherUpdaterHelper { - suspend fun executeWork(context: Context): Boolean { + suspend fun executeWork(context: Context): Result { + var result = Result.success() + var locationChanged = false if (settingsManager.getDataSync() == WearableDataSync.OFF) { @@ -145,12 +143,12 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C if (settingsManager.isWeatherLoaded()) { if (settingsManager.getDataSync() == WearableDataSync.OFF && settingsManager.useFollowGPS()) { try { - val result = updateLocation() + val locationResult = updateLocation() - when (result) { + when (locationResult) { is LocationResult.Changed -> { locationChanged = true - settingsManager.saveLastGPSLocData(result.data) + settingsManager.saveLastGPSLocData(locationResult.data) } else -> { // no-op @@ -166,11 +164,11 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C } // Update for home - val weather = getWeather() + val weatherResult = getWeather() WidgetUpdaterWorker.requestWidgetUpdate(context) - if (weather == null) { + if (weatherResult !is WeatherResult.Success && weatherResult !is WeatherResult.WeatherWithError) { if (settingsManager.getDataSync() != WearableDataSync.OFF) { // Check if data has been updated WearableWorker.enqueueAction( @@ -178,38 +176,52 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C WearableWorkerActions.ACTION_REQUESTWEATHERUPDATE ) } - Timber.tag(TAG).i("Work failed...") - return false + } + + result = when (weatherResult) { + is WeatherResult.Success -> Result.success() + is WeatherResult.NoWeather -> Result.failure() + is WeatherResult.Error, + is WeatherResult.WeatherWithError -> Result.retry() } } - Timber.tag(TAG).i("Work completed successfully...") - return true + when (result) { + Result.success() -> { + Timber.tag(TAG).i("Work completed successfully...") + } + + Result.retry() -> { + Timber.tag(TAG).w("Work failed. Will retry...") + } + + Result.failure() -> { + Timber.tag(TAG).e("Work failed...") + } + } + + return result } - private suspend fun getWeather(): Weather? = withContext(Dispatchers.IO) { + private suspend fun getWeather(): WeatherResult = withContext(Dispatchers.IO) { Timber.tag(TAG).d("Getting weather data...") - val weather = try { - val locData = settingsManager.getHomeData() ?: return@withContext null - WeatherDataLoader(locData) - .loadWeatherData( - WeatherRequest.Builder() - .run { - if (settingsManager.getDataSync() == WearableDataSync.OFF) { - this.forceRefresh(false) - .loadAlerts() - } else { - this.forceLoadSavedData() - } + val locData = + settingsManager.getHomeData() ?: return@withContext WeatherResult.NoWeather() + + WeatherDataLoader(locData) + .loadWeatherResult( + WeatherRequest.Builder() + .run { + if (settingsManager.getDataSync() == WearableDataSync.OFF) { + this.forceRefresh(false) + .loadAlerts() + } else { + this.forceLoadSavedData() } - .build() - ) - } catch (ex: Exception) { - Logger.writeLine(Log.ERROR, ex, "%s: getWeather error", TAG) - null - } - weather + } + .build() + ) } private suspend fun updateLocation(): LocationResult { diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt index 55c73002..e637160d 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt @@ -6,12 +6,14 @@ import androidx.work.* import com.thewizrd.common.utils.LiveDataUtils.awaitWithTimeout import com.thewizrd.common.weatherdata.WeatherDataLoader import com.thewizrd.common.weatherdata.WeatherRequest +import com.thewizrd.common.weatherdata.WeatherResult import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.di.settingsManager +import com.thewizrd.shared_resources.exceptions.ErrorStatus +import com.thewizrd.shared_resources.exceptions.WeatherException import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.wearable.WearableDataSync -import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.services.ServiceNotificationHelper.JOB_ID import com.thewizrd.simpleweather.services.ServiceNotificationHelper.getForegroundNotification import com.thewizrd.simpleweather.services.ServiceNotificationHelper.initChannel @@ -120,17 +122,19 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co } override suspend fun doWork(): Result { - WidgetUpdaterWork.executeWork(applicationContext) - return Result.success() + return WidgetUpdaterWork.executeWork(applicationContext) } private object WidgetUpdaterWork { - suspend fun executeWork(context: Context) { + suspend fun executeWork(context: Context): Result { + var result = Result.success() + val settingsManager = SettingsManager(context.applicationContext) if (settingsManager.isWeatherLoaded()) { // If saved data DNE (for current location), refresh weather - if (getWeather() == null) { + var weatherResult = loadWeather() + if (weatherResult !is WeatherResult.Success && weatherResult !is WeatherResult.WeatherWithError) { if (settingsManager.getDataSync() != WearableDataSync.OFF) { // Check if data has been updated WearableWorker.enqueueAction( @@ -138,14 +142,35 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co WearableWorkerActions.ACTION_REQUESTWEATHERUPDATE ) } else { - getWeather(true) + weatherResult = loadWeather(true) } } + result = when (weatherResult) { + is WeatherResult.Success -> Result.success() + is WeatherResult.NoWeather -> Result.failure() + is WeatherResult.Error, + is WeatherResult.WeatherWithError -> Result.retry() + } + requestWidgetUpdate(context) } - Timber.tag(TAG).i("Work completed successfully...") + when (result) { + Result.success() -> { + Timber.tag(TAG).i("Work completed successfully...") + } + + Result.retry() -> { + Timber.tag(TAG).w("Work failed. Will retry...") + } + + Result.failure() -> { + Timber.tag(TAG).e("Work failed...") + } + } + + return result } fun requestWidgetUpdate(context: Context) { @@ -156,30 +181,28 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co WeatherTileHelper.requestTileUpdateAll(context.applicationContext) } - private suspend fun getWeather(forceRefresh: Boolean = false): Weather? = + private suspend fun loadWeather(forceRefresh: Boolean = false): WeatherResult = withContext(Dispatchers.IO) { Timber.tag(TAG).d("Getting weather data...") - val weather = try { - val locData = settingsManager.getHomeData() ?: return@withContext null - WeatherDataLoader(locData) - .loadWeatherData( - WeatherRequest.Builder() - .run { - if (forceRefresh && settingsManager.getDataSync() == WearableDataSync.OFF) { - this.forceRefresh(false) - .loadAlerts() - } else { - this.forceLoadSavedData() - } + val locData = + settingsManager.getHomeData() ?: return@withContext WeatherResult.Error( + WeatherException(ErrorStatus.NOWEATHER) + ) + + WeatherDataLoader(locData) + .loadWeatherResult( + WeatherRequest.Builder() + .run { + if (forceRefresh && settingsManager.getDataSync() == WearableDataSync.OFF) { + this.forceRefresh(false) + .loadAlerts() + } else { + this.forceLoadSavedData() } - .build() - ) - } catch (ex: Exception) { - Logger.writeLine(Log.ERROR, ex, "%s: getWeather error", TAG) - null - } - weather + } + .build() + ) } } } \ No newline at end of file From a23ac3e12d41e6630a4f0d6c11197e996a819e61 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 03:39:36 -0400 Subject: [PATCH 04/33] SimpleWeather: v5.12.0-build0 --- .aiexclude | 38 ++++++++++++++++++++++++++++++++++++++ app/build.gradle | 4 ++-- wearapp/build.gradle | 4 ++-- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .aiexclude diff --git a/.aiexclude b/.aiexclude new file mode 100644 index 00000000..47da38f6 --- /dev/null +++ b/.aiexclude @@ -0,0 +1,38 @@ +*.iml +*.bak +*.orig +.gradle +/local.properties +/.idea/assetWizardSettings.xml +/.idea/workspace.xml +/.idea/libraries +/.idea/caches +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +API_KEY.txt +.cxx +/weather-api/src/main/jni/fullgms +/weather-api/src/fullgms/java/com/thewizrd/weather_api/keys/Keys.java +/weather-api/src/fullgms/java/com/thewizrd/weather_api/keys/WeatherKitConfig.kt +/app/debug +/app/release +/app/fullgms +/app/nongms +/wearapp/debug +/wearapp/release +/wearapp/fullgms +/wearapp/nongms +/keycheck.bat +/keycheck.ps1 +/ImportTranslations.bat +/secure.properties +/weathericons/ +/extras/ +*.7z +google-services.json +/.privado/ +/.detekt +/.kotlin \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6dc408b9..c5c445bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 345110040 - versionName "5.11.1" + versionCode 355120000 + versionName "5.12.0" vectorDrawables { useSupportLibrary true diff --git a/wearapp/build.gradle b/wearapp/build.gradle index 375216ab..c9390c7b 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,8 +16,8 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 345110041 - versionName "5.11.1" + versionCode 355120001 + versionName "5.12.0" vectorDrawables { useSupportLibrary true From 08ff93225fc031cc7caa27e7e4031308565ae457 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 13:04:21 -0400 Subject: [PATCH 05/33] gradle: update dependencies + compileSdk v36 --- build.gradle | 14 +++++++------- wearapp/build.gradle | 4 ++-- weather-api/build.gradle | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index fc0b8159..c4cb1547 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - compileSdkVersion = 35 + compileSdkVersion = 36 minSdkVersion = 23 targetSdkVersion = 35 @@ -12,7 +12,7 @@ buildscript { desugar_version = '2.1.5' - firebase_version = '33.13.0' + firebase_version = '33.15.0' gms_location_version = '21.3.0' gms_base_version = '18.7.0' gms_basement_version = '18.7.0' @@ -21,12 +21,12 @@ buildscript { annotation_version = '1.9.1' activity_version = '1.10.1' - appcompat_version = '1.7.0' + appcompat_version = '1.7.1' constraintlayout_version = '2.2.1' core_version = '1.16.0' arch_core_runtime_version = '2.2.0' - fragment_version = '1.8.6' - lifecycle_version = '2.9.0' + fragment_version = '1.8.8' + lifecycle_version = '2.9.1' nav_version = '2.9.0' paging_version = '3.3.6' preference_version = '1.2.1' @@ -46,9 +46,9 @@ buildscript { material_version = '1.12.0' compose_compiler_version = '1.5.15' - compose_bom_version = '2025.05.00' + compose_bom_version = '2025.06.00' wear_compose_version = '1.4.1' - wear_tiles_version = '1.4.1' + wear_tiles_version = '1.5.0' wear_watchface_version = '1.2.1' horologist_version = '0.6.23' accompanist_version = '0.37.3' diff --git a/wearapp/build.gradle b/wearapp/build.gradle index c9390c7b..d4c9ff9f 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -165,7 +165,7 @@ dependencies { testImplementation "androidx.wear.tiles:tiles-testing:$wear_tiles_version" implementation "androidx.wear.tiles:tiles-tooling:$wear_tiles_version" implementation "androidx.wear.tiles:tiles-tooling-preview:$wear_tiles_version" - implementation 'androidx.wear.protolayout:protolayout-material:1.2.1' + implementation 'androidx.wear.protolayout:protolayout-material:1.3.0' implementation "com.google.android.horologist:horologist-tiles:$horologist_version" // WearOS Compose @@ -189,7 +189,7 @@ dependencies { implementation "com.google.android.horologist:horologist-compose-material:$horologist_version" implementation "com.google.android.horologist:horologist-compose-tools:$horologist_version" - implementation 'sh.calvin.reorderable:reorderable:2.4.3' + implementation 'sh.calvin.reorderable:reorderable:2.5.1' androidTestImplementation platform("androidx.compose:compose-bom:$compose_bom_version") androidTestImplementation "androidx.compose.ui:ui-test-junit4" diff --git a/weather-api/build.gradle b/weather-api/build.gradle index ddff91a7..cc43c671 100644 --- a/weather-api/build.gradle +++ b/weather-api/build.gradle @@ -133,7 +133,7 @@ dependencies { implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.github.nename0:moshi-java-codegen:1.1.1" - fullgmsImplementation 'com.google.android.libraries.places:places:4.2.0' + fullgmsImplementation 'com.google.android.libraries.places:places:4.3.1' fullgmsImplementation 'com.android.volley:volley:1.2.1' // Required by Places API ^^^ fullgmsImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_version" } From 76860371c3d522e4270fa787f9a66ce973d56f15 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 18:30:30 -0400 Subject: [PATCH 06/33] GoogleLocationProvider: update mapping --- .../thewizrd/simpleweather/test/UnitTests.kt | 6 +- .../google/location/GoogleLocationProvider.kt | 40 +++++----- .../google/location/LocationData.kt | 78 +++++++++++-------- 3 files changed, 69 insertions(+), 55 deletions(-) diff --git a/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt b/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt index 4d6bcb64..6d689107 100644 --- a/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt +++ b/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt @@ -378,7 +378,9 @@ class UnitTests { val queryVM = locations.firstOrNull() assertNotNull(queryVM) - val locModel = if (locationProvider.needsLocationFromName()) { + val locModel = if (locationProvider.needsLocationFromID()) { + locationProvider.getLocationFromID(queryVM!!) + } else if (locationProvider.needsLocationFromName()) { locationProvider.getLocationFromName(queryVM!!) } else if (locationProvider.needsLocationFromGeocoder()) { locationProvider.getLocation( @@ -387,8 +389,6 @@ class UnitTests { queryVM.locationLong ), WeatherAPI.GOOGLE ) - } else if (locationProvider.needsLocationFromID()) { - locationProvider.getLocationFromID(queryVM!!) } else { queryVM } diff --git a/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/GoogleLocationProvider.kt b/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/GoogleLocationProvider.kt index b39453f8..46722da2 100644 --- a/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/GoogleLocationProvider.kt +++ b/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/GoogleLocationProvider.kt @@ -7,7 +7,7 @@ import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.libraries.places.api.Places import com.google.android.libraries.places.api.model.AutocompleteSessionToken import com.google.android.libraries.places.api.model.Place -import com.google.android.libraries.places.api.model.TypeFilter +import com.google.android.libraries.places.api.model.PlaceTypes import com.google.android.libraries.places.api.net.FetchPlaceRequest import com.google.android.libraries.places.api.net.FetchPlaceResponse import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest @@ -33,9 +33,9 @@ class GoogleLocationProvider : WeatherLocationProviderImpl() { companion object { private val BASIC_PLACE_FIELDS = listOf( Place.Field.ADDRESS_COMPONENTS, - Place.Field.ADDRESS, - Place.Field.LAT_LNG, - Place.Field.NAME, + Place.Field.FORMATTED_ADDRESS, + Place.Field.LOCATION, + Place.Field.DISPLAY_NAME, Place.Field.TYPES ) } @@ -90,10 +90,10 @@ class GoogleLocationProvider : WeatherLocationProviderImpl() { // Use the builder to create a FindAutocompletePredictionsRequest. val request = FindAutocompletePredictionsRequest.builder() - .setTypeFilter(TypeFilter.CITIES) - .setSessionToken(autocompleteToken) - .setQuery(ac_query) - .build() + .setTypesFilter(listOf(PlaceTypes.CITIES)) + .setSessionToken(autocompleteToken) + .setQuery(ac_query) + .build() val response = placesClient.findAutocompletePredictions(request).await() locations = HashSet() @@ -153,14 +153,14 @@ class GoogleLocationProvider : WeatherLocationProviderImpl() { var wEx: WeatherException? = null try { - val request = FetchPlaceRequest.builder(model.locationQuery, BASIC_PLACE_FIELDS) + val request = FetchPlaceRequest.builder(model.locationQuery!!, BASIC_PLACE_FIELDS) .setSessionToken(autocompleteToken) .build() - response = placesClient.fetchPlace(request).await() + response = placesClient.fetchPlace(request).await() - autocompleteToken = null - } catch (e: Throwable) { + autocompleteToken = null + } catch (e: Throwable) { var ex = e if (ex is ExecutionException && ex.cause is Throwable) { @@ -188,17 +188,17 @@ class GoogleLocationProvider : WeatherLocationProviderImpl() { } } } - Logger.writeLine(Log.ERROR, ex, "GoogleLocationProvider: error getting location") - } + Logger.writeLine(Log.ERROR, ex, "GoogleLocationProvider: error getting location") + } - if (wEx != null) throw wEx + if (wEx != null) throw wEx - location = response?.let { - createLocationModel(it, model.weatherSource) - } ?: LocationQuery() + location = response?.let { + createLocationModel(it, model.weatherSource) + } ?: LocationQuery() - return@withContext location - } + return@withContext location + } @Throws(WeatherException::class) override suspend fun getLocationFromName( diff --git a/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/LocationData.kt b/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/LocationData.kt index 1a4a3a84..504749c3 100644 --- a/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/LocationData.kt +++ b/weather-api/src/fullgms/java/com/thewizrd/weather_api/google/location/LocationData.kt @@ -32,50 +32,61 @@ fun createLocationModel( @WeatherAPI.WeatherProviders weatherAPI: String? ): LocationQuery { return LocationQuery().apply { - var town: String? = null - var region: String? = null - var adminArea: String? = null - var countryName: String? = null + var town: AddressComponent? = null + var region: AddressComponent? = null + var adminArea: AddressComponent? = null + var country: AddressComponent? = null val addressComponents: List? = response.place.addressComponents?.asList() if (!addressComponents.isNullOrEmpty()) { for (addrCmp in addressComponents) { - if (town.isNullOrBlank() && addrCmp?.types?.contains("locality") == true) { - town = addrCmp.name + if (town == null && addrCmp?.types?.contains("locality") == true) { + town = addrCmp } - if (adminArea.isNullOrBlank() && addrCmp?.types?.contains("administrative_area_level_2") == true) { - adminArea = addrCmp.shortName + if (adminArea == null && addrCmp?.types?.contains("administrative_area_level_2") == true) { + adminArea = addrCmp } - if (region.isNullOrBlank() && addrCmp?.types?.contains("administrative_area_level_1") == true) { - region = addrCmp.shortName + if (region == null && addrCmp?.types?.contains("administrative_area_level_1") == true) { + region = addrCmp } - if (locationCountry.isNullOrBlank() && addrCmp?.types?.contains("country") == true) { - countryName = addrCmp.name + if (country == null && addrCmp?.types?.contains("country") == true) { + country = addrCmp locationCountry = addrCmp.shortName } - if (town != null && adminArea != null && region != null && locationCountry != null) { + if (town != null && adminArea != null && region != null && country != null) { break } } } - if (!town.isNullOrBlank() && !region.isNullOrBlank() && !adminArea.isNullOrBlank() && - !(adminArea == region || adminArea == town)) { - locationName = String.format("%s, %s, %s", town, adminArea, region) - } else if (!town.isNullOrBlank() && !region.isNullOrBlank()) { - locationName = if (town == region) { - String.format("%s, %s", town, countryName) + val isUS = country?.shortName == "US" || country?.name == "United States" + + if (town != null && region != null && adminArea != null && !(adminArea.name == region.name || adminArea.name.contains( + region.name, + true + ) || adminArea.name == town.name || adminArea.name.contains(town.name, true)) + ) { + locationName = String.format( + "%s, %s, %s", + town.name, + adminArea.name, + if (isUS) region.shortName else region.name + ) + } else if (town != null && region != null) { + locationName = if (town.name == region.name) { + String.format("%s, %s", town.name, country?.name) } else { - String.format("%s, %s", town, region) + String.format("%s, %s", town.name, if (isUS) region.shortName else region.name) } } else { - if (town.isNullOrBlank() || region.isNullOrBlank()) { - if (!response.place.name.isNullOrBlank()) { - val placeName = response.place.name + if (town == null || region == null) { + if (!response.place.displayName.isNullOrBlank()) { + val placeName = response.place.displayName + locationName = when { - placeName?.contains(", $countryName") == true -> { - placeName.replace(", $countryName", "") + placeName?.contains(", ${country?.name}") == true -> { + placeName.replace(", ${country?.name}", "") } placeName?.contains(", $locationCountry") == true -> { placeName.replace(", $locationCountry", "") @@ -85,24 +96,27 @@ fun createLocationModel( } } } else { - locationName = if (town.isNullOrBlank()) { - String.format("%s, %s", region, countryName) + locationName = if (town == null) { + String.format("%s, %s", region?.name, country?.name) } else { - String.format("%s, %s", town, countryName) + String.format("%s, %s", town.name, country?.name) } } } } if (locationName.isNullOrBlank()) { - locationName = response.place.name + locationName = response.place.displayName } if (locationCountry.isNullOrBlank()) { - locationCountry = countryName + locationCountry = country?.shortName ?: country?.name + } + if (locationRegion.isNullOrBlank()) { + locationRegion = region?.name ?: adminArea?.name } - locationLat = response.place.latLng!!.latitude - locationLong = response.place.latLng!!.longitude + locationLat = response.place.location!!.latitude + locationLong = response.place.location!!.longitude locationTZLong = null From 8e64bf233fb893958be3ea980fd76059f56d88c9 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 15 Jun 2025 20:36:01 -0400 Subject: [PATCH 07/33] WeatherNowFragment: fix issue with detail panel not loading on startup --- .../simpleweather/main/WeatherListFragment.kt | 30 +++++++++++-------- .../simpleweather/main/WeatherNowFragment.kt | 22 ++++++++++++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherListFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherListFragment.kt index aadfcb44..e541cf15 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherListFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherListFragment.kt @@ -182,8 +182,12 @@ class WeatherListFragment : ToolbarFragment() { if (args.data.isNullOrBlank() && savedInstanceState?.containsKey(Constants.KEY_DATA) != true) { viewLifecycleOwner.lifecycleScope.launch { wNowViewModel.uiState.collect { + val oldData = locationData locationData = it.locationData - initialize() + + if (oldData != locationData) { + initialize() + } } } } @@ -195,18 +199,20 @@ class WeatherListFragment : ToolbarFragment() { } // - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - runCatching { - delay(5000) - - if (isActive && inAppReviewManager.shouldShowReviewFlow()) { - // Wait for no movement - while (isActive && binding.recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) { - delay(1500) - } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + runCatching { + delay(5000) + + if (isActive && isVisible && isViewAlive && inAppReviewManager.shouldShowReviewFlow()) { + // Wait for no movement + while (isActive && binding.recyclerView.scrollState != RecyclerView.SCROLL_STATE_IDLE) { + delay(1500) + } - activity?.run { - inAppReviewManager.showReviewFlow(this) + activity?.run { + inAppReviewManager.showReviewFlow(this) + } } } } diff --git a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt index fca881c5..73206807 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt @@ -11,6 +11,7 @@ import android.hardware.SensorManager import android.os.Build import android.os.Bundle import android.text.method.LinkMovementMethod +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewConfiguration @@ -84,6 +85,7 @@ import com.thewizrd.shared_resources.utils.ContextUtils.getOrientation import com.thewizrd.shared_resources.utils.ContextUtils.isLargeTablet import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth import com.thewizrd.shared_resources.utils.JSONParser +import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.utils.UserThemeMode import com.thewizrd.shared_resources.weatherdata.model.LocationType import com.thewizrd.shared_resources.weatherdata.model.MoonPhase.MoonPhaseType @@ -888,8 +890,6 @@ class WeatherNowFragment : AbstractWeatherListDetailFragment(), BannerManagerInt detailPaneNavHostFragment.arguments = Bundle() } - detailPaneNavHostFragment.arguments?.putString("testing", "value") - slidingPaneLayout.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> val paneLayout = v as SlidingPaneLayout twoPaneStateViewModel.updateSideBySide(!paneLayout.isSlideable) @@ -986,6 +986,24 @@ class WeatherNowFragment : AbstractWeatherListDetailFragment(), BannerManagerInt WidgetWorker.enqueueRefreshWidgets(context, locationData) } } + + // NOTE: Startup workaround for unloaded detail fragment + runWithView { + runCatching { + if (it?.isValid == true && isVisible && + detailPaneNavHostFragment.isAdded && + twoPaneStateViewModel.twoPaneState.value.isSideBySide && + detailPaneNavHostFragment.navController.visibleEntries.value.size == 1 + ) { + openDetails( + TwoPaneNavGraphDirections.actionGlobalWeatherListFragment2() + .setWeatherListType(WeatherListType.FORECAST) + ) + } + }.onFailure { + Logger.writeLine(Log.ERROR, it) + } + } } } } From 410278fe714a30e272a63dfd23d29fd7b8caa1bf Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Mon, 16 Jun 2025 23:41:20 -0400 Subject: [PATCH 08/33] PoPChanceNotificationHelper: make notification text clearer * Likely instead of definitive * Use expected time instead of duration --- .../PoPChanceNotificationHelper.kt | 61 ++++++++++--------- app/src/main/res/values/strings.xml | 3 + .../preferences/SettingsManager.kt | 4 +- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt index 27062037..fe15ba74 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt @@ -6,22 +6,25 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import android.text.format.DateFormat import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.getSystemService import com.thewizrd.common.helpers.areNotificationsEnabled +import com.thewizrd.shared_resources.DateTimeConstants import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.helpers.toImmutableCompatFlag import com.thewizrd.shared_resources.icons.WeatherIcons import com.thewizrd.shared_resources.icons.WeatherIconsEFProvider import com.thewizrd.shared_resources.locationdata.LocationData -import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.sharedDeps +import com.thewizrd.shared_resources.utils.DateTimeUtils import com.thewizrd.shared_resources.weatherdata.model.HourlyForecast import com.thewizrd.shared_resources.weatherdata.model.MinutelyForecast import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.main.MainActivity import java.time.Duration +import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.temporal.ChronoUnit @@ -31,13 +34,12 @@ object PoPChanceNotificationHelper { private const val NOT_CHANNEL_ID = "SimpleWeather.chancenotification" suspend fun postNotification(context: Context) { - val settingsManager = SettingsManager(context.applicationContext) - if (!settingsManager.isPoPChanceNotificationEnabled() || !settingsManager.isWeatherLoaded() || !context.areNotificationsEnabled()) return val now = ZonedDateTime.now(ZoneOffset.UTC) val lastPostTime = settingsManager.getLastPoPChanceNotificationTime() + // TODO: change time period // We already posted today; post any chance tomorrow if (now.toLocalDate() .isEqual(lastPostTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDate()) @@ -81,9 +83,9 @@ object PoPChanceNotificationHelper { val forecast = hrForecasts.find { it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() } - // Proceed if within the next 3hrs + // Proceed if within the next 2hrs if (forecast == null || Duration.between(now.truncatedTo(ChronoUnit.HOURS), forecast.date) - .toHours() > 3 + .toHours() >= 2 ) return false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -93,16 +95,23 @@ object PoPChanceNotificationHelper { val wim = sharedDeps.weatherIconsManager // Should be within 0-3 hours - val duration = Duration.between(now, forecast.date).toMinutes() - val duraStr = if (duration <= 60) { - context.getString(R.string.precipitation_nexthour_text_format, forecast.extras.pop) - } else if (duration < 120) { - context.getString(R.string.precipitation_text_format, forecast.extras.pop, - context.getString(R.string.refresh_30min).replace("30", duration.toString())) + val dt = + forecast.date.truncatedTo(ChronoUnit.HOURS).withZoneSameInstant(ZoneId.systemDefault()) + val time = if (DateFormat.is24HourFormat(context)) { + val skeleton = DateTimeConstants.SKELETON_24HR + dt.format( + DateTimeUtils.ofPatternForUserLocale( + DateTimeUtils.getBestPatternForSkeleton( + skeleton + ) + ) + ) } else { - context.getString(R.string.precipitation_text_format, forecast.extras.pop, - context.getString(R.string.refresh_12hrs).replace("12", (duration / 60).toString())) + val pattern = DateTimeConstants.ABBREV_12HR_AMPM + dt.format(DateTimeUtils.ofPatternForUserLocale(pattern)) } + val duraStr = + context.getString(R.string.precipitation_likely_text_format, time, forecast.extras.pop) val notifBuilder = NotificationCompat.Builder(context, NOT_CHANNEL_ID) .setOnlyAlertOnce(true) @@ -162,26 +171,18 @@ object PoPChanceNotificationHelper { val wim = sharedDeps.weatherIconsManager val formatStrResId = if (isRainingMinute != null) { - R.string.precipitation_minutely_stopping_text_format + R.string.precipitation_likely_minutely_stopping_text_format } else { - R.string.precipitation_minutely_starting_text_format + R.string.precipitation_likely_minutely_starting_text_format } - val duration = Duration.between(now, minute.date).toMinutes() - val duraStr = when { - duration < 120 -> { - context.getString( - formatStrResId, - context.getString(R.string.refresh_30min).replace("30", duration.toString()) - ) - } - else -> { - context.getString( - formatStrResId, - context.getString(R.string.refresh_12hrs) - .replace("12", (duration / 60).toString()) - ) - } + val dt = + minute.date.truncatedTo(ChronoUnit.MINUTES).withZoneSameInstant(ZoneId.systemDefault()) + val time = if (DateFormat.is24HourFormat(context)) { + dt.format(DateTimeUtils.ofPatternForUserLocale(DateTimeConstants.CLOCK_FORMAT_24HR)) + } else { + dt.format(DateTimeUtils.ofPatternForUserLocale(DateTimeConstants.CLOCK_FORMAT_12HR_AMPM)) } + val duraStr = context.getString(formatStrResId, time) val notifBuilder = NotificationCompat.Builder(context, NOT_CHANNEL_ID) .setOnlyAlertOnce(true) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 075852cd..60951873 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,9 @@ %1$d%% chance of rain in the next hour Rain will start in about %1$s Rain will stop in about %1$s + Rain likely around %1$s (%2$d%% chance) + Rain expected to start around %1$s + Rain expected to stop around %1$s Weather (1x1) diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt index 51800827..dcca8f3c 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt @@ -1012,7 +1012,7 @@ class SettingsManager(context: Context) { } fun getPoPChanceMinimumPercentage(): Int { - return preferences.getString(KEY_POPCHANCEPCT, "60")?.toIntOrNull() ?: 60 + return preferences.getString(KEY_POPCHANCEPCT, "50")?.toIntOrNull() ?: 50 } fun setPoPChanceMinimumPercentage( @@ -1027,7 +1027,7 @@ class SettingsManager(context: Context) { if (pct in 40..90) { pct } else { - 60 + 50 }.toString() ) } From 8b5b8fb089ae0f7e373011afa26af31226a87787 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Mon, 16 Jun 2025 23:50:03 -0400 Subject: [PATCH 09/33] Import translations --- app/src/main/res/values-de/strings.xml | 8 ++--- app/src/main/res/values-es/strings.xml | 3 ++ app/src/main/res/values-fr/strings.xml | 11 ++---- app/src/main/res/values-nl/strings.xml | 3 ++ app/src/main/res/values-pl/strings.xml | 5 +-- app/src/main/res/values-sk/strings.xml | 3 ++ app/src/main/res/values-zh-rCN/strings.xml | 3 ++ .../src/main/res/values-de/strings.xml | 34 +------------------ .../src/main/res/values-es/strings.xml | 2 ++ .../src/main/res/values-fr/strings.xml | 2 ++ .../src/main/res/values-nl/strings.xml | 2 ++ .../src/main/res/values-pl/strings.xml | 2 ++ .../src/main/res/values-sk/strings.xml | 2 ++ .../main/res/values-zh-rCN/beaufort_scale.xml | 1 - .../src/main/res/values-zh-rCN/strings.xml | 2 ++ 15 files changed, 34 insertions(+), 49 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1be100d4..82bf1121 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -12,8 +12,6 @@ "Beaufort" "Luftqualitätsindex" "Mondphase" - - "Sonnenphase" "Wetterradar" "Bild in der Ortsliste" @@ -73,14 +71,11 @@ "Bewertung und Überprüfung" - "Mag die App? Helfen Sie bei der Unterstützung durch eine Rezension" "Übersetzung" - - "Bei der Übersetzung der Anwendung helfen" @@ -163,4 +158,7 @@ "Extrem" "Textschatten" "Schattenebene zum Text hinzufügen" + "Regen wahrscheinlich gegen %1$s (%2$d%% Chance)" + "Regenbeginn voraussichtlich um %1$s" + "Regenende voraussichtlich um %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index df43c5da..e8ac1a75 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -167,4 +167,7 @@ "Extremo" "Sombra de texto" "Añadir capa de sombra al texto" + "Probabilidad de lluvia alrededor de las %1$s (%2$d%% de probabilidad)" + "Se espera que la lluvia comience alrededor de las %1$s" + "Se espera que la lluvia pare alrededor de las %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2e84d5bc..b280740a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -62,8 +62,6 @@ "Commentaires" - - "Rapporter tout problème ou suggestion pour améliorer l'application" @@ -137,13 +135,7 @@ "Taille des icônes et de la police" "Taille de la police" "Taille de l'icône" - - "La pluie va commencer dans environ %1$s" - - "La pluie s'arrêtera dans environ %1$s" @@ -154,4 +146,7 @@ "Extrême" "Ombre du texte" "Ajouter une couche d'ombre au texte" + "Pluie probable vers %1$s (probabilité de %2$d%%)" + "La pluie devrait commencer vers %1$s" + "La pluie devrait cesser vers %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0688eb47..771c87f0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -143,4 +143,7 @@ "Extreem" "Tekst Schaduw" "Schaduwlaag toevoegen aan tekst" + "Waarschijnlijk regen rond %1$s (%2$d%% kans)" + "Regen begint naar verwachting rond %1$s" + "Regen stopt naar verwachting rond %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8c7ff5b5..ade5976c 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -104,8 +104,6 @@ "%1$d%% szans na opady w ciągu %2$s" - - "%1$d%% szans na opady w ciągu najbliższej godziny" @@ -145,4 +143,7 @@ "Ekstremalny" "Cień tekstu" "Dodaj warstwę cienia do tekstu" + "Prawdopodobny deszcz około %1$s (szansa %2$d%%)" + "Oczekuje się, że deszcz zacznie padać około %1$s" + "Oczekuje się, że deszcz przestanie padać około %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 950f1a8c..864489d9 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -143,4 +143,7 @@ "Extrémne" "Textový tieň" "Pridajte do textu tieňovú vrstvu" + "Je pravdepodobné, že okolo %1$s bude pršať (%2$d%% šanca)" + "Očakáva sa, že dážď začne okolo %1$s" + "Očakáva sa, že dážď prestane okolo %1$s" \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e068461a..8fadb8bf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -220,4 +220,7 @@ "极端" "文字阴影" "为文本添加阴影图层" + "%1$s左右可能下雨(%2$d%%的几率)" + "预计%1$s左右开始下雨" + "预计%1$s左右雨停" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-de/strings.xml b/shared_resources/src/main/res/values-de/strings.xml index b24212ab..9bfb756e 100644 --- a/shared_resources/src/main/res/values-de/strings.xml +++ b/shared_resources/src/main/res/values-de/strings.xml @@ -11,35 +11,21 @@ "Nur eine einfache Wetter-App" "Ort hinzufügen" "Einstellungen anpassen" - - "Vorwärts" - - "Zurück" "Wetter" - - "Einstellungen" - - "Erneut versuchen" "Details" - - "Luftfeuchtigkeit" - - "Luftdruck" "Sichtweite" "Gefühlt" "Wind" - - "Vorhersage" "Bedingung" "Sonnenauf- und -untergang" @@ -48,14 +34,10 @@ "Stündlich" "Niederschlag" - + "Niederschlagsgefahr" "Bewölkung" - - "Regen" - - "Schnee" "Daten von" "Foto von" @@ -74,8 +56,6 @@ "Abend" "Morgen" "Nacht" - - "Temp" @@ -90,8 +70,6 @@ "Version" "Einheiten" "Einheiten in Fahrenheit" - - "Einheiten in Celsius" "Über" "Über SimpleWeather" @@ -103,20 +81,12 @@ "Anbieterschlüssel eingeben" "Schlüssel bestätigt" "Schlüssel ungültig" - - "Wetter-Benachrichtigung" - - "Dauerhafte Benachrichtigung der aktuellen Wetterbedingungen anzeigen" - - "Wetteranbieter" "Benachrichtigungen" "Benachrichtigungssymbol" "Wetterwarnungen" - - "Benachrichtigungen für Wetterwarnungen anzeigen" "Wetterwarnungen werden von diesem Wetteranbieter nicht unterstützt" "Benutzerkonto erstellen" @@ -236,8 +206,6 @@ "Um den Standort im Hintergrund für Kacheln und Komplikationen zu aktualisieren, setzen Sie bitte die Standortberechtigung auf '%1$s'" "Aktuell" - "Vorhersage im Minutentakt" "Nutzername" diff --git a/shared_resources/src/main/res/values-es/strings.xml b/shared_resources/src/main/res/values-es/strings.xml index dadec5ad..4940c766 100644 --- a/shared_resources/src/main/res/values-es/strings.xml +++ b/shared_resources/src/main/res/values-es/strings.xml @@ -33,6 +33,8 @@ "Puesta del sol" "Cada hora" "Precipitación" + + "Probabilidad" "Nubosidad" "Lluvia" diff --git a/shared_resources/src/main/res/values-fr/strings.xml b/shared_resources/src/main/res/values-fr/strings.xml index 1884a08c..b08e5e6c 100644 --- a/shared_resources/src/main/res/values-fr/strings.xml +++ b/shared_resources/src/main/res/values-fr/strings.xml @@ -33,6 +33,8 @@ "Coucher de soleil" "Par heure" "Précipitation" + + "Chance" "Nébulosité" "Pluie" diff --git a/shared_resources/src/main/res/values-nl/strings.xml b/shared_resources/src/main/res/values-nl/strings.xml index b2d9df26..cce662a8 100644 --- a/shared_resources/src/main/res/values-nl/strings.xml +++ b/shared_resources/src/main/res/values-nl/strings.xml @@ -33,6 +33,8 @@ "Zon onder" "Per uur" "Neerslag" + + "Kans" "Bewolking" "Regen" diff --git a/shared_resources/src/main/res/values-pl/strings.xml b/shared_resources/src/main/res/values-pl/strings.xml index e3a3854d..6e77474c 100644 --- a/shared_resources/src/main/res/values-pl/strings.xml +++ b/shared_resources/src/main/res/values-pl/strings.xml @@ -33,6 +33,8 @@ "Zachód słońca" "Godzinowa" "Opady" + + "Szansa na opady" "Zachmurzenie" "Deszcz" diff --git a/shared_resources/src/main/res/values-sk/strings.xml b/shared_resources/src/main/res/values-sk/strings.xml index 369dadda..5532743d 100644 --- a/shared_resources/src/main/res/values-sk/strings.xml +++ b/shared_resources/src/main/res/values-sk/strings.xml @@ -33,6 +33,8 @@ "Súmrak" "Hodinovo" "Zrážky" + + "Šanca" "Oblačnosť" "Dážď" diff --git a/shared_resources/src/main/res/values-zh-rCN/beaufort_scale.xml b/shared_resources/src/main/res/values-zh-rCN/beaufort_scale.xml index 459fce22..2cf3951f 100644 --- a/shared_resources/src/main/res/values-zh-rCN/beaufort_scale.xml +++ b/shared_resources/src/main/res/values-zh-rCN/beaufort_scale.xml @@ -1,7 +1,6 @@ - "无风" diff --git a/shared_resources/src/main/res/values-zh-rCN/strings.xml b/shared_resources/src/main/res/values-zh-rCN/strings.xml index 3fcca99d..ceb22da7 100644 --- a/shared_resources/src/main/res/values-zh-rCN/strings.xml +++ b/shared_resources/src/main/res/values-zh-rCN/strings.xml @@ -33,6 +33,8 @@ "日落" "小时概览" "降水" + + "概率" "云量" "雨量" From 4e39ce5a19416cd3aac29812d36575cbc3cc9c76 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Mon, 16 Jun 2025 23:51:09 -0400 Subject: [PATCH 10/33] SimpleWeather: v5.12.0-build1 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c5c445bd..0edf1e0a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120000 + versionCode 355120010 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index d4c9ff9f..d5819b6c 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120001 + versionCode 355120011 versionName "5.12.0" vectorDrawables { From 21f439e069807f3cfdf277e6f7e42753f73230f5 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Thu, 19 Jun 2025 15:06:12 -0400 Subject: [PATCH 11/33] LocationSearchActivity: hide keyboard when showing error snackbar --- .../thewizrd/simpleweather/activities/LocationSearchActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt b/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt index 67df9af3..4430e8b5 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/activities/LocationSearchActivity.kt @@ -289,6 +289,8 @@ class LocationSearchActivity : WindowColorActivity() { } private fun onErrorMessage(error: ErrorMessage) { + binding.searchView.clearFocusAndHideKeyboard() + when (error) { is ErrorMessage.Resource -> { Snackbar.make(binding.root, error.stringId, Snackbar.LENGTH_SHORT).apply { From 7a46c0a9ed998a02db826606d550f4bfa042b274 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Thu, 19 Jun 2025 15:21:57 -0400 Subject: [PATCH 12/33] PoPChanceNotificationHelper: schedule an update once rain event ends --- .../PoPChanceNotificationHelper.kt | 55 ++++++--- .../services/WidgetUpdaterWorker.kt | 105 ++++++++++++++++-- 2 files changed, 134 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt index fe15ba74..cc5f02f8 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt @@ -19,15 +19,18 @@ import com.thewizrd.shared_resources.icons.WeatherIconsEFProvider import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.shared_resources.sharedDeps import com.thewizrd.shared_resources.utils.DateTimeUtils +import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.weatherdata.model.HourlyForecast import com.thewizrd.shared_resources.weatherdata.model.MinutelyForecast import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.main.MainActivity +import com.thewizrd.simpleweather.services.WidgetUpdaterWorker import java.time.Duration import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.temporal.ChronoUnit +import java.util.concurrent.TimeUnit object PoPChanceNotificationHelper { // Sets an ID for the notification @@ -39,11 +42,8 @@ object PoPChanceNotificationHelper { val now = ZonedDateTime.now(ZoneOffset.UTC) val lastPostTime = settingsManager.getLastPoPChanceNotificationTime() - // TODO: change time period - // We already posted today; post any chance tomorrow - if (now.toLocalDate() - .isEqual(lastPostTime.withZoneSameInstant(ZoneOffset.UTC).toLocalDate()) - ) return + // We already posted recently; skip for now + if (now.isBefore(lastPostTime)) return // Get the forecast for the next 12 hours val location = settingsManager.getHomeData() ?: return @@ -67,8 +67,6 @@ object PoPChanceNotificationHelper { return } } - - settingsManager.setLastPoPChanceNotificationTime(now) } private fun createPoPNotification( @@ -114,12 +112,13 @@ object PoPChanceNotificationHelper { context.getString(R.string.precipitation_likely_text_format, time, forecast.extras.pop) val notifBuilder = NotificationCompat.Builder(context, NOT_CHANNEL_ID) - .setOnlyAlertOnce(true) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(getOnClickIntent(context, location)) - .setContentTitle(duraStr) - .setContentText(location.name) + //.setOnlyAlertOnce(true) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(getOnClickIntent(context, location)) + .setContentTitle(duraStr) + .setContentText(location.name) + .setTimeoutAfter(TimeUnit.HOURS.toMillis(3)) if (wim.isFontIcon) { notifBuilder.setSmallIcon(wim.getWeatherIconResource(WeatherIcons.UMBRELLA)) @@ -132,10 +131,17 @@ object PoPChanceNotificationHelper { val notifMgr = context.getSystemService()!! notifMgr.notify(NOT_CHANNEL_ID, location.hashCode(), notifBuilder.build()) + // Find the next hour with < 60% or higher chance of precipitation + val stopForecast = + hrForecasts.find { it.extras?.pop == null || it.extras.pop < settingsManager.getPoPChanceMinimumPercentage() } + // Delay further notifications until this time + val nextTime = stopForecast?.date?.truncatedTo(ChronoUnit.HOURS) ?: now.plusHours(2) + settingsManager.setLastPoPChanceNotificationTime(nextTime.minusMinutes(5)) + return true } - private fun createMinutelyNotification( + private suspend fun createMinutelyNotification( context: Context, location: LocationData, minForecasts: List?, @@ -182,15 +188,18 @@ object PoPChanceNotificationHelper { } else { dt.format(DateTimeUtils.ofPatternForUserLocale(DateTimeConstants.CLOCK_FORMAT_12HR_AMPM)) } - val duraStr = context.getString(formatStrResId, time) + val formatStr = context.getString(formatStrResId, time) + + val duration = Duration.between(now, minute.date).abs() val notifBuilder = NotificationCompat.Builder(context, NOT_CHANNEL_ID) - .setOnlyAlertOnce(true) + //.setOnlyAlertOnce(true) .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(getOnClickIntent(context, location)) - .setContentTitle(duraStr) + .setContentTitle(formatStr) .setContentText(location.name) + .setTimeoutAfter(duration.multipliedBy(2).toMillis()) if (wim.isFontIcon) { notifBuilder.setSmallIcon(wim.getWeatherIconResource(WeatherIcons.UMBRELLA)) @@ -203,6 +212,18 @@ object PoPChanceNotificationHelper { val notifMgr = context.getSystemService()!! notifMgr.notify(NOT_CHANNEL_ID, location.hashCode(), notifBuilder.build()) + // Delay further notifications until this time + val nextTime = minute.date.truncatedTo(ChronoUnit.MINUTES) + settingsManager.setLastPoPChanceNotificationTime(nextTime) + + if (duration.toMinutes() < 60) { + runCatching { + WidgetUpdaterWorker.schedulePoPNotification(context, duration) + }.onFailure { + Logger.error("PoPChanceNotificationHelper", it) + } + } + return true } diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt index b0b4a724..5b698644 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt @@ -4,7 +4,19 @@ import android.content.Context import android.content.Intent import android.os.Build import android.util.Log -import androidx.work.* +import androidx.annotation.IntDef +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters import androidx.work.multiprocess.RemoteWorkManager import com.thewizrd.common.utils.LiveDataUtils.awaitWithTimeout import com.thewizrd.common.wearable.WearableSettings @@ -27,11 +39,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import java.time.Duration import java.util.concurrent.TimeUnit class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) { companion object { private const val TAG = "WidgetUpdaterWorker" + private const val KEY_ACTION = "action" const val ACTION_UPDATEWIDGETS = "SimpleWeather.Droid.action.UPDATE_WIDGETS" const val ACTION_ENQUEUEWORK = "SimpleWeather.Droid.action.START_ALARM" @@ -95,6 +109,57 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co Logger.writeLine(Log.INFO, "%s: Work enqueued", TAG) } + @JvmStatic + suspend fun schedulePoPNotification( + context: Context, + duration: Duration, + expedited: Boolean = false + ) { + // Check if there is existing work pending + val tag = "${TAG}_sched_pop" + val workMgr = WorkManager.getInstance(context.applicationContext) + val workInfos = withContext(Dispatchers.IO) { + runCatching { + workMgr.getWorkInfosForUniqueWork(tag).get(10, TimeUnit.SECONDS) + }.getOrElse { emptyList() } + } + + var existingWorkPolicy = ExistingWorkPolicy.REPLACE + val requestedDelayInMillis = duration.toMillis() + val nextScheduleTimeMillis = System.currentTimeMillis() + requestedDelayInMillis + + // If any existing work exists with a shorter delay, keep it, else replace it + for (workInfo in workInfos) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { + val estimatedDelayInMillis = + workInfo.nextScheduleTimeMillis - System.currentTimeMillis() + + if (estimatedDelayInMillis in 1..() + .apply { + if (expedited) { + setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + } + } + .setConstraints(Constraints.NONE) + .setInputData( + Data.Builder() + .putInt(KEY_ACTION, WidgetUpdaterWork.ACTION_UPDATEPOPNOTIFICATION) + .build() + ) + .setInitialDelay(requestedDelayInMillis, TimeUnit.MILLISECONDS) + .build() + + RemoteWorkManager.getInstance(context) + .enqueueUniqueWork(tag, existingWorkPolicy, request) + } + private suspend fun isWorkScheduled(context: Context): Boolean { val workMgr = WorkManager.getInstance(context.applicationContext) val statuses = workMgr.getWorkInfosForUniqueWorkLiveData(TAG).awaitWithTimeout(10000) @@ -125,11 +190,29 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co } override suspend fun doWork(): Result { - return WidgetUpdaterWork.executeWork(applicationContext) + val action = inputData.getInt(KEY_ACTION, WidgetUpdaterWork.ACTION_UPDATEALL) + return WidgetUpdaterWork.executeWork(applicationContext, action) } private object WidgetUpdaterWork { - suspend fun executeWork(context: Context): Result { + const val ACTION_UPDATEALL = 0.inv() + const val ACTION_UPDATEWIDGETS = 1 + const val ACTION_UPDATENOTIFICATION = 1 shl 1 + const val ACTION_UPDATEPOPNOTIFICATION = 1 shl 2 + + @Retention(AnnotationRetention.SOURCE) + @IntDef( + ACTION_UPDATEALL, + ACTION_UPDATEWIDGETS, + ACTION_UPDATENOTIFICATION, + ACTION_UPDATEPOPNOTIFICATION + ) + annotation class UpdateAction + + suspend fun executeWork( + context: Context, + @UpdateAction action: Int = ACTION_UPDATEALL + ): Result { var result = Result.success() val settingsManager = SettingsManager(context.applicationContext) @@ -151,24 +234,28 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co result = when (weatherResult) { is WeatherResult.Success -> Result.success() is WeatherResult.NoWeather -> Result.failure() - is WeatherResult.Error, - is WeatherResult.WeatherWithError -> Result.retry() + is WeatherResult.Error -> Result.retry() + is WeatherResult.WeatherWithError -> if (action == ACTION_UPDATENOTIFICATION || action == ACTION_UPDATEPOPNOTIFICATION) { + Result.success() + } else { + Result.retry() + } } } - if (WidgetUpdaterHelper.widgetsExist()) { + if (action and ACTION_UPDATEWIDGETS != 0 && WidgetUpdaterHelper.widgetsExist()) { WidgetUpdaterHelper.refreshWidgets(context) } - if (settingsManager.showOngoingNotification()) { + if (action and ACTION_UPDATENOTIFICATION != 0 && settingsManager.showOngoingNotification()) { WeatherNotificationWorker.refreshNotification(context) } - if (settingsManager.isPoPChanceNotificationEnabled()) { + if (action and ACTION_UPDATEPOPNOTIFICATION != 0 && settingsManager.isPoPChanceNotificationEnabled()) { PoPChanceNotificationHelper.postNotification(context) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + if (action and ACTION_UPDATEWIDGETS != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { ShortcutCreatorWorker.updateShortcuts(context) } } From 610864c630295e4636cd7853e7a98e3c0026124f Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Thu, 19 Jun 2025 15:26:01 -0400 Subject: [PATCH 13/33] WeatherWidget4x2Tomorrow: schedule an update once rain event ends --- .../simpleweather/services/WidgetWorker.kt | 71 ++++++++++- .../widgets/WidgetUpdaterHelper.kt | 4 +- .../WeatherWidget4x2TomorrowCreator.kt | 114 ++++++++++++------ 3 files changed, 148 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetWorker.kt index 0a432d66..15b20558 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetWorker.kt @@ -3,7 +3,17 @@ package com.thewizrd.simpleweather.services import android.appwidget.AppWidgetManager import android.content.Context import android.os.Build -import androidx.work.* +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.multiprocess.RemoteWorkManager import com.thewizrd.shared_resources.Constants import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.simpleweather.widgets.WeatherWidgetProvider.Companion.EXTRA_WIDGET_IDS @@ -12,10 +22,15 @@ import com.thewizrd.simpleweather.widgets.WidgetProviderInfo import com.thewizrd.simpleweather.widgets.WidgetType import com.thewizrd.simpleweather.widgets.WidgetUpdaterHelper import com.thewizrd.simpleweather.widgets.WidgetUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.time.Duration +import java.util.concurrent.TimeUnit class WidgetWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) { companion object { + private const val TAG = "WidgetWorker" private const val KEY_ACTION = "action" // Widget Actions @@ -119,6 +134,60 @@ class WidgetWorker(appContext: Context, params: WorkerParameters) : workMgr.enqueue(request) } + + suspend fun scheduleRefreshWidget( + context: Context, + appWidgetId: Int, + info: WidgetProviderInfo, + duration: Duration, + expedited: Boolean = false + ) { + // Check if there is existing work pending + val tag = "${TAG}_sched_${appWidgetId}" + val workMgr = WorkManager.getInstance(context.applicationContext) + val workInfos = withContext(Dispatchers.IO) { + runCatching { + workMgr.getWorkInfosForUniqueWork(tag).get(10, TimeUnit.SECONDS) + }.getOrElse { emptyList() } + } + + var existingWorkPolicy = ExistingWorkPolicy.REPLACE + val requestedDelayInMillis = duration.toMillis() + val nextScheduleTimeMillis = System.currentTimeMillis() + requestedDelayInMillis + + // If any existing work exists with a shorter delay, keep it, else replace it + for (workInfo in workInfos) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { + val estimatedDelayInMillis = + workInfo.nextScheduleTimeMillis - System.currentTimeMillis() + + if (estimatedDelayInMillis in 1..() + .apply { + if (expedited) { + setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + } + } + .setConstraints(Constraints.NONE) + .setInputData( + Data.Builder() + .putString(KEY_ACTION, ACTION_REFRESHWIDGET) + .putIntArray(EXTRA_WIDGET_IDS, intArrayOf(appWidgetId)) + .putInt(EXTRA_WIDGET_TYPE, info.widgetType.value) + .build() + ) + .setInitialDelay(requestedDelayInMillis, TimeUnit.MILLISECONDS) + .build() + + RemoteWorkManager.getInstance(context) + .enqueueUniqueWork(tag, existingWorkPolicy, request) + } } override suspend fun getForegroundInfo(): ForegroundInfo { diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt index f5e9c1ee..78932316 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt @@ -60,7 +60,7 @@ object WidgetUpdaterHelper { @JvmStatic fun widgetsExist(): Boolean { - for (widgetType in WidgetType.values()) { + for (widgetType in WidgetType.entries) { val info = WidgetUtils.getWidgetProviderInfoFromType(widgetType) if (info?.hasInstances == true) return true } @@ -73,7 +73,7 @@ object WidgetUpdaterHelper { val appWidgetManager = AppWidgetManager.getInstance(context) val jobs = mutableListOf>() - for (widgetType in WidgetType.values()) { + for (widgetType in WidgetType.entries) { val info = WidgetUtils.getWidgetProviderInfoFromType(widgetType) if (info != null) { diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt index 804d1e12..79a415d9 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt @@ -16,6 +16,7 @@ import com.thewizrd.common.controls.WeatherUiModel import com.thewizrd.common.helpers.ColorsUtils import com.thewizrd.common.utils.ImageUtils import com.thewizrd.shared_resources.DateTimeConstants +import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.icons.WeatherIcons import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.shared_resources.sharedDeps @@ -27,6 +28,7 @@ import com.thewizrd.shared_resources.utils.TextUtils.applySpan import com.thewizrd.shared_resources.weatherdata.model.HourlyForecast import com.thewizrd.shared_resources.weatherdata.model.MinutelyForecast import com.thewizrd.simpleweather.R +import com.thewizrd.simpleweather.services.WidgetWorker import com.thewizrd.simpleweather.widgets.WeatherWidgetProvider4x2Tomorrow import com.thewizrd.simpleweather.widgets.WidgetProviderInfo import com.thewizrd.simpleweather.widgets.WidgetUtils @@ -41,8 +43,10 @@ import com.thewizrd.simpleweather.widgets.preferences.KEY_TEXTSIZE import com.thewizrd.simpleweather.widgets.preferences.KEY_TXTCOLORCODE import com.thewizrd.simpleweather.widgets.preferences.KEY_TXTSHADOW import com.thewizrd.simpleweather.widgets.preferences.KEY_USETIMEZONE +import kotlinx.coroutines.launch import java.time.Duration import java.time.LocalDateTime +import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.temporal.ChronoUnit @@ -243,7 +247,17 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean backgroundColor, panelTextColor, textAppearanceSpan - ) + ) { + // Schedule an update if below WidgetUpdateWorker update interval + val now = ZonedDateTime.now() + val duration = Duration.between(now, it) + + if (duration.toMinutes() < 60) { + appLib.appScope.launch { + WidgetWorker.scheduleRefreshWidget(context, appWidgetId, info, duration) + } + } + } weather.airQuality?.let { val dotSize = (context.dpToPx(24f) * txtSizeMultiplier).toInt() @@ -377,7 +391,8 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean txtSizeMultiplier: Float, backgroundColor: Int, textColor: Int, - shadowSpan: TextAppearanceSpan? = null + shadowSpan: TextAppearanceSpan? = null, + nextTimeResult: ((ZonedDateTime) -> Unit)? = null ) { val now = ZonedDateTime.now(location.tzOffset ?: ZoneOffset.UTC) val minForecasts = @@ -386,7 +401,13 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean } // Create minutely precipitation text if possible - if (!buildMinutelyForecast(updateViews, minForecasts, now, shadowSpan)) { + if (!buildMinutelyForecast( + updateViews, + minForecasts, + now, + shadowSpan + ) { nextTimeResult?.invoke(it) } + ) { // If not fallback to PoP% text val nowHour = now.withZoneSameInstant(location.tzOffset).truncatedTo(ChronoUnit.HOURS) val hrForecasts = @@ -396,7 +417,13 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean nowHour ) - if (!buildPoPForecast(updateViews, hrForecasts, now, shadowSpan)) { + if (!buildPoPForecast( + updateViews, + hrForecasts, + now, + shadowSpan + ) { nextTimeResult?.invoke(it) } + ) { updateViews.setTextViewText( R.id.precipitation_text, (weather.weatherDetailsMap[WeatherDetailsType.POPCHANCE]?.value @@ -464,7 +491,8 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean updateViews: RemoteViews, minForecasts: List?, now: ZonedDateTime, - shadowSpan: TextAppearanceSpan? = null + shadowSpan: TextAppearanceSpan? = null, + nextTimeResult: ((ZonedDateTime) -> Unit)? = null ): Boolean { if (minForecasts.isNullOrEmpty()) return false @@ -490,28 +518,24 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean } ?: return false val formatStrResId = if (isRainingMinute != null) { - R.string.precipitation_minutely_stopping_text_format + R.string.precipitation_likely_minutely_stopping_text_format } else { - R.string.precipitation_minutely_starting_text_format + R.string.precipitation_likely_minutely_starting_text_format } - val duration = Duration.between(now, minute.date).toMinutes() - val duraStr = when { - duration < 120 -> { - context.getString( - formatStrResId, - context.getString(R.string.refresh_30min).replace("30", duration.toString()) - ) - } - else -> { - context.getString( - formatStrResId, - context.getString(R.string.refresh_12hrs) - .replace("12", (duration / 60).toString()) - ) - } + val dt = + minute.date.truncatedTo(ChronoUnit.MINUTES).withZoneSameInstant(ZoneId.systemDefault()) + val time = if (DateFormat.is24HourFormat(context)) { + dt.format(DateTimeUtils.ofPatternForUserLocale(DateTimeConstants.CLOCK_FORMAT_24HR)) + } else { + dt.format(DateTimeUtils.ofPatternForUserLocale(DateTimeConstants.CLOCK_FORMAT_12HR_AMPM)) } + val formatStr = context.getString(formatStrResId, time) + + updateViews.setTextViewText(R.id.precipitation_text, formatStr.applySpan(shadowSpan)) + + // Schedule an update at this time + nextTimeResult?.invoke(minute.date.truncatedTo(ChronoUnit.MINUTES)) - updateViews.setTextViewText(R.id.precipitation_text, duraStr.applySpan(shadowSpan)) return true } @@ -519,35 +543,49 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean updateViews: RemoteViews, hrForecasts: List?, now: ZonedDateTime, - shadowSpan: TextAppearanceSpan? = null + shadowSpan: TextAppearanceSpan? = null, + nextTimeResult: ((ZonedDateTime) -> Unit)? = null ): Boolean { if (hrForecasts.isNullOrEmpty()) return false // Find the next hour with a 60% or higher chance of precipitation - val forecast = hrForecasts.find { it.extras?.pop != null && it.extras.pop >= 60 } + val forecast = + hrForecasts.find { it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() } - // Proceed if within the next 3hrs + // Proceed if within the next 2hrs if (forecast == null || Duration.between(now.truncatedTo(ChronoUnit.HOURS), forecast.date) - .toHours() > 3 + .toHours() >= 2 ) return false // Should be within 0-3 hours - val duration = Duration.between(now, forecast.date).toMinutes() - val duraStr = if (duration <= 60) { - context.getString(R.string.precipitation_nexthour_text_format, forecast.extras.pop) - } else if (duration < 120) { - context.getString( - R.string.precipitation_text_format, forecast.extras.pop, - context.getString(R.string.refresh_30min).replace("30", duration.toString()) + val dt = + forecast.date.truncatedTo(ChronoUnit.HOURS).withZoneSameInstant(ZoneId.systemDefault()) + val time = if (DateFormat.is24HourFormat(context)) { + val skeleton = DateTimeConstants.SKELETON_24HR + dt.format( + DateTimeUtils.ofPatternForUserLocale( + DateTimeUtils.getBestPatternForSkeleton( + skeleton + ) + ) ) } else { - context.getString( - R.string.precipitation_text_format, forecast.extras.pop, - context.getString(R.string.refresh_12hrs).replace("12", (duration / 60).toString()) - ) + val pattern = DateTimeConstants.ABBREV_12HR_AMPM + dt.format(DateTimeUtils.ofPatternForUserLocale(pattern)) } + val duraStr = + context.getString(R.string.precipitation_likely_text_format, time, forecast.extras.pop) updateViews.setTextViewText(R.id.precipitation_text, duraStr.applySpan(shadowSpan)) + + // Find the next hour with < 60% or higher chance of precipitation + val stopForecast = + hrForecasts.find { it.extras?.pop == null || it.extras.pop < com.thewizrd.shared_resources.di.settingsManager.getPoPChanceMinimumPercentage() } + // Delay further notifications until this time + if (stopForecast != null) { + nextTimeResult?.invoke(stopForecast.date.truncatedTo(ChronoUnit.HOURS)) + } + return true } From 6c25a651e690f8e302852f4880b2ebbccffe5a71 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Thu, 19 Jun 2025 17:18:33 -0400 Subject: [PATCH 14/33] SimpleWeather: fallback to default provider if location is not supported --- .../simpleweather/main/LocationsFragment.kt | 12 +++++-- .../simpleweather/main/WeatherNowFragment.kt | 11 +++++-- .../viewmodels/WeatherNowViewModel.kt | 28 ---------------- app/src/main/res/navigation/nav_graph.xml | 3 ++ .../viewmodels/LocationSearchViewModel.kt | 31 ----------------- .../common/weatherdata/WeatherDataLoader.kt | 33 +++++++++---------- .../exceptions/WeatherException.kt | 10 +++++- .../shared_resources/utils/CustomException.kt | 21 ++---------- .../src/main/res/values-de/strings.xml | 1 + .../src/main/res/values-es/strings.xml | 1 + .../src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-nl/strings.xml | 1 + .../src/main/res/values-pl/strings.xml | 1 + .../src/main/res/values-sk/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + .../viewmodels/WeatherNowViewModel.kt | 27 --------------- .../brightsky/BrightSkyProvider.kt | 5 ++- .../weather_api/eccc/ECCCWeatherProvider.kt | 5 ++- .../weather/MeteoFranceProvider.kt | 5 ++- .../weather_api/nws/NWSWeatherProvider.kt | 6 ++-- 21 files changed, 66 insertions(+), 139 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/main/LocationsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/main/LocationsFragment.kt index 5d2d9f07..772a5ea6 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/main/LocationsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/main/LocationsFragment.kt @@ -71,6 +71,7 @@ import com.thewizrd.shared_resources.utils.ContextUtils.getOrientation import com.thewizrd.shared_resources.utils.ContextUtils.isLargeTablet import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.weatherdata.model.LocationType +import com.thewizrd.simpleweather.NavGraphDirections import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.activities.LocationSearch import com.thewizrd.simpleweather.adapters.FavoritesPanelAdapter @@ -631,9 +632,16 @@ class LocationsFragment : ToolbarFragment() { } showSnackbar(snackbar) } - ErrorStatus.QUERYNOTFOUND -> { + ErrorStatus.LOCATIONNOTSUPPORTED -> { showSnackbar( - Snackbar.make(rootView.context, wEx.message, Snackbar.Duration.LONG) + Snackbar.make(rootView.context, wEx.message, Snackbar.Duration.LONG).apply { + setAction(R.string.action_settings) { + runCatching { + rootView.findNavController() + .safeNavigate(NavGraphDirections.actionGlobalSettingsFragment()) + } + } + } ) } else -> { diff --git a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt index 73206807..50931faa 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/main/WeatherNowFragment.kt @@ -1177,9 +1177,16 @@ class WeatherNowFragment : AbstractWeatherListDetailFragment(), BannerManagerInt } }) } - ErrorStatus.QUERYNOTFOUND -> { + ErrorStatus.LOCATIONNOTSUPPORTED -> { showSnackbar( - Snackbar.make(binding.root.context, wEx.message, Snackbar.Duration.LONG) + Snackbar.make(binding.root.context, wEx.message, Snackbar.Duration.LONG).apply { + setAction(R.string.action_settings) { + runCatching { + binding.root.findNavController() + .safeNavigate(WeatherNowFragmentDirections.actionWeatherNowFragmentToSettingsFragment()) + } + } + } ) } else -> { diff --git a/app/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt b/app/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt index bbc47225..2380fa26 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt @@ -26,12 +26,10 @@ import com.thewizrd.shared_resources.exceptions.WeatherException import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.shared_resources.locationdata.buildEmptyGPSLocation import com.thewizrd.shared_resources.utils.CommonActions -import com.thewizrd.shared_resources.utils.CustomException import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.weatherdata.model.LocationType import com.thewizrd.shared_resources.weatherdata.model.WeatherAlert -import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.controls.ImageDataViewModel import com.thewizrd.weather_api.weatherModule import kotlinx.coroutines.Dispatchers @@ -269,19 +267,6 @@ class WeatherNowViewModel(app: Application) : AndroidViewModel(app) { when (result) { is WeatherResult.Error -> { viewModelState.update { state -> - if (state.locationData?.let { !wm.isRegionSupported(it) } == true) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(state.locationData), - state.locationData.countryCode - ) - Logger.writeLine( - Log.WARN, - CustomException(R.string.error_message_weather_region_unsupported) - ) - } - val errorMessages = state.errorMessages + ErrorMessage.WeatherError(result.exception) state.copy( @@ -328,19 +313,6 @@ class WeatherNowViewModel(app: Application) : AndroidViewModel(app) { val weatherData = result.data.toUiModel() viewModelState.update { state -> - if (state.locationData?.let { !wm.isRegionSupported(it) } == true) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(state.locationData), - state.locationData.countryCode - ) - Logger.writeLine( - Log.WARN, - CustomException(R.string.error_message_weather_region_unsupported) - ) - } - val errorMessages = state.errorMessages + ErrorMessage.WeatherError(result.exception) state.copy( diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index b24034b7..c6c5772b 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -149,4 +149,7 @@ + \ No newline at end of file diff --git a/common/src/main/java/com/thewizrd/common/viewmodels/LocationSearchViewModel.kt b/common/src/main/java/com/thewizrd/common/viewmodels/LocationSearchViewModel.kt index fa76aee1..7689051b 100644 --- a/common/src/main/java/com/thewizrd/common/viewmodels/LocationSearchViewModel.kt +++ b/common/src/main/java/com/thewizrd/common/viewmodels/LocationSearchViewModel.kt @@ -1,7 +1,6 @@ package com.thewizrd.common.viewmodels import android.app.Application -import android.util.Log import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope @@ -17,8 +16,6 @@ import com.thewizrd.shared_resources.locationdata.LocationQuery import com.thewizrd.shared_resources.locationdata.toLocationData import com.thewizrd.shared_resources.remoteconfig.remoteConfigService import com.thewizrd.shared_resources.utils.Coordinate -import com.thewizrd.shared_resources.utils.JSONParser -import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.weatherdata.model.LocationType import com.thewizrd.weather_api.weatherModule import kotlinx.coroutines.Dispatchers @@ -165,20 +162,6 @@ class LocationSearchViewModel(app: Application) : AndroidViewModel(app) { return@launch } - if (!wm.isRegionSupported(locQuery)) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(locQuery.toLocationData(LocationType.GPS)), - locQuery.locationCountry - ) - postErrorMessage(R.string.error_message_weather_region_unsupported) - viewModelState.update { - it.copy(isLoading = false) - } - return@launch - } - viewModelState.update { it.copy(currentLocation = locQuery.toLocationData(LocationType.GPS)) } @@ -297,20 +280,6 @@ class LocationSearchViewModel(app: Application) : AndroidViewModel(app) { wm.updateAPI() } - if (!wm.isRegionSupported(queryResult)) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(queryResult.toLocationData()), - locQuery.locationCountry - ) - postErrorMessage(R.string.error_message_weather_region_unsupported) - viewModelState.update { - it.copy(isLoading = false) - } - return@launch - } - // Check if location already exists val locData = settingsManager.getLocationData() val loc = locData.find { input -> input.query == queryResult.locationQuery } diff --git a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt index c2b115ea..35233eea 100644 --- a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt +++ b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt @@ -5,12 +5,12 @@ import android.util.Log import androidx.core.util.ObjectsCompat import com.ibm.icu.util.ULocale import com.thewizrd.shared_resources.Constants -import com.thewizrd.shared_resources.R import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.di.localBroadcastManager import com.thewizrd.shared_resources.exceptions.ErrorStatus import com.thewizrd.shared_resources.exceptions.WeatherException import com.thewizrd.shared_resources.locationdata.LocationData +import com.thewizrd.shared_resources.remoteconfig.remoteConfigService import com.thewizrd.shared_resources.utils.* import com.thewizrd.shared_resources.utils.NumberUtils.getValueOrDefault import com.thewizrd.shared_resources.weatherdata.model.* @@ -150,28 +150,24 @@ class WeatherDataLoader { if (!wm.isRegionSupported(location)) { if (location.latitude != 0.0 && location.longitude != 0.0) { - // If location data hasn't been updated, try loading weather from the previous provider - if (!location.weatherSource.isNullOrBlank()) { - val provider = - weatherModule.weatherManager.getWeatherProvider(location.weatherSource) - if (provider.isRegionSupported(location)) { - weather = provider.getWeather(location) - } + // If location data hasn't been updated, try loading weather from the default provider + val provider = remoteConfigService.getDefaultWeatherProvider() + .takeIf { it.isNotBlank() && remoteConfigService.isProviderEnabled(it) } + ?.let { weatherModule.weatherManager.getWeatherProvider(it) } + + if (provider?.isRegionSupported(location) == true) { + weather = provider.getWeather(location) } // Nothing to fallback on; error out if (weather == null) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(location), - location.countryCode - ) - throw WeatherException(ErrorStatus.QUERYNOTFOUND).initCause( - CustomException( - R.string.error_message_weather_region_unsupported + throw WeatherException(ErrorStatus.LOCATIONNOTSUPPORTED) + .initCause( + createUnsupportedLocationException( + wm.getWeatherAPI(), + location + ) ) - ) } } } else { @@ -186,6 +182,7 @@ class WeatherDataLoader { weather = null } catch (ex: Exception) { Logger.writeLine(Log.ERROR, ex, "WeatherDataLoader: error getting weather data") + wEx = WeatherException(ErrorStatus.NOWEATHER).initCause(ex) weather = null } diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/exceptions/WeatherException.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/exceptions/WeatherException.kt index f95af1cc..a813bf77 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/exceptions/WeatherException.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/exceptions/WeatherException.kt @@ -4,7 +4,7 @@ import com.thewizrd.shared_resources.R import com.thewizrd.shared_resources.sharedDeps enum class ErrorStatus { - UNKNOWN, SUCCESS, NOWEATHER, NETWORKERROR, INVALIDAPIKEY, QUERYNOTFOUND, RATELIMITED + UNKNOWN, SUCCESS, NOWEATHER, NETWORKERROR, INVALIDAPIKEY, QUERYNOTFOUND, LOCATIONNOTSUPPORTED, RATELIMITED } class WeatherException : Exception { @@ -33,6 +33,9 @@ class WeatherException : Exception { ErrorStatus.QUERYNOTFOUND -> { sharedDeps.context.getString(R.string.werror_querynotfound) } + ErrorStatus.LOCATIONNOTSUPPORTED -> { + sharedDeps.context.getString(R.string.werror_locationnotsupported) + } ErrorStatus.RATELIMITED -> { sharedDeps.context.getString(R.string.werror_ratelimited) } @@ -42,4 +45,9 @@ class WeatherException : Exception { } } } + + override fun initCause(cause: Throwable?): WeatherException { + super.initCause(cause) + return this + } } \ No newline at end of file diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/CustomException.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/CustomException.kt index 28ae1fb5..45723c4b 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/CustomException.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/utils/CustomException.kt @@ -1,22 +1,7 @@ package com.thewizrd.shared_resources.utils -import androidx.annotation.StringRes -import com.thewizrd.shared_resources.sharedDeps +import com.thewizrd.shared_resources.locationdata.LocationData -class CustomException(@StringRes stringResId: Int) : Exception() { - @StringRes - var stringResId = Int.MIN_VALUE - - init { - this.stringResId = stringResId - } - - override val message: String? - get() { - if (stringResId != Int.MIN_VALUE) { - return sharedDeps.context.getString(stringResId) - } - - return super.message - } +fun createUnsupportedLocationException(weatherApi: String, location: LocationData): Exception { + return Exception("Unsupported location - weatherapi: ${weatherApi}, countryCode: ${location.countryCode}, query: [${location.query}]") } \ No newline at end of file diff --git a/shared_resources/src/main/res/values-de/strings.xml b/shared_resources/src/main/res/values-de/strings.xml index 9bfb756e..e88a50e7 100644 --- a/shared_resources/src/main/res/values-de/strings.xml +++ b/shared_resources/src/main/res/values-de/strings.xml @@ -219,4 +219,5 @@ "Hoch" "Hoch" "Zu viele Anfragen. Bitte versuchen Sie es nach ein paar Minuten erneut" + "Dieser Standort wird von diesem Wetteranbieter nicht unterstützt" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-es/strings.xml b/shared_resources/src/main/res/values-es/strings.xml index 4940c766..7587d44a 100644 --- a/shared_resources/src/main/res/values-es/strings.xml +++ b/shared_resources/src/main/res/values-es/strings.xml @@ -240,4 +240,5 @@ "Alto" "Alto" "Demasiadas solicitudes. Vuelva a intentarlo pasados unos minutos" + "Esta ubicación no es compatible con este proveedor meteorológico" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-fr/strings.xml b/shared_resources/src/main/res/values-fr/strings.xml index b08e5e6c..4edbb3da 100644 --- a/shared_resources/src/main/res/values-fr/strings.xml +++ b/shared_resources/src/main/res/values-fr/strings.xml @@ -207,4 +207,5 @@ "Élevé" "Élevé" "Trop de demandes. Veuillez réessayer après quelques minutes" + "Cet emplacement n'est pas pris en charge par ce fournisseur météo" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-nl/strings.xml b/shared_resources/src/main/res/values-nl/strings.xml index cce662a8..dc5bf69d 100644 --- a/shared_resources/src/main/res/values-nl/strings.xml +++ b/shared_resources/src/main/res/values-nl/strings.xml @@ -211,4 +211,5 @@ "Hoog" "Hoog" "Te veel aanvragen. Probeer het opnieuw na een paar minuten" + "Locatie wordt niet ondersteund door huidige weerprovider" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-pl/strings.xml b/shared_resources/src/main/res/values-pl/strings.xml index 6e77474c..f6bc6811 100644 --- a/shared_resources/src/main/res/values-pl/strings.xml +++ b/shared_resources/src/main/res/values-pl/strings.xml @@ -214,4 +214,5 @@ "Wysoki" "Wysoki" "Zbyt wiele żądań. Spróbuj ponownie za kilka minut" + "Ta lokalizacja nie jest obsługiwana przez tego dostawcę pogody" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-sk/strings.xml b/shared_resources/src/main/res/values-sk/strings.xml index 5532743d..4a3da548 100644 --- a/shared_resources/src/main/res/values-sk/strings.xml +++ b/shared_resources/src/main/res/values-sk/strings.xml @@ -211,4 +211,5 @@ "Vysoká" "Vysoká" "Príliš veľa požiadaviek. Prosím, skúste to znova po niekoľkých minútach" + "Tento poskytovateľ počasia túto lokalitu nepodporuje." \ No newline at end of file diff --git a/shared_resources/src/main/res/values-zh-rCN/strings.xml b/shared_resources/src/main/res/values-zh-rCN/strings.xml index ceb22da7..134ef670 100644 --- a/shared_resources/src/main/res/values-zh-rCN/strings.xml +++ b/shared_resources/src/main/res/values-zh-rCN/strings.xml @@ -231,4 +231,5 @@ "高" "很高" "请求太多。请几分钟后再试一次" + "此天气提供商不支持该位置" \ No newline at end of file diff --git a/shared_resources/src/main/res/values/strings.xml b/shared_resources/src/main/res/values/strings.xml index 73f1b3ea..cd6889cb 100644 --- a/shared_resources/src/main/res/values/strings.xml +++ b/shared_resources/src/main/res/values/strings.xml @@ -153,6 +153,7 @@ No cities match your search query Unknown error occurred Too many requests. Please try again after a few minutes + Location not supported by this weather provider Update Interval diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt index 6de0adf2..2c10a75d 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/viewmodels/WeatherNowViewModel.kt @@ -26,7 +26,6 @@ import com.thewizrd.shared_resources.exceptions.WeatherException import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.shared_resources.locationdata.buildEmptyGPSLocation import com.thewizrd.shared_resources.preferences.SettingsManager -import com.thewizrd.shared_resources.utils.CustomException import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.wearable.WearableDataSync @@ -290,19 +289,6 @@ class WeatherNowViewModel(private val app: Application) : AndroidViewModel(app), viewModelState.update { state -> when (result) { is WeatherResult.Error -> { - if (state.locationData?.let { !wm.isRegionSupported(it) } == true) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(state.locationData), - state.locationData.countryCode - ) - Logger.writeLine( - Log.WARN, - CustomException(R.string.error_message_weather_region_unsupported) - ) - } - val errorMessages = state.errorMessages + ErrorMessage.WeatherError(result.exception) state.copy( @@ -329,19 +315,6 @@ class WeatherNowViewModel(private val app: Application) : AndroidViewModel(app), ) } is WeatherResult.WeatherWithError -> { - if (state.locationData?.let { !wm.isRegionSupported(it) } == true) { - Logger.writeLine( - Log.WARN, - "Location: %s; countryCode: %s", - JSONParser.serializer(state.locationData), - state.locationData.countryCode - ) - Logger.writeLine( - Log.WARN, - CustomException(R.string.error_message_weather_region_unsupported) - ) - } - val errorMessages = state.errorMessages + ErrorMessage.WeatherError(result.exception) state.copy( diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/brightsky/BrightSkyProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/brightsky/BrightSkyProvider.kt index 825210fc..a663491c 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/brightsky/BrightSkyProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/brightsky/BrightSkyProvider.kt @@ -106,9 +106,8 @@ class BrightSkyProvider : WeatherProviderImpl() { // DWD is best in Germany if (!LocationUtils.isGermany(location)) { - throw WeatherException(ErrorStatus.QUERYNOTFOUND).apply { - initCause(Exception("Unsupported country code: provider (${getWeatherAPI()}), country (${location.countryCode})")) - } + throw WeatherException(ErrorStatus.LOCATIONNOTSUPPORTED) + .initCause(createUnsupportedLocationException(getWeatherAPI(), location)) } val query = updateLocationQuery(location) diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/eccc/ECCCWeatherProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/eccc/ECCCWeatherProvider.kt index 79c37d7b..e20b55e7 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/eccc/ECCCWeatherProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/eccc/ECCCWeatherProvider.kt @@ -104,9 +104,8 @@ class ECCCWeatherProvider : WeatherProviderImpl() { // ECCC if (!LocationUtils.isCanada(location)) { - throw WeatherException(ErrorStatus.QUERYNOTFOUND).apply { - initCause(Exception("Unsupported country code: provider (${getWeatherAPI()}), country (${location.countryCode})")) - } + throw WeatherException(ErrorStatus.LOCATIONNOTSUPPORTED) + .initCause(createUnsupportedLocationException(getWeatherAPI(), location)) } val uLocale = ULocale.forLocale(LocaleUtils.getLocale()) diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/meteofrance/weather/MeteoFranceProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/meteofrance/weather/MeteoFranceProvider.kt index 9618e5c8..b8263006 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/meteofrance/weather/MeteoFranceProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/meteofrance/weather/MeteoFranceProvider.kt @@ -116,9 +116,8 @@ class MeteoFranceProvider : WeatherProviderImpl() { // MeteoFrance only supports locations in France if (!LocationUtils.isFrance(location)) { - throw WeatherException(ErrorStatus.QUERYNOTFOUND).apply { - initCause(Exception("Unsupported country code: provider (${getWeatherAPI()}), country (${location.countryCode})")) - } + throw WeatherException(ErrorStatus.LOCATIONNOTSUPPORTED) + .initCause(createUnsupportedLocationException(getWeatherAPI(), location)) } val uLocale = ULocale.forLocale(LocaleUtils.getLocale()) diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/nws/NWSWeatherProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/nws/NWSWeatherProvider.kt index 09e99650..5c7da403 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/nws/NWSWeatherProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/nws/NWSWeatherProvider.kt @@ -16,6 +16,7 @@ import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.LocationUtils import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.utils.ZoneIdCompat +import com.thewizrd.shared_resources.utils.createUnsupportedLocationException import com.thewizrd.shared_resources.utils.forEachString import com.thewizrd.shared_resources.utils.getAsJSONArray import com.thewizrd.shared_resources.utils.getAsJSONObject @@ -115,9 +116,8 @@ class NWSWeatherProvider : WeatherProviderImpl() { // NWS only supports locations in U.S. or U.S. territories if (!LocationUtils.isNWSSupported(location)) { - throw WeatherException(ErrorStatus.QUERYNOTFOUND).apply { - initCause(Exception("Unsupported country code: provider (${getWeatherAPI()}), country (${location.countryCode})")) - } + throw WeatherException(ErrorStatus.LOCATIONNOTSUPPORTED) + .initCause(createUnsupportedLocationException(getWeatherAPI(), location)) } val query = updateLocationQuery(location) From 2c30fade8e9bd5628545814428729e5d544a9707 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Thu, 19 Jun 2025 15:33:49 -0400 Subject: [PATCH 15/33] SimpleWeather: v5.12.0-build2 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0edf1e0a..6e511f47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120010 + versionCode 355120020 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index d5819b6c..aa1cc923 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120011 + versionCode 355120021 versionName "5.12.0" vectorDrawables { From aa7f085bf8314cb98fa16d60ab0bedea145520dc Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Fri, 20 Jun 2025 23:31:11 -0400 Subject: [PATCH 16/33] PoPChanceNotificationHelper: update forecast date filter --- .../notifications/PoPChanceNotificationHelper.kt | 10 +++++++--- .../remoteviews/WeatherWidget4x2TomorrowCreator.kt | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt index cc5f02f8..63c34e01 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/notifications/PoPChanceNotificationHelper.kt @@ -79,7 +79,11 @@ object PoPChanceNotificationHelper { // Find the next hour with a 60% or higher chance of precipitation val forecast = - hrForecasts.find { it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() } + hrForecasts.find { + !it.date.isBefore( + now.truncatedTo(ChronoUnit.HOURS).plusHours(1) + ) && it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() + } // Proceed if within the next 2hrs if (forecast == null || Duration.between(now.truncatedTo(ChronoUnit.HOURS), forecast.date) @@ -133,9 +137,9 @@ object PoPChanceNotificationHelper { // Find the next hour with < 60% or higher chance of precipitation val stopForecast = - hrForecasts.find { it.extras?.pop == null || it.extras.pop < settingsManager.getPoPChanceMinimumPercentage() } + hrForecasts.find { it.date.isAfter(forecast.date) && (it.extras?.pop == null || it.extras.pop < settingsManager.getPoPChanceMinimumPercentage()) } // Delay further notifications until this time - val nextTime = stopForecast?.date?.truncatedTo(ChronoUnit.HOURS) ?: now.plusHours(2) + val nextTime = stopForecast?.date?.truncatedTo(ChronoUnit.HOURS) ?: now.plusHours(1) settingsManager.setLastPoPChanceNotificationTime(nextTime.minusMinutes(5)) return true diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt index 79a415d9..ecf341a1 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2TomorrowCreator.kt @@ -550,7 +550,11 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean // Find the next hour with a 60% or higher chance of precipitation val forecast = - hrForecasts.find { it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() } + hrForecasts.find { + !it.date.isBefore( + now.truncatedTo(ChronoUnit.HOURS).plusHours(1) + ) && it.extras?.pop != null && it.extras.pop >= settingsManager.getPoPChanceMinimumPercentage() + } // Proceed if within the next 2hrs if (forecast == null || Duration.between(now.truncatedTo(ChronoUnit.HOURS), forecast.date) @@ -580,7 +584,7 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean // Find the next hour with < 60% or higher chance of precipitation val stopForecast = - hrForecasts.find { it.extras?.pop == null || it.extras.pop < com.thewizrd.shared_resources.di.settingsManager.getPoPChanceMinimumPercentage() } + hrForecasts.find { it.date.isAfter(forecast.date) && (it.extras?.pop == null || it.extras.pop < com.thewizrd.shared_resources.di.settingsManager.getPoPChanceMinimumPercentage()) } // Delay further notifications until this time if (stopForecast != null) { nextTimeResult?.invoke(stopForecast.date.truncatedTo(ChronoUnit.HOURS)) From 0a98b34fab88f382cad0d3704257f3315a21e349 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Fri, 20 Jun 2025 23:32:06 -0400 Subject: [PATCH 17/33] SimpleWeather: v5.12.0-build3 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 6e511f47..d395a16f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120020 + versionCode 355120030 versionName "5.12.0" vectorDrawables { From 9790dde7e16893c1469df80af50c4ae2fe4b544e Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 28 Jun 2025 14:29:56 -0400 Subject: [PATCH 18/33] gradle: update dependencies --- build.gradle | 14 +++++++------- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index c4cb1547..9f9ec86c 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { desugar_version = '2.1.5' - firebase_version = '33.15.0' + firebase_version = '33.16.0' gms_location_version = '21.3.0' gms_base_version = '18.7.0' gms_basement_version = '18.7.0' @@ -31,10 +31,10 @@ buildscript { paging_version = '3.3.6' preference_version = '1.2.1' recyclerview_version = '1.4.0' - room_version = '2.7.1' + room_version = '2.7.2' coresplash_version = '1.0.1' vectordrawable_version = '1.2.0' - work_version = '2.10.1' + work_version = '2.10.2' test_core_version = '1.6.1' test_runner_version = '1.6.2' @@ -46,7 +46,7 @@ buildscript { material_version = '1.12.0' compose_compiler_version = '1.5.15' - compose_bom_version = '2025.06.00' + compose_bom_version = '2025.06.01' wear_compose_version = '1.4.1' wear_tiles_version = '1.5.0' wear_watchface_version = '1.2.1' @@ -68,10 +68,10 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.10.1' + classpath 'com.android.tools.build:gradle:8.11.0' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" - classpath 'com.google.gms:google-services:4.4.2' - classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.3' + classpath 'com.google.gms:google-services:4.4.3' + classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.4' classpath 'com.google.firebase:perf-plugin:1.4.2' classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" classpath "androidx.room:room-gradle-plugin:$room_version" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7a1c33da..8aa99728 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 31 15:05:29 EDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From a3c23d9c4931cf505cf7c5462dac9ca4ab7700d9 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 28 Jun 2025 15:48:11 -0400 Subject: [PATCH 19/33] SimpleWeather: v5.12.0-build4 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d395a16f..76c9162c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120030 + versionCode 355120040 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index aa1cc923..79b19fab 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120021 + versionCode 355120041 versionName "5.12.0" vectorDrawables { From 227495d6b171b0ee19ccc953295b495aa2ff9803 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 11:59:29 -0400 Subject: [PATCH 20/33] RemoteConfigService: add additional location specific providers as defaults --- .../remoteconfig/RemoteConfigServiceImpl.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/shared_resources/src/fullgms/java/com/thewizrd/shared_resources/remoteconfig/RemoteConfigServiceImpl.kt b/shared_resources/src/fullgms/java/com/thewizrd/shared_resources/remoteconfig/RemoteConfigServiceImpl.kt index 5955b629..206bcea4 100644 --- a/shared_resources/src/fullgms/java/com/thewizrd/shared_resources/remoteconfig/RemoteConfigServiceImpl.kt +++ b/shared_resources/src/fullgms/java/com/thewizrd/shared_resources/remoteconfig/RemoteConfigServiceImpl.kt @@ -81,6 +81,14 @@ class RemoteConfigServiceImpl : RemoteConfigService { WeatherAPI.METEOFRANCE } + LocationUtils.isGermany(location) && isProviderEnabled(WeatherAPI.DWD) -> { + WeatherAPI.DWD + } + + LocationUtils.isCanada(location) && isProviderEnabled(WeatherAPI.ECCC) -> { + WeatherAPI.ECCC + } + else -> { getDefaultWeatherProvider() } @@ -98,6 +106,14 @@ class RemoteConfigServiceImpl : RemoteConfigService { WeatherAPI.METEOFRANCE } + LocationUtils.isGermany(location) && isProviderEnabled(WeatherAPI.DWD) -> { + WeatherAPI.DWD + } + + LocationUtils.isCanada(location) && isProviderEnabled(WeatherAPI.ECCC) -> { + WeatherAPI.ECCC + } + else -> { getDefaultWeatherProvider() } From cf2423aada224e6a472c59dd3268fe082c1c7711 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 13:44:22 -0400 Subject: [PATCH 21/33] [Setup]SettingsFragment: notify user if permission is denied * Prevent potential StackOverflowError --- .../notifications/NotificationUtils.kt | 15 ++++ .../preferences/SettingsFragment.kt | 77 ++++++++++++++++--- .../setup/SetupSettingsFragment.kt | 56 ++++++++++++-- .../src/main/res/values-de/strings.xml | 1 + .../src/main/res/values-es/strings.xml | 1 + .../src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-nl/strings.xml | 1 + .../src/main/res/values-pl/strings.xml | 1 + .../src/main/res/values-sk/strings.xml | 1 + .../src/main/res/values-zh-rCN/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + 11 files changed, 139 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/notifications/NotificationUtils.kt b/app/src/main/java/com/thewizrd/simpleweather/notifications/NotificationUtils.kt index 0816b86d..c7781d71 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/notifications/NotificationUtils.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/notifications/NotificationUtils.kt @@ -45,5 +45,20 @@ class NotificationUtils { Notification.Builder(context) } } + + fun Context.openAppSettingsActivity() { + val intent = getAppSettingsActivityIntent(this) + if (intent.resolveActivity(packageManager) != null) { + this.startActivity(intent) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + fun Context.openAppNotificationSettingsActivity() { + val intent = getAppNotificationSettingsActivityIntent(this) + if (intent.resolveActivity(packageManager) != null) { + this.startActivity(intent) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt index 3842c8a1..cfbb22e1 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt @@ -46,6 +46,7 @@ import com.thewizrd.common.helpers.backgroundLocationPermissionEnabled import com.thewizrd.common.helpers.getBackgroundLocationRationale import com.thewizrd.common.helpers.locationPermissionEnabled import com.thewizrd.common.helpers.notificationPermissionEnabled +import com.thewizrd.common.helpers.openAppSettingsActivity import com.thewizrd.common.preferences.KeyEntryPreferenceDialogFragment import com.thewizrd.shared_resources.Constants import com.thewizrd.shared_resources.appLib @@ -88,6 +89,7 @@ import com.thewizrd.simpleweather.extras.setupReviewPreference import com.thewizrd.simpleweather.fragments.ToolbarFragment import com.thewizrd.simpleweather.locale.InstallRequest import com.thewizrd.simpleweather.locale.LocaleInstaller +import com.thewizrd.simpleweather.notifications.NotificationUtils.Companion.openAppNotificationSettingsActivity import com.thewizrd.simpleweather.notifications.WeatherNotificationWorker import com.thewizrd.simpleweather.preferences.chippreference.ChipPreference import com.thewizrd.simpleweather.preferences.iconpreference.IconProviderPickerFragment @@ -1292,9 +1294,28 @@ class SettingsFragment : BaseSettingsFragment(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) onGoingNotifPermissionLauncher = PermissionLauncher(this) { results -> - val isChecked = results.all { it.value } - if (onGoingNotification.callChangeListener(isChecked)) { - onGoingNotification.isChecked = isChecked + val isChecked = results.isNotEmpty() && results.all { it.value } + if (isChecked) { + onGoingNotification.isChecked = true + } else { + context?.let { + showSnackbar( + Snackbar.make( + it, + R.string.notification_perm_denied, + Snackbar.Duration.SHORT + ).apply { + setAction(R.string.action_settings) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.context.openAppNotificationSettingsActivity() + } else { + it.context.openAppSettingsActivity() + } + } + } + }) + } } } } @@ -1391,15 +1412,53 @@ class SettingsFragment : BaseSettingsFragment(), override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) alertNotifPermissionLauncher = PermissionLauncher(this) { results -> - val isChecked = results.all { it.value } - if (alertNotification.callChangeListener(isChecked)) { - alertNotification.isChecked = isChecked + val isChecked = results.isNotEmpty() && results.all { it.value } + if (isChecked) { + alertNotification.isChecked = true + } else { + context?.let { + showSnackbar( + Snackbar.make( + it, + R.string.notification_perm_denied, + Snackbar.Duration.SHORT + ).apply { + setAction(R.string.action_settings) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.context.openAppNotificationSettingsActivity() + } else { + it.context.openAppSettingsActivity() + } + } + } + }) + } } } popChanceNotifPermissionLauncher = PermissionLauncher(this) { results -> - val isChecked = results.all { it.value } - if (popChanceNotifPref.callChangeListener(isChecked)) { - popChanceNotifPref.isChecked = isChecked + val isChecked = results.isNotEmpty() && results.all { it.value } + if (isChecked) { + popChanceNotifPref.isChecked = true + } else { + context?.let { + showSnackbar( + Snackbar.make( + it, + R.string.notification_perm_denied, + Snackbar.Duration.SHORT + ).apply { + setAction(R.string.action_settings) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.context.openAppNotificationSettingsActivity() + } else { + it.context.openAppSettingsActivity() + } + } + } + }) + } } } } diff --git a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupSettingsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupSettingsFragment.kt index 453a1e80..2a7ae9f7 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/setup/SetupSettingsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/setup/SetupSettingsFragment.kt @@ -2,12 +2,12 @@ package com.thewizrd.simpleweather.setup import android.Manifest import android.app.Activity -import android.graphics.drawable.ColorDrawable import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.graphics.drawable.toDrawable import androidx.core.text.util.LocalePreferences import androidx.preference.ListPreference import androidx.preference.SwitchPreferenceCompat @@ -18,6 +18,7 @@ import com.thewizrd.common.helpers.PermissionLauncher import com.thewizrd.common.helpers.backgroundLocationPermissionEnabled import com.thewizrd.common.helpers.getBackgroundLocationRationale import com.thewizrd.common.helpers.notificationPermissionEnabled +import com.thewizrd.common.helpers.openAppSettingsActivity import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.utils.ContextUtils.dpToPx @@ -25,6 +26,7 @@ import com.thewizrd.shared_resources.utils.ContextUtils.getAttrColor import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.databinding.FragmentSetupSettingsBinding import com.thewizrd.simpleweather.extras.enableAdditionalRefreshIntervals +import com.thewizrd.simpleweather.notifications.NotificationUtils.Companion.openAppNotificationSettingsActivity import com.thewizrd.simpleweather.preferences.CustomPreferenceFragmentCompat import com.thewizrd.simpleweather.snackbar.Snackbar import com.thewizrd.simpleweather.snackbar.SnackbarManager @@ -50,15 +52,53 @@ class SetupSettingsFragment : CustomPreferenceFragmentCompat() { locationPermissionLauncher = LocationPermissionLauncher(this) onGoingNotifPermissionLauncher = PermissionLauncher(this) { results -> - val isChecked = results.all { it.value } - if (onGoingPref.callChangeListener(isChecked)) { - onGoingPref.isChecked = isChecked + val isChecked = results.isNotEmpty() && results.all { it.value } + if (isChecked) { + onGoingPref.isChecked = true + } else { + context?.let { + showSnackbar( + Snackbar.make( + it, + R.string.notification_perm_denied, + Snackbar.Duration.SHORT + ).apply { + setAction(R.string.action_settings) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.context.openAppNotificationSettingsActivity() + } else { + it.context.openAppSettingsActivity() + } + } + } + }) + } } } alertNotifPermissionLauncher = PermissionLauncher(this) { results -> - val isChecked = results.all { it.value } - if (alertsPref.callChangeListener(isChecked)) { - alertsPref.isChecked = isChecked + val isChecked = results.isNotEmpty() && results.all { it.value } + if (isChecked) { + alertsPref.isChecked = true + } else { + context?.let { + showSnackbar( + Snackbar.make( + it, + R.string.notification_perm_denied, + Snackbar.Duration.SHORT + ).apply { + setAction(R.string.action_settings) { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.context.openAppNotificationSettingsActivity() + } else { + it.context.openAppSettingsActivity() + } + } + } + }) + } } } } @@ -84,7 +124,7 @@ class SetupSettingsFragment : CustomPreferenceFragmentCompat() { binding.fragmentContainer.addView(inflatedView) - setDivider(ColorDrawable(root.context.getAttrColor(R.attr.colorOnSurface))) + setDivider(root.context.getAttrColor(R.attr.colorOnSurface).toDrawable()) setDividerHeight(root.context.dpToPx(1f).toInt()) return root diff --git a/shared_resources/src/main/res/values-de/strings.xml b/shared_resources/src/main/res/values-de/strings.xml index e88a50e7..aa852bf3 100644 --- a/shared_resources/src/main/res/values-de/strings.xml +++ b/shared_resources/src/main/res/values-de/strings.xml @@ -220,4 +220,5 @@ "Hoch" "Zu viele Anfragen. Bitte versuchen Sie es nach ein paar Minuten erneut" "Dieser Standort wird von diesem Wetteranbieter nicht unterstützt" + "Benachrichtigungserlaubnis verweigert" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-es/strings.xml b/shared_resources/src/main/res/values-es/strings.xml index 7587d44a..067818d7 100644 --- a/shared_resources/src/main/res/values-es/strings.xml +++ b/shared_resources/src/main/res/values-es/strings.xml @@ -241,4 +241,5 @@ "Alto" "Demasiadas solicitudes. Vuelva a intentarlo pasados unos minutos" "Esta ubicación no es compatible con este proveedor meteorológico" + "Permiso de notificación denegado" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-fr/strings.xml b/shared_resources/src/main/res/values-fr/strings.xml index 4edbb3da..ba3f0ad7 100644 --- a/shared_resources/src/main/res/values-fr/strings.xml +++ b/shared_resources/src/main/res/values-fr/strings.xml @@ -208,4 +208,5 @@ "Élevé" "Trop de demandes. Veuillez réessayer après quelques minutes" "Cet emplacement n'est pas pris en charge par ce fournisseur météo" + "Autorisation de notification refusée" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-nl/strings.xml b/shared_resources/src/main/res/values-nl/strings.xml index dc5bf69d..a966163f 100644 --- a/shared_resources/src/main/res/values-nl/strings.xml +++ b/shared_resources/src/main/res/values-nl/strings.xml @@ -212,4 +212,5 @@ "Hoog" "Te veel aanvragen. Probeer het opnieuw na een paar minuten" "Locatie wordt niet ondersteund door huidige weerprovider" + "Melding toestemming geweigerd" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-pl/strings.xml b/shared_resources/src/main/res/values-pl/strings.xml index f6bc6811..a4731dc6 100644 --- a/shared_resources/src/main/res/values-pl/strings.xml +++ b/shared_resources/src/main/res/values-pl/strings.xml @@ -215,4 +215,5 @@ "Wysoki" "Zbyt wiele żądań. Spróbuj ponownie za kilka minut" "Ta lokalizacja nie jest obsługiwana przez tego dostawcę pogody" + "Odmowa pozwolenia na powiadomienie" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-sk/strings.xml b/shared_resources/src/main/res/values-sk/strings.xml index 4a3da548..f7d33d2b 100644 --- a/shared_resources/src/main/res/values-sk/strings.xml +++ b/shared_resources/src/main/res/values-sk/strings.xml @@ -212,4 +212,5 @@ "Vysoká" "Príliš veľa požiadaviek. Prosím, skúste to znova po niekoľkých minútach" "Tento poskytovateľ počasia túto lokalitu nepodporuje." + "Povolenie na oznámenie zamietnuté" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-zh-rCN/strings.xml b/shared_resources/src/main/res/values-zh-rCN/strings.xml index 134ef670..12eed9fc 100644 --- a/shared_resources/src/main/res/values-zh-rCN/strings.xml +++ b/shared_resources/src/main/res/values-zh-rCN/strings.xml @@ -232,4 +232,5 @@ "很高" "请求太多。请几分钟后再试一次" "此天气提供商不支持该位置" + "拒绝通知许可" \ No newline at end of file diff --git a/shared_resources/src/main/res/values/strings.xml b/shared_resources/src/main/res/values/strings.xml index cd6889cb..f906b694 100644 --- a/shared_resources/src/main/res/values/strings.xml +++ b/shared_resources/src/main/res/values/strings.xml @@ -176,6 +176,7 @@ Notifications for weather alerts Service notifications Updating weather… + Notification permission denied %1$dd ago From 343ba630bee55a5578644d16cb96ce1431eef120 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 14:16:29 -0400 Subject: [PATCH 22/33] [TEMP] gradle: update R8 version * Update to fix: "java.lang.IllegalAccessError: Final field '...' cannot be written to by method '...'" * Reference: https://issuetracker.google.com/issues/423658598 --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 9f9ec86c..157fb1a4 100644 --- a/build.gradle +++ b/build.gradle @@ -65,9 +65,11 @@ buildscript { mavenCentral() google() maven { url 'https://jitpack.io' } + maven { url 'https://storage.googleapis.com/r8-releases/raw' } // TODO: remove when resolved } dependencies { + classpath 'com.android.tools:r8:8.12.9-dev' // TODO: remove when resolved classpath 'com.android.tools.build:gradle:8.11.0' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath 'com.google.gms:google-services:4.4.3' From 773f0c455687a104de9dadce2d5f2242e48b5f12 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 14:17:39 -0400 Subject: [PATCH 23/33] SimpleWeather: v5.12.0-build5 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 76c9162c..44c4113c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120040 + versionCode 355120050 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index 79b19fab..559666a2 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120041 + versionCode 355120051 versionName "5.12.0" vectorDrawables { From 49e411b6550c5b868499bb446c2133e2fefaff61 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 23:51:09 -0400 Subject: [PATCH 24/33] gradle: remove r8 override + rollback gradle version --- build.gradle | 2 -- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 157fb1a4..9f9ec86c 100644 --- a/build.gradle +++ b/build.gradle @@ -65,11 +65,9 @@ buildscript { mavenCentral() google() maven { url 'https://jitpack.io' } - maven { url 'https://storage.googleapis.com/r8-releases/raw' } // TODO: remove when resolved } dependencies { - classpath 'com.android.tools:r8:8.12.9-dev' // TODO: remove when resolved classpath 'com.android.tools.build:gradle:8.11.0' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath 'com.google.gms:google-services:4.4.3' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8aa99728..54fddc2e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 31 15:05:29 EDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 6bcba035c76eda7c55c0971cd119390bf27b1069 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sun, 29 Jun 2025 23:52:08 -0400 Subject: [PATCH 25/33] SimpleWeather: v5.12.0-build6 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 44c4113c..b41533f0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120050 + versionCode 355120060 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index 559666a2..227ed8ab 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120051 + versionCode 355120061 versionName "5.12.0" vectorDrawables { From c9a7dfefefa8189d42b1cc7e0e0ec6e3da226edb Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Mon, 30 Jun 2025 01:45:14 -0400 Subject: [PATCH 26/33] SimpleWeather: v5.12.0-build7 --- app/build.gradle | 2 +- build.gradle | 2 ++ wearapp/build.gradle | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b41533f0..79b7524b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120060 + versionCode 355120070 versionName "5.12.0" vectorDrawables { diff --git a/build.gradle b/build.gradle index 9f9ec86c..157fb1a4 100644 --- a/build.gradle +++ b/build.gradle @@ -65,9 +65,11 @@ buildscript { mavenCentral() google() maven { url 'https://jitpack.io' } + maven { url 'https://storage.googleapis.com/r8-releases/raw' } // TODO: remove when resolved } dependencies { + classpath 'com.android.tools:r8:8.12.9-dev' // TODO: remove when resolved classpath 'com.android.tools.build:gradle:8.11.0' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath 'com.google.gms:google-services:4.4.3' diff --git a/wearapp/build.gradle b/wearapp/build.gradle index 227ed8ab..e478549d 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120061 + versionCode 355120071 versionName "5.12.0" vectorDrawables { From a15dd1207594363621346659e18862e0ada582fe Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Tue, 1 Jul 2025 19:01:06 -0400 Subject: [PATCH 27/33] SimpleWeather: v5.12.0-build8 * Rollback AGP version --- app/build.gradle | 2 +- build.gradle | 4 +--- gradle/wrapper/gradle-wrapper.properties | 2 +- wearapp/build.gradle | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 79b7524b..1065472e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120070 + versionCode 355120080 versionName "5.12.0" vectorDrawables { diff --git a/build.gradle b/build.gradle index 157fb1a4..78433e2e 100644 --- a/build.gradle +++ b/build.gradle @@ -65,12 +65,10 @@ buildscript { mavenCentral() google() maven { url 'https://jitpack.io' } - maven { url 'https://storage.googleapis.com/r8-releases/raw' } // TODO: remove when resolved } dependencies { - classpath 'com.android.tools:r8:8.12.9-dev' // TODO: remove when resolved - classpath 'com.android.tools.build:gradle:8.11.0' + classpath 'com.android.tools.build:gradle:8.10.1' classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$ksp_version" classpath 'com.google.gms:google-services:4.4.3' classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.4' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 54fddc2e..7a1c33da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 31 15:05:29 EDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/wearapp/build.gradle b/wearapp/build.gradle index e478549d..a82e0f8d 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120071 + versionCode 355120081 versionName "5.12.0" vectorDrawables { From 8283b672807abfb39861de350d486bad613148c4 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 19 Jul 2025 20:53:04 -0400 Subject: [PATCH 28/33] HEREWeatherProvider: adjust ttl --- .../shared_resources/preferences/SettingsManager.kt | 13 +++++++++++++ .../weather_api/here/weather/WeatherData.kt | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt index dcca8f3c..dd0f896c 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/preferences/SettingsManager.kt @@ -24,6 +24,8 @@ import com.thewizrd.shared_resources.icons.WeatherIconsEFProvider import com.thewizrd.shared_resources.locationdata.LocationData import com.thewizrd.shared_resources.locationdata.buildEmptyGPSLocation import com.thewizrd.shared_resources.remoteconfig.remoteConfigService +import com.thewizrd.shared_resources.utils.AnalyticsLogger +import com.thewizrd.shared_resources.utils.AnalyticsProps import com.thewizrd.shared_resources.utils.CommonActions import com.thewizrd.shared_resources.utils.DateTimeUtils import com.thewizrd.shared_resources.utils.DateTimeUtils.LOCAL_DATE_TIME_FORMATTER @@ -610,6 +612,10 @@ class SettingsManager(context: Context) { preferences.edit { putString(KEY_API, api) } + AnalyticsLogger.setUserProperty( + AnalyticsProps.WEATHER_PROVIDER, + api + ) } @Deprecated("Use getAPIKey()", ReplaceWith("getAPIKey()")) @@ -868,6 +874,13 @@ class SettingsManager(context: Context) { remove(prefKey) } } + + if (provider == getAPI()) { + AnalyticsLogger.setUserProperty( + AnalyticsProps.USING_PERSONAL_KEY, + value + ) + } } fun getVersionCode(): Long { diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/WeatherData.kt b/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/WeatherData.kt index 06bd4c83..68f6b40e 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/WeatherData.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/WeatherData.kt @@ -77,7 +77,7 @@ fun createWeatherData(root: PlacesItem): Weather { atmosphere = createAtmosphere(observation) astronomy = createAstronomy(root.astronomyForecasts!![0].forecasts!!) precipitation = createPrecipitation(observation, todaysForecast) - ttl = 120 + ttl = 180 source = WeatherAPI.HERE } From c33e901145f054aa7b600c4b4ab340d2f5bfd65b Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 19 Jul 2025 20:56:10 -0400 Subject: [PATCH 29/33] Settings: add links and Firebase settings --- .../thewizrd/simpleweather/extras/Extras.kt | 15 +++++++++++ .../preferences/DevSettingsFragment.kt | 27 +++++++++++++++++++ .../preferences/SettingsFragment.kt | 3 +++ .../main/res/drawable/ic_outline_contract.xml | 12 +++++++++ app/src/main/res/drawable/ic_privacy_tip.xml | 12 +++++++++ app/src/main/res/xml/pref_aboutapp.xml | 25 +++++++++++++++++ .../thewizrd/simpleweather/extras/Extras.kt | 5 ++++ 7 files changed, 99 insertions(+) create mode 100644 app/src/main/res/drawable/ic_outline_contract.xml create mode 100644 app/src/main/res/drawable/ic_privacy_tip.xml diff --git a/app/src/fullgms/java/com/thewizrd/simpleweather/extras/Extras.kt b/app/src/fullgms/java/com/thewizrd/simpleweather/extras/Extras.kt index b7361e8c..d24066de 100644 --- a/app/src/fullgms/java/com/thewizrd/simpleweather/extras/Extras.kt +++ b/app/src/fullgms/java/com/thewizrd/simpleweather/extras/Extras.kt @@ -6,11 +6,14 @@ package com.thewizrd.simpleweather.extras import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent +import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.preference.Preference import com.google.android.gms.maps.MapsInitializer import com.google.android.gms.maps.MapsInitializer.Renderer import com.google.android.play.core.splitcompat.SplitCompat +import com.google.firebase.installations.ktx.installations +import com.google.firebase.ktx.Firebase import com.thewizrd.extras.extrasModule import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.store.PlayStoreUtils @@ -21,10 +24,13 @@ import com.thewizrd.simpleweather.NavGraphDirections import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.locale.UserLocaleActivity import com.thewizrd.simpleweather.preferences.BaseSettingsFragment +import com.thewizrd.simpleweather.preferences.DevSettingsFragment import com.thewizrd.simpleweather.preferences.SettingsFragment import com.thewizrd.simpleweather.preferences.`SettingsFragment$IconsFragmentDirections` import com.thewizrd.simpleweather.snackbar.Snackbar import com.thewizrd.simpleweather.utils.NavigationUtils.safeNavigate +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import timber.log.Timber fun initializeExtras() { @@ -179,4 +185,13 @@ fun SettingsFragment.AboutAppFragment.setupReviewPreference(preference: Preferen } } } +} + +fun DevSettingsFragment.updateFirebaseIdPreference(preference: Preference) { + runCatching { + lifecycleScope.launch { + val firebaseId = Firebase.installations.id.await() + preference.summary = firebaseId + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/thewizrd/simpleweather/preferences/DevSettingsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/preferences/DevSettingsFragment.kt index 24b15e5a..b2dbaae1 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/preferences/DevSettingsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/preferences/DevSettingsFragment.kt @@ -1,5 +1,7 @@ package com.thewizrd.simpleweather.preferences +import android.content.ClipData +import android.content.ClipboardManager import android.os.Bundle import android.widget.Toast import androidx.activity.result.ActivityResultLauncher @@ -13,7 +15,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.weatherdata.WeatherAPI +import com.thewizrd.simpleweather.BuildConfig import com.thewizrd.simpleweather.R +import com.thewizrd.simpleweather.extras.updateFirebaseIdPreference import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -62,11 +66,16 @@ class DevSettingsFragment : ToolbarPreferenceFragmentCompat() { preferenceScreen = preferenceManager.createPreferenceScreen(context) val apiKeyCategory: PreferenceCategory + val firebaseCategory: PreferenceCategory val miscCategory: PreferenceCategory preferenceScreen.addPreference(PreferenceCategory(context).apply { title = "API Keys" }.also { apiKeyCategory = it }) + preferenceScreen.addPreference(PreferenceCategory(context).apply { + title = "Firebase" + isVisible = !BuildConfig.IS_NONGMS + }.also { firebaseCategory = it }) preferenceScreen.addPreference(PreferenceCategory(context).apply { title = "Misc" }.also { miscCategory = it }) @@ -139,6 +148,24 @@ class DevSettingsFragment : ToolbarPreferenceFragmentCompat() { } }) + firebaseCategory.addPreference(Preference(context).apply { + title = "Firebase Id" + isPersistent = false + onPreferenceClickListener = + Preference.OnPreferenceClickListener { preference -> + val clipService = + preference.context.getSystemService(ClipboardManager::class.java) + val clipData = ClipData.newPlainText("Firebase Id", preference.summary) + clipService.setPrimaryClip(clipData) + + Toast.makeText(preference.context, "Copied to clipboard", Toast.LENGTH_SHORT) + .show() + true + } + }.also { preference -> + updateFirebaseIdPreference(preference) + }) + miscCategory.addPreference(SwitchPreference(context).apply { title = "Enable Debug Mode" isPersistent = false diff --git a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt index cfbb22e1..61338563 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt @@ -1546,6 +1546,7 @@ class SettingsFragment : BaseSettingsFragment(), private const val KEY_FEEDBACK = "key_feedback" private const val KEY_RATEREVIEW = "key_ratereview" private const val KEY_TRANSLATE = "key_translate" + private const val KEY_PRIVACY_TOS = "key_privacy_tos" private const val KEY_ABOUTVERSION = "key_aboutversion" } @@ -1600,6 +1601,8 @@ class SettingsFragment : BaseSettingsFragment(), true } + findPreference(KEY_PRIVACY_TOS)?.isVisible = !BuildConfig.IS_NONGMS + runCatching { val packageInfo = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0) diff --git a/app/src/main/res/drawable/ic_outline_contract.xml b/app/src/main/res/drawable/ic_outline_contract.xml new file mode 100644 index 00000000..42250063 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_contract.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_privacy_tip.xml b/app/src/main/res/drawable/ic_privacy_tip.xml new file mode 100644 index 00000000..ebd2d3ce --- /dev/null +++ b/app/src/main/res/drawable/ic_privacy_tip.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/xml/pref_aboutapp.xml b/app/src/main/res/xml/pref_aboutapp.xml index a3acad6b..ae79d1b7 100644 --- a/app/src/main/res/xml/pref_aboutapp.xml +++ b/app/src/main/res/xml/pref_aboutapp.xml @@ -39,6 +39,31 @@ + + + + + + + + + + + + Date: Sat, 19 Jul 2025 22:25:26 -0400 Subject: [PATCH 30/33] Worker: keep widgets intact if retryable --- .../WeatherNotificationWorker.kt | 42 ++++++++++++++----- .../services/WeatherUpdaterWorker.kt | 37 ++++++++++------ .../services/WidgetUpdaterWorker.kt | 9 +++- .../widgets/WidgetUpdaterHelper.kt | 37 +++++++++++++--- 4 files changed, 95 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/notifications/WeatherNotificationWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/notifications/WeatherNotificationWorker.kt index b4d3505c..85b1ff5d 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/notifications/WeatherNotificationWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/notifications/WeatherNotificationWorker.kt @@ -6,11 +6,17 @@ import android.content.Context import android.content.Intent import android.os.Build import android.util.Log -import androidx.work.* +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters import com.thewizrd.common.controls.WeatherUiModel import com.thewizrd.common.helpers.areNotificationsEnabled import com.thewizrd.common.weatherdata.WeatherDataLoader import com.thewizrd.common.weatherdata.WeatherRequest +import com.thewizrd.common.weatherdata.WeatherResult import com.thewizrd.shared_resources.preferences.SettingsManager import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.simpleweather.R @@ -30,8 +36,16 @@ class WeatherNotificationWorker(context: Context, workerParams: WorkerParameters // Extras private const val EXTRA_FORCEREFRESH = "SimpleWeather.Droid.extra.FORCE_REFRESH" - suspend fun refreshNotification(context: Context, forceRefresh: Boolean = false) { - WeatherNotificationHelper.refreshNotification(context.applicationContext, forceRefresh) + suspend fun refreshNotification( + context: Context, + forceRefresh: Boolean = false, + resetIfUnavailable: Boolean = true + ) { + WeatherNotificationHelper.refreshNotification( + context.applicationContext, + forceRefresh, + resetIfUnavailable + ) } @JvmStatic @@ -91,7 +105,11 @@ class WeatherNotificationWorker(context: Context, workerParams: WorkerParameters private const val JOB_ID = 1003 private const val PERSISTENT_NOT_ID = JOB_ID - suspend fun refreshNotification(context: Context, forceRefresh: Boolean) { + suspend fun refreshNotification( + context: Context, + forceRefresh: Boolean, + resetIfUnavailable: Boolean = true + ) { Timber.tag("WeatherNotifWorker") .d("Refreshing notification (forceRefresh = $forceRefresh)...") @@ -100,7 +118,7 @@ class WeatherNotificationWorker(context: Context, workerParams: WorkerParameters if (settingsManager.isWeatherLoaded() && context.areNotificationsEnabled()) { if (settingsManager.showOngoingNotification()) { val locData = settingsManager.getHomeData() ?: return - val weather = withContext(Dispatchers.IO) { + val weatherResult = withContext(Dispatchers.IO) { val wLoader = WeatherDataLoader(locData) val request = WeatherRequest.Builder() if (forceRefresh) @@ -108,11 +126,15 @@ class WeatherNotificationWorker(context: Context, workerParams: WorkerParameters else request.forceLoadSavedData() - try { - wLoader.loadWeatherData(request.build()) - } catch (e: Exception) { - null - } + wLoader.loadWeatherResult(request.build()) + } + + val weather = when (weatherResult) { + is WeatherResult.Error, + is WeatherResult.NoWeather -> if (resetIfUnavailable) weatherResult.data else return + + is WeatherResult.Success, + is WeatherResult.WeatherWithError -> weatherResult.data } // Gets an instance of the NotificationManager service diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt index 6d944f7d..3e83d730 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WeatherUpdaterWorker.kt @@ -197,12 +197,17 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C val weatherResult = getWeather() + val willRetry = result == Result.retry() + if (WidgetUpdaterHelper.widgetsExist()) { - WidgetUpdaterHelper.refreshWidgets(context) + WidgetUpdaterHelper.refreshWidgets(context, resetIfUnavailable = !willRetry) } if (settingsManager.showOngoingNotification()) { - WeatherNotificationWorker.refreshNotification(context) + WeatherNotificationWorker.refreshNotification( + context, + resetIfUnavailable = !willRetry + ) } if (settingsManager.isPoPChanceNotificationEnabled()) { @@ -276,25 +281,33 @@ class WeatherUpdaterWorker(context: Context, workerParams: WorkerParameters) : C } private suspend fun preloadWeather() = withContext(Dispatchers.IO) { + val results = mutableListOf() + val locations = settingsManager.getFavorites() ?: emptyList() Timber.tag(TAG).d("Preloading weather data for favorites...") for (location in locations) { if (WidgetUtils.exists(location.query)) { - try { - WeatherDataLoader(location) - .loadWeatherData( - WeatherRequest.Builder() - .forceRefresh(false) - .loadAlerts() - .build() - ) - } catch (ex: Exception) { - Logger.writeLine(Log.ERROR, ex, "%s: preloadWeather error", TAG) + val result = WeatherDataLoader(location) + .loadWeatherResult( + WeatherRequest.Builder() + .forceRefresh(false) + .loadAlerts() + .build() + ) + + if (result is WeatherResult.Error) { + Logger.error(TAG, result.exception, "preloadWeather error") + } else if (result is WeatherResult.WeatherWithError) { + Logger.error(TAG, result.exception, "preloadWeather error") } + + results.add(result is WeatherResult.Success || result is WeatherResult.WeatherWithError) } } + + results.all { it } } private suspend fun updateLocation(): LocationResult { diff --git a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt index 5b698644..18baf736 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/services/WidgetUpdaterWorker.kt @@ -243,12 +243,17 @@ class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Co } } + val willRetry = result == Result.retry() + if (action and ACTION_UPDATEWIDGETS != 0 && WidgetUpdaterHelper.widgetsExist()) { - WidgetUpdaterHelper.refreshWidgets(context) + WidgetUpdaterHelper.refreshWidgets(context, resetIfUnavailable = !willRetry) } if (action and ACTION_UPDATENOTIFICATION != 0 && settingsManager.showOngoingNotification()) { - WeatherNotificationWorker.refreshNotification(context) + WeatherNotificationWorker.refreshNotification( + context, + resetIfUnavailable = !willRetry + ) } if (action and ACTION_UPDATEPOPNOTIFICATION != 0 && settingsManager.isPoPChanceNotificationEnabled()) { diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt index 78932316..1c4de561 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/WidgetUpdaterHelper.kt @@ -68,7 +68,7 @@ object WidgetUpdaterHelper { return false } - suspend fun refreshWidgets(context: Context) { + suspend fun refreshWidgets(context: Context, resetIfUnavailable: Boolean = true) { coroutineScope { val appWidgetManager = AppWidgetManager.getInstance(context) val jobs = mutableListOf>() @@ -80,7 +80,13 @@ object WidgetUpdaterHelper { jobs.add( async { runCatching { - refreshWidget(context, info, appWidgetManager, info.appWidgetIds) + refreshWidget( + context, + info, + appWidgetManager, + info.appWidgetIds, + resetIfUnavailable + ) } } ) @@ -131,7 +137,11 @@ object WidgetUpdaterHelper { } } - suspend fun refreshWidgets(context: Context, location_query: String) { + suspend fun refreshWidgets( + context: Context, + location_query: String, + resetIfUnavailable: Boolean = true + ) { coroutineScope { val appWidgetManager = AppWidgetManager.getInstance(context) val appWidgetIds = WidgetUtils.getWidgetIds(location_query) @@ -144,7 +154,13 @@ object WidgetUpdaterHelper { val info = WidgetUtils.getWidgetProviderInfoFromType(widgetType) ?: return@async - refreshWidget(context, info, appWidgetManager, IntArray(1) { appWidgetId }) + refreshWidget( + context, + info, + appWidgetManager, + IntArray(1) { appWidgetId }, + resetIfUnavailable + ) } ) } @@ -153,7 +169,13 @@ object WidgetUpdaterHelper { } } - suspend fun refreshWidget(context: Context, info: WidgetProviderInfo, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + suspend fun refreshWidget( + context: Context, + info: WidgetProviderInfo, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + resetIfUnavailable: Boolean = true + ) { for (appWidgetId in appWidgetIds) { val creator = WidgetUtils.getRemoteViewCreator(context, appWidgetId) val newOptions = appWidgetManager.getAppWidgetOptions(appWidgetId) @@ -170,7 +192,10 @@ object WidgetUpdaterHelper { info.className, appWidgetId ) - resetWidget(context, appWidgetId, appWidgetManager) + + if (resetIfUnavailable) { + resetWidget(context, appWidgetId, appWidgetManager) + } } } } From 44f90a030cbee896dd884d14a4758689cd348417 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 19 Jul 2025 22:40:08 -0400 Subject: [PATCH 31/33] WeatherDataLoader: remove usage of loadWeatherData --- .../shortcuts/ShortcutCreatorWorker.kt | 9 ++++----- .../remoteviews/AbstractWidgetRemoteViewCreator.kt | 14 +++++++++----- .../common/weatherdata/WeatherDataLoader.kt | 4 ++++ .../complications/AQIComplicationService.kt | 6 ++++-- .../WeatherForecastComplicationService.kt | 6 ++++-- .../WeatherHourlyForecastComplicationService.kt | 6 ++++-- .../wearable/tiles/WeatherCoroutinesTileService.kt | 9 ++++----- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/shortcuts/ShortcutCreatorWorker.kt b/app/src/main/java/com/thewizrd/simpleweather/shortcuts/ShortcutCreatorWorker.kt index 46c7d0d1..4627409b 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/shortcuts/ShortcutCreatorWorker.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/shortcuts/ShortcutCreatorWorker.kt @@ -95,16 +95,15 @@ class ShortcutCreatorWorker(context: Context, workerParams: WorkerParameters) : var i = 0 while (i < MAX_SHORTCUTS) { val location = locations[i] - val weather = try { + val result = runCatching { WeatherDataLoader(location) - .loadWeatherData( + .loadWeatherResult( WeatherRequest.Builder() .forceLoadSavedData() .build() ) - } catch (e: Exception) { - null - } + }.getOrNull() + val weather = result?.data if (weather == null || !weather.isValid || (location.name == null && weather.location?.name == null)) { locations.removeAt(i) diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/AbstractWidgetRemoteViewCreator.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/AbstractWidgetRemoteViewCreator.kt index 0b364815..f4657845 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/AbstractWidgetRemoteViewCreator.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/AbstractWidgetRemoteViewCreator.kt @@ -18,7 +18,11 @@ import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.main.MainActivity -import com.thewizrd.simpleweather.widgets.* +import com.thewizrd.simpleweather.widgets.WeatherWidgetConfigActivity +import com.thewizrd.simpleweather.widgets.WeatherWidgetProvider +import com.thewizrd.simpleweather.widgets.WidgetProviderInfo +import com.thewizrd.simpleweather.widgets.WidgetUpdaterHelper +import com.thewizrd.simpleweather.widgets.WidgetUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -55,14 +59,14 @@ abstract class AbstractWidgetRemoteViewCreator(protected val context: Context) { return try { // If saved data DNE (for current location), refresh weather val wLoader = WeatherDataLoader(locData) - var weather = wLoader.loadWeatherData( + var result = wLoader.loadWeatherResult( WeatherRequest.Builder() .forceLoadSavedData() .build() ) - if (weather == null) { - weather = wLoader.loadWeatherData( + if (result.data == null) { + result = wLoader.loadWeatherResult( WeatherRequest.Builder() .forceRefresh(false) .loadAlerts() @@ -71,7 +75,7 @@ abstract class AbstractWidgetRemoteViewCreator(protected val context: Context) { ) } - weather + result.data } catch (e: Exception) { null } diff --git a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt index 35233eea..7df770b8 100644 --- a/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt +++ b/common/src/main/java/com/thewizrd/common/weatherdata/WeatherDataLoader.kt @@ -61,6 +61,10 @@ class WeatherDataLoader { private val wm = weatherModule.weatherManager private val settingsMgr = appLib.settingsManager + @Deprecated( + "Use loadWeatherResult(WeatherRequest)", + replaceWith = ReplaceWith("loadWeatherResult(request)") + ) suspend fun loadWeatherData(request: WeatherRequest): Weather? { val result = getWeatherResult(request) diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/AQIComplicationService.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/AQIComplicationService.kt index 3453bbbb..2784e624 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/AQIComplicationService.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/AQIComplicationService.kt @@ -50,9 +50,9 @@ class AQIComplicationService : BaseWeatherComplicationService() { if (settingsManager.isWeatherLoaded()) { complicationData = settingsManager.getHomeData()?.let { locData -> val weather = withContext(Dispatchers.IO) { - try { + val result = try { WeatherDataLoader(locData) - .loadWeatherData( + .loadWeatherResult( WeatherRequest.Builder() .loadForecasts() .forceLoadSavedData() @@ -61,6 +61,8 @@ class AQIComplicationService : BaseWeatherComplicationService() { } catch (e: Exception) { null } + + result?.data } val today = LocalDate.now(locData.tzOffset) diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherForecastComplicationService.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherForecastComplicationService.kt index 46e143e0..82a51cb9 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherForecastComplicationService.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherForecastComplicationService.kt @@ -31,9 +31,9 @@ abstract class WeatherForecastComplicationService : BaseWeatherComplicationServi if (settingsManager.isWeatherLoaded()) { complicationData = settingsManager.getHomeData()?.let { locData -> val weather = withContext(Dispatchers.IO) { - try { + val result = try { WeatherDataLoader(locData) - .loadWeatherData( + .loadWeatherResult( WeatherRequest.Builder() .forceLoadSavedData() .build() @@ -41,6 +41,8 @@ abstract class WeatherForecastComplicationService : BaseWeatherComplicationServi } catch (e: Exception) { null } + + result?.data } val forecast = diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherHourlyForecastComplicationService.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherHourlyForecastComplicationService.kt index 206509a8..6af19bee 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherHourlyForecastComplicationService.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherHourlyForecastComplicationService.kt @@ -35,9 +35,9 @@ abstract class WeatherHourlyForecastComplicationService : BaseWeatherComplicatio if (settingsManager.isWeatherLoaded()) { complicationData = settingsManager.getHomeData()?.let { locData -> val weather = withContext(Dispatchers.IO) { - try { + val result = try { WeatherDataLoader(locData) - .loadWeatherData( + .loadWeatherResult( WeatherRequest.Builder() .forceLoadSavedData() .build() @@ -45,6 +45,8 @@ abstract class WeatherHourlyForecastComplicationService : BaseWeatherComplicatio } catch (e: Exception) { null } + + result?.data } val now = ZonedDateTime.now().withZoneSameInstant(locData.tzOffset) diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/WeatherCoroutinesTileService.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/WeatherCoroutinesTileService.kt index caafc0c5..53d56274 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/WeatherCoroutinesTileService.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/WeatherCoroutinesTileService.kt @@ -63,15 +63,14 @@ abstract class WeatherCoroutinesTileService : SuspendingTileService() { val locData = settingsManager.getHomeData() ?: return@withContext null // If saved data DNE (for current location), refresh weather val wLoader = WeatherDataLoader(locData) - - var weather = wLoader.loadWeatherData( + var result = wLoader.loadWeatherResult( WeatherRequest.Builder() .forceLoadSavedData() .build() ) - if (weather == null && settingsManager.getDataSync() == WearableDataSync.OFF) { - weather = wLoader.loadWeatherData( + if (result.data == null && settingsManager.getDataSync() == WearableDataSync.OFF) { + result = wLoader.loadWeatherResult( WeatherRequest.Builder() .forceRefresh(false) .loadAlerts() @@ -80,7 +79,7 @@ abstract class WeatherCoroutinesTileService : SuspendingTileService() { ) } - weather + result.data } catch (e: Exception) { null } From f943de3fbeb1d1c083ee9ce3b901c32674b28574 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 19 Jul 2025 23:41:42 -0400 Subject: [PATCH 32/33] minor bug fixes --- .../adapters/HourlyForecastItemAdapter.kt | 6 ++++++ .../simpleweather/adapters/MoonPhaseAdapter.kt | 6 ++++++ .../viewmodels/HourlyForecastNowViewModel.kt | 3 ++- .../simpleweather/preferences/SettingsFragment.kt | 12 ++++++++---- .../complications/WeatherComplicationService.kt | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/thewizrd/simpleweather/adapters/HourlyForecastItemAdapter.kt b/app/src/main/java/com/thewizrd/simpleweather/adapters/HourlyForecastItemAdapter.kt index ecb86c74..32634a10 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/adapters/HourlyForecastItemAdapter.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/adapters/HourlyForecastItemAdapter.kt @@ -33,4 +33,10 @@ class HourlyForecastItemAdapter : ListAdapter= 0 && forecast.windDegrees != null && forecast.windDegrees >= 0) { val unit = settingsManager.getSpeedUnit() diff --git a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt index 61338563..c493590b 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/preferences/SettingsFragment.kt @@ -607,8 +607,10 @@ class SettingsFragment : BaseSettingsFragment(), val providerEntry = providers.find { entry -> entry.value == selectedProvider } - updateKeySummary(providerEntry!!.display) - updateRegisterLink(providerEntry.value) + runWithView { + updateKeySummary(providerEntry!!.display) + updateRegisterLink(providerEntry.value) + } } else { settingsManager.setKeyVerified(selectedProvider, false) keyEntry.isEnabled = false @@ -621,8 +623,10 @@ class SettingsFragment : BaseSettingsFragment(), apiCategory.removePreference(personalKeyPref) apiCategory.removePreference(keyEntry) apiCategory.removePreference(registerPref) - updateKeySummary() - updateRegisterLink() + runWithView { + updateKeySummary() + updateRegisterLink() + } } lifecycleScope.launch(Dispatchers.Main) { diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherComplicationService.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherComplicationService.kt index d680494a..d8d61521 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherComplicationService.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/complications/WeatherComplicationService.kt @@ -126,7 +126,7 @@ class WeatherComplicationService : WeatherForecastComplicationService() { weather.condition!!.weather } else { provider.getWeatherCondition(weather.condition!!.icon) - } + } ?: getString(R.string.weather_notavailable) val wim = sharedDeps.weatherIconsManager val weatherIcon = wim.getWeatherIconResource(weather.condition!!.icon) From dababcadc7e184a4ace312d7efcefbb38c702500 Mon Sep 17 00:00:00 2001 From: Dave Antoine Date: Sat, 19 Jul 2025 23:43:15 -0400 Subject: [PATCH 33/33] SimpleWeather: v5.12.0-build9 --- app/build.gradle | 2 +- wearapp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1065472e..139e469a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) // ex) 345100131 = (34, 5.10, 013, 0) - versionCode 355120080 + versionCode 355120090 versionName "5.12.0" vectorDrawables { diff --git a/wearapp/build.gradle b/wearapp/build.gradle index a82e0f8d..4ccfeec6 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -16,7 +16,7 @@ android { targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code [Android: 0, WearOS: 1]) // ex) 345100131 = (34, 5.10, 013, 1) - versionCode 355120081 + versionCode 355120091 versionName "5.12.0" vectorDrawables {