diff --git a/app/build.gradle b/app/build.gradle index 0bfdfe926..ae3be2945 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) - versionCode 345100070 - versionName "5.10.1" + versionCode 345100100 + versionName "5.10.3" vectorDrawables { useSupportLibrary true 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 b68a1994b..9d9427984 100644 --- a/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt +++ b/app/src/androidTestFullgms/java/com/thewizrd/simpleweather/test/UnitTests.kt @@ -24,6 +24,7 @@ import com.thewizrd.shared_resources.utils.Coordinate import com.thewizrd.shared_resources.utils.DateTimeUtils import com.thewizrd.shared_resources.utils.JSONParser import com.thewizrd.shared_resources.utils.LocaleUtils +import com.thewizrd.shared_resources.utils.StringUtils.isNullOrWhitespace import com.thewizrd.shared_resources.utils.ZoneIdCompat import com.thewizrd.shared_resources.weatherdata.WeatherAPI import com.thewizrd.shared_resources.weatherdata.WeatherProvider @@ -33,11 +34,12 @@ import com.thewizrd.simpleweather.images.ImageDatabase import com.thewizrd.simpleweather.images.model.ImageData import com.thewizrd.simpleweather.updates.UpdateInfo import com.thewizrd.weather_api.aqicn.AQICNProvider +import com.thewizrd.weather_api.google.location.AndroidLocationProvider import com.thewizrd.weather_api.google.location.GoogleLocationProvider -import com.thewizrd.weather_api.google.location.createLocationModel import com.thewizrd.weather_api.google.location.getFromLocationNameAsync import com.thewizrd.weather_api.google.location.isGeocoderAvailable import com.thewizrd.weather_api.here.auth.hereOAuthService +import com.thewizrd.weather_api.locationiq.LocationIQProvider import com.thewizrd.weather_api.nws.SolCalcAstroProvider import com.thewizrd.weather_api.nws.alerts.NWSAlertProvider import com.thewizrd.weather_api.smc.SunMoonCalcProvider @@ -348,16 +350,12 @@ class UnitTests { fun androidGeocoderTest() { runBlocking(Dispatchers.Default) { assertTrue(isGeocoderAvailable()) - val geocoder = Geocoder(context, Locale.getDefault()) - val addressList = withContext(Dispatchers.IO) { - //geocoder.getFromLocation(47.6721646, -122.1706614, 1); // Washington - geocoder.getFromLocation(51.5073884, -0.1334347, 1) // London + val locationProvider = AndroidLocationProvider() + val location = withContext(Dispatchers.IO) { + locationProvider.getLocation(Coordinate(51.5073884, -0.1334347), WeatherAPI.ANDROID) } - assertFalse(addressList.isNullOrEmpty()) - val result = addressList!![0] - assertNotNull(result) - val locQVM = createLocationModel(result, WeatherAPI.ANDROID) - assertFalse(locQVM.locationName.toString().contains("null")) + assertNotNull(location) + assertFalse(location.locationName.toString().contains("null")) } } @@ -381,7 +379,7 @@ class UnitTests { Coordinate( queryVM!!.locationLat, queryVM.locationLong - ), WeatherAPI.OPENWEATHERMAP + ), WeatherAPI.GOOGLE ) } else if (locationProvider.needsLocationFromID()) { locationProvider.getLocationFromID(queryVM!!) @@ -390,18 +388,57 @@ class UnitTests { } assertNotNull(locModel) + assertFalse(locModel?.locationName.isNullOrWhitespace()) + assertTrue(locModel?.toLocationData()?.isValid == true) + } + } + + @Test + @Throws(WeatherException::class) + fun locIQLocationTest() { + runBlocking(Dispatchers.Default) { + val locationProvider: WeatherLocationProvider = LocationIQProvider() + val locations = withContext(Dispatchers.IO) { + locationProvider.getLocations("Redmond, WA", WeatherAPI.LOCATIONIQ) + } + assertFalse(locations.isEmpty()) + + val queryVM = locations.find { it.locationName?.startsWith("Redmond") == true } + assertNotNull(queryVM) - if (locModel!!.locationTZLong.isNullOrBlank() && locModel.locationLat != 0.0 && locModel.locationLong != 0.0) { - val tzId = weatherModule.tzdbService.getTimeZone( - locModel.locationLat, - locModel.locationLong + val locModel = if (locationProvider.needsLocationFromName()) { + locationProvider.getLocationFromName(queryVM!!) + } else if (locationProvider.needsLocationFromGeocoder()) { + locationProvider.getLocation( + Coordinate( + queryVM!!.locationLat, + queryVM.locationLong + ), WeatherAPI.LOCATIONIQ ) - if ("unknown" != tzId) - locModel.locationTZLong = tzId + } else if (locationProvider.needsLocationFromID()) { + locationProvider.getLocationFromID(queryVM!!) + } else { + queryVM + } + + assertNotNull(locModel) + assertFalse(locModel?.locationName.isNullOrWhitespace()) + assertTrue(locModel?.toLocationData()?.isValid == true) + } + } + + @Test + @Throws(WeatherException::class) + fun locIQLocationGeocoderTest() { + runBlocking(Dispatchers.Default) { + val locationProvider: WeatherLocationProvider = LocationIQProvider() + val locModel = withContext(Dispatchers.IO) { + locationProvider.getLocation(Coordinate(51.5073884, -0.1334347), WeatherAPI.ANDROID) } - assertFalse(locModel.locationTZLong.isNullOrEmpty()) - assertTrue(locModel.toLocationData().isValid) + assertNotNull(locModel) + assertFalse(locModel?.locationName.isNullOrWhitespace()) + assertTrue(locModel?.toLocationData()?.isValid == true) } } @@ -425,7 +462,7 @@ class UnitTests { Coordinate( queryVM!!.locationLat, queryVM.locationLong - ), WeatherAPI.OPENWEATHERMAP + ), WeatherAPI.WEATHERAPI ) } else if (locationProvider.needsLocationFromID()) { locationProvider.getLocationFromID(queryVM!!) @@ -434,18 +471,8 @@ class UnitTests { } assertNotNull(locModel) - - if (locModel!!.locationTZLong.isNullOrBlank() && locModel.locationLat != 0.0 && locModel.locationLong != 0.0) { - val tzId = weatherModule.tzdbService.getTimeZone( - locModel.locationLat, - locModel.locationLong - ) - if ("unknown" != tzId) - locModel.locationTZLong = tzId - } - - assertFalse(locModel.locationTZLong.isNullOrEmpty()) - assertTrue(locModel.toLocationData().isValid) + assertFalse(locModel?.locationName.isNullOrWhitespace()) + assertTrue(locModel?.toLocationData()?.isValid == true) } } diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/preferences/AbstractWeatherWidgetPreferenceFragment.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/preferences/AbstractWeatherWidgetPreferenceFragment.kt index 3243c0973..860a9bbca 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/preferences/AbstractWeatherWidgetPreferenceFragment.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/preferences/AbstractWeatherWidgetPreferenceFragment.kt @@ -56,6 +56,7 @@ import com.thewizrd.shared_resources.utils.ContextUtils.isSmallestWidth import com.thewizrd.shared_resources.utils.Logger import com.thewizrd.shared_resources.weatherdata.model.AirQuality import com.thewizrd.shared_resources.weatherdata.model.Atmosphere +import com.thewizrd.shared_resources.weatherdata.model.Beaufort import com.thewizrd.shared_resources.weatherdata.model.Condition import com.thewizrd.shared_resources.weatherdata.model.Forecast import com.thewizrd.shared_resources.weatherdata.model.ForecastExtras @@ -63,7 +64,9 @@ import com.thewizrd.shared_resources.weatherdata.model.HourlyForecast import com.thewizrd.shared_resources.weatherdata.model.Location import com.thewizrd.shared_resources.weatherdata.model.LocationType import com.thewizrd.shared_resources.weatherdata.model.MinutelyForecast +import com.thewizrd.shared_resources.weatherdata.model.Pollen import com.thewizrd.shared_resources.weatherdata.model.Precipitation +import com.thewizrd.shared_resources.weatherdata.model.UV import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.activities.LocationSearch @@ -596,15 +599,27 @@ abstract class AbstractWeatherWidgetPreferenceFragment : ToolbarPreferenceFragme weather = getString(R.string.weather_sunny) tempF = 70f tempC = 21f + windDegrees = 292 windMph = 5f windKph = 8f + windGustMph = 15f + windGustKph = 25f + feelslikeF = 75f + feelslikeC = 23f highF = 75f highC = 23f lowF = 60f lowC = 15f icon = WeatherIcons.DAY_SUNNY airQuality = AirQuality().apply { - index = 46 + index = Random.nextInt(0, 301) + } + beaufort = Beaufort(Beaufort.BeaufortScale.valueOf(Random.nextInt(0, 12))) + uv = UV(Random.nextInt(0, 11).toFloat()) + pollen = Pollen().apply { + treePollenCount = Pollen.PollenCount.VERY_HIGH + grassPollenCount = Pollen.PollenCount.LOW + ragweedPollenCount = Pollen.PollenCount.MODERATE } } atmosphere = Atmosphere() diff --git a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2Creator.kt b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2Creator.kt index 2fab12e2f..a4853c318 100644 --- a/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2Creator.kt +++ b/app/src/main/java/com/thewizrd/simpleweather/widgets/remoteviews/WeatherWidget4x2Creator.kt @@ -10,6 +10,7 @@ import android.text.style.TextAppearanceSpan import android.util.TypedValue import android.view.View import android.widget.RemoteViews +import com.thewizrd.common.controls.WeatherDetailsType import com.thewizrd.common.controls.WeatherUiModel import com.thewizrd.common.helpers.ColorsUtils import com.thewizrd.common.utils.ImageUtils @@ -153,6 +154,10 @@ class WeatherWidget4x2Creator(context: Context, loadBackground: Boolean = true) R.id.condition_weather, textColor ) + updateViews.setTextColor( + R.id.condition_feelslike, + textColor + ) updateViews.setTextColor(R.id.date_panel, textColor) updateViews.setTextColor(R.id.clock_panel, textColor) @@ -170,6 +175,15 @@ class WeatherWidget4x2Creator(context: Context, loadBackground: Boolean = true) weather.curTemp?.applySpan(textAppearanceSpan) ) + weather.weatherDetailsMap[WeatherDetailsType.FEELSLIKE]?.let { + updateViews.setTextViewText( + R.id.condition_feelslike, + "${it.label}: ${it.value}".applySpan(textAppearanceSpan) + ) + } ?: run { + updateViews.setViewVisibility(R.id.condition_feelslike, View.GONE) + } + buildDate(location, updateViews, appWidgetId, newOptions) // Open default clock/calendar app updateViews.setOnClickPendingIntent( @@ -361,6 +375,11 @@ class WeatherWidget4x2Creator(context: Context, loadBackground: Boolean = true) TypedValue.COMPLEX_UNIT_SP, 12f * txtSizeMultiplier ) + updateViews.setTextViewTextSize( + R.id.condition_feelslike, + TypedValue.COMPLEX_UNIT_SP, + 12f * txtSizeMultiplier + ) } private fun updateClockSize(views: RemoteViews, appWidgetId: Int, newOptions: Bundle) { 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 b0cf634ba..804d1e12d 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 @@ -172,6 +172,10 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean R.id.condition_weather, textColor ) + updateViews.setTextColor( + R.id.condition_feelslike, + textColor + ) updateViews.setTextColor(R.id.date_panel, textColor) updateViews.setTextColor(R.id.clock_panel, textColor) @@ -189,6 +193,15 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean weather.curTemp?.applySpan(textAppearanceSpan) ) + weather.weatherDetailsMap[WeatherDetailsType.FEELSLIKE]?.let { + updateViews.setTextViewText( + R.id.condition_feelslike, + "${it.label}: ${it.value}".applySpan(textAppearanceSpan) + ) + } ?: run { + updateViews.setViewVisibility(R.id.condition_feelslike, View.GONE) + } + buildDate(location, updateViews, appWidgetId, newOptions) // Open default clock/calendar app updateViews.setOnClickPendingIntent( @@ -659,6 +672,11 @@ class WeatherWidget4x2TomorrowCreator(context: Context, loadBackground: Boolean TypedValue.COMPLEX_UNIT_SP, 12f * txtSizeMultiplier ) + updateViews.setTextViewTextSize( + R.id.condition_feelslike, + TypedValue.COMPLEX_UNIT_SP, + 12f * txtSizeMultiplier + ) } private fun updateClockSize(views: RemoteViews, appWidgetId: Int, newOptions: Bundle) { diff --git a/app/src/main/res/layout/app_widget_4x2.xml b/app/src/main/res/layout/app_widget_4x2.xml index c359b16e3..f729cd584 100644 --- a/app/src/main/res/layout/app_widget_4x2.xml +++ b/app/src/main/res/layout/app_widget_4x2.xml @@ -114,7 +114,8 @@ @@ -156,41 +157,61 @@ - + android:layout_height="match_parent" + android:layout_gravity="center_vertical" + android:gravity="center_vertical" + android:orientation="vertical"> - + android:orientation="horizontal" + android:paddingVertical="2dp"> + + + + + + + android:textSize="12sp" + tools:ignore="UnusedAttribute" + tools:text="Feels like: 75°" /> - + diff --git a/app/src/main/res/layout/app_widget_4x2_preview.xml b/app/src/main/res/layout/app_widget_4x2_preview.xml index a64effdc8..604a14811 100644 --- a/app/src/main/res/layout/app_widget_4x2_preview.xml +++ b/app/src/main/res/layout/app_widget_4x2_preview.xml @@ -78,9 +78,10 @@ + android:layout_height="0dp" + android:layout_weight="1" + android:baselineAligned="false" + android:orientation="horizontal"> - - - + + + android:orientation="horizontal" + android:paddingVertical="2dp"> + + + + + + + android:textSize="12sp" + android:text="Feels like: 75°" /> - + diff --git a/app/src/main/res/layout/app_widget_4x2_tomorrow.xml b/app/src/main/res/layout/app_widget_4x2_tomorrow.xml index 0dc8b39c3..d7894197a 100644 --- a/app/src/main/res/layout/app_widget_4x2_tomorrow.xml +++ b/app/src/main/res/layout/app_widget_4x2_tomorrow.xml @@ -114,7 +114,8 @@ @@ -156,41 +157,61 @@ - + android:layout_height="match_parent" + android:layout_gravity="center_vertical" + android:gravity="center_vertical" + android:orientation="vertical"> - + android:orientation="horizontal" + android:paddingVertical="2dp"> + + + + + + + android:textSize="12sp" + tools:ignore="UnusedAttribute" + tools:text="Feels like: 75°" /> - + diff --git a/app/src/main/res/layout/app_widget_4x2_tomorrow_preview.xml b/app/src/main/res/layout/app_widget_4x2_tomorrow_preview.xml index 84ab1b763..94e3053a1 100644 --- a/app/src/main/res/layout/app_widget_4x2_tomorrow_preview.xml +++ b/app/src/main/res/layout/app_widget_4x2_tomorrow_preview.xml @@ -78,9 +78,10 @@ + android:layout_height="0dp" + android:layout_weight="1" + android:baselineAligned="false" + android:orientation="horizontal"> - - - + + + android:orientation="horizontal" + android:paddingVertical="2dp"> + + + + + + + android:textSize="12sp" + android:text="Feels like: 75°" /> - + diff --git a/build.gradle b/build.gradle index 61b359311..b20169ab4 100644 --- a/build.gradle +++ b/build.gradle @@ -12,12 +12,12 @@ buildscript { desugar_version = '2.1.3' - firebase_version = '33.5.1' + firebase_version = '33.7.0' gms_location_version = '21.3.0' gms_base_version = '18.5.0' - gms_basement_version = '18.4.0' + gms_basement_version = '18.5.0' gms_tasks_version = '18.2.0' - gms_wearable_version = '18.2.0' + gms_wearable_version = '19.0.0' annotation_version = '1.9.1' activity_version = '1.9.3' @@ -27,8 +27,8 @@ buildscript { arch_core_runtime_version = '2.2.0' fragment_version = '1.8.5' lifecycle_version = '2.8.7' - nav_version = '2.8.3' - paging_version = '3.3.2' + nav_version = '2.8.4' + paging_version = '3.3.4' preference_version = '1.2.1' recyclerview_version = '1.3.2' room_version = '2.6.1' @@ -46,17 +46,17 @@ buildscript { material_version = '1.12.0' compose_compiler_version = '1.5.15' - compose_bom_version = '2024.10.01' + compose_bom_version = '2024.11.00' wear_compose_version = '1.4.0' wear_tiles_version = '1.4.1' wear_watchface_version = '1.2.1' - horologist_version = '0.6.20' + horologist_version = '0.6.21' accompanist_version = '0.36.0' glide_version = '4.16.0' icu4j_version = '76.1' jjwt_version = '0.12.6' - moshi_version = '1.15.1' + moshi_version = '1.15.2' okhttp_version = '4.12.0' timber_version = '5.0.1' } @@ -68,7 +68,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.7.2' + classpath 'com.android.tools.build:gradle:8.7.3' 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.2' diff --git a/common/src/main/java/com/thewizrd/common/controls/DetailItemViewModel.java b/common/src/main/java/com/thewizrd/common/controls/DetailItemViewModel.java index d16edb6fd..20f23db8f 100644 --- a/common/src/main/java/com/thewizrd/common/controls/DetailItemViewModel.java +++ b/common/src/main/java/com/thewizrd/common/controls/DetailItemViewModel.java @@ -148,38 +148,44 @@ public DetailItemViewModel(@NonNull MoonPhase.MoonPhaseType moonPhaseType) { case NEWMOON: this.icon = WeatherIcons.MOON_NEW; this.value = context.getString(R.string.moonphase_new); + this.shortValue = context.getString(R.string.moonphase_new_short); break; case WAXING_CRESCENT: this.icon = WeatherIcons.MOON_ALT_WAXING_CRESCENT_3; this.value = context.getString(R.string.moonphase_waxcrescent); + this.shortValue = context.getString(R.string.moonphase_waxcrescent_short); break; case FIRST_QTR: this.icon = WeatherIcons.MOON_ALT_FIRST_QUARTER; this.value = context.getString(R.string.moonphase_firstqtr); + this.shortValue = context.getString(R.string.moonphase_firstqtr_short); break; case WAXING_GIBBOUS: this.icon = WeatherIcons.MOON_ALT_WAXING_GIBBOUS_3; this.value = context.getString(R.string.moonphase_waxgibbous); + this.shortValue = context.getString(R.string.moonphase_waxgibbous_short); break; case FULL_MOON: this.icon = WeatherIcons.MOON_ALT_FULL; this.value = context.getString(R.string.moonphase_full); + this.shortValue = context.getString(R.string.moonphase_full_short); break; case WANING_GIBBOUS: this.icon = WeatherIcons.MOON_ALT_WANING_GIBBOUS_3; this.value = context.getString(R.string.moonphase_wangibbous); + this.shortValue = context.getString(R.string.moonphase_wangibbous_short); break; case LAST_QTR: this.icon = WeatherIcons.MOON_ALT_THIRD_QUARTER; this.value = context.getString(R.string.moonphase_lastqtr); + this.shortValue = context.getString(R.string.moonphase_lastqtr_short); break; case WANING_CRESCENT: this.icon = WeatherIcons.MOON_ALT_WANING_CRESCENT_3; this.value = context.getString(R.string.moonphase_wancrescent); + this.shortValue = context.getString(R.string.moonphase_wancrescent_short); break; } - - this.shortValue = value; } public DetailItemViewModel(@NonNull Beaufort.BeaufortScale beaufortScale) { diff --git a/common/src/main/java/com/thewizrd/common/controls/PollenViewModel.kt b/common/src/main/java/com/thewizrd/common/controls/PollenViewModel.kt index 3fa23987a..bbca9d15e 100644 --- a/common/src/main/java/com/thewizrd/common/controls/PollenViewModel.kt +++ b/common/src/main/java/com/thewizrd/common/controls/PollenViewModel.kt @@ -3,6 +3,7 @@ package com.thewizrd.common.controls import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan +import androidx.annotation.ColorInt import com.thewizrd.shared_resources.R import com.thewizrd.shared_resources.icons.WeatherIcons import com.thewizrd.shared_resources.sharedDeps @@ -12,56 +13,98 @@ import com.thewizrd.shared_resources.weatherdata.model.Pollen class PollenViewModel(pollenData: Pollen) { var treePollenDesc: CharSequence private set + var treePollenShortDesc: CharSequence + private set var grassPollenDesc: CharSequence private set + var grassPollenShortDesc: CharSequence + private set var ragweedPollenDesc: CharSequence private set + var ragweedPollenShortDesc: CharSequence + private set + + var treePollenProgress: Int = pollenData.treePollenCount?.ordinal ?: 0 + private set + var grassPollenProgress: Int = pollenData.grassPollenCount?.ordinal ?: 0 + private set + var ragweedPollenProgress: Int = pollenData.ragweedPollenCount?.ordinal ?: 0 + private set + + var treePollenProgressColor: Int = pollenData.treePollenCount.toColor() + private set + var grassPollenProgressColor: Int = pollenData.grassPollenCount.toColor() + private set + var ragweedPollenProgressColor: Int = pollenData.ragweedPollenCount.toColor() + private set + + val progressMax: Int = Pollen.PollenCount.VERY_HIGH.ordinal init { - treePollenDesc = getPollenCountDescription(pollenData.treePollenCount) - grassPollenDesc = getPollenCountDescription(pollenData.grassPollenCount) - ragweedPollenDesc = getPollenCountDescription(pollenData.ragweedPollenCount) + getPollenCountDescription(pollenData.treePollenCount).run { + treePollenDesc = first + treePollenShortDesc = second + } + + getPollenCountDescription(pollenData.grassPollenCount).run { + grassPollenDesc = first + grassPollenShortDesc = second + } + + getPollenCountDescription(pollenData.ragweedPollenCount).run { + ragweedPollenDesc = first + ragweedPollenShortDesc = second + } } - private fun getPollenCountDescription(pollenCount: Pollen.PollenCount?): CharSequence { + private fun getPollenCountDescription(pollenCount: Pollen.PollenCount?): Pair { val context = sharedDeps.context - return when (pollenCount) { - Pollen.PollenCount.LOW -> SpannableString(context.getString(R.string.label_count_low)).apply { - setSpan( - ForegroundColorSpan(Colors.LIMEGREEN), - 0, - this.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - Pollen.PollenCount.MODERATE -> SpannableString(context.getString(R.string.label_count_moderate)).apply { - setSpan( - ForegroundColorSpan(Colors.ORANGE), - 0, - this.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - Pollen.PollenCount.HIGH -> SpannableString(context.getString(R.string.label_count_high)).apply { - setSpan( - ForegroundColorSpan(Colors.ORANGERED), - 0, - this.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - Pollen.PollenCount.VERY_HIGH -> SpannableString(context.getString(R.string.label_count_veryhigh)).apply { - setSpan( - ForegroundColorSpan(Colors.RED), - 0, - this.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - else -> WeatherIcons.EM_DASH + val description = when (pollenCount) { + Pollen.PollenCount.LOW -> SpannableString(context.getString(R.string.label_count_low)) to SpannableString( + context.getString(R.string.label_count_low_short) + ) + + Pollen.PollenCount.MODERATE -> SpannableString(context.getString(R.string.label_count_moderate)) to SpannableString( + context.getString(R.string.label_count_moderate_short) + ) + + Pollen.PollenCount.HIGH -> SpannableString(context.getString(R.string.label_count_high)) to SpannableString( + context.getString(R.string.label_count_high_short) + ) + + Pollen.PollenCount.VERY_HIGH -> SpannableString(context.getString(R.string.label_count_veryhigh)) to SpannableString( + context.getString(R.string.label_count_veryhigh_short) + ) + + else -> WeatherIcons.EM_DASH to WeatherIcons.EM_DASH + } + + return description.apply { + first.setDescriptionSpan(pollenCount) + second.setDescriptionSpan(pollenCount) + } + } + + @ColorInt + private fun Pollen.PollenCount?.toColor(): Int = when (this) { + Pollen.PollenCount.LOW -> Colors.LIMEGREEN + Pollen.PollenCount.MODERATE -> Colors.ORANGE + Pollen.PollenCount.HIGH -> Colors.ORANGERED + Pollen.PollenCount.VERY_HIGH -> Colors.RED + else -> Colors.TRANSPARENT + } + + private fun CharSequence.setDescriptionSpan(pollenCount: Pollen.PollenCount?) { + if (this is Spannable) { + this.setSpan( + ForegroundColorSpan(pollenCount.toColor()), + 0, + this.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) } } } \ No newline at end of file diff --git a/common/src/main/java/com/thewizrd/common/controls/WeatherUiModel.kt b/common/src/main/java/com/thewizrd/common/controls/WeatherUiModel.kt index df0ad20d6..80b8fa57c 100644 --- a/common/src/main/java/com/thewizrd/common/controls/WeatherUiModel.kt +++ b/common/src/main/java/com/thewizrd/common/controls/WeatherUiModel.kt @@ -8,6 +8,7 @@ import com.thewizrd.common.weatherdata.NoopWeatherProvider import com.thewizrd.shared_resources.DateTimeConstants import com.thewizrd.shared_resources.R import com.thewizrd.shared_resources.appLib +import com.thewizrd.shared_resources.designer.isInEditMode import com.thewizrd.shared_resources.di.settingsManager import com.thewizrd.shared_resources.icons.WeatherIcons import com.thewizrd.shared_resources.sharedDeps @@ -154,7 +155,7 @@ class WeatherUiModel() { } private fun refreshView() { - val provider: WeatherProvider = if (appLib.properties.getBoolean("isInEditMode", false)) { + val provider: WeatherProvider = if (appLib.isInEditMode()) { NoopWeatherProvider() } else { weatherModule.weatherManager.getWeatherProvider(weatherData!!.source) @@ -608,16 +609,19 @@ class WeatherUiModel() { weatherDetailsMap[WeatherDetailsType.TREEPOLLEN] = DetailItemViewModel( WeatherDetailsType.TREEPOLLEN, pollenVM.treePollenDesc.toString(), + pollenVM.treePollenShortDesc.toString(), 0 ) weatherDetailsMap[WeatherDetailsType.GRASSPOLLEN] = DetailItemViewModel( WeatherDetailsType.GRASSPOLLEN, pollenVM.grassPollenDesc.toString(), + pollenVM.grassPollenShortDesc.toString(), 0 ) weatherDetailsMap[WeatherDetailsType.RAGWEEDPOLLEN] = DetailItemViewModel( WeatherDetailsType.RAGWEEDPOLLEN, pollenVM.ragweedPollenDesc.toString(), + pollenVM.ragweedPollenShortDesc.toString(), 0 ) } diff --git a/shared_resources/src/main/java/com/thewizrd/shared_resources/designer/DesignerUtils.kt b/shared_resources/src/main/java/com/thewizrd/shared_resources/designer/DesignerUtils.kt index 3e0173c86..4d3691726 100644 --- a/shared_resources/src/main/java/com/thewizrd/shared_resources/designer/DesignerUtils.kt +++ b/shared_resources/src/main/java/com/thewizrd/shared_resources/designer/DesignerUtils.kt @@ -50,4 +50,6 @@ fun Context.initializeDependencies(isPhone: Boolean = true) { get() = SettingsManager(appContext) } -} \ No newline at end of file +} + +fun ApplicationLib.isInEditMode(): Boolean = properties.getBoolean("isInEditMode", false) \ No newline at end of file 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 a7ae29f7d..f95af1cc0 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 + UNKNOWN, SUCCESS, NOWEATHER, NETWORKERROR, INVALIDAPIKEY, QUERYNOTFOUND, RATELIMITED } class WeatherException : Exception { @@ -33,6 +33,9 @@ class WeatherException : Exception { ErrorStatus.QUERYNOTFOUND -> { sharedDeps.context.getString(R.string.werror_querynotfound) } + ErrorStatus.RATELIMITED -> { + sharedDeps.context.getString(R.string.werror_ratelimited) + } else -> { // ErrorStatus.UNKNOWN sharedDeps.context.getString(R.string.werror_unknown) diff --git a/shared_resources/src/main/res/values-de/moonphases.xml b/shared_resources/src/main/res/values-de/moonphases.xml index 365482b70..16c1f14ee 100644 --- a/shared_resources/src/main/res/values-de/moonphases.xml +++ b/shared_resources/src/main/res/values-de/moonphases.xml @@ -2,10 +2,18 @@ "Neumond" "Zunehmender Sichelmond" - "Zunehmender Halbmond" + "Erstes Viertel" "Zunehmender Mond" "Vollmond" "Abnehmender Mond" - "Abnehmender Halbmond" + "Letztes Viertel" "Abnehmender Sichelmond" + "Neu" + "Zunehm." + "Erstes V." + "Zunehm." + "Voll" + "Abnehm." + "Letztes V." + "Abnehm." \ 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 3cd1701f7..b24212ab5 100644 --- a/shared_resources/src/main/res/values-de/strings.xml +++ b/shared_resources/src/main/res/values-de/strings.xml @@ -246,4 +246,9 @@ Minute-by-Minute Forecast --> "Nutzungsbedingungen" "Mehr Infos" "Zurücksetzen" + "Niedrig" + "Moderat" + "Hoch" + "Hoch" + "Zu viele Anfragen. Bitte versuchen Sie es nach ein paar Minuten erneut" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-es/moonphases.xml b/shared_resources/src/main/res/values-es/moonphases.xml index ac256db2d..ba18fcbbe 100644 --- a/shared_resources/src/main/res/values-es/moonphases.xml +++ b/shared_resources/src/main/res/values-es/moonphases.xml @@ -8,4 +8,12 @@ "Luna gibosa menguante" "Cuarto menguante" "Luna menguante" + "Nueva" + "Crec." + "1/4 cr." + "Crec." + "Llena" + "Meng." + "1/4 m." + "Meng." \ 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 74187c5fc..dadec5ad2 100644 --- a/shared_resources/src/main/res/values-es/strings.xml +++ b/shared_resources/src/main/res/values-es/strings.xml @@ -233,4 +233,9 @@ "Zmluvné podmienky" "Más información" "Restablecer" + "Bajo" + "Medio" + "Alto" + "Alto" + "Demasiadas solicitudes. Vuelva a intentarlo pasados unos minutos" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-fr/moonphases.xml b/shared_resources/src/main/res/values-fr/moonphases.xml index 298449bee..08090e635 100644 --- a/shared_resources/src/main/res/values-fr/moonphases.xml +++ b/shared_resources/src/main/res/values-fr/moonphases.xml @@ -8,4 +8,12 @@ "Gibbeuse décroissante" "Dernier quartier" "Dernier croissant" + "Nouvelle" + "Ascend." + "1er qu." + "Ascend." + "Pleine" + "Descend." + "Dern. qr." + "Descend." \ 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 c0d611363..1884a08ce 100644 --- a/shared_resources/src/main/res/values-fr/strings.xml +++ b/shared_resources/src/main/res/values-fr/strings.xml @@ -200,4 +200,9 @@ "Conditions d'utilisation" "Plus d'informations" "Réinitialiser" + "Bas" + "Modéré" + "Élevé" + "Élevé" + "Trop de demandes. Veuillez réessayer après quelques minutes" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-nl/moonphases.xml b/shared_resources/src/main/res/values-nl/moonphases.xml index 7dc544335..123261839 100644 --- a/shared_resources/src/main/res/values-nl/moonphases.xml +++ b/shared_resources/src/main/res/values-nl/moonphases.xml @@ -8,4 +8,12 @@ "Afnemende maan" "Laatste kwartier" "Afnemende maansikkel" + "Nieuw" + "Wassend" + "Eerste k" + "Wassend" + "Vol" + "Afnemend" + "Laatste k" + "Afnemend" \ 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 3e5fe2dbd..b2d9df26a 100644 --- a/shared_resources/src/main/res/values-nl/strings.xml +++ b/shared_resources/src/main/res/values-nl/strings.xml @@ -204,4 +204,9 @@ "Servicevoorwaarden" "Meer informatie" "Resetten" + "Laag" + "Matig" + "Hoog" + "Hoog" + "Te veel aanvragen. Probeer het opnieuw na een paar minuten" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-pl/moonphases.xml b/shared_resources/src/main/res/values-pl/moonphases.xml index de4dcdc01..495ea02c2 100644 --- a/shared_resources/src/main/res/values-pl/moonphases.xml +++ b/shared_resources/src/main/res/values-pl/moonphases.xml @@ -8,4 +8,12 @@ "Ubywający księżyc garbaty" "Trzecia kwadra" "Sierp ubywający" + "Nów" + "Przyb." + "1 kw." + "Przyb." + "Pełnia" + "Ubyw." + "3 kw." + "Ubyw." \ 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 6b960621a..e3a3854d0 100644 --- a/shared_resources/src/main/res/values-pl/strings.xml +++ b/shared_resources/src/main/res/values-pl/strings.xml @@ -207,4 +207,9 @@ "Warunki korzystania z usługi" "Więcej informacji" "Resetuj" + "Niski" + "Średni" + "Wysoki" + "Wysoki" + "Zbyt wiele żądań. Spróbuj ponownie za kilka minut" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-sk/moonphases.xml b/shared_resources/src/main/res/values-sk/moonphases.xml index d5b8a72af..750ceadd6 100644 --- a/shared_resources/src/main/res/values-sk/moonphases.xml +++ b/shared_resources/src/main/res/values-sk/moonphases.xml @@ -8,4 +8,12 @@ "zmenšujúci sa mesiac" "posledná štvrť" "zmenšujúci sa polmesiac" + "Nov" + "Dorast." + "1. štvrť" + "Dorast." + "Spln" + "Zmenš." + "3. štvrť" + "Zmenš." \ 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 1aaa5894e..369daddad 100644 --- a/shared_resources/src/main/res/values-sk/strings.xml +++ b/shared_resources/src/main/res/values-sk/strings.xml @@ -204,4 +204,9 @@ "Podmienky služby" "Viac informácií" "Resetovať" + "Nízka" + "Stredná" + "Vysoká" + "Vysoká" + "Príliš veľa požiadaviek. Prosím, skúste to znova po niekoľkých minútach" \ No newline at end of file diff --git a/shared_resources/src/main/res/values-zh-rCN/moonphases.xml b/shared_resources/src/main/res/values-zh-rCN/moonphases.xml index 678282ee8..f32e7516f 100644 --- a/shared_resources/src/main/res/values-zh-rCN/moonphases.xml +++ b/shared_resources/src/main/res/values-zh-rCN/moonphases.xml @@ -8,4 +8,12 @@ "亏凸" "下弦月" "残月" + "新月" + "眉月" + "上弦月" + "盈凸" + "满月" + "亏凸" + "下弦月" + "残月" \ 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 f129fbb02..3fcca99d0 100644 --- a/shared_resources/src/main/res/values-zh-rCN/strings.xml +++ b/shared_resources/src/main/res/values-zh-rCN/strings.xml @@ -224,4 +224,9 @@ "更多信息" "重置" + "低" + "中" + "高" + "很高" + "请求太多。请几分钟后再试一次" \ No newline at end of file diff --git a/shared_resources/src/main/res/values/moonphases.xml b/shared_resources/src/main/res/values/moonphases.xml index d6b1f5c19..19cf167da 100644 --- a/shared_resources/src/main/res/values/moonphases.xml +++ b/shared_resources/src/main/res/values/moonphases.xml @@ -1,11 +1,19 @@ New Moon + New Waxing Crescent + Waxing First Quarter + First Q Waxing Gibbous + Waxing Full Moon + Full Waning Gibbous + Waning Last Quarter + Last Q Waning Crescent + Waning \ 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 1831937a5..296c41591 100644 --- a/shared_resources/src/main/res/values/strings.xml +++ b/shared_resources/src/main/res/values/strings.xml @@ -151,6 +151,7 @@ Invalid Provider Key No cities match your search query Unknown error occurred + Too many requests. Please try again after a few minutes Update Interval @@ -220,9 +221,13 @@ Ragweed Ragweed Pollen Low + Low Moderate + Medium High + High Very High + V. High Current Refresh diff --git a/wearapp/build.gradle b/wearapp/build.gradle index 4bf1ddcc5..faa9bc259 100644 --- a/wearapp/build.gradle +++ b/wearapp/build.gradle @@ -17,8 +17,8 @@ android { minSdkVersion 26 targetSdkVersion rootProject.targetSdkVersion // NOTE: Version Code Format (TargetSDK, Version Name, Build Number, Variant Code (Android: 0, WearOS: 1) - versionCode 345100081 - versionName "5.10.1" + versionCode 345100101 + versionName "5.10.3" vectorDrawables { useSupportLibrary true @@ -171,7 +171,7 @@ dependencies { implementation "androidx.compose.animation:animation-graphics" implementation "androidx.compose.runtime:runtime-livedata" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" - implementation 'androidx.paging:paging-compose:3.3.2' + implementation "androidx.paging:paging-compose:$paging_version" implementation "androidx.wear.compose:compose-foundation:$wear_compose_version" implementation "androidx.wear.compose:compose-material:$wear_compose_version" implementation "androidx.wear.compose:compose-navigation:$wear_compose_version" diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/preferences/DetailsWeatherTileUtils.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/preferences/DetailsWeatherTileUtils.kt index 28d99f888..5b46c39ed 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/preferences/DetailsWeatherTileUtils.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/preferences/DetailsWeatherTileUtils.kt @@ -26,14 +26,7 @@ object DetailsWeatherTileUtils { } fun isTypeAllowed(detailsType: WeatherDetailsType): Boolean { - return when (detailsType) { - WeatherDetailsType.MOONPHASE, - WeatherDetailsType.TREEPOLLEN, - WeatherDetailsType.GRASSPOLLEN, - WeatherDetailsType.RAGWEEDPOLLEN -> false - - else -> true - } + return true } fun getTileConfig(): List? { diff --git a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/layouts/DetailsWeatherTileLayout.kt b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/layouts/DetailsWeatherTileLayout.kt index 5be77253d..6f11fa34a 100644 --- a/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/layouts/DetailsWeatherTileLayout.kt +++ b/wearapp/src/main/java/com/thewizrd/simpleweather/wearable/tiles/layouts/DetailsWeatherTileLayout.kt @@ -1,14 +1,17 @@ package com.thewizrd.simpleweather.wearable.tiles.layouts import android.content.Context +import androidx.annotation.OptIn import androidx.compose.ui.util.fastCoerceIn import androidx.core.graphics.ColorUtils import androidx.wear.protolayout.ActionBuilders import androidx.wear.protolayout.ColorBuilders.ColorProp import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters +import androidx.wear.protolayout.DimensionBuilders.DpProp import androidx.wear.protolayout.DimensionBuilders.degrees import androidx.wear.protolayout.DimensionBuilders.dp import androidx.wear.protolayout.DimensionBuilders.expand +import androidx.wear.protolayout.DimensionBuilders.wrap import androidx.wear.protolayout.LayoutElementBuilders.Box import androidx.wear.protolayout.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT import androidx.wear.protolayout.LayoutElementBuilders.Column @@ -16,29 +19,44 @@ import androidx.wear.protolayout.LayoutElementBuilders.FONT_WEIGHT_NORMAL import androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER import androidx.wear.protolayout.LayoutElementBuilders.Image import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement +import androidx.wear.protolayout.LayoutElementBuilders.Row import androidx.wear.protolayout.LayoutElementBuilders.Spacer +import androidx.wear.protolayout.LayoutElementBuilders.TEXT_OVERFLOW_MARQUEE +import androidx.wear.protolayout.LayoutElementBuilders.VERTICAL_ALIGN_CENTER +import androidx.wear.protolayout.ModifiersBuilders import androidx.wear.protolayout.ModifiersBuilders.Background +import androidx.wear.protolayout.ModifiersBuilders.Clickable import androidx.wear.protolayout.ModifiersBuilders.Corner import androidx.wear.protolayout.ModifiersBuilders.Modifiers +import androidx.wear.protolayout.ModifiersBuilders.Padding +import androidx.wear.protolayout.ModifiersBuilders.Semantics import androidx.wear.protolayout.ModifiersBuilders.Transformation import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.expression.ProtoLayoutExperimental +import androidx.wear.protolayout.material.Chip +import androidx.wear.protolayout.material.ChipColors import androidx.wear.protolayout.material.CircularProgressIndicator +import androidx.wear.protolayout.material.CompactChip import androidx.wear.protolayout.material.ProgressIndicatorColors import androidx.wear.protolayout.material.Text import androidx.wear.protolayout.material.Typography import androidx.wear.protolayout.material.layouts.MultiButtonLayout +import androidx.wear.protolayout.material.layouts.PrimaryLayout import androidx.wear.tiles.tooling.preview.TilePreviewData import androidx.wear.tiles.tooling.preview.TilePreviewHelper import com.google.android.horologist.tiles.images.toImageResource import com.thewizrd.common.controls.AirQualityViewModel import com.thewizrd.common.controls.BeaufortViewModel import com.thewizrd.common.controls.DetailItemViewModel +import com.thewizrd.common.controls.PollenViewModel import com.thewizrd.common.controls.UVIndexViewModel import com.thewizrd.common.controls.WeatherDetailsType import com.thewizrd.common.controls.toUiModel import com.thewizrd.common.utils.ImageUtils import com.thewizrd.common.utils.ImageUtils.rotate +import com.thewizrd.shared_resources.appLib import com.thewizrd.shared_resources.designer.initializeDependencies +import com.thewizrd.shared_resources.designer.isInEditMode import com.thewizrd.shared_resources.icons.WeatherIcons import com.thewizrd.shared_resources.sharedDeps import com.thewizrd.shared_resources.utils.Colors @@ -53,11 +71,13 @@ import com.thewizrd.shared_resources.weatherdata.model.ForecastExtras import com.thewizrd.shared_resources.weatherdata.model.HourlyForecast import com.thewizrd.shared_resources.weatherdata.model.Location import com.thewizrd.shared_resources.weatherdata.model.MinutelyForecast +import com.thewizrd.shared_resources.weatherdata.model.MoonPhase import com.thewizrd.shared_resources.weatherdata.model.Pollen import com.thewizrd.shared_resources.weatherdata.model.Precipitation import com.thewizrd.shared_resources.weatherdata.model.UV import com.thewizrd.shared_resources.weatherdata.model.Weather import com.thewizrd.simpleweather.LaunchActivity +import com.thewizrd.simpleweather.R import com.thewizrd.simpleweather.preferences.DetailsWeatherTileUtils import com.thewizrd.simpleweather.ui.tiles.tools.WearPreviewDevices import com.thewizrd.simpleweather.wearable.tiles.ID_WEATHER_ICON_PREFIX @@ -93,29 +113,171 @@ internal fun detailsWeatherTileLayout( ): LayoutElement { val tileConfig = DetailsWeatherTileUtils.getTileConfig() ?: DetailsWeatherTileUtils.DEFAULT_ITEMS - val maxButtons = DetailsWeatherTileUtils.MAX_BUTTONS val filteredDetails = weatherDetails.toList() .filter { tileConfig.contains(it.first) } .sortedBy { tileConfig.indexOf(it.first) } + return detailsWeatherTileLayout( + context, + deviceParameters, + weather, + filteredDetails + ) +} + +internal fun detailsWeatherTileLayout( + context: Context, + deviceParameters: DeviceParameters, + weather: Weather?, + filteredTileConfig: List> +): LayoutElement { + val detailItems = filteredTileConfig.take(DetailsWeatherTileUtils.MAX_BUTTONS) + return Box.Builder() .setHeight(expand()) .setWidth(expand()) + .setVerticalAlignment(VERTICAL_ALIGN_CENTER) + .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) .addContent( - MultiButtonLayout.Builder() - .apply { - filteredDetails.take(maxButtons) - .forEach { (type, model) -> - addButtonContent( - detailButtonItem(context, deviceParameters, weather, type, model) + when (detailItems.size) { + 0 -> { + Text.Builder(context, context.getString(R.string.error_noresults)) + .setTypography(Typography.TYPOGRAPHY_BODY1) + .setColor(ColorProp.Builder(Colors.WHITE).build()) + .setMaxLines(1) + .build() + } + + 1, 2 -> { + PrimaryLayout.Builder(deviceParameters) + .setResponsiveContentInsetEnabled(true) + .setPrimaryChipContent( + CompactChip.Builder( + context, + Clickable.Builder() + .setOnClick(getLaunchAction(context)) + .build(), + deviceParameters ) + .setTextContent(context.getString(R.string.label_nav_weathernow)) + .build() + ) + .setContent( + Column.Builder() + .setWidth(expand()) + .apply { + detailItems.forEachIndexed { index, (type, model) -> + addContent( + detailChipItem( + context, deviceParameters, weather, type, model + ) + ) + if (index != detailItems.size - 1) { + addContent(Spacer.Builder().setHeight(dp(4f)).build()) + } + } + } + .build() + ) + .apply { + if (deviceParameters.screenWidthDp >= 225) { + weather?.location?.name?.let { + setPrimaryLabelTextContent(detailLocation(context, it)) + } + } } + .build() } - .build() + + 3, 4 -> { + Column.Builder() + .setHeight(wrap()) + .setWidth(wrap()) + .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) + .setModifiers( + Modifiers.Builder() + .setPadding( + Padding.Builder() + .setStart(dp(4f)) + .setEnd(dp(4f)) + .build() + ) + .build() + ) + .apply { + detailItems.chunked(2) + .forEachIndexed { index, list -> + addContent( + Row.Builder() + .setWidth(wrap()) + .setHeight(wrap()) + .setVerticalAlignment(VERTICAL_ALIGN_CENTER) + .apply { + list.forEachIndexed { index, (type, model) -> + addContent( + Column.Builder() + .setWidth(dp(66f)) + .setHeight(dp(66f)) + .setHorizontalAlignment( + HORIZONTAL_ALIGN_CENTER + ) + .addContent( + detailButtonItem( + context, + deviceParameters, + weather, + type, + model, + buttonSize = dp(66f), + imageSize = dp(24f), + spacerSize = dp(10f), + typography = Typography.TYPOGRAPHY_CAPTION2 + ) + ) + .build() + ) + if (index != list.size - 1) { + addContent( + Spacer.Builder().setWidth(dp(4f)) + .build() + ) + } + } + } + .build() + ) + + if (index != detailItems.size - 1) { + addContent(Spacer.Builder().setHeight(dp(4f)).build()) + } + } + } + .build() + } + + else -> { + MultiButtonLayout.Builder() + .apply { + detailItems.forEach { (type, model) -> + addButtonContent( + detailButtonItem( + context, + deviceParameters, + weather, + type, + model + ) + ) + } + } + .build() + } + } ) .build() } +@OptIn(ProtoLayoutExperimental::class) internal fun detailLocation( context: Context, location: String @@ -123,6 +285,7 @@ internal fun detailLocation( return Text.Builder(context, location.split(',').firstOrNull() ?: location) .setTypography(Typography.TYPOGRAPHY_BODY1) .setColor(ColorProp.Builder(Colors.WHITE).build()) + .setOverflow(TEXT_OVERFLOW_MARQUEE) .setMaxLines(1) .build() } @@ -132,7 +295,11 @@ internal fun detailButtonItem( deviceParameters: DeviceParameters, weather: Weather?, detailsType: WeatherDetailsType, - model: DetailItemViewModel + model: DetailItemViewModel, + buttonSize: DpProp = dp(52f), + imageSize: DpProp = dp(18f), + spacerSize: DpProp = dp(8f), + typography: Int? = null ): LayoutElement { return Box.Builder() .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) @@ -141,15 +308,15 @@ internal fun detailButtonItem( .addContent( Column.Builder() .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER) - .setHeight(expand()) - .setWidth(expand()) + .setHeight(buttonSize) + .setWidth(buttonSize) .setModifiers( Modifiers.Builder() .setBackground( Background.Builder() .setCorner( Corner.Builder() - .setRadius(dp(32f)) + .setRadius(dp(buttonSize.value * 13f / 8)) .build() ) .setColor( @@ -161,12 +328,12 @@ internal fun detailButtonItem( .build() ) .addContent( - Spacer.Builder().setHeight(dp(10f)).build() + Spacer.Builder().setHeight(spacerSize).build() ) .addContent( Image.Builder() - .setWidth(dp(14f)) - .setHeight(dp(14f)) + .setWidth(imageSize) + .setHeight(imageSize) .setModifiers( Modifiers.Builder() .apply { @@ -191,15 +358,37 @@ internal fun detailButtonItem( .build() ) .addContent( - Spacer.Builder().setHeight(dp(4f)).build() + Spacer.Builder().setHeight(dp(3f)).build() ) .addContent( Text.Builder(context, model.shortValue.toString()) - .setTypography(Typography.TYPOGRAPHY_CAPTION3) + .setTypography( + typography ?: when (detailsType) { + WeatherDetailsType.FEELSLIKE, + WeatherDetailsType.HUMIDITY, + WeatherDetailsType.POPCLOUDINESS, + WeatherDetailsType.POPCHANCE, + WeatherDetailsType.DEWPOINT, + WeatherDetailsType.BEAUFORT, + WeatherDetailsType.UV, + WeatherDetailsType.AIRQUALITY -> Typography.TYPOGRAPHY_CAPTION2 + + else -> Typography.TYPOGRAPHY_CAPTION3 + } + ) .setWeight(FONT_WEIGHT_NORMAL) .setColor(ColorProp.Builder(Colors.WHITE).build()) .setMaxLines(1) .setScalable(false) + .setModifiers( + Modifiers.Builder() + .setSemantics( + Semantics.Builder() + .setRole(ModifiersBuilders.SEMANTICS_ROLE_BUTTON) + .build() + ) + .build() + ) .build() ) .build() @@ -217,10 +406,7 @@ internal fun detailButtonItem( WeatherDetailsType.DEWPOINT, WeatherDetailsType.MOONRISE, WeatherDetailsType.MOONSET, - WeatherDetailsType.MOONPHASE, - WeatherDetailsType.TREEPOLLEN, - WeatherDetailsType.GRASSPOLLEN, - WeatherDetailsType.RAGWEEDPOLLEN -> { + WeatherDetailsType.MOONPHASE -> { } WeatherDetailsType.UV -> { @@ -351,11 +537,116 @@ internal fun detailButtonItem( ) } } + + WeatherDetailsType.TREEPOLLEN -> { + weather?.condition?.pollen?.let { + val pollenModel = PollenViewModel(it) + + addContent( + CircularProgressIndicator.Builder() + .setProgress( + pollenModel.treePollenProgress.div(pollenModel.progressMax.toFloat()) + .fastCoerceIn(0f, 1f) + ) + .setStrokeWidth(dp(2f)) + .setOuterMarginApplied(false) + .setCircularProgressIndicatorColors( + ProgressIndicatorColors( + pollenModel.treePollenProgressColor, + ColorUtils.blendARGB( + pollenModel.treePollenProgressColor, + 0xFF1B1B1B.toInt(), + 0.95f + ) + ) + ) + .build() + ) + } + } + + WeatherDetailsType.GRASSPOLLEN -> { + weather?.condition?.pollen?.let { + val pollenModel = PollenViewModel(it) + + addContent( + CircularProgressIndicator.Builder() + .setProgress( + pollenModel.grassPollenProgress.div(pollenModel.progressMax.toFloat()) + .fastCoerceIn(0f, 1f) + ) + .setStrokeWidth(dp(2f)) + .setOuterMarginApplied(false) + .setCircularProgressIndicatorColors( + ProgressIndicatorColors( + pollenModel.grassPollenProgressColor, + ColorUtils.blendARGB( + pollenModel.grassPollenProgressColor, + 0xFF1B1B1B.toInt(), + 0.95f + ) + ) + ) + .build() + ) + } + } + + WeatherDetailsType.RAGWEEDPOLLEN -> { + weather?.condition?.pollen?.let { + val pollenModel = PollenViewModel(it) + + addContent( + CircularProgressIndicator.Builder() + .setProgress( + pollenModel.ragweedPollenProgress.div(pollenModel.progressMax.toFloat()) + .fastCoerceIn(0f, 1f) + ) + .setStrokeWidth(dp(2f)) + .setOuterMarginApplied(false) + .setCircularProgressIndicatorColors( + ProgressIndicatorColors( + pollenModel.ragweedPollenProgressColor, + ColorUtils.blendARGB( + pollenModel.ragweedPollenProgressColor, + 0xFF1B1B1B.toInt(), + 0.95f + ) + ) + ) + .build() + ) + } + } } } .build() } +internal fun detailChipItem( + context: Context, + deviceParameters: DeviceParameters, + weather: Weather?, + detailsType: WeatherDetailsType, + model: DetailItemViewModel +): LayoutElement { + return Chip.Builder(context, Clickable.Builder().build(), deviceParameters) + .setWidth(expand()) + .setIconContent( + if (appLib.isInEditMode() || deviceParameters.supportsTransformation() || model.iconRotation == 0) { + "${ID_WEATHER_ICON_PREFIX}${model.icon}" + } else { + "${ID_ROTATION_PREFIX}${model.iconRotation}:${ID_WEATHER_ICON_PREFIX}${model.icon}" + } + ) + .setPrimaryLabelContent(model.label.toString()) + .setSecondaryLabelContent(model.value.toString()) + .setChipColors( + ChipColors.secondaryChipColors(androidx.wear.protolayout.material.Colors.DEFAULT) + ) + .build() +} + private fun getLaunchAction(context: Context): ActionBuilders.Action { return ActionBuilders.LaunchAction.Builder() .setAndroidActivity( @@ -370,11 +661,18 @@ private fun getLaunchAction(context: Context): ActionBuilders.Action { @WearPreviewDevices private fun detailsWeatherTilePreview(context: Context): TilePreviewData { context.initializeDependencies(isPhone = false) + val wim = sharedDeps.weatherIconsManager.iconProvider val weather = buildMockWeatherData() val viewModel = weather.toUiModel() val weatherDetails = viewModel.weatherDetailsMap + val sampleTileConfig = weatherDetails + .filterKeys { DetailsWeatherTileUtils.isTypeAllowed(it) } + .toList() + .shuffled() + .take(Random.nextInt(1, DetailsWeatherTileUtils.MAX_BUTTONS + 1 /* exclusive */)) + .shuffled() return TilePreviewData( onTileResourceRequest = { request -> @@ -408,8 +706,8 @@ private fun detailsWeatherTilePreview(context: Context): TilePreviewData { detailsWeatherTileLayout( context, request.deviceConfiguration, - weather = weather, - weatherDetails = weatherDetails + weather, + sampleTileConfig ) ).build() } @@ -524,12 +822,12 @@ private fun buildMockWeatherData(): Weather { lowC = 15f icon = WeatherIcons.DAY_SUNNY airQuality = AirQuality().apply { - index = 46 + index = Random.nextInt(0, 301) } - beaufort = Beaufort(Beaufort.BeaufortScale.B1) - uv = UV(3f) + beaufort = Beaufort(Beaufort.BeaufortScale.valueOf(Random.nextInt(0, 12))) + uv = UV(Random.nextInt(0, 11).toFloat()) pollen = Pollen().apply { - treePollenCount = Pollen.PollenCount.HIGH + treePollenCount = Pollen.PollenCount.VERY_HIGH grassPollenCount = Pollen.PollenCount.LOW ragweedPollenCount = Pollen.PollenCount.MODERATE } @@ -544,16 +842,19 @@ private fun buildMockWeatherData(): Weather { dewpointC = 10f } precipitation = Precipitation().apply { - pop = 15 - cloudiness = 25 + pop = 100 + cloudiness = 100 qpfRainIn = 0.05f qpfRainMm = 1.27f - qpfSnowIn = 0f - qpfSnowCm = 0f + qpfSnowIn = 10.1f + qpfSnowCm = 25.4f } astronomy = Astronomy().apply { - sunrise = LocalDateTime.of(LocalDate.now(), LocalTime.of(6, 0)) - sunset = LocalDateTime.of(LocalDate.now(), LocalTime.of(18, 0)) + sunrise = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0)) + sunset = LocalDateTime.of(LocalDate.now(), LocalTime.of(12, 0)) + moonrise = LocalDateTime.of(LocalDate.now(), LocalTime.of(12, 43)) + moonset = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 46)) + moonPhase = MoonPhase(MoonPhase.MoonPhaseType.entries[Random.nextInt(0, 7)]) } source = WeatherAPI.ANDROID query = "" diff --git a/weather-api/build.gradle b/weather-api/build.gradle index b12cdfd8d..d57b276c3 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.0.0' + fullgmsImplementation 'com.google.android.libraries.places:places:4.1.0' fullgmsImplementation 'com.android.volley:volley:1.2.1' // Required by Places API ^^^ fullgmsImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_version" } diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/HEREWeatherProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/HEREWeatherProvider.kt index 3f8b50fc0..c20b3f6cd 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/HEREWeatherProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/here/weather/HEREWeatherProvider.kt @@ -100,7 +100,7 @@ class HEREWeatherProvider : WeatherProviderImpl() { val authorization = hereOAuthService.getBearerToken(false) if (authorization.isNullOrBlank()) { - throw WeatherException(ErrorStatus.NETWORKERROR).apply { + throw WeatherException(ErrorStatus.INVALIDAPIKEY).apply { initCause(Exception("Invalid bearer token: $authorization")) } } diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationData.kt b/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationData.kt index d2796e9f4..172cffb30 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationData.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationData.kt @@ -2,16 +2,13 @@ package com.thewizrd.weather_api.locationiq import com.thewizrd.shared_resources.locationdata.LocationQuery import com.thewizrd.shared_resources.weatherdata.WeatherAPI -import java.util.* +import java.util.Locale /* LocationIQ AutoComplete */ fun createLocationModel(result: AutoCompleteQuery, weatherAPI: String): LocationQuery { return LocationQuery().apply { - val town: String - val region: String - // Try to get district name or fallback to city name - town = if (!result.address.neighbourhood.isNullOrBlank()) + val town = if (!result.address.neighbourhood.isNullOrBlank()) result.address.neighbourhood else if (!result.address.hamlet.isNullOrBlank()) result.address.hamlet @@ -27,7 +24,7 @@ fun createLocationModel(result: AutoCompleteQuery, weatherAPI: String): Location result.address.name // Try to get district name or fallback to city name - region = if (!result.address.region.isNullOrBlank()) + val region = if (!result.address.region.isNullOrBlank()) result.address.region else if (!result.address.county.isNullOrBlank()) result.address.county @@ -44,7 +41,7 @@ fun createLocationModel(result: AutoCompleteQuery, weatherAPI: String): Location String.format("%s, %s", town, region) locationCountry = if (!result.address.countryCode.isNullOrBlank()) - result.address.countryCode.toUpperCase(Locale.ROOT) + result.address.countryCode.uppercase(Locale.ROOT) else result.address.country @@ -62,44 +59,30 @@ fun createLocationModel(result: AutoCompleteQuery, weatherAPI: String): Location /* LocationIQ Geocoder */ fun createLocationModel(result: GeoLocation, weatherAPI: String): LocationQuery { return LocationQuery().apply { - val town: String - val region: String - // Try to get district name or fallback to city name - town = if (!result.address.neighbourhood.isNullOrBlank()) + val town = if (!result.address.neighbourhood.isNullOrBlank()) result.address.neighbourhood - else if (!result.address.hamlet.isNullOrBlank()) - result.address.hamlet else if (!result.address.suburb.isNullOrBlank()) result.address.suburb - else if (!result.address.village.isNullOrBlank()) - result.address.village - else if (!result.address.town.isNullOrBlank()) - result.address.town - else if (!result.address.city.isNullOrBlank()) - result.address.city + else if (!result.address.county.isNullOrBlank()) + result.address.county else - result.address.name + result.address.city // Try to get district name or fallback to city name - region = if (!result.address.region.isNullOrBlank()) - result.address.region - else if (!result.address.county.isNullOrBlank()) + val region = if (!result.address.county.isNullOrBlank() && town != result.address.city) result.address.county - else if (!result.address.stateDistrict.isNullOrBlank()) - result.address.stateDistrict + else if (!result.address.city.isNullOrBlank() && town != result.address.city) + result.address.city else if (!result.address.state.isNullOrBlank()) result.address.state else result.address.country - locationName = if (!result.address.name.isNullOrBlank() && result.address.name != town) - String.format("%s, %s, %s", result.address.name, town, region) - else - String.format("%s, %s", town, region) + locationName = String.format("%s, %s", town, region) locationCountry = if (!result.address.countryCode.isNullOrBlank()) - result.address.countryCode.toUpperCase(Locale.ROOT) + result.address.countryCode.uppercase(Locale.ROOT) else result.address.country diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationIQProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationIQProvider.kt index f4655a013..ac6d4ba7e 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationIQProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/locationiq/LocationIQProvider.kt @@ -35,7 +35,7 @@ class LocationIQProvider : WeatherLocationProviderImpl() { private const val AUTOCOMPLETE_QUERY_URL = "https://api.locationiq.com/v1/autocomplete.php?key=%s&q=%s&limit=10&normalizecity=1&addressdetails=1&accept-language=%s" private const val GEOLOCATION_QUERY_URL = - "https://api.locationiq.com/v1/reverse.php?key=%s&lat=%s&lon=%s&format=json&zoom=14&namedetails=0&addressdetails=1&accept-language=%s&normalizecity=1" + "https://api.locationiq.com/v1/reverse.php?key=%s&lat=%s&lon=%s&format=json&zoom=14&namedetails=0&addressdetails=1&accept-language=%s&normalizecity=1&normalizeaddress=1" private const val KEY_QUERY_URL = "https://us1.unwiredlabs.com/v2/timezone.php?token=%s" } diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/utils/APIRequestUtils.kt b/weather-api/src/main/java/com/thewizrd/weather_api/utils/APIRequestUtils.kt index f8e698def..adc08004b 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/utils/APIRequestUtils.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/utils/APIRequestUtils.kt @@ -97,7 +97,7 @@ object APIRequestUtils { setRetryCount(apiID, 1) setNextRetryTime(apiID, retryTimeInMs) } - throw WeatherException(ErrorStatus.NETWORKERROR) + throw WeatherException(ErrorStatus.RATELIMITED) .initCause(createThrowable(response)) } } @@ -138,7 +138,7 @@ object APIRequestUtils { val nextRetryTime = getNextRetryTime(apiID) if (currentTime < nextRetryTime) { - throw WeatherException(ErrorStatus.NETWORKERROR) + throw WeatherException(ErrorStatus.RATELIMITED) } } diff --git a/weather-api/src/main/java/com/thewizrd/weather_api/weatherkit/WeatherKitProvider.kt b/weather-api/src/main/java/com/thewizrd/weather_api/weatherkit/WeatherKitProvider.kt index c7b74d5b3..b5b3dd9ee 100644 --- a/weather-api/src/main/java/com/thewizrd/weather_api/weatherkit/WeatherKitProvider.kt +++ b/weather-api/src/main/java/com/thewizrd/weather_api/weatherkit/WeatherKitProvider.kt @@ -121,7 +121,7 @@ class WeatherKitProvider : WeatherProviderImpl() { val authorization = weatherKitJwtService.getBearerToken(false) if (authorization.isNullOrBlank()) { - throw WeatherException(ErrorStatus.NETWORKERROR).apply { + throw WeatherException(ErrorStatus.INVALIDAPIKEY).apply { initCause(Exception("Invalid bearer token: $authorization")) } }