├── MODULE_LICENSE_APACHE2 ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── themes.xml │ │ │ │ ├── app_icon_background.xml │ │ │ │ ├── attrs.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── arrays.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ │ ├── themes.xml │ │ │ │ └── colors.xml │ │ │ ├── .DS_Store │ │ │ ├── drawable-hdpi │ │ │ │ ├── ice.png │ │ │ │ ├── fire.png │ │ │ │ ├── flood.png │ │ │ │ ├── wave.png │ │ │ │ ├── windy.png │ │ │ │ ├── blizzard.png │ │ │ │ ├── nws_logo.png │ │ │ │ ├── tornado.png │ │ │ │ ├── winter.png │ │ │ │ ├── blue_button.9.png │ │ │ │ ├── grey_button.9.png │ │ │ │ ├── red_button.9.png │ │ │ │ ├── thunderstorm.png │ │ │ │ ├── black_button.9.png │ │ │ │ ├── orange_button.9.png │ │ │ │ └── yellow_button.9.png │ │ │ ├── mipmap-hdpi │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_round.png │ │ │ │ └── app_icon_foreground.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_round.png │ │ │ │ └── app_icon_foreground.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_round.png │ │ │ │ └── app_icon_foreground.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_round.png │ │ │ │ └── app_icon_foreground.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_round.png │ │ │ │ └── app_icon_foreground.png │ │ │ ├── xml │ │ │ │ ├── backup_descriptor.xml │ │ │ │ ├── network_security_config.xml │ │ │ │ ├── data_extraction_rules.xml │ │ │ │ └── alerts_widget_info.xml │ │ │ ├── drawable │ │ │ │ ├── semitransparent_background.xml │ │ │ │ └── widget_frame.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── app_icon.xml │ │ │ │ └── app_icon_round.xml │ │ │ ├── menu │ │ │ │ ├── alerts_display_options.xml │ │ │ │ └── detail.xml │ │ │ ├── layout │ │ │ │ ├── privacy_policy_fragment.xml │ │ │ │ ├── alerts_widget_loading.xml │ │ │ │ ├── spinner_layout.xml │ │ │ │ ├── fragment_changelog.xml │ │ │ │ ├── alerts_widget_list_item.xml │ │ │ │ ├── main_activity.xml │ │ │ │ ├── alert_types_fragment.xml │ │ │ │ ├── alerts_display_fragment.xml │ │ │ │ ├── about.xml │ │ │ │ ├── alerts_widget_configure.xml │ │ │ │ ├── alerts_widget.xml │ │ │ │ ├── fragment_instructions.xml │ │ │ │ ├── activity_alertdetail.xml │ │ │ │ └── debug_fragment.xml │ │ │ ├── layout-land │ │ │ │ ├── alert_types_fragment.xml │ │ │ │ ├── activity_alertdetail.xml │ │ │ │ └── debug_fragment.xml │ │ │ ├── raw │ │ │ │ └── isrg_root_x1.crt │ │ │ └── navigation │ │ │ │ └── main_navigation.xml │ │ ├── app_icon-playstore.png │ │ ├── java │ │ │ └── net │ │ │ │ └── justdave │ │ │ │ └── nwsweatheralertswidget │ │ │ │ ├── objects │ │ │ │ ├── NWSZone.kt │ │ │ │ ├── NWSArea.kt │ │ │ │ └── NWSAlert.kt │ │ │ │ ├── JsonConfiguration.kt │ │ │ │ ├── widget │ │ │ │ ├── NWSWidgetService.kt │ │ │ │ ├── DebugPrefs.kt │ │ │ │ ├── NWSWidgetFactory.kt │ │ │ │ ├── NWSWidgetPrefs.kt │ │ │ │ └── NWSWidgetConfigureActivity.kt │ │ │ │ ├── AlertsDisplayViewModel.kt │ │ │ │ ├── PrivacyPolicyFragment.kt │ │ │ │ ├── ChangelogFragment.kt │ │ │ │ ├── DebugViewModel.kt │ │ │ │ ├── AlertTypesFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── Dialogs.kt │ │ │ │ ├── BroadcastReceiver.kt │ │ │ │ ├── AlertTypesAdapter.kt │ │ │ │ ├── InstructionsFragment.kt │ │ │ │ ├── DebugAlertsAdapter.kt │ │ │ │ ├── AlertsDisplayFragment.kt │ │ │ │ ├── AlertDetailActivity.kt │ │ │ │ └── AlertsUpdateService.kt │ │ ├── assets │ │ │ ├── ABOUT.md │ │ │ ├── PRIVACY.md │ │ │ └── CHANGES.md │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── net │ │ │ └── justdave │ │ │ └── nwsweatheralertswidget │ │ │ └── NWSAlertTest.kt │ └── androidTest │ │ └── java │ │ └── net │ │ └── justdave │ │ └── nwsweatheralertswidget │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── src-images ├── fire.xcf ├── wave.xcf ├── app_icon.xcf ├── blizzard.xcf ├── buttons.xcf ├── blowingsnow.png ├── app_icon_transparent.png ├── PlayStoreFeatureGraphic.xcf ├── SOURCES ├── zeimusu_Fire_Icon.svg ├── thundercloud.svg ├── icicles.svg └── water1.svg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── setup_hooks.sh ├── README.md ├── gradle.properties ├── .github └── workflows │ ├── checkin.yml │ └── version_bump.yml ├── PRIVACY.md ├── gradlew.bat ├── gradlew └── CHANGES.md /MODULE_LICENSE_APACHE2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "NWSWeatherAlertsWidget" -------------------------------------------------------------------------------- /src-images/fire.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/fire.xcf -------------------------------------------------------------------------------- /src-images/wave.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/wave.xcf -------------------------------------------------------------------------------- /src-images/app_icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/app_icon.xcf -------------------------------------------------------------------------------- /src-images/blizzard.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/blizzard.xcf -------------------------------------------------------------------------------- /src-images/buttons.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/buttons.xcf -------------------------------------------------------------------------------- /app/src/main/res/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/.DS_Store -------------------------------------------------------------------------------- /src-images/blowingsnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/blowingsnow.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/app_icon-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/app_icon-playstore.png -------------------------------------------------------------------------------- /src-images/app_icon_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/app_icon_transparent.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/ice.png -------------------------------------------------------------------------------- /src-images/PlayStoreFeatureGraphic.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/src-images/PlayStoreFeatureGraphic.xcf -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/fire.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/flood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/flood.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/wave.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/windy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/windy.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/blizzard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/blizzard.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/nws_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/nws_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/tornado.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/winter.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-hdpi/app_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-mdpi/app_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xhdpi/app_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxhdpi/app_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxxhdpi/app_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/blue_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/blue_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/grey_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/grey_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/red_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/red_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/thunderstorm.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-hdpi/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-mdpi/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xhdpi/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/black_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/black_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/orange_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/orange_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/yellow_button.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/drawable-hdpi/yellow_button.9.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxhdpi/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxxhdpi/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-hdpi/app_icon_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-mdpi/app_icon_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xhdpi/app_icon_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxhdpi/app_icon_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/HEAD/app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/values/app_icon_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #A1B8FE 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /gen 3 | /.idea 4 | /app/app.iml 5 | /app/build 6 | /app/release 7 | /build 8 | /.gradle 9 | /README.md~ 10 | /app/*.apk 11 | /local.properties 12 | /*.iml 13 | /.DS_Store -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @color/white 4 | @color/black 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/semitransparent_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 02 21:05:20 EST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/app_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/objects/NWSZone.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget.objects 2 | 3 | data class NWSZone(val id: String, val name: String) { 4 | 5 | //to display object as a string in spinner 6 | override fun toString(): String { 7 | return name 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/objects/NWSArea.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget.objects 2 | 3 | data class NWSArea(val id: String, val name: String) { 4 | 5 | //to display object as a string in spinner 6 | override fun toString(): String { 7 | return name 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/theme_semitransparent 5 | @string/theme_light 6 | @string/theme_dark 7 | @string/theme_system 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/JsonConfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | /** 6 | * A shared, lenient JSON configuration for the entire app. 7 | * `ignoreUnknownKeys = true` makes the app resilient to new fields being added to the NWS API. 8 | */ 9 | val lenientJson = Json { ignoreUnknownKeys = true } 10 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/widget/NWSWidgetService.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget.widget 2 | 3 | import android.content.Intent 4 | import android.widget.RemoteViewsService 5 | 6 | class NWSWidgetService : RemoteViewsService() { 7 | override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { 8 | return NWSWidgetFactory(applicationContext, intent) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/alerts_display_options.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/privacy_policy_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/alerts_widget_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/detail.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/alerts_widget_info.xml: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/AlertsDisplayViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.lifecycle.ViewModel 6 | 7 | class AlertsDisplayViewModel : ViewModel() { 8 | 9 | private lateinit var nwsapi: NWSAPI 10 | 11 | init { 12 | Log.i("AlertsDisplayViewModel","Created!") 13 | } 14 | 15 | fun initializeContext(context: Context) { 16 | nwsapi = NWSAPI.getInstance(context) 17 | } 18 | 19 | override fun onCleared() { 20 | super.onCleared() 21 | Log.i("AlertsDisplayViewModel", "destroyed!") 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3dp 5 | 3dp 6 | 4dp 7 | 4dp 8 | 4dp 9 | 4dp 10 | 11 | 15 | 0dp 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | #000000 5 | #000000 6 | #FFFFFF 7 | #FFFFFF 8 | 9 | 10 | #FFFFFF 11 | #000000 12 | #55777777 13 | 14 | 15 | 16 | 17 | @color/black 18 | @color/white 19 | 20 | -------------------------------------------------------------------------------- /app/src/test/java/net/justdave/nwsweatheralertswidget/NWSAlertTest.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import net.justdave.nwsweatheralertswidget.objects.NWSAlert 4 | import org.json.JSONObject 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import org.robolectric.RobolectricTestRunner 9 | 10 | @RunWith(RobolectricTestRunner::class) 11 | class NWSAlertTest { 12 | @Test 13 | fun parsing_isCorrect() { 14 | val fakeAlert = 15 | """{ "properties": { 16 | "headline": "Fake Headline", 17 | "description": "Fake Description" 18 | }}""" 19 | val alert = NWSAlert(JSONObject(fakeAlert)) 20 | assertEquals("Fake Headline", alert.toString()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/net/justdave/nwsweatheralertswidget/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("net.justdave.nwsweatheralertswidget", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /src-images/SOURCES: -------------------------------------------------------------------------------- 1 | Image sources: 2 | 3 | Ice: https://openclipart.org/detail/26214/icicles-by-anonymous 4 | Thunderstorm: https://openclipart.org/detail/167269/thundercloud-by-halattas 5 | Tornado: https://openclipart.org/detail/104887/tornado-by-laabadon 6 | Snow: https://openclipart.org/detail/65497/winter-by-laobc (trimmed by justdave) 7 | Flood: https://openclipart.org/detail/93091/water-by-photothailand 8 | Wind: https://openclipart.org/detail/170683/weather-icon---windy-by-gnokii-170683 9 | Blizzard: https://www.nws.noaa.gov/weather/images/fcicons/blowingsnow.png (upscaled and traced by justdave and combined with "Snow") - URL no longer works 10 | Fire: https://openclipart.org/detail/2242/fire-icon-by-zeimusu 11 | Wave: https://openclipart.org/detail/3166/crashing-wave-by-johnny_automatic 12 | 13 | Background buttons: https://openclipart.org/detail/75115/glossy-pill-buttons-by-inky2010 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/spinner_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /setup_hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | HOOK_DIR=".git/hooks" 4 | PRE_COMMIT_HOOK="$HOOK_DIR/pre-commit" 5 | 6 | # Ensure the hooks directory exists 7 | mkdir -p "$HOOK_DIR" 8 | 9 | # Create the pre-commit hook script 10 | cat > "$PRE_COMMIT_HOOK" < 2 | 7 | 8 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/widget/DebugPrefs.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget.widget 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.edit 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | import androidx.datastore.preferences.preferencesDataStore 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.coroutines.flow.map 11 | 12 | private val Context.debugDataStore: DataStore by preferencesDataStore(name = "debug_settings") 13 | 14 | private val KEY_AREA = stringPreferencesKey("debug_area") 15 | private val KEY_ZONE = stringPreferencesKey("debug_zone") 16 | 17 | suspend fun saveDebugPrefs(context: Context, areaId: String, zoneId: String) { 18 | context.debugDataStore.edit { 19 | it[KEY_AREA] = areaId 20 | it[KEY_ZONE] = zoneId 21 | } 22 | } 23 | 24 | suspend fun loadDebugPrefs(context: Context): Pair { 25 | return context.debugDataStore.data.map { 26 | val area = it[KEY_AREA] ?: "us-all" 27 | val zone = it[KEY_ZONE] ?: "all" 28 | Pair(area, zone) 29 | }.first() 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/PrivacyPolicyFragment.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.TextView 8 | import androidx.fragment.app.Fragment 9 | import io.noties.markwon.Markwon 10 | 11 | class PrivacyPolicyFragment : Fragment() { 12 | 13 | override fun onCreateView( 14 | inflater: LayoutInflater, 15 | container: ViewGroup?, 16 | savedInstanceState: Bundle? 17 | ): View? { 18 | // Inflate the layout for this fragment 19 | return inflater.inflate(R.layout.privacy_policy_fragment, container, false) 20 | } 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | 25 | val textView = view.findViewById(R.id.privacy_policy_text) 26 | val markdown = requireContext().assets.open("PRIVACY.md").bufferedReader().use { it.readText() } 27 | 28 | // get an instance of Markwon 29 | val markwon = Markwon.create(requireContext()) 30 | 31 | // set markdown 32 | markwon.setMarkdown(textView, markdown) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NWS Weather Alerts Widget 2 | ========================= 3 | 4 | Android home screen widget to display current weather alerts from the US National Weather Service 5 | 6 | I created this because I wanted a tablet on the wall in my kitchen to display weather alerts on the screen, and for all the plethora of weather apps out there, I couldn't find one that showed anything more than a (!) icon on their widgets for alerts, and you had to click through to find out what they were. Some of them would put the alerts into the notification bar, but that wasn't much better. So this one displays a list of the current alerts right on the widget, and that is the only purpose of the widget. If there's more than fits, the list scrolls, and you can tap on an alert to open the full text of the alert in your web browser. 7 | 8 | There's also an app with a user interface to go with it, mostly for debugging, but also gets you instructions how to use it, and the About box and so forth. The app lets you look at the raw XML feed from the NWS, for example. That might eventually go away now that it's mostly working. 9 | 10 | If you feel like helping out or adding cool new features, I welcome pull requests. Feel free to file issues, too (and check the issue queue for bug reports and feature requests if you're looking for something to do). 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout-land/alert_types_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/alerts_widget_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 19 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/assets/ABOUT.md: -------------------------------------------------------------------------------- 1 | Copyright 2014-2025 David D. Miller and [additional contributors](https://github.com/justdave/nwsweatheralertswidget/graphs/contributors). 2 | 3 | This app and its source code are licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 4 | 5 | This app is not affiliated with or endorsed by the National Weather Service. The weather data is provided by the [National Weather Service](https://www.weather.gov/), which is a public-domain source. The NWS logo is a trademark of the National Weather Service and its use does not imply endorsement. 6 | 7 | Special thanks to the developers of the following open-source libraries used in this app: 8 | 9 | * [Kotlin](https://kotlinlang.org/) 10 | * [AndroidX](https://developer.android.com/jetpack/androidx) 11 | * [Material Components for Android](https://github.com/material-components/material-components-android) 12 | * [Markwon](https://github.com/noties/Markwon) 13 | 14 | Useful links: 15 | * [Feature requests and bug reports](https://github.com/justdave/nwsweatheralertswidget/issues) 16 | * [Change log](https://github.com/justdave/nwsweatheralertswidget/releases) 17 | * [Image/icon credits](https://raw.githubusercontent.com/justdave/nwsweatheralertswidget/refs/heads/master/src-images/SOURCES) 18 | * ["NWS" name and logo usage guidelines](https://www.weather.gov/disclaimer) 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.nonTransitiveRClass=true 23 | android.nonFinalResIds=true 24 | org.gradle.warning.mode=all 25 | MAVEN_REPOSITORY=https://repo.maven.apache.org/maven2/ -------------------------------------------------------------------------------- /app/src/main/res/layout/alert_types_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/ChangelogFragment.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.os.Bundle 4 | import android.text.method.LinkMovementMethod 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.fragment.app.Fragment 10 | import io.noties.markwon.Markwon 11 | 12 | class ChangelogFragment : Fragment() { 13 | 14 | override fun onCreateView( 15 | inflater: LayoutInflater, 16 | container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View? { 19 | return inflater.inflate(R.layout.fragment_changelog, container, false) 20 | } 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | 25 | val textView = view.findViewById(R.id.changelog_text) 26 | val markwon = Markwon.create(requireContext()) 27 | 28 | try { 29 | val markdown = requireContext().assets.open("CHANGES.md").bufferedReader().use { it.readText() } 30 | markwon.setMarkdown(textView, markdown) 31 | textView.movementMethod = LinkMovementMethod.getInstance() 32 | } catch (e: Exception) { 33 | e.printStackTrace() 34 | textView.setText(R.string.changelog_error) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/checkin.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Compare PRIVACY.md files 30 | run: diff PRIVACY.md app/src/main/assets/PRIVACY.md 31 | 32 | - name: set up JDK 17 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: 17 36 | distribution: 'temurin' 37 | 38 | - name: Build with Gradle 39 | id: build 40 | run: ./gradlew build 41 | 42 | - name: Run Unit Tests 43 | id: tests 44 | run: ./gradlew test 45 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/DebugViewModel.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.lifecycle.ViewModel 6 | import com.android.volley.Response 7 | import net.justdave.nwsweatheralertswidget.objects.NWSAlert 8 | import net.justdave.nwsweatheralertswidget.objects.NWSArea 9 | import net.justdave.nwsweatheralertswidget.objects.NWSZone 10 | 11 | class DebugViewModel : ViewModel() { 12 | private lateinit var nwsapi: NWSAPI 13 | 14 | init { 15 | Log.i("DebugViewModel","Created!") 16 | } 17 | 18 | fun initializeContext(context: Context) { 19 | nwsapi = NWSAPI.getInstance(context) 20 | } 21 | 22 | override fun onCleared() { 23 | super.onCleared() 24 | Log.i("DebugViewModel", "destroyed!") 25 | } 26 | 27 | fun getAreaPopupContent(listener: Response.Listener>) { 28 | return nwsapi.getAreas(listener) 29 | } 30 | 31 | fun getZonePopupContent(area: NWSArea, listener: Response.Listener>) { 32 | return nwsapi.getZones(area, listener) 33 | } 34 | 35 | fun getDebugContent( 36 | area: NWSArea, 37 | zone: NWSZone, 38 | listener: Response.Listener>, 39 | errorListener: Response.ErrorListener 40 | ) { 41 | return nwsapi.getActiveAlerts(area, zone, listener, errorListener) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for NWS Weather Alerts Widget 2 | 3 | **Last Updated: 2025-12-13** 4 | 5 | This Privacy Policy describes how your personal information is handled in the NWS Weather Alerts Widget for Android. 6 | 7 | **Information We Collect and Use** 8 | 9 | To provide weather alerts for your chosen location, the app sends your selected county or zone ID directly to the US National Weather Service (NWS) API. This is required to retrieve relevant alert information. Your device's IP address may also be visible to the NWS as part of this standard internet communication. 10 | 11 | We do **not** collect, log, store, or transmit any other personally identifiable information about you. All other data, including your widget configurations, is stored locally on your device and is never sent to our servers. 12 | 13 | **Permissions** 14 | 15 | The app requires the following permissions to function: 16 | 17 | * **Internet**: To download weather alert data from the NWS. 18 | * **Foreground Service**: To keep the alert checking service running in the background so your widgets stay up to date. 19 | * **Post Notifications**: To display the persistent notification required for the background service to run. 20 | 21 | **Changes to This Privacy Policy** 22 | 23 | We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 24 | 25 | **Contact Us** 26 | 27 | If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us at playstoresupport@justdave.net. 28 | -------------------------------------------------------------------------------- /app/src/main/assets/PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for NWS Weather Alerts Widget 2 | 3 | **Last Updated: 2025-12-13** 4 | 5 | This Privacy Policy describes how your personal information is handled in the NWS Weather Alerts Widget for Android. 6 | 7 | **Information We Collect and Use** 8 | 9 | To provide weather alerts for your chosen location, the app sends your selected county or zone ID directly to the US National Weather Service (NWS) API. This is required to retrieve relevant alert information. Your device's IP address may also be visible to the NWS as part of this standard internet communication. 10 | 11 | We do **not** collect, log, store, or transmit any other personally identifiable information about you. All other data, including your widget configurations, is stored locally on your device and is never sent to our servers. 12 | 13 | **Permissions** 14 | 15 | The app requires the following permissions to function: 16 | 17 | * **Internet**: To download weather alert data from the NWS. 18 | * **Foreground Service**: To keep the alert checking service running in the background so your widgets stay up to date. 19 | * **Post Notifications**: To display the persistent notification required for the background service to run. 20 | 21 | **Changes to This Privacy Policy** 22 | 23 | We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 24 | 25 | **Contact Us** 26 | 27 | If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us at playstoresupport@justdave.net. 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/alerts_display_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 22 | 23 | 24 | 25 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/AlertTypesFragment.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | 14 | class AlertTypesFragment : Fragment() { 15 | 16 | private lateinit var nwsapi: NWSAPI 17 | private lateinit var recyclerView: RecyclerView 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View? { 24 | // Inflate the layout for this fragment 25 | val view = inflater.inflate(R.layout.alert_types_fragment, container, false) 26 | recyclerView = view.findViewById(R.id.alert_types_recycler_view) 27 | recyclerView.layoutManager = LinearLayoutManager(context) 28 | return view 29 | } 30 | 31 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 32 | super.onViewCreated(view, savedInstanceState) 33 | 34 | nwsapi = NWSAPI.getInstance(requireContext()) 35 | 36 | CoroutineScope(Dispatchers.Main).launch { 37 | val alertTypes = nwsapi.getAlertTypes() 38 | recyclerView.adapter = AlertTypesAdapter(alertTypes) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.content.Intent 4 | import android.os.Build 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.navigation.NavController 8 | import androidx.navigation.findNavController 9 | import androidx.navigation.ui.NavigationUI 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | private lateinit var navController : NavController 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(R.layout.main_activity) 18 | setSupportActionBar(findViewById(R.id.my_toolbar)) 19 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 20 | //supportActionBar?.setLogo(R.mipmap.app_icon) 21 | 22 | // As a developer convenience, start the service if it isn't already running. 23 | if (!AlertsUpdateService.isRunning) { 24 | val serviceIntent = Intent(this, AlertsUpdateService::class.java).apply { 25 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 26 | } 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 28 | startForegroundService(serviceIntent) 29 | } else { 30 | startService(serviceIntent) 31 | } 32 | } 33 | } 34 | 35 | override fun onSupportNavigateUp(): Boolean { 36 | return navController.navigateUp() 37 | } 38 | 39 | override fun onPostCreate( 40 | savedInstanceState: Bundle? 41 | ) { 42 | super.onPostCreate(savedInstanceState) 43 | navController = this.findNavController(R.id.my_nav_host_fragment) 44 | NavigationUI.setupActionBarWithNavController(this, navController) 45 | //NavigationUI.setupWithNavController(navigationView, navController) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/Dialogs.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.app.AlertDialog 4 | import android.content.Context 5 | import android.text.method.LinkMovementMethod 6 | import android.view.LayoutInflater 7 | import android.widget.TextView 8 | import androidx.core.content.pm.PackageInfoCompat 9 | import androidx.navigation.NavController 10 | import io.noties.markwon.Markwon 11 | 12 | fun showAboutDialog(context: Context, navController: NavController) { 13 | val builder = AlertDialog.Builder(context) 14 | val inflater = LayoutInflater.from(context) 15 | val view = inflater.inflate(R.layout.about, null) 16 | 17 | val versionView = view.findViewById(R.id.version_string) 18 | val aboutView = view.findViewById(R.id.info_text) 19 | 20 | // Set version string 21 | val info = context.packageManager.getPackageInfo(context.packageName, 0) 22 | versionView.text = context.getString(R.string.about_version, info.versionName, PackageInfoCompat.getLongVersionCode(info)) 23 | 24 | // Set about text and make links clickable 25 | val markdown = context.assets.open("ABOUT.md").bufferedReader().use { it.readText() } 26 | val markwon = Markwon.create(context) 27 | markwon.setMarkdown(aboutView, markdown) 28 | aboutView.movementMethod = LinkMovementMethod.getInstance() 29 | 30 | builder.setTitle(R.string.action_about) 31 | builder.setView(view) 32 | 33 | builder.setPositiveButton(R.string.ok) { dialog, _ -> 34 | dialog.dismiss() 35 | } 36 | 37 | builder.setNeutralButton("Privacy Policy") { dialog, _ -> 38 | navController.navigate(R.id.privacyPolicyFragment) 39 | dialog.dismiss() 40 | } 41 | 42 | builder.setNegativeButton("Changelog") { dialog, _ -> 43 | navController.navigate(R.id.changelogFragment) 44 | dialog.dismiss() 45 | } 46 | 47 | builder.create().show() 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/version_bump.yml: -------------------------------------------------------------------------------- 1 | name: Bump Version Suffix 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | bump-version: 9 | # Only run on tags that look like a version number 10 | if: startsWith(github.ref, 'refs/tags/v') 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Get version from tag 17 | id: get_version 18 | run: echo "VERSION=${{ github.event.release.tag_name }}" | sed 's/^v//' >> $GITHUB_ENV 19 | 20 | - name: Update CHANGES.md 21 | run: | 22 | # Insert a new "Unreleased Changes" section after the main title 23 | sed -i'.bak' '/^# NWS Weather Alerts Widget Change History/a\ 24 | \ 25 | ## Unreleased Changes\ 26 | \ 27 | * ' CHANGES.md 28 | 29 | - name: Update versionName in build.gradle 30 | # This script appends a '+' to the end of the versionName string 31 | run: | 32 | awk '/versionName/ {sub(/\x27$/, "+\x27")} 1' app/build.gradle > app/build.gradle.tmp && mv app/build.gradle.tmp app/build.gradle 33 | 34 | - name: Increment versionCode 35 | run: | 36 | awk '/versionCode/ {match($0, /[0-9]+/); num=substr($0, RSTART, RLENGTH); sub(num, num+1)} 1' app/build.gradle > app/build.gradle.tmp && mv app/build.gradle.tmp app/build.gradle 37 | 38 | - name: Configure Git 39 | run: | 40 | git config user.name "github-actions[bot]" 41 | git config user.email "github-actions[bot]@users.noreply.github.com" 42 | 43 | - name: Commit and push changes 44 | env: 45 | NEW_VERSION: ${{ env.VERSION }}+ 46 | run: | 47 | git add app/build.gradle CHANGES.md 48 | git commit -m "updating version to $NEW_VERSION for development" 49 | # Pushing to the 'master' branch as used in other workflows 50 | git push origin HEAD:master 51 | -------------------------------------------------------------------------------- /app/src/main/res/raw/isrg_root_x1.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw 3 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 4 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 5 | WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu 6 | ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY 7 | MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc 8 | h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 9 | 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U 10 | A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW 11 | T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH 12 | B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC 13 | B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv 14 | KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn 15 | OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn 16 | jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw 17 | qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI 18 | rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV 19 | HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq 20 | hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL 21 | ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 22 | 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK 23 | NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 24 | ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur 25 | TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC 26 | jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc 27 | oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 28 | 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA 29 | mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d 30 | emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/BroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Build 9 | import android.os.SystemClock 10 | import android.util.Log 11 | 12 | class BroadcastReceiver : BroadcastReceiver() { 13 | override fun onReceive(context: Context, intent: Intent) { 14 | Log.i("BroadcastReceiver", "Intent received: ".plus(intent.action)) 15 | 16 | // Schedule an immediate alarm to kick off the update chain. 17 | val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager 18 | val serviceIntent = Intent(context, AlertsUpdateService::class.java).apply { 19 | addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES) 20 | } 21 | 22 | // Use getService instead of getForegroundService. This allows the service to start 23 | // as a background service (whitelisted by the exact alarm) without creating a 24 | // strict contract to become a foreground service immediately, avoiding crashes 25 | // if startForeground() is restricted (e.g. dataSync type at boot on Android 14+). 26 | val pendingIntent = PendingIntent.getService(context, 0, serviceIntent, PendingIntent.FLAG_IMMUTABLE) 27 | 28 | // How long to wait before triggering the initial update task 29 | val triggerTime = SystemClock.elapsedRealtime() 30 | 31 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || alarmManager.canScheduleExactAlarms()) { 32 | Log.i("BroadcastReceiver","Exactly scheduling initial update task in 0 seconds.") 33 | alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, pendingIntent) 34 | } else { 35 | Log.i("BroadcastReceiver","Inexactly setting initial update task in 0 seconds.") 36 | alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerTime, pendingIntent) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/AlertTypesAdapter.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.recyclerview.widget.RecyclerView 9 | import net.justdave.nwsweatheralertswidget.objects.NWSAlert 10 | 11 | class AlertTypesAdapter(private val alertTypes: List) : 12 | RecyclerView.Adapter() { 13 | 14 | /** 15 | * Provide a reference to the type of views that you are using 16 | * (custom ViewHolder). 17 | */ 18 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 19 | val textView: TextView 20 | val imageView: ImageView 21 | 22 | init { 23 | // Define click listener for the ViewHolder's View. 24 | textView = view.findViewById(R.id.alert_item_text) 25 | imageView = view.findViewById(R.id.alert_item_icon) 26 | } 27 | } 28 | 29 | // Create new views (invoked by the layout manager) 30 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 31 | // Create a new view, which defines the UI of the list item 32 | val view = LayoutInflater.from(viewGroup.context) 33 | .inflate(R.layout.alerts_widget_list_item, viewGroup, false) 34 | 35 | return ViewHolder(view) 36 | } 37 | 38 | // Replace the contents of a view (invoked by the layout manager) 39 | override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { 40 | 41 | // Get element from your dataset at this position and replace the 42 | // contents of the view with that element 43 | val alert = NWSAlert().copy(event = alertTypes[position]) 44 | viewHolder.textView.text = alert.event 45 | viewHolder.imageView.setImageResource(alert.getIcon()) 46 | viewHolder.itemView.setBackgroundResource(alert.getBackground()) 47 | } 48 | 49 | // Return the size of your dataset (invoked by the layout manager) 50 | override fun getItemCount() = alertTypes.size 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/net/justdave/nwsweatheralertswidget/InstructionsFragment.kt: -------------------------------------------------------------------------------- 1 | package net.justdave.nwsweatheralertswidget 2 | 3 | import android.app.AlarmManager 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.provider.Settings 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.Button 13 | import androidx.cardview.widget.CardView 14 | import androidx.fragment.app.Fragment 15 | import androidx.navigation.fragment.findNavController 16 | 17 | class InstructionsFragment : Fragment() { 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? { 23 | // Inflate the layout for this fragment 24 | return inflater.inflate(R.layout.fragment_instructions, container, false) 25 | } 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | 30 | view.findViewById