├── domain ├── .gitignore ├── src │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── antyzero │ │ │ └── smoksmog │ │ │ ├── storage │ │ │ ├── PersistentStorage.kt │ │ │ ├── Storage.kt │ │ │ └── model │ │ │ │ ├── Module.kt │ │ │ │ └── Item.kt │ │ │ ├── model │ │ │ └── Page.kt │ │ │ ├── location │ │ │ ├── LocationProvider.kt │ │ │ └── SimpleLocationProvider.kt │ │ │ └── SmokSmog.kt │ ├── integrationTest │ │ └── kotlin │ │ │ └── com │ │ │ └── antyzero │ │ │ └── smoksmog │ │ │ ├── StringRandom.kt │ │ │ ├── api │ │ │ └── ApiIntegrationTest.kt │ │ │ └── DataCollectionTest.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── antyzero │ │ └── smoksmog │ │ ├── SmokSmogTest.kt │ │ └── storage │ │ └── PersistentStorageTest.kt └── build.gradle ├── android ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable │ │ │ │ └── gradient_pollution.xml │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── colors.xml │ │ │ └── values-pl │ │ │ │ └── strings.xml │ │ └── kotlin │ │ │ └── smoksmog │ │ │ ├── air │ │ │ ├── ValueCheck.kt │ │ │ ├── AirQualityIndex.kt │ │ │ └── AirQuality.kt │ │ │ └── logger │ │ │ ├── Logger.kt │ │ │ ├── SilentLogger.kt │ │ │ ├── AndroidLogger.kt │ │ │ ├── LevelBlockingLogger.kt │ │ │ └── AggregatingLogger.kt │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── antyzero │ │ │ └── smoksmog │ │ │ └── android │ │ │ └── ApplicationTest.java │ └── test │ │ └── java │ │ └── smoksmog │ │ └── air │ │ └── AirQualityIndexTest.kt ├── proguard-rules.pro └── build.gradle ├── app ├── .gitignore ├── src │ ├── main │ │ ├── kotlin │ │ │ ├── error │ │ │ │ └── NonExistentClass.kt │ │ │ └── com │ │ │ │ ├── antyzero │ │ │ │ └── smoksmog │ │ │ │ │ ├── dsl │ │ │ │ │ ├── AnyExtension.kt │ │ │ │ │ ├── RxExtension.kt │ │ │ │ │ ├── ViewExtension.kt │ │ │ │ │ ├── RemoteViewsExtension.kt │ │ │ │ │ ├── PreferenceExtension.kt │ │ │ │ │ ├── ViewHolderExtension.kt │ │ │ │ │ ├── DependencyInjectionExtension.kt │ │ │ │ │ ├── ContextExtension.kt │ │ │ │ │ ├── ActivityExtension.kt │ │ │ │ │ ├── CompatExtension.kt │ │ │ │ │ ├── DimenUtils.kt │ │ │ │ │ └── DialogFramgmentExtension.kt │ │ │ │ │ ├── changelog │ │ │ │ │ ├── model │ │ │ │ │ │ ├── ChangeType.kt │ │ │ │ │ │ ├── Changelog.kt │ │ │ │ │ │ ├── Change.kt │ │ │ │ │ │ └── Version.kt │ │ │ │ │ └── ChangelogReader.kt │ │ │ │ │ ├── i18n │ │ │ │ │ ├── time │ │ │ │ │ │ ├── Countdown.kt │ │ │ │ │ │ ├── CountdownProvider.kt │ │ │ │ │ │ ├── EnglishCountdown.kt │ │ │ │ │ │ └── PolishCountdown.kt │ │ │ │ │ ├── LocaleProvider.kt │ │ │ │ │ ├── ContextLocaleProvider.kt │ │ │ │ │ └── I18nModule.kt │ │ │ │ │ ├── ui │ │ │ │ │ ├── screen │ │ │ │ │ │ ├── start │ │ │ │ │ │ │ ├── TitleProvider.kt │ │ │ │ │ │ │ ├── item │ │ │ │ │ │ │ │ ├── ViewDelegate.kt │ │ │ │ │ │ │ │ ├── ListViewHolder.kt │ │ │ │ │ │ │ │ ├── AirQualityViewDelegate.kt │ │ │ │ │ │ │ │ └── ParticulateViewDelegate.kt │ │ │ │ │ │ │ ├── fragment │ │ │ │ │ │ │ │ ├── LocationStationFragmentComponent.kt │ │ │ │ │ │ │ │ └── NetworkStationFragment.kt │ │ │ │ │ │ │ ├── PageSave.kt │ │ │ │ │ │ │ └── StationSlideAdapter.kt │ │ │ │ │ │ ├── FragmentModule.kt │ │ │ │ │ │ ├── SupportFragmentModule.kt │ │ │ │ │ │ ├── order │ │ │ │ │ │ │ ├── ItemTouchHelperAdapter.kt │ │ │ │ │ │ │ ├── OnStartDragListener.kt │ │ │ │ │ │ │ ├── OrderItemViewHolder.kt │ │ │ │ │ │ │ └── SimpleItemTouchHelperCallback.kt │ │ │ │ │ │ ├── about │ │ │ │ │ │ │ ├── AboutActivityComponent.kt │ │ │ │ │ │ │ └── AboutActivity.kt │ │ │ │ │ │ ├── FragmentComponent.kt │ │ │ │ │ │ ├── history │ │ │ │ │ │ │ └── HistoryAdapter.kt │ │ │ │ │ │ ├── ActivityComponent.kt │ │ │ │ │ │ ├── ActivityModule.kt │ │ │ │ │ │ ├── settings │ │ │ │ │ │ │ └── SettingsActivity.kt │ │ │ │ │ │ └── SimpleStationAdapter.kt │ │ │ │ │ ├── BasePreferenceFragment.kt │ │ │ │ │ ├── typeface │ │ │ │ │ │ └── TypefaceProvider.kt │ │ │ │ │ ├── widget │ │ │ │ │ │ ├── WidgetModule.kt │ │ │ │ │ │ ├── StationWidgetData.kt │ │ │ │ │ │ ├── StationWidget.kt │ │ │ │ │ │ └── StationWidgetService.kt │ │ │ │ │ ├── BaseFragment.kt │ │ │ │ │ ├── dialog │ │ │ │ │ │ ├── AirQualityDialog.kt │ │ │ │ │ │ ├── BaseDialog.kt │ │ │ │ │ │ ├── FacebookDialog.kt │ │ │ │ │ │ ├── InfoDialog.kt │ │ │ │ │ │ └── AboutDialog.kt │ │ │ │ │ └── BaseActivity.kt │ │ │ │ │ ├── firebase │ │ │ │ │ ├── SmokSmogFirebaseMessagingService.kt │ │ │ │ │ ├── SmokSmogFirebaseInstanceIdService.kt │ │ │ │ │ └── FirebaseEvents.kt │ │ │ │ │ ├── eventbus │ │ │ │ │ ├── EventBusModule.kt │ │ │ │ │ └── RxBus.kt │ │ │ │ │ ├── user │ │ │ │ │ ├── UserModule.kt │ │ │ │ │ └── User.kt │ │ │ │ │ ├── settings │ │ │ │ │ ├── SettingsModule.kt │ │ │ │ │ └── Percent.kt │ │ │ │ │ ├── error │ │ │ │ │ ├── ErrorReporter.kt │ │ │ │ │ └── SnackBarErrorReporter.kt │ │ │ │ │ ├── fabric │ │ │ │ │ ├── StationShowEvent.kt │ │ │ │ │ └── FabricModule.kt │ │ │ │ │ ├── job │ │ │ │ │ ├── JobModule.kt │ │ │ │ │ └── SmokSmogJobService.kt │ │ │ │ │ ├── google │ │ │ │ │ └── GoogleModule.kt │ │ │ │ │ ├── tracking │ │ │ │ │ └── Tracking.kt │ │ │ │ │ ├── permission │ │ │ │ │ └── PermissionHelper.kt │ │ │ │ │ ├── logger │ │ │ │ │ └── LoggerModule.kt │ │ │ │ │ ├── ApplicationModule.kt │ │ │ │ │ ├── utils │ │ │ │ │ └── TextUtils.kt │ │ │ │ │ └── ApplicationComponent.kt │ │ │ │ └── firebase │ │ │ │ └── jobdispatcher │ │ │ │ └── TriggerConfigurator.kt │ │ ├── assets │ │ │ └── fonts │ │ │ │ ├── Lato-Light.ttf │ │ │ │ ├── Roboto-Thin.ttf │ │ │ │ ├── RobotoCondensed-Light.ttf │ │ │ │ └── RobotoCondensed-Regular.ttf │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── smoksmog.png │ │ │ ├── ic_add_white_48dp.png │ │ │ ├── ic_info_black_24dp.png │ │ │ ├── ic_menu_white_24dp.png │ │ │ ├── ic_sync_black_24dp.png │ │ │ ├── ic_refresh_white_48dp.png │ │ │ ├── ic_search_white_24dp.png │ │ │ ├── ic_timeline_white_36dp.png │ │ │ ├── ic_info_outline_white_36dp.png │ │ │ ├── ic_my_location_white_48dp.png │ │ │ └── ic_notifications_black_24dp.png │ │ │ ├── drawable-mdpi │ │ │ ├── smoksmog.png │ │ │ ├── ic_add_white_48dp.png │ │ │ ├── ic_info_black_24dp.png │ │ │ ├── ic_menu_white_24dp.png │ │ │ ├── ic_sync_black_24dp.png │ │ │ ├── ic_refresh_white_48dp.png │ │ │ ├── ic_search_white_24dp.png │ │ │ ├── ic_timeline_white_36dp.png │ │ │ ├── ic_info_outline_white_36dp.png │ │ │ ├── ic_my_location_white_48dp.png │ │ │ └── ic_notifications_black_24dp.png │ │ │ ├── drawable-nodpi │ │ │ └── facebook.png │ │ │ ├── drawable-xhdpi │ │ │ ├── smoksmog.png │ │ │ ├── ic_add_white_48dp.png │ │ │ ├── ic_info_black_24dp.png │ │ │ ├── ic_menu_white_24dp.png │ │ │ ├── ic_search_white_24dp.png │ │ │ ├── ic_sync_black_24dp.png │ │ │ ├── ic_refresh_white_48dp.png │ │ │ ├── ic_timeline_white_36dp.png │ │ │ ├── ic_my_location_white_48dp.png │ │ │ ├── ic_info_outline_white_36dp.png │ │ │ └── ic_notifications_black_24dp.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── smoksmog.png │ │ │ ├── ic_add_white_48dp.png │ │ │ ├── ic_info_black_24dp.png │ │ │ ├── ic_menu_white_24dp.png │ │ │ ├── ic_sync_black_24dp.png │ │ │ ├── ic_refresh_white_48dp.png │ │ │ ├── ic_search_white_24dp.png │ │ │ ├── ic_timeline_white_36dp.png │ │ │ ├── ic_info_outline_white_36dp.png │ │ │ ├── ic_my_location_white_48dp.png │ │ │ └── ic_notifications_black_24dp.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── smoksmog.png │ │ │ ├── ic_add_white_48dp.png │ │ │ ├── ic_info_black_24dp.png │ │ │ ├── ic_menu_white_24dp.png │ │ │ ├── ic_sync_black_24dp.png │ │ │ ├── ic_search_white_24dp.png │ │ │ ├── ic_refresh_white_48dp.png │ │ │ ├── ic_timeline_white_36dp.png │ │ │ ├── ic_my_location_white_48dp.png │ │ │ ├── ic_info_outline_white_36dp.png │ │ │ └── ic_notifications_black_24dp.png │ │ │ ├── values-land │ │ │ └── styles.xml │ │ │ ├── layout │ │ │ ├── dialog_toolbar.xml │ │ │ ├── view_recyclerview.xml │ │ │ ├── item_chart.xml │ │ │ ├── toolbar.xml │ │ │ ├── activity_pick_station.xml │ │ │ ├── activity_settings.xml │ │ │ ├── activity_base.xml │ │ │ ├── activity_history.xml │ │ │ ├── item_order.xml │ │ │ ├── dialog_info_air_quality.xml │ │ │ ├── activity_order.xml │ │ │ ├── widget_station.xml │ │ │ └── dialog_info_facebook.xml │ │ │ ├── values-v21 │ │ │ └── themes.xml │ │ │ ├── values-v19 │ │ │ └── themes.xml │ │ │ ├── drawable │ │ │ ├── triangle.xml │ │ │ ├── shape_oval.xml │ │ │ ├── shape_oval_iron.xml │ │ │ ├── ic_signal_cellular_4_bar_black_24dp.xml │ │ │ ├── shape_oval_iron_border.xml │ │ │ └── ic_share_white_24dp.xml │ │ │ ├── values-w820dp │ │ │ └── dimens.xml │ │ │ ├── drawable-v21 │ │ │ ├── ic_info_black_24dp.xml │ │ │ ├── ic_notifications_black_24dp.xml │ │ │ └── ic_sync_black_24dp.xml │ │ │ ├── menu │ │ │ ├── pick_station.xml │ │ │ └── main.xml │ │ │ ├── values │ │ │ ├── constants.xml │ │ │ ├── themes.xml │ │ │ ├── dimens.xml │ │ │ ├── arrays.xml │ │ │ ├── preferences.xml │ │ │ ├── styles.xml │ │ │ └── about.xml │ │ │ ├── xml │ │ │ ├── widget_station.xml │ │ │ ├── settings_old_general.xml │ │ │ └── settings_general.xml │ │ │ └── values-pl │ │ │ └── about.xml │ ├── test │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── antyzero │ │ │ │ └── smoksmog │ │ │ │ ├── dumb.kt │ │ │ │ └── changelog │ │ │ │ └── ChangelogReaderTest.kt │ │ └── resources │ │ │ └── changelog_simple.json │ ├── debug │ │ ├── res │ │ │ └── values │ │ │ │ └── constants.xml │ │ └── AndroidManifest.xml │ └── androidTest │ │ ├── java │ │ ├── com │ │ │ └── antyzero │ │ │ │ └── smoksmog │ │ │ │ ├── utils │ │ │ │ └── Resources.java │ │ │ │ ├── rules │ │ │ │ ├── rx │ │ │ │ │ └── SchedulersHook.java │ │ │ │ ├── SpoonRule.java │ │ │ │ └── RxSchedulerTestRule.java │ │ │ │ ├── CustomTestRunner.java │ │ │ │ ├── screen │ │ │ │ ├── HistoryActivityTestRule.java │ │ │ │ ├── SettingsActivityTest.java │ │ │ │ ├── HistoryActivityTest.java │ │ │ │ └── StartActivityTest.java │ │ │ │ └── migration │ │ │ │ └── OldToNewStationListTest.java │ │ └── rx │ │ │ └── plugins │ │ │ └── RxJavaTestPlugins.java │ │ └── resources │ │ ├── particulates-1.json │ │ └── station-4.json ├── debug.keystore └── proguard-rules.pro ├── settings.gradle ├── lint.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.md ├── network ├── src │ ├── main │ │ └── kotlin │ │ │ └── pl │ │ │ └── malopolska │ │ │ └── smoksmog │ │ │ ├── model │ │ │ ├── Description.kt │ │ │ ├── History.kt │ │ │ ├── Station.kt │ │ │ ├── ParticulateEnum.kt │ │ │ └── Particulate.kt │ │ │ ├── ApiUtils.kt │ │ │ ├── Api.kt │ │ │ ├── DateTimeDeserializer.kt │ │ │ └── utils │ │ │ └── StationUtils.kt │ └── test │ │ ├── java │ │ └── pl │ │ │ └── malopolska │ │ │ └── smoksmog │ │ │ ├── model │ │ │ └── ParticulateEnumTest.java │ │ │ ├── RestClientTest.java │ │ │ └── TestUtils.java │ │ └── resources │ │ ├── responseParticulateDescription.json │ │ └── responseStation.json └── build.gradle ├── .gitignore ├── gradle.properties ├── .travis.yml-disabled └── Jenkinsfile /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | fabric.properties 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':network', ':android', ':domain' 2 | -------------------------------------------------------------------------------- /app/src/main/kotlin/error/NonExistentClass.kt: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | class NonExistentClass -------------------------------------------------------------------------------- /app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/debug.keystore -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/assets/fonts/Lato-Light.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/assets/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/smoksmog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/smoksmog.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/smoksmog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/smoksmog.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-nodpi/facebook.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/smoksmog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/smoksmog.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/smoksmog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/smoksmog.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/smoksmog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/smoksmog.png -------------------------------------------------------------------------------- /app/src/test/kotlin/com/antyzero/smoksmog/dumb.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | /** 4 | * Created by iwopolanski on 26.01.2017. 5 | */ 6 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/AnyExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | 4 | fun Any.tag(): String = this.javaClass.simpleName -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/assets/fonts/RobotoCondensed-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/assets/fonts/RobotoCondensed-Light.ttf -------------------------------------------------------------------------------- /app/src/main/assets/fonts/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/assets/fonts/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_refresh_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_refresh_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_refresh_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_refresh_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_add_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_info_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_menu_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_menu_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_sync_black_24dp.png -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/air/ValueCheck.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.air 2 | 3 | 4 | interface ValueCheck { 5 | 6 | fun isValueInRange(value: Double): Boolean 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/changelog/model/ChangeType.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog.model 2 | 3 | 4 | enum class ChangeType { 5 | FIX, NEW 6 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_timeline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_timeline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_timeline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_timeline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_refresh_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_refresh_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_timeline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_timeline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_refresh_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_refresh_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png -------------------------------------------------------------------------------- /app/src/debug/res/values/constants.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {D}SmokSmog 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_info_outline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_info_outline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_my_location_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_my_location_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_info_outline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_info_outline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_my_location_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_my_location_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_my_location_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_my_location_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_timeline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_timeline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_refresh_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_timeline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_timeline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_info_outline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_info_outline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_info_outline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_info_outline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_my_location_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_my_location_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_my_location_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_my_location_white_48dp.png -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/utils/Resources.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.utils; 2 | 3 | /** 4 | * 5 | */ 6 | public class Resources { 7 | 8 | 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_info_outline_white_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SmokSmog/smoksmog-android/HEAD/app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/storage/PersistentStorage.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.storage 2 | 3 | interface PersistentStorage : Storage { 4 | 5 | fun clear() 6 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/changelog/model/Changelog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog.model 2 | 3 | data class Changelog( 4 | val versions: List = listOf()) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/time/Countdown.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n.time 2 | 3 | interface Countdown { 4 | 5 | operator fun get(givenSeconds: Int): String 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smoksmog-android 2 | 3 | [![Build Status](https://travis-ci.org/SmokSmog/smoksmog-android.svg)](https://travis-ci.org/SmokSmog/smoksmog-android) 4 | 5 | SmokSmog application for Android. 6 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/LocaleProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n 2 | 3 | import java.util.* 4 | 5 | 6 | interface LocaleProvider { 7 | 8 | fun get(): Locale 9 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/model/Description.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model 2 | 3 | data class Description( 4 | val desc: String) { 5 | override fun toString() = desc 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/RxExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import rx.Observable 4 | 5 | /** 6 | * Rx related 7 | */ 8 | 9 | fun T.observable() = Observable.just(this) 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/TitleProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start 2 | 3 | interface TitleProvider { 4 | 5 | val title: String 6 | 7 | val subtitle: String 8 | } 9 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/model/History.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model 2 | 3 | import org.joda.time.LocalDate 4 | 5 | data class History( 6 | val value: Float, 7 | val date: LocalDate) { 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/BasePreferenceFragment.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui 2 | 3 | 4 | import com.trello.rxlifecycle.components.RxPreferenceFragment 5 | 6 | abstract class BasePreferenceFragment : RxPreferenceFragment() 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/FragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import android.app.Fragment 4 | 5 | import dagger.Module 6 | 7 | @Module 8 | class FragmentModule(private val fragment: Fragment) 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-land/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/model/Page.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.model 2 | 3 | import com.antyzero.smoksmog.storage.model.Item 4 | import pl.malopolska.smoksmog.model.Station 5 | 6 | data class Page(val item: Item, val station: Station) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/changelog/model/Change.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog.model 2 | 3 | 4 | data class Change( 5 | val type: ChangeType, 6 | val text: String, 7 | val translations: Map = mapOf()) -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/SupportFragmentModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import android.support.v4.app.Fragment 4 | 5 | import dagger.Module 6 | 7 | @Module 8 | class SupportFragmentModule(private val fragment: Fragment) 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/ViewExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.support.annotation.IdRes 4 | import android.view.View 5 | 6 | @Suppress("UNCHECKED_CAST") 7 | fun View.findView(@IdRes id: Int): T = this.findViewById(id) as T -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/firebase/SmokSmogFirebaseMessagingService.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.firebase 2 | 3 | import com.google.firebase.messaging.FirebaseMessagingService 4 | 5 | class SmokSmogFirebaseMessagingService : FirebaseMessagingService() { 6 | 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/order/ItemTouchHelperAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.order 2 | 3 | interface ItemTouchHelperAdapter { 4 | 5 | fun onItemMove(fromPosition: Int, toPosition: Int) 6 | 7 | fun onItemDismiss(position: Int) 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/order/OnStartDragListener.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.order 2 | 3 | import android.support.v7.widget.RecyclerView 4 | 5 | interface OnStartDragListener { 6 | 7 | fun onStartDrag(viewHolder: RecyclerView.ViewHolder) 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Sep 14 09:14:04 CEST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/RemoteViewsExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.support.annotation.IdRes 4 | import android.widget.RemoteViews 5 | 6 | fun RemoteViews.setBackgroundColor(@IdRes viewId: Int, color: Int) = this.setInt(viewId, "setBackgroundColor", color) -------------------------------------------------------------------------------- /app/src/main/res/layout/view_recyclerview.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/changelog/model/Version.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog.model 2 | 3 | import org.joda.time.LocalDate 4 | 5 | 6 | data class Version( 7 | val name: String, 8 | val code: Int, 9 | val date: LocalDate, 10 | val changes: List = listOf()) -------------------------------------------------------------------------------- /app/src/main/res/values-v19/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/PreferenceExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.preference.Preference 4 | import android.preference.PreferenceFragment 5 | 6 | 7 | @Suppress("UNCHECKED_CAST") 8 | fun PreferenceFragment.findPreference(key: String): T? = this.findPreference(key) as T -------------------------------------------------------------------------------- /app/src/main/res/drawable/triangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_oval.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_oval_iron.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_chart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/androidTest/java/rx/plugins/RxJavaTestPlugins.java: -------------------------------------------------------------------------------- 1 | package rx.plugins; 2 | 3 | /** 4 | * Allows to access reser 5 | */ 6 | public class RxJavaTestPlugins extends RxJavaPlugins { 7 | 8 | RxJavaTestPlugins() { 9 | super(); 10 | } 11 | 12 | public static void resetPlugins() { 13 | getInstance().reset(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/firebase/jobdispatcher/TriggerConfigurator.kt: -------------------------------------------------------------------------------- 1 | package com.firebase.jobdispatcher 2 | 3 | 4 | class TriggerConfigurator { 5 | companion object { 6 | fun executionWindow(builder: Job.Builder, windowStart: Int, windowEnd: Int) { 7 | builder.trigger = Trigger.executionWindow(windowStart, windowEnd) 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/eventbus/EventBusModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.eventbus 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import javax.inject.Singleton 6 | 7 | @Module 8 | class EventBusModule { 9 | 10 | @Provides 11 | @Singleton 12 | internal fun provideRxBus(): RxBus { 13 | return RxBus() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/about/AboutActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.about 2 | 3 | import com.antyzero.smoksmog.ui.screen.ActivityModule 4 | 5 | import dagger.Subcomponent 6 | 7 | @Subcomponent(modules = arrayOf(ActivityModule::class)) 8 | interface AboutActivityComponent { 9 | fun inject(activity: AboutActivity) 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_signal_cellular_4_bar_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/location/LocationProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.location 2 | 3 | import rx.Observable 4 | 5 | interface LocationProvider { 6 | 7 | fun location(): Observable 8 | } 9 | 10 | sealed class Location { 11 | 12 | class Position(val coordinates: Pair) : Location() 13 | 14 | class Unknown : Location() 15 | } -------------------------------------------------------------------------------- /app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/user/UserModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.user 2 | 3 | 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import javax.inject.Singleton 8 | 9 | @Module 10 | @Singleton 11 | class UserModule { 12 | 13 | @Provides 14 | @Singleton 15 | internal fun provideUser(context: Context): User = User(context) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/typeface/TypefaceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.typeface 2 | 3 | 4 | import android.content.Context 5 | import android.graphics.Typeface 6 | 7 | class TypefaceProvider(context: Context) { 8 | 9 | val default: Typeface 10 | 11 | init { 12 | default = Typeface.createFromAsset(context.assets, "fonts/Lato-Light.ttf") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/settings/SettingsModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.settings 2 | 3 | import android.content.Context 4 | import dagger.Module 5 | import dagger.Provides 6 | import javax.inject.Singleton 7 | 8 | @Singleton 9 | @Module 10 | class SettingsModule { 11 | 12 | @Provides 13 | @Singleton 14 | fun provideSettingsHelper(context: Context) = SettingsHelper(context) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/item/ViewDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.item 2 | 3 | import android.view.ViewGroup 4 | 5 | abstract class ViewDelegate, R>(val viewType: Int) { 6 | 7 | abstract fun onCreateViewHolder(parent: ViewGroup): T 8 | 9 | fun onBindViewHolder(holder: T, data: R) { 10 | holder.bind(data) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/gradient_pollution.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/error/ErrorReporter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.error 2 | 3 | import android.support.annotation.StringRes 4 | 5 | /** 6 | * Common interface for ui elements to report errors 7 | */ 8 | interface ErrorReporter { 9 | 10 | fun report(message: String) 11 | 12 | fun report(@StringRes stringId: Int) 13 | 14 | fun report(@StringRes stringId: Int, vararg objects: Any) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_info_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_oval_iron_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/fabric/StationShowEvent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.fabric 2 | 3 | import com.crashlytics.android.answers.ContentViewEvent 4 | 5 | import pl.malopolska.smoksmog.model.Station 6 | 7 | class StationShowEvent(station: Station) : ContentViewEvent() { 8 | 9 | init { 10 | putContentId(station.id.toString()) 11 | putContentName(station.name) 12 | putContentType("Station") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/item/ListViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.item 2 | 3 | import android.content.Context 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | 7 | abstract class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 8 | 9 | val context: Context = itemView.context 10 | 11 | open fun bind(data: T) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/ViewHolderExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.support.annotation.IdRes 4 | import android.support.v7.widget.RecyclerView 5 | import android.view.View 6 | 7 | fun RecyclerView.ViewHolder.findViewById(@IdRes id: Int): View = this.itemView.findViewById(id) 8 | 9 | @Suppress("UNCHECKED_CAST") 10 | fun RecyclerView.ViewHolder.findView(@IdRes id: Int): T = this.itemView.findViewById(id) as T -------------------------------------------------------------------------------- /domain/src/integrationTest/kotlin/com/antyzero/smoksmog/StringRandom.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import java.util.* 4 | 5 | 6 | class StringRandom(seed: Long = System.nanoTime()) { 7 | 8 | private val LOWER_CASE_A = 97 9 | private val random = Random(seed) 10 | 11 | fun random(length: Int = 8) = (1..length).fold("") { 12 | previous, position -> 13 | previous + (LOWER_CASE_A + random.nextInt(22)).toChar() 14 | } 15 | } -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/location/SimpleLocationProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.location 2 | 3 | import rx.Observable 4 | import rx.subjects.BehaviorSubject 5 | 6 | class SimpleLocationProvider(initialLocation: Location = Location.Unknown()) : LocationProvider { 7 | 8 | val locationSubject: BehaviorSubject = BehaviorSubject.create(initialLocation) 9 | 10 | override fun location(): Observable = locationSubject 11 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/eventbus/RxBus.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.eventbus 2 | 3 | import rx.Observable 4 | import rx.subjects.PublishSubject 5 | import rx.subjects.SerializedSubject 6 | 7 | class RxBus { 8 | 9 | private val _bus = SerializedSubject(PublishSubject.create()) 10 | 11 | fun send(o: Any) { 12 | _bus.onNext(o) 13 | } 14 | 15 | fun toObserverable(): Observable { 16 | return _bus 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/widget/WidgetModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.widget 2 | 3 | 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import javax.inject.Singleton 8 | 9 | @Module 10 | @Singleton 11 | class WidgetModule { 12 | 13 | @Provides 14 | @Singleton 15 | fun provideStationWidgetData(context: Context): StationWidgetData { 16 | return StationWidgetData(context) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/pick_station.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | -------------------------------------------------------------------------------- /android/src/androidTest/java/com/antyzero/smoksmog/android/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.android; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui 2 | 3 | import android.os.Bundle 4 | import android.support.annotation.CallSuper 5 | import android.view.View 6 | import com.trello.rxlifecycle.components.RxFragment 7 | 8 | abstract class BaseFragment : RxFragment() { 9 | 10 | @CallSuper 11 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 12 | super.onViewCreated(view, savedInstanceState) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/fragment/LocationStationFragmentComponent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.fragment 2 | 3 | 4 | import com.antyzero.smoksmog.google.GoogleModule 5 | import com.antyzero.smoksmog.ui.screen.FragmentModule 6 | 7 | import dagger.Subcomponent 8 | 9 | @Subcomponent(modules = arrayOf(FragmentModule::class, GoogleModule::class)) 10 | interface LocationStationFragmentComponent { 11 | 12 | fun inject(fragment: LocationStationFragment) 13 | } 14 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/model/Station.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.util.* 5 | 6 | data class Station( 7 | 8 | val id: Long, 9 | val name: String, 10 | @SerializedName("lon") val longitude: Float = 0f, 11 | @SerializedName("lat") val latitude: Float = 0f) { 12 | 13 | val particulates: List = ArrayList() 14 | 15 | override fun toString() = name 16 | } 17 | -------------------------------------------------------------------------------- /app/src/androidTest/resources/particulates-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "Dwutlenek siarki powstaje w wyniku spalania, zawieraj\u0105cych \u015bladowe ilo\u015bci siarki, paliw kopalnych. Wi\u0119kszo\u015b\u0107 SO\u2082 emitowana jest podczas produkcji energii elektrycznej oraz w niewielkim stopniu przez \u015brodki transportu. Wdychanie SO\u2082 mo\u017ce powodowa\u0107 choroby uk\u0142adu oddechowego. Kwas siarkowy powsta\u0142y w wyniku atmosferycznej reakcji SO\u2082 jest jednym ze sk\u0142adnik\u00f3w kwa\u015bnych deszcz\u00f3w." 3 | } -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | very good air quality 5 | good air quality 6 | moderate air quality 7 | sufficient air quality 8 | bad air quality 9 | very bad air quality 10 | 11 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/rules/rx/SchedulersHook.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.rules.rx; 2 | 3 | import rx.Scheduler; 4 | import rx.plugins.RxJavaSchedulersHook; 5 | 6 | 7 | public class SchedulersHook extends RxJavaSchedulersHook { 8 | 9 | private final Scheduler scheduler; 10 | 11 | public SchedulersHook(Scheduler scheduler) { 12 | this.scheduler = scheduler; 13 | } 14 | 15 | @Override 16 | public Scheduler getNewThreadScheduler() { 17 | return scheduler; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/ContextLocaleProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import java.util.* 6 | 7 | internal class ContextLocaleProvider(private val context: Context) : LocaleProvider { 8 | 9 | @Suppress("DEPRECATION") 10 | override fun get(): Locale = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ){ 11 | context.resources.configuration.locales[0] 12 | } else { 13 | context.resources.configuration.locale 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/FragmentComponent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import com.antyzero.smoksmog.ui.screen.settings.GeneralSettingsFragment 4 | import com.antyzero.smoksmog.ui.screen.start.fragment.NetworkStationFragment 5 | 6 | import dagger.Subcomponent 7 | 8 | @Subcomponent(modules = arrayOf(FragmentModule::class)) 9 | interface FragmentComponent { 10 | 11 | fun inject(generalSettingsFragment: GeneralSettingsFragment) 12 | 13 | fun inject(networkStationFragment: NetworkStationFragment) 14 | } 15 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/storage/Storage.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.storage 2 | 3 | import com.antyzero.smoksmog.storage.model.Item 4 | 5 | 6 | interface Storage { 7 | 8 | fun addStation(id: Long): Boolean { 9 | return add(Item.Station(id)) 10 | } 11 | 12 | fun add(item: Item): Boolean 13 | 14 | fun removeById(id: Long) 15 | 16 | fun removeAt(i: Int) 17 | 18 | fun update(id: Long, itemUpdate: Item) 19 | 20 | fun fetchAll(): List 21 | 22 | fun set(itemCollection: Collection) 23 | } -------------------------------------------------------------------------------- /android/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | zła jakość powietrza 5 | dobra jakość powietrza 6 | umiarkowana jakość powietrza 7 | dostateczna jakość powietrza 8 | bardzo zła jakość powietrza 9 | bardzo dobra jakość powietrza 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Android Studio 26 | *.iml 27 | .idea/ 28 | 29 | # Crashlytics 30 | crashlytics.properties 31 | crashlytics-build.properties 32 | app/src/main/res/values/com_crashlytics_export_strings.xml 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/ic_sync_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/time/CountdownProvider.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n.time 2 | 3 | 4 | import com.antyzero.smoksmog.i18n.LocaleProvider 5 | import java.util.* 6 | 7 | class CountdownProvider(private val localeProvider: LocaleProvider) { 8 | 9 | operator fun get(seconds: Int): String = when (localeProvider.get()) { 10 | LOCALE_POLISH -> PolishCountdown()[seconds] 11 | else -> EnglishCountdown()[seconds] 12 | } 13 | 14 | companion object { 15 | 16 | private val LOCALE_POLISH = Locale("pl", "PL") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/constants.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SmokSmog 4 | 5 | 6 | Facebook 7 | Beta 8 | 9 | 24h 10 | 1h 11 | 12 | %.1f %s 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/job/JobModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.job 2 | 3 | import android.content.Context 4 | import com.firebase.jobdispatcher.FirebaseJobDispatcher 5 | import com.firebase.jobdispatcher.GooglePlayDriver 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | @Singleton 12 | class JobModule { 13 | 14 | @Provides 15 | @Singleton 16 | internal fun provideFirebaseJobDispatcher(context: Context): FirebaseJobDispatcher { 17 | return FirebaseJobDispatcher(GooglePlayDriver(context)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/res/xml/widget_station.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /network/src/test/java/pl/malopolska/smoksmog/model/ParticulateEnumTest.java: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | public class ParticulateEnumTest { 8 | 9 | @Test 10 | public void testNonSupportedId() throws Exception { 11 | 12 | // given 13 | long id = -34; 14 | 15 | // when 16 | ParticulateEnum result = ParticulateEnum.Companion.findById(-34); 17 | 18 | // then 19 | assertThat(result).isNotNull(); 20 | assertThat(result).isEqualTo(ParticulateEnum.UNKNOWN); 21 | } 22 | } -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/model/ParticulateEnum.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model 2 | 3 | enum class ParticulateEnum(val id: Long) { 4 | 5 | SO2(1), NO(2), NO2(3), CO(4), O3(5), NOx(6), PM10(7), PM25(8), C6H6(11), UNKNOWN(-1); 6 | 7 | override fun toString() = "$name {id=$id}" 8 | 9 | companion object { 10 | 11 | fun findById(id: Long): ParticulateEnum { 12 | 13 | for (particulateEnum in values()) { 14 | if (particulateEnum.id == id) { 15 | return particulateEnum 16 | } 17 | } 18 | return UNKNOWN 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/fabric/FabricModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.fabric 2 | 3 | 4 | import com.crashlytics.android.answers.Answers 5 | import com.crashlytics.android.core.CrashlyticsCore 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | @Singleton 11 | @Module 12 | class FabricModule { 13 | 14 | @Provides 15 | @Singleton 16 | internal fun provideAnswers(): Answers { 17 | return Answers.getInstance() 18 | } 19 | 20 | @Provides 21 | @Singleton 22 | internal fun provideCrashlyticsCore(): CrashlyticsCore { 23 | return CrashlyticsCore.getInstance() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/item/AirQualityViewDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.item 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | 6 | import com.antyzero.smoksmog.R 7 | 8 | import pl.malopolska.smoksmog.model.Station 9 | 10 | class AirQualityViewDelegate(viewType: Int) : ViewDelegate(viewType) { 11 | 12 | override fun onCreateViewHolder(parent: ViewGroup): AirQualityViewHolder { 13 | val layoutInflater = LayoutInflater.from(parent.context) 14 | return AirQualityViewHolder(layoutInflater.inflate(R.layout.item_air_quility, parent, false)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/item/ParticulateViewDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.item 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | 6 | import com.antyzero.smoksmog.R 7 | 8 | import pl.malopolska.smoksmog.model.Particulate 9 | 10 | class ParticulateViewDelegate(viewType: Int) : ViewDelegate(viewType) { 11 | 12 | override fun onCreateViewHolder(parent: ViewGroup): ParticulateViewHolder { 13 | val inflater = LayoutInflater.from(parent.context) 14 | return ParticulateViewHolder(inflater.inflate(R.layout.item_particulate, parent, false)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/ApiUtils.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog 2 | 3 | 4 | import pl.malopolska.smoksmog.model.Particulate 5 | import rx.Observable 6 | 7 | class ApiUtils private constructor() { 8 | 9 | init { 10 | throw IllegalAccessError() 11 | } 12 | 13 | companion object { 14 | 15 | fun sortParticulates(particulates: Collection): Observable { 16 | 17 | return Observable.from(particulates) 18 | .toSortedList { first, second -> first.position - second.position } 19 | .flatMap { particulates -> Observable.from(particulates) } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/DependencyInjectionExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import com.antyzero.smoksmog.SmokSmogApplication 6 | import com.antyzero.smoksmog.ui.screen.ActivityModule 7 | 8 | /** 9 | * ApplicationComponent access 10 | */ 11 | fun Any.appComponent(context: Context) = SmokSmogApplication[context].appComponent 12 | 13 | fun Context.appComponent() = appComponent(this) 14 | 15 | /** 16 | * ActivityComponent access 17 | */ 18 | fun Any.activityComponent(activity: Activity) = appComponent(activity).plus(ActivityModule(activity)) 19 | 20 | fun Activity.activityComponent() = activityComponent(this) -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/ContextExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.Context 5 | import android.support.annotation.StringRes 6 | import android.widget.Toast 7 | 8 | fun Context.toast(chars: CharSequence, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, chars, duration).show() 9 | 10 | fun Context.toast(chars: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, chars, duration).show() 11 | 12 | fun Context.toast(@StringRes stringRes: Int, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, stringRes, duration).show() 13 | 14 | fun Context.appWidgetManager() = AppWidgetManager.getInstance(this) -------------------------------------------------------------------------------- /app/src/test/resources/changelog_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": [ 3 | { 4 | "name": "0.9.0", 5 | "code": 90, 6 | "date": 123123123, 7 | "changes": [ 8 | { 9 | "type": "fix", 10 | "text": "We fixed something", 11 | "translations": { 12 | "pl": "Coś tam naprawiliśmy" 13 | } 14 | } 15 | ] 16 | }, 17 | { 18 | "name": "0.8.0", 19 | "code": 60, 20 | "date": 14654465464, 21 | "changes": [ 22 | { 23 | "type": "new", 24 | "text": "We added something", 25 | "": { 26 | "pl": "Coś tam dodalismy" 27 | } 28 | } 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /opt/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/I18nModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n 2 | 3 | import android.content.Context 4 | import com.antyzero.smoksmog.i18n.time.CountdownProvider 5 | import dagger.Module 6 | import dagger.Provides 7 | import javax.inject.Singleton 8 | 9 | @Module 10 | @Singleton 11 | class I18nModule { 12 | 13 | @Provides 14 | @Singleton 15 | internal fun provideLocalProvider(context: Context): LocaleProvider { 16 | return ContextLocaleProvider(context) 17 | } 18 | 19 | @Provides 20 | @Singleton 21 | internal fun provideCountdownProvider(localeProvider: LocaleProvider): CountdownProvider { 22 | return CountdownProvider(localeProvider) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /network/src/test/resources/responseParticulateDescription.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "NO\u2093 jest terminem u\u017cywanym do opisania mieszaniny tlenku azotu (NO) i dwutlenek azotu (NO\u2082). S\u0105 nieorganicznymi gazami utworzonymi przez po\u0142\u0105czenie tlenu z azotem z powietrza. NO jest wytwarzany w znacznie wi\u0119kszych ilo\u015bciach ni\u017c NO\u2082, ale utlenia si\u0119 do NO\u2082 w atmosferze. NO\u2082 powoduje szkodliwe skutki dla dr\u00f3g oddechowych. St\u0119\u017cenia dwutlenku azotu cz\u0119sto zbli\u017caj\u0105 si\u0119, a czasemi przekraczaj\u0105 normy jako\u015bci powietrza w wielu miastach europejskich. NO\u2093 emitowane gdy spalane jest paliwo np. w transporcie, procesach przemys\u0142owych i energetycznych." 3 | } 4 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/google/GoogleModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.google 2 | 3 | import android.content.Context 4 | 5 | import com.google.android.gms.common.api.GoogleApiClient 6 | import com.google.android.gms.location.LocationServices 7 | 8 | import dagger.Module 9 | import dagger.Provides 10 | 11 | @Module 12 | class GoogleModule(private val connectionCallbacks: GoogleApiClient.ConnectionCallbacks) { 13 | 14 | @Provides 15 | internal fun provideGoogleApiClient(context: Context): GoogleApiClient { 16 | return GoogleApiClient.Builder(context) 17 | .addConnectionCallbacks(connectionCallbacks) 18 | .addApi(LocationServices.API) 19 | .build() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/iwopolanski/Workspace/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/job/SmokSmogJobService.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.job 2 | 3 | import com.antyzero.smoksmog.dsl.appComponent 4 | import com.antyzero.smoksmog.ui.widget.StationWidgetService 5 | import com.firebase.jobdispatcher.JobParameters 6 | import com.firebase.jobdispatcher.JobService 7 | 8 | class SmokSmogJobService : JobService() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | appComponent().inject(this) 13 | } 14 | 15 | override fun onStartJob(job: JobParameters?): Boolean { 16 | StationWidgetService.updateAll(this) 17 | return false 18 | } 19 | 20 | override fun onStopJob(job: JobParameters?): Boolean { 21 | return false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings_old_general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/model/Particulate.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import org.joda.time.DateTime 5 | import java.util.* 6 | 7 | data class Particulate( 8 | val id: Long, 9 | val name: String, 10 | @SerializedName("short_name") val shortName: String, 11 | val value: Float = 0f, 12 | val unit: String, 13 | val norm: Float = 0f, 14 | val date: DateTime, 15 | @SerializedName("avg") val average: Float = 0f, 16 | val position: Int = 0, 17 | val values: List = ArrayList()) { 18 | 19 | val enum: ParticulateEnum 20 | get() = ParticulateEnum.findById(id) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/dialog/AirQualityDialog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.dialog 2 | 3 | 4 | import android.text.method.LinkMovementMethod 5 | import android.view.View 6 | import android.widget.TextView 7 | import com.antyzero.smoksmog.R 8 | import com.antyzero.smoksmog.dsl.compatFromHtml 9 | 10 | class AirQualityDialog : InfoDialog() { 11 | 12 | override fun getLayoutId(): Int = R.layout.dialog_info_air_quality 13 | 14 | override fun initView(view: View) { 15 | super.initView(view) 16 | 17 | val textView = view.findViewById(R.id.textView) as TextView 18 | 19 | textView.compatFromHtml(R.string.desc_air_quality) 20 | textView.movementMethod = LinkMovementMethod.getInstance() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | 60dp 7 | 8 | 30dp 9 | 10 | 8dp 11 | 7dp 12 | 13 | 10dp 14 | 15 | 8dp 16 | 16dp 17 | 18 | 80dp 19 | 20 | 2dp 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/order/OrderItemViewHolder.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.order 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.View 5 | import android.widget.TextView 6 | import com.antyzero.smoksmog.R 7 | import com.antyzero.smoksmog.dsl.findView 8 | import com.antyzero.smoksmog.dsl.findViewById 9 | import com.antyzero.smoksmog.storage.model.Item 10 | import pl.malopolska.smoksmog.model.Station 11 | 12 | class OrderItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 13 | 14 | val textView: TextView = findView(R.id.textView) 15 | val viewHandle: View = findViewById(R.id.viewHandle) 16 | 17 | fun bind(stationName: String?) { 18 | textView.text = stationName 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/rules/SpoonRule.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.rules; 2 | 3 | import android.support.test.rule.ActivityTestRule; 4 | 5 | import com.squareup.spoon.Spoon; 6 | 7 | import org.junit.rules.ExternalResource; 8 | 9 | /** 10 | * 11 | */ 12 | public class SpoonRule extends ExternalResource { 13 | 14 | private final ActivityTestRule activityTestRule; 15 | 16 | public SpoonRule(ActivityTestRule activityTestRule) { 17 | this.activityTestRule = activityTestRule; 18 | } 19 | 20 | public void screenshot(String tag) { 21 | try { 22 | Spoon.screenshot(activityTestRule.getActivity(), tag); 23 | } catch (Exception e) { 24 | System.err.println("Missing Spoon"); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/PageSave.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | 6 | class PageSave(context: Context) { 7 | 8 | private val preferences: SharedPreferences 9 | 10 | init { 11 | preferences = context.getSharedPreferences(TAG, Context.MODE_PRIVATE) 12 | } 13 | 14 | fun savePage(pageOrder: Int) { 15 | preferences.edit().putInt(KEY_ORDER, pageOrder).apply() 16 | } 17 | 18 | fun restorePage(): Int { 19 | return preferences.getInt(KEY_ORDER, 0) 20 | } 21 | 22 | companion object { 23 | 24 | private val TAG = PageSave::class.java.simpleName 25 | 26 | private val KEY_ORDER = "keyOrder" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_pick_station.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 19 | 20 | -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/logger/Logger.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.logger 2 | 3 | 4 | interface Logger { 5 | 6 | // Verbose 7 | 8 | fun v(tag: String, message: String) 9 | 10 | fun v(tag: String, message: String, throwable: Throwable) 11 | 12 | // Debug 13 | 14 | fun d(tag: String, message: String) 15 | 16 | fun d(tag: String, message: String, throwable: Throwable) 17 | 18 | // Info 19 | 20 | fun i(tag: String, message: String) 21 | 22 | fun i(tag: String, message: String, throwable: Throwable) 23 | 24 | // Warning 25 | 26 | fun w(tag: String, message: String) 27 | 28 | fun w(tag: String, message: String, throwable: Throwable) 29 | 30 | // Error 31 | 32 | fun e(tag: String, message: String) 33 | 34 | fun e(tag: String, message: String, throwable: Throwable) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/settings/Percent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.settings 2 | 3 | import android.content.Context 4 | import android.support.annotation.StringRes 5 | 6 | import com.antyzero.smoksmog.R 7 | 8 | /** 9 | * Indicates from which period measurement should be taken 10 | */ 11 | enum class Percent constructor(@StringRes private val value: Int) { 12 | 13 | HOUR(R.string.pref_percent_value_hour), DAY(R.string.pref_percent_value_day); 14 | 15 | companion object { 16 | 17 | fun find(context: Context, value: String): Percent { 18 | 19 | values() 20 | .filter { context.getString(it.value) == value } 21 | .forEach { return it } 22 | 23 | throw IllegalArgumentException("Unable to find proper enum value for \"" + value + "\"") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/storage/model/Module.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.storage.model 2 | 3 | sealed class Module { 4 | 5 | private val _class: String = javaClass.canonicalName 6 | 7 | /** 8 | * Show AQI and it's type 9 | */ 10 | class AirQualityIndex(val type: Type = AirQualityIndex.Type.POLISH) : Module() { 11 | 12 | enum class Type { 13 | POLISH 14 | } 15 | } 16 | 17 | /** 18 | * List of measurements for particulates 19 | */ 20 | class Measurements : Module() 21 | 22 | override fun equals(other: Any?): Boolean { 23 | if (this === other) return true 24 | if (other !is Module) return false 25 | 26 | if (_class != other._class) return false 27 | 28 | return true 29 | } 30 | 31 | override fun hashCode(): Int { 32 | return _class.hashCode() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/widget/StationWidgetData.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.widget 2 | 3 | import android.content.Context 4 | import android.content.Context.MODE_PRIVATE 5 | import android.content.SharedPreferences 6 | import com.antyzero.smoksmog.dsl.tag 7 | 8 | 9 | class StationWidgetData(context: Context) { 10 | 11 | val sharedPreferences: SharedPreferences 12 | 13 | init { 14 | sharedPreferences = context.getSharedPreferences(tag(), MODE_PRIVATE) 15 | } 16 | 17 | fun addWidget(widgetId: Int, stationId: Long) { 18 | sharedPreferences.edit().putLong(widgetId.toString(), stationId).apply() 19 | } 20 | 21 | fun removeWidget(widgetId: Int) { 22 | sharedPreferences.edit().remove(widgetId.toString()).apply() 23 | } 24 | 25 | fun widgetStationId(widgetId: Int): Long { 26 | return sharedPreferences.getLong(widgetId.toString(), -1) 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/ActivityExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.app.Activity 4 | import android.content.res.Configuration 5 | import android.support.annotation.IdRes 6 | import android.view.View 7 | 8 | 9 | fun Activity.fullscreen() { 10 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 11 | if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { 12 | window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 13 | } 14 | } 15 | 16 | fun Activity.statusBarHeight(): Int { 17 | val resource = resources.getIdentifier("status_bar_height", "dimen", "android") 18 | if (resource > 0) { 19 | return resources.getDimensionPixelSize(resource) 20 | } 21 | return 0 22 | } 23 | 24 | @Suppress("UNCHECKED_CAST") 25 | fun Activity.findView(@IdRes id: Int): T = this.findViewById(id) as T -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/tracking/Tracking.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.tracking 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import org.joda.time.DateTime 6 | 7 | class Tracking(context: Context) { 8 | 9 | private var preferences: SharedPreferences 10 | 11 | init { 12 | preferences = context.getSharedPreferences("TrackerSharedPreferences", Context.MODE_PRIVATE) 13 | 14 | // Tracking 15 | trackFirstRun() 16 | } 17 | 18 | private fun trackFirstRun() { 19 | if (preferences.contains(KEY_FIRST_RUN_TIME).not()) { 20 | preferences.edit().putLong(KEY_FIRST_RUN_TIME, System.currentTimeMillis()).apply() 21 | } 22 | } 23 | 24 | fun getFirstRunDateTime(): DateTime = DateTime(preferences.getLong(KEY_FIRST_RUN_TIME, 0)) 25 | 26 | private companion object { 27 | 28 | private val KEY_FIRST_RUN_TIME = "firstRun" 29 | } 30 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/logger/SilentLogger.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.logger 2 | 3 | class SilentLogger : Logger { 4 | 5 | override fun v(tag: String, message: String) { 6 | 7 | } 8 | 9 | override fun v(tag: String, message: String, throwable: Throwable) { 10 | 11 | } 12 | 13 | override fun d(tag: String, message: String) { 14 | 15 | } 16 | 17 | override fun d(tag: String, message: String, throwable: Throwable) { 18 | 19 | } 20 | 21 | override fun i(tag: String, message: String) { 22 | 23 | } 24 | 25 | override fun i(tag: String, message: String, throwable: Throwable) { 26 | 27 | } 28 | 29 | override fun w(tag: String, message: String) { 30 | 31 | } 32 | 33 | override fun w(tag: String, message: String, throwable: Throwable) { 34 | 35 | } 36 | 37 | override fun e(tag: String, message: String) { 38 | 39 | } 40 | 41 | override fun e(tag: String, message: String, throwable: Throwable) { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/history/HistoryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.history 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | 7 | import com.antyzero.smoksmog.R 8 | 9 | import pl.malopolska.smoksmog.model.Particulate 10 | 11 | 12 | class HistoryAdapter(private val particulates: List) : RecyclerView.Adapter() { 13 | 14 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParticulateHistoryViewHolder { 15 | val inflater = LayoutInflater.from(parent.context) 16 | return ParticulateHistoryViewHolder(inflater.inflate(R.layout.item_chart, parent, false)) 17 | } 18 | 19 | override fun getItemCount(): Int { 20 | return particulates.size 21 | } 22 | 23 | override fun onBindViewHolder(holder: ParticulateHistoryViewHolder, position: Int) { 24 | holder.bind(particulates[position]) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /domain/src/integrationTest/kotlin/com/antyzero/smoksmog/api/ApiIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.api 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Before 5 | import org.junit.Test 6 | import pl.malopolska.smoksmog.RestClient 7 | import pl.malopolska.smoksmog.model.Station 8 | import rx.Observable 9 | import rx.observers.TestSubscriber 10 | 11 | class ApiIntegrationTest { 12 | 13 | lateinit private var api: RestClient 14 | 15 | @Before 16 | fun setUp() { 17 | api = RestClient.Builder().build() 18 | } 19 | 20 | @Test 21 | fun stations() { 22 | val testSubscriber = TestSubscriber() 23 | 24 | api.stations() 25 | .flatMap { Observable.from(it) } 26 | .subscribe(testSubscriber) 27 | 28 | testSubscriber.assertNoErrors() 29 | testSubscriber.assertCompleted() 30 | assertThat(testSubscriber.onNextEvents.size).isGreaterThanOrEqualTo(50) // we should have that much 31 | } 32 | } -------------------------------------------------------------------------------- /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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 10 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | org.gradle.daemon=true 16 | org.gradle.parallel=true 17 | org.gradle.jvmargs=-Xmx2048M 18 | appVersionName=1.9.8 19 | appVersionCode=198000 20 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/permission/PermissionHelper.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.permission 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.support.v4.content.ContextCompat 8 | 9 | 10 | /** 11 | * Makes permission checks easier 12 | * 13 | * Provides scenarios for post and pre Marshmallow OS versions 14 | */ 15 | class PermissionHelper(private val context: Context) { 16 | 17 | val isGrantedAccessCoarseLocation: Boolean 18 | get() = isGranted(Manifest.permission.ACCESS_COARSE_LOCATION) 19 | 20 | fun get(permissionKey: String) = isGranted(permissionKey) 21 | 22 | private fun isGranted(permission: String): Boolean { 23 | 24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 25 | return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED 26 | } else { 27 | return true // TODO check manifest 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/changelog/ChangelogReader.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog 2 | 3 | import com.antyzero.smoksmog.changelog.model.ChangeType 4 | import com.antyzero.smoksmog.changelog.model.Changelog 5 | import com.google.gson.Gson 6 | import com.google.gson.GsonBuilder 7 | import com.google.gson.JsonDeserializer 8 | import org.joda.time.LocalDate 9 | import java.io.File 10 | import java.util.* 11 | 12 | class ChangelogReader(locale: Locale, changelogFile: File) { 13 | 14 | val changelog: Changelog 15 | 16 | private var gson: Gson 17 | 18 | init { 19 | gson = GsonBuilder().apply { 20 | registerTypeAdapter(LocalDate::class.java, JsonDeserializer { json, typeOfT, context -> LocalDate(json.asLong) }) 21 | registerTypeAdapter(ChangeType::class.java, JsonDeserializer { json, typeOfT, context -> ChangeType.valueOf(json.asString.toUpperCase()) }) 22 | }.create() 23 | 24 | changelog = gson.fromJson(changelogFile.readText(), Changelog::class.java) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/androidTest/resources/station-4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4", 3 | "name": "Krak\u00f3w - Aleja Krasi\u0144skiego", 4 | "particulates": [ 5 | { 6 | "id": "7", 7 | "name": "Py\u0142 zawieszony", 8 | "short_name": "PM\u2081\u2080", 9 | "value": "112.675", 10 | "unit": "\u00b5g\/m\u00b3", 11 | "norm": "50", 12 | "date": "2015-12-17 10:11:02", 13 | "avg": "146.08", 14 | "position": "1" 15 | }, 16 | { 17 | "id": "3", 18 | "name": "Dwutlenek azotu", 19 | "short_name": "NO\u2082", 20 | "value": "75.8005", 21 | "unit": "\u00b5g\/m\u00b3", 22 | "norm": "200", 23 | "date": "2015-12-17 10:11:02", 24 | "avg": "54.08", 25 | "position": "5" 26 | }, 27 | { 28 | "id": "4", 29 | "name": "Tlenek w\u0119gla", 30 | "short_name": "CO", 31 | "value": "1832.72", 32 | "unit": "\u00b5g\/m\u00b3", 33 | "norm": "10000", 34 | "date": "2015-12-17 10:11:02", 35 | "avg": "1655.32", 36 | "position": "6" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/firebase/SmokSmogFirebaseInstanceIdService.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.firebase 2 | 3 | import android.util.Log 4 | import com.antyzero.smoksmog.BuildConfig 5 | import com.antyzero.smoksmog.dsl.appComponent 6 | import com.antyzero.smoksmog.dsl.tag 7 | import com.crashlytics.android.core.CrashlyticsCore 8 | import com.google.firebase.iid.FirebaseInstanceId 9 | import com.google.firebase.iid.FirebaseInstanceIdService 10 | import javax.inject.Inject 11 | 12 | class SmokSmogFirebaseInstanceIdService : FirebaseInstanceIdService() { 13 | 14 | @Inject lateinit var crashlyticsCore: CrashlyticsCore 15 | 16 | override fun onCreate() { 17 | super.onCreate() 18 | appComponent().inject(this) 19 | } 20 | 21 | override fun onTokenRefresh() { 22 | super.onTokenRefresh() 23 | val token = FirebaseInstanceId.getInstance().token 24 | if (BuildConfig.DEBUG) { 25 | Log.i(tag(), "FCM token: $token") 26 | } 27 | crashlyticsCore.setString("FCM_TOKEN", token) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_base.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/pref_station_selection_mode_entries_last 5 | @string/pref_station_selection_mode_entries_closest 6 | @string/pref_station_selection_mode_entries_defined 7 | 8 | 9 | @string/pref_station_default_value_last 10 | @string/pref_station_default_value_closest 11 | @string/pref_station_default_value_defined 12 | 13 | 14 | 15 | @string/pref_percent_entry_day 16 | @string/pref_percent_entry_hour 17 | 18 | 19 | @string/pref_percent_value_day 20 | @string/pref_percent_value_hour 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings_general.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 17 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/Api.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog 2 | 3 | import pl.malopolska.smoksmog.model.Description 4 | import pl.malopolska.smoksmog.model.Station 5 | import retrofit2.http.GET 6 | import retrofit2.http.Path 7 | import rx.Observable 8 | 9 | interface Api { 10 | 11 | @GET("stations") 12 | fun stations(): Observable> 13 | 14 | @GET("stations/{stationId}") 15 | fun station(@Path("stationId") stationId: Long): Observable 16 | 17 | @GET("stations/{lat}/{lon}") 18 | fun stationByLocation(@Path("lat") latitude: Double, @Path("lon") longitude: Double): Observable 19 | 20 | @GET("stations/{stationId}/history") 21 | fun stationHistory(@Path("stationId") stationId: Long): Observable 22 | 23 | @GET("stations/{lat}/{lon}/history") 24 | fun stationHistoryByLocation(@Path("lat") latitude: Double, @Path("lon") longitude: Double): Observable 25 | 26 | @GET("particulates/{id}/desc") 27 | fun particulateDescription(@Path("id") particulateId: Long): Observable 28 | } 29 | -------------------------------------------------------------------------------- /android/src/test/java/smoksmog/air/AirQualityIndexTest.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.air 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.Test 5 | 6 | 7 | class AirQualityIndexTest { 8 | 9 | @Test 10 | fun firstRangeCorrectResult() { 11 | var value = 0f 12 | do { 13 | val index = AirQualityIndex.calculateBenzene(value) 14 | assertThat(index).isLessThan(1f) 15 | value += 0.001f 16 | } while (value < 5f) 17 | } 18 | 19 | @Test 20 | fun secondRangeCorrectResult() { 21 | var value = 5f 22 | do { 23 | val index = AirQualityIndex.calculateBenzene(value) 24 | assertThat(index).isLessThan(7f) 25 | value += 0.001f 26 | } while (value < 20f) 27 | } 28 | 29 | @Test 30 | fun thirdRangeCorrectResult() { 31 | var value = 20f 32 | do { 33 | val index = AirQualityIndex.calculateBenzene(value) 34 | assertThat(index).isLessThan(10f) 35 | value += 0.001f 36 | } while (value < 50f) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/CustomTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog; 2 | 3 | import android.os.AsyncTask; 4 | import android.support.test.runner.AndroidJUnitRunner; 5 | 6 | import rx.Scheduler; 7 | import rx.functions.Func1; 8 | import rx.plugins.RxJavaHooks; 9 | import rx.schedulers.Schedulers; 10 | 11 | /** 12 | * http://collectiveidea.com/blog/archives/2016/10/13/retrofitting-espresso 13 | */ 14 | public class CustomTestRunner extends AndroidJUnitRunner { 15 | 16 | private static final Func1 SCHEDULER = new Func1() { 17 | @Override 18 | public Scheduler call(Scheduler scheduler) { 19 | return Schedulers.from(AsyncTask.THREAD_POOL_EXECUTOR); 20 | } 21 | }; 22 | 23 | @Override 24 | public void onStart() { 25 | RxJavaHooks.setOnIOScheduler(SCHEDULER); 26 | RxJavaHooks.setOnNewThreadScheduler(SCHEDULER); 27 | super.onStart(); 28 | } 29 | 30 | @Override 31 | public void onDestroy() { 32 | super.onDestroy(); 33 | RxJavaHooks.reset(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/kotlin/com/antyzero/smoksmog/changelog/ChangelogReaderTest.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.changelog 2 | 3 | import com.antyzero.smoksmog.changelog.model.ChangeType.FIX 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.junit.Test 6 | import java.io.File 7 | import java.util.* 8 | 9 | 10 | class ChangelogReaderTest { 11 | 12 | val localePolish = Locale("pl", "PL") 13 | 14 | @Test 15 | fun readChangelog() { 16 | val changelogReader = createChangelog(localePolish, "/changelog_simple.json") 17 | val changelog = changelogReader.changelog 18 | 19 | assertThat(changelog).isNotNull() 20 | 21 | println(changelog) 22 | 23 | with(changelog.versions) { 24 | assertThat(this).hasSize(2) 25 | assertThat(this[0].changes[0].type).isEqualTo(FIX) 26 | assertThat(this[0].changes[0].translations["pl"]).isNotNull() 27 | } 28 | } 29 | 30 | private fun createChangelog(locale: Locale, resourcePath: String) = ChangelogReader( 31 | locale, File(ChangelogReaderTest::class.java.getResource(resourcePath).toURI()) 32 | ) 33 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/logger/LoggerModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.logger 2 | 3 | import com.antyzero.smoksmog.BuildConfig 4 | import com.antyzero.smoksmog.logger.CrashlyticsLogger.ExceptionLevel.ERROR 5 | import com.antyzero.smoksmog.user.User 6 | import com.crashlytics.android.core.CrashlyticsCore 7 | import dagger.Module 8 | import dagger.Provides 9 | import smoksmog.logger.AndroidLogger 10 | import smoksmog.logger.Logger 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | @Module 15 | class LoggerModule { 16 | 17 | @Provides 18 | @Singleton 19 | internal fun provideLogger(callback: CrashlyticsLogger.ConfigurationCallback): Logger { 20 | return if (BuildConfig.DEBUG) AndroidLogger() else CrashlyticsLogger(ERROR, callback) 21 | } 22 | 23 | @Provides 24 | @Singleton 25 | internal fun provideConfigurationCallback(user: User): CrashlyticsLogger.ConfigurationCallback { 26 | return object : CrashlyticsLogger.ConfigurationCallback { 27 | override fun onConfiguration(instance: CrashlyticsCore) { 28 | instance.setUserIdentifier(user.identifier) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/error/SnackBarErrorReporter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.error 2 | 3 | import android.app.Activity 4 | import android.support.annotation.StringRes 5 | import android.support.design.widget.Snackbar 6 | 7 | /** 8 | * Error reporting via SnackBar 9 | */ 10 | class SnackBarErrorReporter(private val activity: Activity) : ErrorReporter { 11 | 12 | override fun report(message: String) { 13 | processSnackBar(Snackbar.make(activity.findViewById(android.R.id.content), message, DURATION)) 14 | } 15 | 16 | override fun report(@StringRes stringId: Int) { 17 | processSnackBar(Snackbar.make(activity.findViewById(android.R.id.content), stringId, DURATION)) 18 | } 19 | 20 | override fun report(@StringRes stringId: Int, vararg objects: Any) { 21 | val message = activity.resources.getString(stringId, *objects) 22 | processSnackBar(Snackbar.make(activity.findViewById(android.R.id.content), message, DURATION)) 23 | } 24 | 25 | private fun processSnackBar(snackBar: Snackbar) { 26 | snackBar.show() 27 | } 28 | 29 | companion object { 30 | private val DURATION = Snackbar.LENGTH_LONG 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/time/EnglishCountdown.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n.time 2 | 3 | class EnglishCountdown : Countdown { 4 | 5 | override fun get(givenSeconds: Int): String { 6 | 7 | val seconds = givenSeconds and 60 8 | val minutes = givenSeconds / 60 % 60 9 | val hours = givenSeconds / 60 / 60 10 | 11 | if (minutes == 0) { 12 | return secondsAgo(seconds) 13 | } else if (hours == 0) { 14 | return minutesAgo(minutes) 15 | } 16 | 17 | return hoursAgo(hours) 18 | } 19 | 20 | private fun hoursAgo(hours: Int): String { 21 | return ago(hours, "hour", "hours") 22 | } 23 | 24 | private fun minutesAgo(minutes: Int): String { 25 | return ago(minutes, "minute", "minutes") 26 | } 27 | 28 | private fun secondsAgo(seconds: Int): String { 29 | return ago(seconds, "seconds", "seconds") 30 | } 31 | 32 | private fun ago(amount: Int, single: String, many: String): String { 33 | if (amount == 1) { 34 | return amount.toString() + " " + single 35 | } else { 36 | return amount.toString() + " " + many 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/screen/HistoryActivityTestRule.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.screen; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | 7 | import com.antyzero.smoksmog.ui.screen.history.HistoryActivity; 8 | 9 | public class HistoryActivityTestRule extends MockedNetworkActivityTestRule { 10 | 11 | public HistoryActivityTestRule() { 12 | super(HistoryActivity.class); 13 | } 14 | 15 | public HistoryActivityTestRule(boolean initialTouchMode) { 16 | super(HistoryActivity.class, initialTouchMode); 17 | } 18 | 19 | public HistoryActivityTestRule(boolean initialTouchMode, boolean launchActivity) { 20 | super(HistoryActivity.class, initialTouchMode, launchActivity); 21 | } 22 | 23 | @Override 24 | protected Intent getActivityIntent() { 25 | Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 26 | try { 27 | return HistoryActivity.Companion.intent(context, 13); 28 | } catch (Exception e) { 29 | throw new IllegalStateException("Unable to create Intent"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/CompatExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.os.Build 7 | import android.support.annotation.ColorRes 8 | import android.support.annotation.StringRes 9 | import android.support.v4.content.ContextCompat 10 | import android.text.Html 11 | import android.widget.TextView 12 | import java.util.* 13 | 14 | /** 15 | * Extensions used with 16 | * - compat utilities 17 | * - for backward compatibility 18 | * - in case od deprecated methods 19 | */ 20 | 21 | @SuppressLint("NewApi") 22 | fun Context.getCompatColor(@ColorRes colorId: Int): Int = when (Build.VERSION.SDK_INT) { 23 | in 1..Build.VERSION_CODES.LOLLIPOP_MR1 -> ContextCompat.getColor(this, colorId) 24 | else -> getColor(colorId) // API23 25 | } 26 | 27 | @Suppress("DEPRECATION") 28 | @SuppressLint("NewApi") 29 | fun TextView.compatFromHtml(@StringRes id: Int) { 30 | text = when (Build.VERSION.SDK_INT) { 31 | in 1..Build.VERSION_CODES.M -> Html.fromHtml(context.getString(id)) 32 | else -> Html.fromHtml(context.getString(id), Html.FROM_HTML_MODE_LEGACY) // API24 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/ActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import com.antyzero.smoksmog.google.GoogleModule 4 | import com.antyzero.smoksmog.ui.screen.about.AboutActivity 5 | import com.antyzero.smoksmog.ui.screen.history.HistoryActivity 6 | import com.antyzero.smoksmog.ui.screen.order.OrderActivity 7 | import com.antyzero.smoksmog.ui.screen.start.StartActivity 8 | import com.antyzero.smoksmog.ui.screen.start.fragment.LocationStationFragmentComponent 9 | import com.antyzero.smoksmog.ui.widget.StationWidgetConfigureActivity 10 | import dagger.Subcomponent 11 | 12 | @Subcomponent(modules = arrayOf(ActivityModule::class)) 13 | interface ActivityComponent { 14 | 15 | operator fun plus(fragmentModule: FragmentModule): FragmentComponent 16 | 17 | fun plus(fragmentModule: FragmentModule, googleModule: GoogleModule): LocationStationFragmentComponent 18 | 19 | fun inject(activity: HistoryActivity) 20 | 21 | fun inject(activity: AboutActivity) 22 | 23 | fun inject(startActivity: StartActivity) 24 | 25 | fun inject(orderActivity: OrderActivity) 26 | 27 | fun inject(pickStationActivity: PickStationActivity) 28 | 29 | fun inject(stationWidgetConfigureActivity: StationWidgetConfigureActivity) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/res/values/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | stationDefault 5 | stationSelected 6 | stationClosest 7 | dragonShow 8 | 9 | 10 | last 11 | closest 12 | defined 13 | 14 | 15 | 16 | 17 | pref_key_percent 18 | 19 | 20 | 1 21 | 24 22 | @string/pref_percent_value_day 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Build 6 | import com.antyzero.smoksmog.R 7 | import com.antyzero.smoksmog.dsl.getCompatColor 8 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 9 | import uk.co.chrisjenx.calligraphy.CalligraphyContextWrapper 10 | 11 | abstract class BaseActivity : RxAppCompatActivity() { 12 | 13 | override fun attachBaseContext(newBase: Context) { 14 | super.attachBaseContext(if (addCalligraphy()) { 15 | CalligraphyContextWrapper.wrap(newBase) 16 | } else { 17 | newBase 18 | }) 19 | } 20 | 21 | open protected fun addCalligraphy() = true 22 | 23 | companion object { 24 | 25 | /** 26 | * Shared initialization among activities, use it you cannot extend BaseActivity 27 | 28 | * @param activity for access to various data 29 | */ 30 | fun initOnCreate(activity: Activity) { 31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 32 | activity.window.navigationBarColor = activity.getCompatColor(R.color.primary) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import android.app.Activity 4 | 5 | import com.antyzero.smoksmog.error.ErrorReporter 6 | import com.antyzero.smoksmog.error.SnackBarErrorReporter 7 | import com.antyzero.smoksmog.firebase.FirebaseEvents 8 | import com.google.firebase.analytics.FirebaseAnalytics 9 | 10 | import dagger.Module 11 | import dagger.Provides 12 | 13 | @Module 14 | class ActivityModule(private val activity: Activity) { 15 | private val firebaseAnalytics: FirebaseAnalytics 16 | 17 | init { 18 | this.firebaseAnalytics = FirebaseAnalytics.getInstance(activity) 19 | } 20 | 21 | @Provides 22 | internal fun provideFirebaseAnalytics(): FirebaseAnalytics { 23 | return firebaseAnalytics 24 | } 25 | 26 | @Provides 27 | internal fun provideFirebaseEvents(firebaseAnalytics: FirebaseAnalytics): FirebaseEvents { 28 | return FirebaseEvents(firebaseAnalytics) 29 | } 30 | 31 | @Provides 32 | internal fun provideActivity(): Activity { 33 | return activity 34 | } 35 | 36 | @Provides 37 | internal fun provideErrorReporter(activity: Activity): ErrorReporter { 38 | return SnackBarErrorReporter(activity) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/user/User.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.user 2 | 3 | import android.content.Context 4 | import java.util.* 5 | 6 | /** 7 | * User management 8 | */ 9 | class User(context: Context) { 10 | 11 | val identifier: String 12 | 13 | init { 14 | 15 | val preferences = context.getSharedPreferences(PREFERENCES_USER, Context.MODE_PRIVATE) 16 | 17 | if (!preferences.contains(KEY_USER_ID)) { 18 | identifier = createIdentifier() 19 | preferences.edit().putString(KEY_USER_ID, identifier).apply() 20 | } else { 21 | identifier = preferences.getString(KEY_USER_ID, DEF_VALUE) 22 | 23 | if (identifier == DEF_VALUE) { 24 | throw IllegalStateException("Missing identifier for user") 25 | } 26 | } 27 | } 28 | 29 | private fun createIdentifier(): String { 30 | val value = Random().nextInt(Integer.MAX_VALUE) 31 | val hashCode = value.toString().hashCode() 32 | return "ID-" + Math.abs(hashCode).toString() 33 | } 34 | 35 | companion object { 36 | 37 | private val PREFERENCES_USER = "USER" 38 | private val KEY_USER_ID = "KEY_USER_ID_V3" 39 | private val DEF_VALUE = "DEF_VALUE" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.antyzero.smoksmog.permission.PermissionHelper 6 | import com.antyzero.smoksmog.tracking.Tracking 7 | import com.antyzero.smoksmog.ui.typeface.TypefaceProvider 8 | import dagger.Module 9 | import dagger.Provides 10 | import javax.inject.Singleton 11 | 12 | @Singleton 13 | @Module 14 | class ApplicationModule(private val application: Application) { 15 | 16 | @Provides 17 | @Singleton 18 | internal fun provideContext(): Context { 19 | return application 20 | } 21 | 22 | @Provides 23 | @Singleton 24 | internal fun provideApplication(): Application { 25 | return application 26 | } 27 | 28 | @Provides 29 | @Singleton 30 | internal fun provideTypefaceProvider(context: Context): TypefaceProvider { 31 | return TypefaceProvider(context) 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | internal fun provideTracker(context: Context): Tracking { 37 | return Tracking(context) 38 | } 39 | 40 | @Provides 41 | @Singleton 42 | internal fun providePermissionHelper(context: Context): PermissionHelper { 43 | return PermissionHelper(context) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/dialog/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.dialog 2 | 3 | import android.app.Dialog 4 | import android.content.DialogInterface 5 | import android.os.Bundle 6 | import android.support.v4.app.DialogFragment 7 | import android.support.v7.app.AlertDialog 8 | import android.support.v7.widget.Toolbar 9 | import android.view.View 10 | import com.antyzero.smoksmog.dsl.setNegativeButton 11 | import com.antyzero.smoksmog.dsl.setPositiveButton 12 | import com.antyzero.smoksmog.dsl.setToolbar 13 | 14 | abstract class BaseDialog : DialogFragment() { 15 | 16 | lateinit protected var toolbar: Toolbar 17 | 18 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 19 | return AlertDialog.Builder(activity).apply { 20 | toolbar = setToolbar(dataToolbarTitle()) 21 | setView(dataContent()) 22 | setPositiveButton(dataPositiveButton()) 23 | setNegativeButton(dataNegativeButton()) 24 | }.create() 25 | } 26 | 27 | abstract protected fun dataToolbarTitle(): String 28 | abstract protected fun dataPositiveButton(): Pair 29 | abstract protected fun dataNegativeButton(): Pair 30 | abstract protected fun dataContent(): View 31 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/order/SimpleItemTouchHelperCallback.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.order 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.support.v7.widget.helper.ItemTouchHelper 5 | 6 | class SimpleItemTouchHelperCallback(private val adapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() { 7 | 8 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { 9 | val drawFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN 10 | val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END 11 | return ItemTouchHelper.Callback.makeMovementFlags(drawFlags, swipeFlags) 12 | } 13 | 14 | override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { 15 | adapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition) 16 | return true 17 | } 18 | 19 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 20 | adapter.onItemDismiss(viewHolder.adapterPosition) 21 | } 22 | 23 | override fun isLongPressDragEnabled(): Boolean { 24 | return true 25 | } 26 | 27 | override fun isItemViewSwipeEnabled(): Boolean { 28 | return true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/DimenUtils.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.content.Context 4 | import android.support.annotation.DimenRes 5 | 6 | private const val idNavBar = "navigation_bar_height" 7 | private const val idStatusBar = "status_bar_height" 8 | 9 | fun Context.navBarHeight(): Int { 10 | val resources = this.resources 11 | val resourceId = resources.getIdentifier(idNavBar, "dimen", "android") 12 | return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 13 | } 14 | 15 | fun Context.navBarHeight(@DimenRes defaultRes: Int): Int { 16 | val resources = this.resources 17 | val resourceId = resources.getIdentifier(idNavBar, "dimen", "android") 18 | return resources.getDimensionPixelSize(if (resourceId > 0) resourceId else defaultRes) 19 | } 20 | 21 | fun Context.getStatusBarHeight(@DimenRes defaultRes: Int): Int { 22 | val resources = this.resources 23 | val resourceId = resources.getIdentifier(idStatusBar, "dimen", "android") 24 | return resources.getDimensionPixelSize(if (resourceId > 0) resourceId else defaultRes) 25 | } 26 | 27 | fun Context.getStatusBarHeight(): Int { 28 | val resources = this.resources 29 | val resourceId = resources.getIdentifier(idStatusBar, "dimen", "android") 30 | return resources.getDimensionPixelSize(resourceId) 31 | } -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/SmokSmog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import com.antyzero.smoksmog.location.Location 4 | import com.antyzero.smoksmog.location.LocationProvider 5 | import com.antyzero.smoksmog.model.Page 6 | import com.antyzero.smoksmog.storage.PersistentStorage 7 | import com.antyzero.smoksmog.storage.model.Item 8 | import pl.malopolska.smoksmog.Api 9 | import pl.malopolska.smoksmog.model.Station 10 | import rx.Observable 11 | 12 | class SmokSmog(val api: Api, val storage: PersistentStorage, val locationProvider: LocationProvider) { 13 | 14 | fun collectData(): Observable = Observable.from(storage.fetchAll()) 15 | .flatMap { collectDataForItem(it) } 16 | 17 | fun collectDataForItem(item: Item): Observable = when (item) { 18 | is Item.Station -> api.station(item.id) 19 | is Item.Nearest -> nearestStation() 20 | else -> throw IllegalStateException("Unsupported item type $item") 21 | }.zipWith(Observable.just(item)) { station, item -> Page(item, station) }.limit(1) 22 | 23 | fun nearestStation(): Observable = locationProvider.location() 24 | .filter { it is Location.Position } 25 | .cast(Location.Position::class.java) 26 | .flatMap { api.stationByLocation(it.coordinates.first, it.coordinates.second) } 27 | .limit(1) 28 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.settings 2 | 3 | 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import com.antyzero.smoksmog.R 8 | import com.antyzero.smoksmog.ui.BaseActivity 9 | import kotlinx.android.synthetic.main.activity_settings.* 10 | 11 | class SettingsActivity : BaseActivity() { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_settings) 16 | setSupportActionBar(toolbar) 17 | 18 | if (supportActionBar != null) { 19 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) 20 | supportActionBar!!.setTitle(R.string.title_settings) 21 | } 22 | 23 | fragmentManager.beginTransaction() 24 | .replace(R.id.contentFragment, GeneralSettingsFragment()) 25 | .commit() 26 | } 27 | 28 | override fun addCalligraphy() = false 29 | 30 | companion object { 31 | 32 | fun start(context: Context) { 33 | context.startActivity(intent(context)) 34 | } 35 | 36 | private fun intent(context: Context): Intent { 37 | return Intent(context, SettingsActivity::class.java) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /network/src/test/resources/responseStation.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4", 3 | "name": "Krak\u00f3w - Aleja Krasi\u0144skiego", 4 | "particulates": [ 5 | { 6 | "id": "7", 7 | "name": "Py\u0142 zawieszony", 8 | "short_name": "PM\u2081\u2080", 9 | "value": "83.4571", 10 | "unit": "\u00b5g\/m\u00b3", 11 | "norm": "50", 12 | "date": "2015-08-25 12:11:03", 13 | "avg": "66.46", 14 | "position": "1" 15 | }, 16 | { 17 | "id": "8", 18 | "name": "Py\u0142 zawieszony 2,5", 19 | "short_name": "PM\u2081\u2080", 20 | "value": "33.4571", 21 | "unit": "\u00b5g\/m\u00b3", 22 | "norm": "50", 23 | "date": "2015-08-25 12:11:03", 24 | "avg": "26.46", 25 | "position": "1" 26 | }, 27 | { 28 | "id": "3", 29 | "name": "Dwutlenek azotu", 30 | "short_name": "NO\u2082", 31 | "value": "109.895", 32 | "unit": "\u00b5g\/m\u00b3", 33 | "norm": "200", 34 | "date": "2015-08-25 12:11:03", 35 | "avg": "84.94", 36 | "position": "5" 37 | }, 38 | { 39 | "id": "4", 40 | "name": "Tlenek w\u0119gla", 41 | "short_name": "CO", 42 | "value": "1180.42", 43 | "unit": "\u00b5g\/m\u00b3", 44 | "norm": "10000", 45 | "date": "2015-08-25 12:11:03", 46 | "avg": "1083.54", 47 | "position": "6" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/logger/AndroidLogger.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.logger 2 | 3 | import android.util.Log 4 | 5 | /** 6 | * Typical Android SDK logging 7 | */ 8 | class AndroidLogger : Logger { 9 | 10 | override fun v(tag: String, message: String) { 11 | Log.v(tag, message) 12 | } 13 | 14 | override fun v(tag: String, message: String, throwable: Throwable) { 15 | Log.v(tag, message, throwable) 16 | } 17 | 18 | override fun d(tag: String, message: String) { 19 | Log.d(tag, message) 20 | } 21 | 22 | override fun d(tag: String, message: String, throwable: Throwable) { 23 | Log.d(tag, message, throwable) 24 | } 25 | 26 | override fun i(tag: String, message: String) { 27 | Log.i(tag, message) 28 | } 29 | 30 | override fun i(tag: String, message: String, throwable: Throwable) { 31 | Log.i(tag, message, throwable) 32 | } 33 | 34 | override fun w(tag: String, message: String) { 35 | Log.w(tag, message) 36 | } 37 | 38 | override fun w(tag: String, message: String, throwable: Throwable) { 39 | Log.w(tag, message, throwable) 40 | } 41 | 42 | override fun e(tag: String, message: String) { 43 | Log.v(tag, message) 44 | } 45 | 46 | override fun e(tag: String, message: String, throwable: Throwable) { 47 | Log.v(tag, message, throwable) 48 | } 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.travis.yml-disabled: -------------------------------------------------------------------------------- 1 | language: android 2 | sudo: false 3 | 4 | env: 5 | global: 6 | - JAVA7_HOME=/usr/lib/jvm/java-7-oracle 7 | - JAVA8_HOME=/usr/lib/jvm/java-8-oracle 8 | - JAVA_HOME=$JAVA7_HOME 9 | - ANDROID_HOME=/usr/local/android-sdk 10 | matrix: 11 | - ANDROID_TARGET=android-18 ANDROID_ABI=armeabi-v7a 12 | 13 | jdk: 14 | - oraclejdk8 15 | 16 | cache: 17 | directories: 18 | - $HOME/.gradle/caches 19 | - $HOME/.gradle/daemon 20 | 21 | android: 22 | coponents: 23 | - platform-tools 24 | - tools 25 | - extra-google-google_play_services 26 | - extra-google-m2repository 27 | - extra-android-m2repository 28 | licenses: 29 | - 'android-sdk-preview-license-52d11cd2' 30 | - 'android-sdk-license-.+' 31 | - 'google-gdk-license-.+' 32 | 33 | script: 34 | - ./gradlew check assemble --stacktrace 35 | 36 | before_script: 37 | - mkdir "$ANDROID_HOME/licenses" || true 38 | - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" 39 | - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" 40 | - rm -rf ./build ./android/build 41 | 42 | after_success: 43 | - ./gradlew jacocoFullReport 44 | - pip install --user codecov 45 | - codecov 46 | 47 | after_failure: 48 | - cat /home/travis/build/SmokSmog/smoksmog-android/android/build/outputs/lint-results-debug.xml -------------------------------------------------------------------------------- /network/src/test/java/pl/malopolska/smoksmog/RestClientTest.java: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog; 2 | 3 | import org.junit.Test; 4 | 5 | import java.text.Collator; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Locale; 10 | 11 | import pl.malopolska.smoksmog.model.Station; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | public class RestClientTest { 16 | 17 | @Test 18 | public void testCreateServerUrlWithLocale() throws Exception { 19 | 20 | // given 21 | Locale locale = Locale.ENGLISH; 22 | RestClient restClient = new RestClient.Builder(locale).build(); 23 | 24 | // when 25 | String result = restClient.getEndpoint(); 26 | 27 | // then 28 | assertThat(result).isEqualTo("http://api.smoksmog.jkostrz.name/" + locale.getLanguage() + "/"); 29 | } 30 | 31 | @Test 32 | public void stations() throws Exception { 33 | 34 | List stations = new RestClient.Builder().build().stations().toBlocking().first(); 35 | List names = new ArrayList<>(); 36 | 37 | for (Station station : stations) { 38 | names.add(station.getName()); 39 | } 40 | 41 | Collections.sort(names, Collator.getInstance()); 42 | 43 | for(String name : names){ 44 | System.out.print(name + ", "); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/screen/SettingsActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.screen; 2 | 3 | 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.rule.ActivityTestRule; 6 | import android.support.test.runner.AndroidJUnit4; 7 | 8 | import com.antyzero.smoksmog.rules.RxSchedulerTestRule; 9 | import com.antyzero.smoksmog.rules.SpoonRule; 10 | import com.antyzero.smoksmog.ui.screen.settings.SettingsActivity; 11 | 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.rules.RuleChain; 15 | import org.junit.rules.TestRule; 16 | import org.junit.runner.RunWith; 17 | 18 | @RunWith(AndroidJUnit4.class) 19 | public class SettingsActivityTest { 20 | 21 | @Rule 22 | public final RxSchedulerTestRule rxSchedulerTestRule = new RxSchedulerTestRule(); 23 | private final ActivityTestRule activityTestRule = new MockedNetworkActivityTestRule<>(SettingsActivity.class); 24 | private final SpoonRule spoonRule = new SpoonRule(activityTestRule); 25 | @Rule 26 | public final TestRule testRule = RuleChain.outerRule(activityTestRule).around(spoonRule); 27 | 28 | @Test 29 | public void checkCreation() { 30 | 31 | // Given 32 | activityTestRule.getActivity(); 33 | 34 | // When 35 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 36 | 37 | // Then 38 | spoonRule.screenshot("Created"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/screen/HistoryActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.screen; 2 | 3 | 4 | import android.content.Intent; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.support.test.runner.AndroidJUnit4; 8 | 9 | import com.antyzero.smoksmog.rules.RxSchedulerTestRule; 10 | import com.antyzero.smoksmog.rules.SpoonRule; 11 | import com.antyzero.smoksmog.ui.screen.history.HistoryActivity; 12 | 13 | import org.junit.Rule; 14 | import org.junit.Test; 15 | import org.junit.rules.RuleChain; 16 | import org.junit.rules.TestRule; 17 | import org.junit.runner.RunWith; 18 | 19 | @RunWith(AndroidJUnit4.class) 20 | public class HistoryActivityTest { 21 | 22 | @Rule 23 | public final RxSchedulerTestRule rxSchedulerTestRule = new RxSchedulerTestRule(); 24 | private final ActivityTestRule activityTestRule = new HistoryActivityTestRule(true, false); 25 | private final SpoonRule spoonRule = new SpoonRule(activityTestRule); 26 | @Rule 27 | public final TestRule testRule = RuleChain.outerRule(activityTestRule).around(spoonRule); 28 | 29 | @Test 30 | public void checkCreation() { 31 | 32 | // given 33 | activityTestRule.launchActivity(HistoryActivity.Companion.fillIntent(new Intent(), 13)); 34 | 35 | // when 36 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 37 | 38 | // then 39 | spoonRule.screenshot("Created"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 21 | 22 | 28 | 29 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 25 | 26 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/utils/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.utils 2 | 3 | 4 | import android.text.Spannable 5 | import android.text.SpannableStringBuilder 6 | import android.text.style.RelativeSizeSpan 7 | import android.text.style.SubscriptSpan 8 | 9 | class TextUtils private constructor() { 10 | 11 | init { 12 | throw IllegalAccessError("Utils class") 13 | } 14 | 15 | companion object { 16 | 17 | fun spannableSubscript(originalText: String): CharSequence { 18 | 19 | val builder = SpannableStringBuilder() 20 | 21 | for (i in 0..originalText.length - 1) { 22 | val code = originalText.codePointAt(i) 23 | when (code) { 24 | in 8320..8329 -> { 25 | builder.append(String(Character.toChars(code - 8272))) 26 | makeCharSmaller(builder, i) 27 | } 28 | 46 -> { 29 | builder.append(originalText[i]) 30 | makeCharSmaller(builder, i) 31 | } 32 | else -> builder.append(originalText[i]) 33 | } 34 | } 35 | 36 | return builder 37 | } 38 | 39 | private fun makeCharSmaller(builder: SpannableStringBuilder, i: Int) { 40 | builder.setSpan(SubscriptSpan(), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 41 | builder.setSpan(RelativeSizeSpan(0.55f), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/dialog/FacebookDialog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.dialog 2 | 3 | import android.app.AlertDialog 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.view.View 7 | import com.antyzero.smoksmog.R 8 | import com.antyzero.smoksmog.SmokSmogApplication 9 | import com.crashlytics.android.answers.Answers 10 | import com.crashlytics.android.answers.CustomEvent 11 | import kotlinx.android.synthetic.main.dialog_info_facebook.* 12 | import javax.inject.Inject 13 | 14 | class FacebookDialog : InfoDialog() { 15 | 16 | @Inject lateinit var answers: Answers 17 | 18 | override fun getLayoutId(): Int = R.layout.dialog_info_facebook 19 | 20 | override fun updateBuilder(builder: AlertDialog.Builder): AlertDialog.Builder { 21 | builder.setPositiveButton("OK, pokaż") { dialog, which -> takeMeToFacebook() }.setNegativeButton("Nie, podziękuję") { dialog, which -> dismiss() } 22 | return builder 23 | } 24 | 25 | override fun initView(view: View) { 26 | super.initView(view) 27 | SmokSmogApplication[view.context].appComponent.inject(this) 28 | imageView.setOnClickListener { takeMeToFacebook() } 29 | } 30 | 31 | private fun takeMeToFacebook() { 32 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/SmokSmog")) 33 | startActivity(browserIntent) 34 | dismiss() 35 | answers.logCustom(FacebookClickedEvent()) 36 | } 37 | 38 | private class FacebookClickedEvent : CustomEvent(FacebookClickedEvent::class.java.simpleName) 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/SimpleStationAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.View.INVISIBLE 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import com.antyzero.smoksmog.R 10 | import pl.malopolska.smoksmog.model.Station 11 | 12 | 13 | class SimpleStationAdapter(val listStation: List, val onStationClick: OnStationClick) : RecyclerView.Adapter() { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 16 | val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_order, parent, false) 17 | return ViewHolder(itemView) 18 | } 19 | 20 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 21 | holder.itemView.setOnClickListener { onStationClick.click(listStation[position]) } 22 | holder.textView.text = listStation[position].name 23 | } 24 | 25 | override fun getItemCount(): Int { 26 | return listStation.size 27 | } 28 | } 29 | 30 | interface OnStationClick { 31 | fun click(station: Station) 32 | } 33 | 34 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 35 | 36 | var textView: TextView 37 | var viewHandle: View 38 | 39 | init { 40 | textView = itemView.findViewById(R.id.textView) as TextView 41 | viewHandle = itemView.findViewById(R.id.viewHandle) 42 | viewHandle.visibility = INVISIBLE 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | Jeżeli chcesz nam pomóc podziel się uwagami bądź sugestiami dotyczącymi SmokSmoga dla Androida, na tej stronie

6 | 7 | https://groups.google.com/forum/#!forum/smoksmog-android

8 | 9 | lub zwyczajnie wysłać nam maila na adres

10 | 11 | smoksmog-android@googlegroups.com

12 | 13 | Będziemy wdzięczni za kontakt z nami przed napisaniem opinii na Google Play, nie dlatego, że nie lubimy tych opinii :-) ale dlatego, że często nie możemy, w wypadku problemów, nawiązać kontaktu z jej autorem.

14 | 15 | Oficjalną stronę projektu SmokSmog znajdziesz pod poniższym linkiem

16 | 17 | http://smoksmog.malopolska.pl/

18 | 19 | SmokSmog na Androida jest projektem Open Source, czyli kod źródłowy jest jawny i dostępny dla wszystkich, którzy chcieliby zweryfikować naszą pracę lub współtworzyć nasz projekt

20 | 21 | https://github.com/SmokSmog/smoksmog-android ]]>
22 |
-------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/DateTimeDeserializer.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog 2 | 3 | import com.google.gson.JsonDeserializationContext 4 | import com.google.gson.JsonDeserializer 5 | import com.google.gson.JsonElement 6 | import com.google.gson.JsonParseException 7 | import org.joda.time.DateTime 8 | import org.joda.time.DateTimeZone 9 | import java.lang.reflect.Type 10 | import java.util.* 11 | 12 | class DateTimeDeserializer : JsonDeserializer { 13 | 14 | override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): DateTime { 15 | 16 | val input = json.asString 17 | 18 | val matcher = "(\\d+)-(\\d+)-(\\d+)\\s+?(\\d+):(\\d+):(\\d+)".toPattern().matcher(input) 19 | 20 | if (!matcher.matches()) { 21 | throw JsonParseException("Invalid date format") 22 | } 23 | 24 | val year = Integer.parseInt(matcher.group(1)) 25 | val month = Integer.parseInt(matcher.group(2)) 26 | val day = Integer.parseInt(matcher.group(3)) 27 | val hour = Integer.parseInt(matcher.group(4)) 28 | val minute = Integer.parseInt(matcher.group(5)) 29 | val second = Integer.parseInt(matcher.group(6)) 30 | 31 | val dateTime = DateTime.now(DateTimeZone.forTimeZone(TimeZone.getTimeZone("Europe/Warsaw"))) 32 | .withYear(year) 33 | .withMonthOfYear(month) 34 | .withDayOfMonth(day) 35 | .withHourOfDay(hour) 36 | .withMinuteOfHour(minute) 37 | .withSecondOfMinute(second) 38 | 39 | return dateTime 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/dsl/DialogFramgmentExtension.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.dsl 2 | 3 | import android.app.FragmentManager 4 | import android.content.DialogInterface 5 | import android.support.v4.app.DialogFragment 6 | import android.support.v4.app.Fragment 7 | import android.support.v7.app.AlertDialog 8 | import android.support.v7.app.AppCompatActivity 9 | import android.support.v7.widget.Toolbar 10 | import android.view.LayoutInflater 11 | import com.antyzero.smoksmog.R 12 | 13 | fun AlertDialog.Builder.setToolbar(toolbarText: String): Toolbar { 14 | val toolbar = LayoutInflater.from(context).inflate(R.layout.dialog_toolbar, null) as Toolbar 15 | toolbar.setTitleTextColor(context.getCompatColor(smoksmog.R.color.text_light)) 16 | toolbar.setBackgroundColor(context.getCompatColor(smoksmog.R.color.primaryDark)) 17 | toolbar.title = toolbarText 18 | this.setCustomTitle(toolbar) 19 | return toolbar 20 | } 21 | 22 | fun AlertDialog.Builder.setPositiveButton(pair: Pair): AlertDialog.Builder { 23 | setPositiveButton(pair.first, pair.second) 24 | return this 25 | } 26 | 27 | fun AlertDialog.Builder.setNegativeButton(pair: Pair): AlertDialog.Builder { 28 | setNegativeButton(pair.first, pair.second) 29 | return this 30 | } 31 | 32 | fun Fragment.layoutInflater() = activity.layoutInflater 33 | 34 | fun DialogFragment.show(appCompatActivity: AppCompatActivity, tag: String) = this.show(appCompatActivity.supportFragmentManager, tag) 35 | 36 | fun android.app.DialogFragment.show(fragmentManager: FragmentManager) = this.show(fragmentManager, this.tag()) -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/firebase/FirebaseEvents.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.firebase 2 | 3 | import android.os.Bundle 4 | import com.google.firebase.analytics.FirebaseAnalytics 5 | 6 | class FirebaseEvents(private val firebaseAnalytics: FirebaseAnalytics) { 7 | 8 | fun logStationCardInView(stationId: Long) { 9 | firebaseAnalytics.logEvent(FirebaseAnalytics.Event.VIEW_ITEM, Bundle().apply { 10 | setItemId(stationId) 11 | setContentType(Content.STATION) 12 | // TODO station name ? 13 | }) 14 | } 15 | 16 | fun logWidgetCreationStarted() { 17 | firebaseAnalytics.logEvent("widget-station-creation-started", Bundle()) 18 | } 19 | 20 | fun logWidgetCreationStation(stationId: Long, stationName: String) { 21 | firebaseAnalytics.logEvent("widget-station-creation-station-id", Bundle().apply { 22 | setItemId(stationId) 23 | setItemName(stationName) 24 | }) 25 | } 26 | 27 | fun logWidgetCreationSuccessful() { 28 | firebaseAnalytics.logEvent("widget-station-creation-successful", Bundle()) 29 | } 30 | } 31 | 32 | private fun Bundle.setContentType(content: Content) = this.putString(FirebaseAnalytics.Param.CONTENT_TYPE, content.toString()) 33 | private fun Bundle.setItemId(id: Long) = putLong(FirebaseAnalytics.Param.ITEM_ID, id) 34 | private fun Bundle.setItemName(stationName: String) = this.putString(FirebaseAnalytics.Param.ITEM_NAME, stationName) 35 | 36 | private enum class Content(private val contentName: String) { 37 | 38 | STATION("station"); 39 | 40 | override fun toString(): String { 41 | return contentName 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/i18n/time/PolishCountdown.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.i18n.time 2 | 3 | class PolishCountdown : Countdown { 4 | 5 | override fun get(givenSeconds: Int): String { 6 | 7 | val seconds = givenSeconds and 60 8 | val minutes = givenSeconds / 60 % 60 9 | val hours = givenSeconds / 60 / 60 10 | 11 | if (minutes == 0) { 12 | return secondsAgo(seconds) 13 | } else if (hours == 0) { 14 | return minutesAgo(minutes) 15 | } 16 | 17 | return hoursAgo(hours) 18 | } 19 | 20 | private fun hoursAgo(hours: Int): String { 21 | return ago(hours, "godzinę", "godziny", "godzin") 22 | } 23 | 24 | private fun minutesAgo(minutes: Int): String { 25 | return ago(minutes, "minutę", "minuty", "minut") 26 | } 27 | 28 | private fun secondsAgo(seconds: Int): String { 29 | return ago(seconds, "sekundę", "sekundy", "sekund") 30 | } 31 | 32 | private fun ago(amount: Int, single: String, some: String, many: String): String { 33 | if (amount == 1) { 34 | return amount.toString() + " " + single 35 | } else if (endsWithTwoToFour(amount)) { 36 | return amount.toString() + " " + some 37 | } else { 38 | return amount.toString() + " " + many 39 | } 40 | } 41 | 42 | private fun endsWithTwoToFour(seconds: Int): Boolean { 43 | val modulo = seconds % 10 44 | if (seconds >= 10 && seconds < 20) { 45 | return false 46 | } else if (modulo >= 2 && modulo <= 4) { 47 | return true 48 | } 49 | return false 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /network/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'kotlin' 3 | 4 | sourceCompatibility = JavaVersion.VERSION_1_7 5 | targetCompatibility = JavaVersion.VERSION_1_7 6 | 7 | //noinspection GroovyAssignabilityCheck 8 | sourceSets { 9 | main.java.srcDirs += 'src/main/kotlin' 10 | } 11 | 12 | dependencies { 13 | compile("com.squareup.retrofit2:retrofit:${libVersions.square.retrofit}") { 14 | exclude module: 'okhttp' 15 | } 16 | compile "com.squareup.retrofit2:converter-gson:${libVersions.square.retrofit}" 17 | compile "com.squareup.retrofit2:adapter-rxjava:${libVersions.square.retrofit}" 18 | 19 | compile "com.squareup.okhttp3:okhttp:${libVersions.square.okhttp}" 20 | compile "io.reactivex:rxjava:${libVersions.rx.java}" 21 | compile 'com.fatboyindustrial.gson-jodatime-serialisers:gson-jodatime-serialisers:1.2.0' 22 | compile "joda-time:joda-time:${libVersions.jodaTime}" 23 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 24 | 25 | testCompile "junit:junit:${libVersions.junit}" 26 | testCompile "org.assertj:assertj-core:${libVersions.assertj}" 27 | testCompile "com.squareup.okhttp3:mockwebserver:${libVersions.square.okhttp}" 28 | } 29 | 30 | /** 31 | * Optionally disable test failures 32 | */ 33 | test { 34 | ignoreFailures = rootProject.ext.ignoreFailures 35 | } 36 | 37 | /** 38 | * This will copy resources files so they will be accessible thought Java getResource() method 39 | * in Android Studio, without it test won't find *.json files 40 | */ 41 | task copyTestResources(type: Copy) { 42 | from "${project.projectDir}/src/test/resources" 43 | into "${project.buildDir}/classes/test" 44 | } 45 | 46 | processTestResources.dependsOn copyTestResources -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_info_air_quality.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 23 | 24 | 28 | 29 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/dialog/InfoDialog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.dialog 2 | 3 | 4 | import android.app.AlertDialog 5 | import android.app.Dialog 6 | import android.app.DialogFragment 7 | import android.app.FragmentManager 8 | import android.os.Bundle 9 | import android.view.View 10 | import com.antyzero.smoksmog.dsl.show 11 | 12 | /** 13 | * For info dialog 14 | */ 15 | abstract class InfoDialog : DialogFragment() { 16 | 17 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 18 | val builder = AlertDialog.Builder(activity) 19 | val view = activity.layoutInflater.inflate(getLayoutId(), null, false) 20 | initView(view) 21 | builder.setView(view) 22 | builder.setPositiveButton(android.R.string.ok) { dialog, which -> dialog.dismiss() } 23 | return updateBuilder(builder).create() 24 | } 25 | 26 | protected open fun updateBuilder(builder: AlertDialog.Builder): AlertDialog.Builder { 27 | return builder 28 | } 29 | 30 | protected open fun initView(view: View) { 31 | // override if needed 32 | } 33 | 34 | protected abstract fun getLayoutId(): Int 35 | 36 | class Event(internal val dialogFragment: Class) 37 | 38 | companion object { 39 | 40 | fun show(fragmentManager: FragmentManager, event: Event<*>) { 41 | val infoDialog: InfoDialog 42 | 43 | try { 44 | infoDialog = event.dialogFragment.newInstance() as InfoDialog 45 | } catch (e: Exception) { 46 | throw IllegalStateException( 47 | "Problem with creating fragment dialog " + event.dialogFragment.simpleName, e) 48 | } 49 | 50 | infoDialog.show(fragmentManager) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/StationSlideAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start 2 | 3 | import android.app.FragmentManager 4 | import android.support.v13.app.FragmentStatePagerAdapter 5 | import android.support.v4.view.PagerAdapter 6 | import android.util.SparseArray 7 | import android.view.ViewGroup 8 | import com.antyzero.smoksmog.storage.model.Item 9 | 10 | import com.antyzero.smoksmog.ui.screen.start.fragment.StationFragment 11 | 12 | import java.lang.ref.WeakReference 13 | 14 | /** 15 | * Adapter for sliding pages left-right 16 | */ 17 | class StationSlideAdapter(fragmentManager: FragmentManager, private val stationIds: List) : FragmentStatePagerAdapter(fragmentManager) { 18 | 19 | private val fragmentRegister = SparseArray?>() 20 | 21 | override fun getItemPosition(`object`: Any?): Int { 22 | return PagerAdapter.POSITION_NONE 23 | } 24 | 25 | override fun instantiateItem(container: ViewGroup, position: Int): Any { 26 | val fragment = super.instantiateItem(container, position) as StationFragment 27 | fragmentRegister.put(position, WeakReference(fragment)) 28 | return fragment 29 | } 30 | 31 | override fun destroyItem(container: ViewGroup?, position: Int, `object`: Any) { 32 | fragmentRegister.remove(position) 33 | super.destroyItem(container, position, `object`) 34 | } 35 | 36 | override fun getItem(position: Int): StationFragment { 37 | return StationFragment.newInstance(stationIds[position].id) 38 | } 39 | 40 | fun getFragmentReference(position: Int): WeakReference? { 41 | return fragmentRegister.get(position) 42 | } 43 | 44 | override fun getCount(): Int { 45 | return stationIds.size 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_order.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /network/src/test/java/pl/malopolska/smoksmog/TestUtils.java: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import java.nio.charset.Charset; 6 | import java.nio.file.Files; 7 | import java.nio.file.Paths; 8 | import java.util.Collection; 9 | import java.util.Iterator; 10 | import java.util.List; 11 | 12 | 13 | public class TestUtils { 14 | 15 | private TestUtils() { 16 | throw new IllegalAccessError("Utils class"); 17 | } 18 | 19 | public static String readFromResources(String pathFile) { 20 | 21 | String path = null; 22 | 23 | try { 24 | final URL resource = TestUtils.class.getResource(pathFile); 25 | 26 | if (resource == null) { 27 | throw new IllegalArgumentException("Unable to find resource for path \"" + pathFile + "\""); 28 | } 29 | 30 | path = resource.getPath(); 31 | 32 | return readFileToString(path, Charset.defaultCharset()); 33 | } catch (Exception e) { 34 | throw new RuntimeException("Unable to read resource file at " + path, e); 35 | } 36 | } 37 | 38 | public static String readFileToString(String path, Charset charset) throws IOException { 39 | List lines = Files.readAllLines(Paths.get(path), charset); 40 | return join("", lines); 41 | } 42 | 43 | private static String join(String delimiter, Collection col) { 44 | StringBuilder sb = new StringBuilder(); 45 | Iterator iterator = col.iterator(); 46 | if (iterator.hasNext()) 47 | sb.append(iterator.next().toString()); 48 | while (iterator.hasNext()) { 49 | sb.append(delimiter); 50 | sb.append(iterator.next().toString()); 51 | } 52 | return sb.toString(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000 7 | #414152 8 | #515965 9 | #5A6273 10 | #CDCDCD 11 | #d6d6d6 12 | #FFFFFF 13 | 14 | #FFA439 15 | 16 | #5ADED5 17 | #2983AC 18 | 19 | #00cbff 20 | #00e600 21 | #ffff00 22 | #ff7e00 23 | #ff0000 24 | #800021 25 | 26 | 27 | 28 | #19d6d6d6 29 | #33d6d6d6 30 | 31 | 32 | 33 | @color/comet 34 | @color/brightGray 35 | @color/viking 36 | 37 | @color/celeste 38 | 39 | @color/white 40 | @color/scarpaFlow 41 | 42 | @color/neonCarrot 43 | @color/viking 44 | 45 | @color/capri 46 | @color/green 47 | @color/yellow 48 | @color/amber 49 | @color/red 50 | @color/burgundy 51 | 52 | @color/iron_10 53 | 54 | 55 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/antyzero/smoksmog/storage/model/Item.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.storage.model 2 | 3 | import com.google.gson.JsonDeserializer 4 | import com.google.gson.JsonObject 5 | import com.google.gson.JsonParseException 6 | 7 | sealed class Item(val id: Long, val modules: MutableSet) { 8 | 9 | @Suppress("unused") 10 | private val _class: String = javaClass.canonicalName 11 | 12 | /** 13 | * Single, if present this represent nearest station 14 | */ 15 | class Nearest(modules: MutableSet = mutableSetOf()) : Item(ID_NEAREST, modules) { 16 | 17 | fun copy(modules: MutableSet = this.modules): Nearest = Nearest(modules) 18 | } 19 | 20 | /** 21 | * Multiple, this represent station item 22 | */ 23 | class Station(id: Long = Long.MIN_VALUE, modules: MutableSet = mutableSetOf()) : Item(id, modules) { 24 | 25 | fun copy(id: Long = this.id, modules: MutableSet = this.modules): Station = Station(id, modules) 26 | } 27 | 28 | override fun equals(other: Any?): Boolean { 29 | if (this === other) return true 30 | if (other !is Item) return false 31 | 32 | if (id != other.id) return false 33 | 34 | return true 35 | } 36 | 37 | override fun hashCode(): Int = id.hashCode() 38 | 39 | companion object { 40 | 41 | private val ID_NEAREST = 0L 42 | 43 | fun deserializer(): JsonDeserializer = JsonDeserializer { json, typeOfT, context -> 44 | 45 | if (json is JsonObject) { 46 | val id = json.get("id").asLong 47 | return@JsonDeserializer when { 48 | id > 0 -> Station(id) 49 | id == 0L -> Nearest() 50 | else -> throw JsonParseException("Unsupported id value: $id") 51 | } 52 | } 53 | throw JsonParseException("Unable to parse") 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_station.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 22 | 23 | 29 | 30 | 38 | 39 | 40 | 41 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/about/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.about 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.text.method.LinkMovementMethod 7 | import com.antyzero.smoksmog.R 8 | import com.antyzero.smoksmog.SmokSmogApplication 9 | import com.antyzero.smoksmog.dsl.compatFromHtml 10 | import com.antyzero.smoksmog.ui.BaseActivity 11 | import com.antyzero.smoksmog.ui.screen.ActivityModule 12 | import kotlinx.android.synthetic.main.dialog_info_about.* 13 | import smoksmog.logger.Logger 14 | import javax.inject.Inject 15 | 16 | class AboutActivity : BaseActivity() { 17 | 18 | @Inject lateinit var logger: Logger 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | SmokSmogApplication[this].appComponent.plus(ActivityModule(this)).inject(this) 23 | setContentView(R.layout.dialog_info_about) 24 | 25 | textView.compatFromHtml(R.string.about) 26 | textView.movementMethod = LinkMovementMethod.getInstance() 27 | 28 | try { 29 | val packageInfo = packageManager.getPackageInfo(packageName, 0) 30 | textViewVersionName.text = getString(R.string.version_name_and_code, 31 | packageInfo.versionName, 32 | packageInfo.versionCode) 33 | } catch (e: Exception) { 34 | logger.i(TAG, "Problem with obtaining version", e) 35 | } 36 | 37 | setSupportActionBar(toolbar) 38 | 39 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 40 | } 41 | 42 | companion object { 43 | 44 | private val TAG = "AboutActivity" 45 | 46 | fun start(context: Context) { 47 | context.startActivity(intent(context)) 48 | } 49 | 50 | fun intent(context: Context): Intent { 51 | return Intent(context, AboutActivity::class.java) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_info_facebook.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 24 | 25 | 29 | 30 | 38 | 39 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy​ 2 | node { 3 | 4 | def ignoreFailures = (env.BRANCH_NAME != 'master') 5 | 6 | // Build start 7 | slackSend channel: 'quality', color: '#0080FF', message: "Started Android _${env.JOB_NAME}_ #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)", teamDomain: 'smoksmog', tokenCredentialId: 'smoksmok-slack' 8 | 9 | stage('Prepare'){ 10 | dir('./build/'){deleteDir()} 11 | dir('./android/build/'){deleteDir()} 12 | dir('./app/build/'){deleteDir()} 13 | dir('./domain/build/'){deleteDir()} 14 | dir('./network/build/'){deleteDir()} 15 | checkout scm 16 | } 17 | 18 | try { 19 | stage('Build'){ 20 | 21 | sh "./gradlew uninstallAll || true" 22 | sh "./gradlew assemble -PignoreFailures=" + ignoreFailures 23 | sh "wake-devices" 24 | sh "./gradlew check connectedCheck -PignoreFailures=" + ignoreFailures 25 | 26 | // Build successful 27 | slackSend channel: 'quality', color: '#80FF00', message: "Success Android _${env.JOB_NAME}_ #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)", teamDomain: 'smoksmog', tokenCredentialId: 'smoksmok-slack' 28 | } 29 | } catch (error) { 30 | // Build failed 31 | slackSend channel: 'quality', color: '#FF0000', message: "Failed Android _${env.JOB_NAME}_ #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)\n\n$error", teamDomain: 'smoksmog', tokenCredentialId: 'smoksmok-slack' 32 | throw new IllegalStateException(error, "Build failed") 33 | } 34 | 35 | stage('Artifacts'){ 36 | 37 | androidLint canComputeNew: false, canRunOnFailed: true, defaultEncoding: '', healthy: '', pattern: '**/build/outputs/lint-results-*.xml', unHealthy: '' 38 | 39 | publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'app/build/reports/androidTests/connected/', reportFiles: 'index.html', reportName: 'Android tests']) 40 | 41 | junit '**/build/**/TEST-*.xml' 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/dialog/AboutDialog.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.dialog 2 | 3 | import android.os.Bundle 4 | import android.text.method.LinkMovementMethod 5 | import android.view.View 6 | import android.widget.TextView 7 | import com.antyzero.smoksmog.R 8 | import com.antyzero.smoksmog.SmokSmogApplication 9 | import com.antyzero.smoksmog.dsl.compatFromHtml 10 | import com.antyzero.smoksmog.dsl.tag 11 | import com.antyzero.smoksmog.user.User 12 | import smoksmog.logger.Logger 13 | import javax.inject.Inject 14 | 15 | class AboutDialog : InfoDialog() { 16 | 17 | @Inject lateinit var logger: Logger 18 | @Inject lateinit var user: User 19 | 20 | lateinit var textView: TextView 21 | lateinit var textViewVersionName: TextView 22 | lateinit var textViewUserId: TextView 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | SmokSmogApplication[activity] 27 | .appComponent 28 | .inject(this) 29 | } 30 | 31 | override fun getLayoutId(): Int = R.layout.dialog_info_about 32 | 33 | override fun initView(view: View) { 34 | super.initView(view) 35 | 36 | with(view) { 37 | textView = findViewById(R.id.textView) as TextView 38 | textViewUserId = findViewById(R.id.textViewUserId) as TextView 39 | textViewVersionName = findViewById(R.id.textViewVersionName) as TextView 40 | } 41 | 42 | textView.compatFromHtml(R.string.about) 43 | textView.movementMethod = LinkMovementMethod.getInstance() 44 | 45 | try { 46 | val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) 47 | textViewVersionName.text = getString(R.string.version_name_and_code, 48 | packageInfo.versionName, 49 | packageInfo.versionCode) 50 | } catch (e: Exception) { 51 | logger.i(tag(), "Problem with obtaining version", e) 52 | } 53 | 54 | textViewUserId.text = user.identifier 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/screen/start/fragment/NetworkStationFragment.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.screen.start.fragment 2 | 3 | import android.os.Bundle 4 | import com.antyzero.smoksmog.R 5 | import com.antyzero.smoksmog.SmokSmogApplication 6 | import com.antyzero.smoksmog.ui.screen.ActivityModule 7 | import com.antyzero.smoksmog.ui.screen.FragmentModule 8 | import com.trello.rxlifecycle.android.FragmentEvent 9 | import pl.malopolska.smoksmog.model.Station 10 | import rx.android.schedulers.AndroidSchedulers 11 | import rx.schedulers.Schedulers 12 | 13 | class NetworkStationFragment : StationFragment() { 14 | 15 | override fun onActivityCreated(savedInstanceState: Bundle?) { 16 | super.onActivityCreated(savedInstanceState) 17 | 18 | val activity = activity 19 | 20 | SmokSmogApplication[activity].appComponent 21 | .plus(ActivityModule(activity)) 22 | .plus(FragmentModule(this)) 23 | .inject(this) 24 | 25 | loadData() 26 | } 27 | 28 | override fun loadData() { 29 | api.station(stationId) 30 | .doOnSubscribe { showLoading() } 31 | .subscribeOn(Schedulers.newThread()) 32 | .observeOn(AndroidSchedulers.mainThread()) 33 | .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) 34 | .cast(Station::class.java) 35 | .subscribe( 36 | { station -> updateUI(station) } 37 | ) { throwable -> 38 | try { 39 | showTryAgain(R.string.error_unable_to_load_station_data) 40 | } catch (e: Exception) { 41 | logger.e(TAG, "Problem with error handling code", e) 42 | } finally { 43 | logger.i(TAG, "Unable to load station data (stationID:$stationId)", throwable) 44 | } 45 | } 46 | } 47 | 48 | companion object { 49 | 50 | private val TAG = NetworkStationFragment::class.java.simpleName 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /domain/src/integrationTest/kotlin/com/antyzero/smoksmog/DataCollectionTest.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import com.antyzero.smoksmog.location.LocationProvider 4 | import com.antyzero.smoksmog.model.Page 5 | import com.antyzero.smoksmog.storage.JsonFileStorage 6 | import com.antyzero.smoksmog.storage.PersistentStorage 7 | import com.antyzero.smoksmog.storage.model.Item 8 | import org.junit.After 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.mockito.Mock 12 | import org.mockito.MockitoAnnotations 13 | import pl.malopolska.smoksmog.Api 14 | import pl.malopolska.smoksmog.RestClient 15 | import rx.observers.TestSubscriber 16 | import java.io.File 17 | 18 | class DataCollectionTest { 19 | 20 | @Mock 21 | lateinit private var locationProvider: LocationProvider 22 | lateinit private var storage: PersistentStorage 23 | lateinit private var smokSmog: SmokSmog 24 | 25 | private var api: Api = RestClient.Builder().build() 26 | 27 | @Before 28 | fun setUp() { 29 | MockitoAnnotations.initMocks(this) 30 | val file = File.createTempFile(StringRandom().random(8), "json").apply { delete() } 31 | storage = JsonFileStorage(file) 32 | smokSmog = SmokSmog(api, storage, locationProvider) 33 | } 34 | 35 | @After 36 | fun tearDown() { 37 | storage.clear() 38 | } 39 | 40 | @Test 41 | fun collectForSingle() { 42 | val testSubscriber = TestSubscriber() 43 | 44 | smokSmog.collectDataForItem(Item.Station(13)).subscribe(testSubscriber) 45 | 46 | testSubscriber.assertNoErrors() 47 | testSubscriber.assertCompleted() 48 | testSubscriber.assertValueCount(1) 49 | } 50 | 51 | @Test 52 | fun fetchAll() { 53 | val testSubscriber = TestSubscriber() 54 | smokSmog.storage.addStation(13) 55 | smokSmog.storage.addStation(17) 56 | 57 | smokSmog.collectData().subscribe(testSubscriber) 58 | 59 | testSubscriber.assertNoErrors() 60 | testSubscriber.assertCompleted() 61 | testSubscriber.assertValueCount(2) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/screen/StartActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.screen; 2 | 3 | import android.support.test.InstrumentationRegistry; 4 | import android.support.test.rule.ActivityTestRule; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import com.antyzero.smoksmog.R; 8 | import com.antyzero.smoksmog.rules.RxSchedulerTestRule; 9 | import com.antyzero.smoksmog.rules.SpoonRule; 10 | import com.antyzero.smoksmog.ui.screen.start.StartActivity; 11 | 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | import org.junit.rules.RuleChain; 15 | import org.junit.rules.TestRule; 16 | import org.junit.runner.RunWith; 17 | 18 | import static android.support.test.espresso.Espresso.onView; 19 | import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; 20 | import static android.support.test.espresso.action.ViewActions.click; 21 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 22 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 23 | 24 | 25 | @RunWith(AndroidJUnit4.class) 26 | public class StartActivityTest { 27 | 28 | @Rule 29 | public final RxSchedulerTestRule rxSchedulerTestRule = new RxSchedulerTestRule(); 30 | private final ActivityTestRule activityTestRule = new MockedNetworkActivityTestRule<>(StartActivity.class, false, true); 31 | private final SpoonRule spoonRule = new SpoonRule(activityTestRule); 32 | @Rule 33 | public final TestRule testRule = RuleChain.outerRule(activityTestRule).around(spoonRule); 34 | 35 | @Test 36 | public void checkCreation() { 37 | spoonRule.screenshot("Creation"); 38 | } 39 | 40 | // @Test 41 | public void addStation() { 42 | 43 | // Given 44 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 45 | 46 | // When 47 | openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext()); 48 | onView(withText(R.string.action_manage_stations)).perform(click()); 49 | onView(withId(R.id.fab)).perform(click()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion androidVars.compileSdkVersion 6 | buildToolsVersion androidVars.buildToolsVersion 7 | 8 | compileOptions { 9 | sourceCompatibility commonVars.javaVersion 10 | targetCompatibility commonVars.javaVersion 11 | } 12 | 13 | defaultConfig { 14 | minSdkVersion androidVars.minSdkVersion 15 | targetSdkVersion androidVars.targetSdkVersion 16 | versionCode 1 17 | versionName "0.0.1" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | 27 | packagingOptions { 28 | exclude 'META-INF/LICENSE' 29 | exclude 'META-INF/LICENSE.txt' 30 | exclude 'META-INF/NOTICE' 31 | exclude 'META-INF/NOTICE.txt' 32 | exclude 'META-INF/services/javax.annotation.processing.Processor' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage', 'GoogleAppIndexingWarning' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | } 43 | 44 | dependencies { 45 | compile project(':network') 46 | 47 | // UI oriented 48 | compile "com.android.support:recyclerview-v7:${libVersions.android.appCompat}" 49 | 50 | // General 51 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 52 | compile "io.reactivex:rxjava:${libVersions.rx.java}" 53 | compile "io.reactivex:rxjava-math:${libVersions.rx.javaMath}" 54 | compile "io.reactivex:rxandroid:${libVersions.rx.android}" 55 | compile "io.reactivex:rxkotlin:${libVersions.rx.kotlin}" 56 | compile "com.trello:rxlifecycle:${libVersions.rx.lifecycle}" 57 | compile "com.trello:rxlifecycle-components:${libVersions.rx.lifecycle}" 58 | 59 | testCompile "junit:junit:${libVersions.junit}" 60 | testCompile 'org.assertj:assertj-core:2.6.0' 61 | } 62 | 63 | repositories { 64 | mavenCentral() 65 | } 66 | -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/logger/LevelBlockingLogger.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.logger 2 | 3 | import android.util.Log 4 | 5 | class LevelBlockingLogger(private val logger: Logger, private val lowestLevelToLog: Int) : Logger { 6 | 7 | override fun v(tag: String, message: String) { 8 | if (lowestLevelToLog >= Log.VERBOSE) { 9 | logger.v(tag, message) 10 | } 11 | } 12 | 13 | override fun v(tag: String, message: String, throwable: Throwable) { 14 | if (lowestLevelToLog >= Log.VERBOSE) { 15 | logger.v(tag, message, throwable) 16 | } 17 | } 18 | 19 | override fun d(tag: String, message: String) { 20 | if (lowestLevelToLog >= Log.DEBUG) { 21 | logger.d(tag, message) 22 | } 23 | } 24 | 25 | override fun d(tag: String, message: String, throwable: Throwable) { 26 | if (lowestLevelToLog >= Log.DEBUG) { 27 | logger.d(tag, message, throwable) 28 | } 29 | } 30 | 31 | override fun i(tag: String, message: String) { 32 | if (lowestLevelToLog >= Log.INFO) { 33 | logger.i(tag, message) 34 | } 35 | } 36 | 37 | override fun i(tag: String, message: String, throwable: Throwable) { 38 | if (lowestLevelToLog >= Log.INFO) { 39 | logger.i(tag, message, throwable) 40 | } 41 | } 42 | 43 | override fun w(tag: String, message: String) { 44 | if (lowestLevelToLog >= Log.WARN) { 45 | logger.w(tag, message) 46 | } 47 | } 48 | 49 | override fun w(tag: String, message: String, throwable: Throwable) { 50 | if (lowestLevelToLog >= Log.WARN) { 51 | logger.w(tag, message, throwable) 52 | } 53 | } 54 | 55 | override fun e(tag: String, message: String) { 56 | if (lowestLevelToLog >= Log.ERROR) { 57 | logger.e(tag, message) 58 | } 59 | } 60 | 61 | override fun e(tag: String, message: String, throwable: Throwable) { 62 | if (lowestLevelToLog >= Log.ERROR) { 63 | logger.e(tag, message, throwable) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/migration/OldToNewStationListTest.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.migration; 2 | 3 | 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | 7 | import android.annotation.SuppressLint; 8 | import android.content.Context; 9 | import android.support.test.InstrumentationRegistry; 10 | import android.support.test.runner.AndroidJUnit4; 11 | 12 | import com.antyzero.smoksmog.permission.PermissionHelper; 13 | import com.antyzero.smoksmog.settings.SettingsHelper; 14 | import com.antyzero.smoksmog.storage.JsonFileStorage; 15 | import com.antyzero.smoksmog.storage.model.Item; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | @RunWith(AndroidJUnit4.class) 24 | public class OldToNewStationListTest { 25 | 26 | @SuppressLint("CommitPrefEdits") 27 | @Test 28 | public void simpleMigration() throws Exception { 29 | 30 | // Given 31 | List longList = new ArrayList<>(Arrays.asList(4L, 3L, 2L)); 32 | Context context = InstrumentationRegistry.getTargetContext(); 33 | SettingsHelper settingsHelper = new SettingsHelper(context); 34 | settingsHelper.getPreferences().edit().clear().commit(); 35 | settingsHelper.setStationIdList(longList); 36 | JsonFileStorage jsonFileStorage = new JsonFileStorage(); 37 | jsonFileStorage.clear(); 38 | 39 | // When 40 | for (Long id : settingsHelper.getStationIdList()) { 41 | jsonFileStorage.addStation(id); 42 | } 43 | settingsHelper.getPreferences().edit().clear().commit(); 44 | settingsHelper.getStationIdList().clear(); 45 | 46 | // Then 47 | List items = jsonFileStorage.fetchAll(); 48 | assertThat(items).hasSize(3); 49 | assertThat(items.get(0).getId()).isEqualTo(4); 50 | assertThat(items.get(1).getId()).isEqualTo(3); 51 | assertThat(items.get(2).getId()).isEqualTo(2); 52 | 53 | assertThat(settingsHelper.getStationIdList()).hasSize(0); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/widget/StationWidget.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.widget 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.appwidget.AppWidgetProvider 5 | import android.content.Context 6 | import android.widget.RemoteViews 7 | import com.antyzero.smoksmog.BuildConfig 8 | import com.antyzero.smoksmog.R 9 | import com.antyzero.smoksmog.dsl.setBackgroundColor 10 | import org.joda.time.DateTime 11 | import pl.malopolska.smoksmog.model.Station 12 | import smoksmog.air.AirQuality 13 | import smoksmog.air.AirQualityIndex 14 | 15 | class StationWidget : AppWidgetProvider() { 16 | 17 | override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { 18 | appWidgetIds.forEach { 19 | StationWidgetService.update(context, it) 20 | } 21 | } 22 | 23 | companion object { 24 | 25 | fun updateWidget(widgetId: Int, context: Context, appWidgetManager: AppWidgetManager, station: Station) { 26 | 27 | val airQualityIndex = AirQualityIndex.calculate(station) 28 | val airQuality = AirQuality.Companion.findByValue(airQualityIndex) 29 | 30 | val views = RemoteViews(context.packageName, R.layout.widget_station) 31 | 32 | var name = station.name 33 | 34 | if (BuildConfig.DEBUG) { 35 | name = "Stacja: ${station.name}\nPomiar: ${station.particulates[0].date}\nAktulizacja: ${DateTime.now()}" 36 | } 37 | 38 | views.setTextViewText(R.id.textViewStation, name) 39 | views.setTextViewText(R.id.textViewAirQuality, airQualityIndex.format(1)) 40 | 41 | views.setTextColor(R.id.textViewAirQuality, airQuality.getColor(context)) 42 | views.setBackgroundColor(R.id.airIndicator1, airQuality.getColor(context)) 43 | views.setBackgroundColor(R.id.airIndicator2, airQuality.getColor(context)) 44 | 45 | appWidgetManager.updateAppWidget(widgetId, views) 46 | } 47 | } 48 | } 49 | 50 | private fun Double.format(digits: Int): CharSequence { 51 | return java.lang.String.format("%.${digits}f", this) 52 | } 53 | -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/logger/AggregatingLogger.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.logger 2 | 3 | 4 | import java.util.Arrays 5 | 6 | class AggregatingLogger(private val loggerCollection: Collection) : Logger { 7 | 8 | constructor(vararg logger: Logger) : this(Arrays.asList(*logger)) { 9 | } 10 | 11 | override fun v(tag: String, message: String) { 12 | for (logger in loggerCollection) { 13 | logger.v(tag, message) 14 | } 15 | } 16 | 17 | override fun v(tag: String, message: String, throwable: Throwable) { 18 | for (logger in loggerCollection) { 19 | logger.v(tag, message, throwable) 20 | } 21 | } 22 | 23 | override fun d(tag: String, message: String) { 24 | for (logger in loggerCollection) { 25 | logger.d(tag, message) 26 | } 27 | } 28 | 29 | override fun d(tag: String, message: String, throwable: Throwable) { 30 | for (logger in loggerCollection) { 31 | logger.d(tag, message, throwable) 32 | } 33 | } 34 | 35 | override fun i(tag: String, message: String) { 36 | for (logger in loggerCollection) { 37 | logger.i(tag, message) 38 | } 39 | } 40 | 41 | override fun i(tag: String, message: String, throwable: Throwable) { 42 | for (logger in loggerCollection) { 43 | logger.i(tag, message, throwable) 44 | } 45 | } 46 | 47 | override fun w(tag: String, message: String) { 48 | for (logger in loggerCollection) { 49 | logger.w(tag, message) 50 | } 51 | } 52 | 53 | override fun w(tag: String, message: String, throwable: Throwable) { 54 | for (logger in loggerCollection) { 55 | logger.w(tag, message, throwable) 56 | } 57 | } 58 | 59 | override fun e(tag: String, message: String) { 60 | for (logger in loggerCollection) { 61 | logger.e(tag, message) 62 | } 63 | } 64 | 65 | override fun e(tag: String, message: String, throwable: Throwable) { 66 | for (logger in loggerCollection) { 67 | logger.e(tag, message, throwable) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /network/src/main/kotlin/pl/malopolska/smoksmog/utils/StationUtils.kt: -------------------------------------------------------------------------------- 1 | package pl.malopolska.smoksmog.utils 2 | 3 | 4 | import pl.malopolska.smoksmog.model.Station 5 | import rx.Observable 6 | 7 | class StationUtils private constructor() { 8 | 9 | init { 10 | throw IllegalAccessError("Utils class") 11 | } 12 | 13 | companion object { 14 | 15 | fun convertStationsToIdsList(stationList: List): List { 16 | return Observable.from(stationList).map { station -> station.id }.toList().toBlocking().first() 17 | } 18 | 19 | fun convertStationsToIdsArray(stations: Collection): LongArray { 20 | val result = LongArray(stations.size) 21 | var i = 0 22 | for ((id) in stations) { 23 | result[i] = id 24 | i++ 25 | } 26 | return result 27 | } 28 | 29 | fun findClosest(stations: Collection, latitude: Double, longitude: Double): Station { 30 | 31 | var closestStation: Station = stations.first() 32 | var distance = java.lang.Double.MAX_VALUE 33 | 34 | for (station in stations) { 35 | 36 | val calculatedDistance = distanceInRadians( 37 | station.latitude.toDouble(), station.longitude.toDouble(), 38 | latitude, longitude) 39 | 40 | if (calculatedDistance < distance) { 41 | closestStation = station 42 | distance = calculatedDistance 43 | } 44 | } 45 | 46 | return closestStation 47 | } 48 | 49 | private fun distanceInRadians(latitude1: Double, longitude1: Double, latitude2: Double, longitude2: Double): Double { 50 | 51 | val x1 = Math.toRadians(latitude1) 52 | val y1 = Math.toRadians(longitude1) 53 | val x2 = Math.toRadians(latitude2) 54 | val y2 = Math.toRadians(longitude2) 55 | 56 | // great circle distance in radians 57 | return Math.acos(Math.sin(x1) * Math.sin(x2) + Math.cos(x1) * Math.cos(x2) * Math.cos(y1 - y2)) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/air/AirQualityIndex.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.air 2 | 3 | import pl.malopolska.smoksmog.model.Particulate 4 | import pl.malopolska.smoksmog.model.ParticulateEnum.* 5 | import pl.malopolska.smoksmog.model.Station 6 | 7 | object AirQualityIndex { 8 | 9 | fun calculate(station: Station): Double { 10 | return calculate(station.particulates) 11 | } 12 | 13 | fun calculate(particulate: Particulate): Double = calculate(listOf(particulate)) 14 | 15 | fun calculate(particulates: Collection): Double { 16 | 17 | var index = 0.0f 18 | 19 | for (particulate in particulates) { 20 | 21 | var particulateValue = 0.0f 22 | 23 | when (particulate.enum) { 24 | PM10 -> particulateValue = particulate.value / 20 25 | PM25 -> particulateValue = particulate.value / 12 26 | SO2 -> particulateValue = particulate.value / 70 27 | NO2 -> particulateValue = particulate.value / 40 28 | CO -> particulateValue = particulate.value / 2000 29 | O3 -> particulateValue = particulate.value / 24 30 | C6H6 -> particulateValue = calculateBenzene(particulate.value) 31 | else -> { 32 | // do nothing 33 | } 34 | } 35 | 36 | index = Math.max(index, particulateValue) 37 | } 38 | 39 | return index.toDouble() 40 | } 41 | 42 | fun calculateBenzene(particulate: Float): Float { 43 | val value = Math.max(particulate, 0f) 44 | return when (value) { 45 | in 0f until 5f -> value * 0.2f// Index 0-1 46 | in 5f until 20f -> value * 0.4f - 1 // Index 1-7 47 | else -> value * 0.1f + 5 // Index 7 and above 48 | } 49 | } 50 | } 51 | 52 | private class FloatRange(override val start: Float, override val endInclusive: Float) : ClosedRange { 53 | override fun contains(value: Float): Boolean = value >= start && value < endInclusive 54 | } 55 | 56 | private infix fun Float.until(to: Float): FloatRange { 57 | if (this > to) throw IllegalArgumentException("The to argument value '$to' was too small.") 58 | return FloatRange(this, to) 59 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/smoksmog/air/AirQuality.kt: -------------------------------------------------------------------------------- 1 | package smoksmog.air 2 | 3 | import android.content.Context 4 | import android.support.annotation.ColorRes 5 | import android.support.annotation.StringRes 6 | 7 | import smoksmog.R 8 | 9 | /** 10 | 11 | */ 12 | enum class AirQuality constructor(@ColorRes val colorResId: Int, @StringRes val titleResId: Int) : ValueCheck { 13 | 14 | VERY_GOOD(R.color.airQualityVeryGood, R.string.airQualityVeryGood) { 15 | override fun isValueInRange(value: Double): Boolean { 16 | return value <= 1 17 | } 18 | }, 19 | 20 | GOOD(R.color.airQualityGood, R.string.airQualityGood) { 21 | override fun isValueInRange(value: Double): Boolean { 22 | return value > 1 && value <= 3 23 | } 24 | }, 25 | 26 | MODERATE(R.color.airQualityModerate, R.string.airQualityModerate) { 27 | override fun isValueInRange(value: Double): Boolean { 28 | return value > 3 && value <= 5 29 | } 30 | }, 31 | 32 | SUFFICIENT(R.color.airQualitySufficient, R.string.airQualitySufficient) { 33 | override fun isValueInRange(value: Double): Boolean { 34 | return value > 5 && value <= 7 35 | } 36 | }, 37 | 38 | BAD(R.color.airQualityBad, R.string.airQualityBad) { 39 | override fun isValueInRange(value: Double): Boolean { 40 | return value > 7 && value <= 10 41 | } 42 | }, 43 | 44 | VERY_BAD(R.color.airQualityVeryBad, R.string.airQualityVeryBad) { 45 | override fun isValueInRange(value: Double): Boolean { 46 | return value > 10 47 | } 48 | }; 49 | 50 | fun getTitle(context: Context): CharSequence { 51 | return context.getString(titleResId) 52 | } 53 | 54 | fun getColor(context: Context): Int { 55 | // Deprecated since 23 API, quite fresh 56 | @Suppress("DEPRECATION") 57 | return context.resources.getColor(colorResId) 58 | } 59 | 60 | companion object { 61 | 62 | fun findByValue(value: Double): AirQuality { 63 | values().filter { it.isValueInRange(value) }.forEach { return it } 64 | throw IllegalStateException("Unable to find AirQuality for given value ($value)") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ui/widget/StationWidgetService.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.ui.widget 2 | 3 | import android.app.IntentService 4 | import android.appwidget.AppWidgetManager 5 | import android.content.ComponentName 6 | import android.content.Context 7 | import android.content.Intent 8 | import com.antyzero.smoksmog.dsl.appComponent 9 | import com.antyzero.smoksmog.dsl.appWidgetManager 10 | import com.antyzero.smoksmog.dsl.tag 11 | import pl.malopolska.smoksmog.Api 12 | import smoksmog.logger.Logger 13 | import javax.inject.Inject 14 | 15 | 16 | class StationWidgetService : IntentService(StationWidgetService::class.java.simpleName) { 17 | 18 | @Inject lateinit var api: Api 19 | @Inject lateinit var widgetData: StationWidgetData 20 | @Inject lateinit var logger: Logger 21 | 22 | lateinit var appWidgetManager: AppWidgetManager 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | appComponent().inject(this) 27 | this.appWidgetManager = appWidgetManager() 28 | } 29 | 30 | override fun onHandleIntent(intent: Intent) { 31 | if (intent.hasExtra(EXTRA_WIDGET_ID)) { 32 | val widgetId = intent.getIntExtra(EXTRA_WIDGET_ID, Int.MIN_VALUE) 33 | try { 34 | val stationId = widgetData.widgetStationId(widgetId) 35 | val station = api.station(stationId).toBlocking().first() 36 | StationWidget.updateWidget(widgetId, this, appWidgetManager, station) 37 | } catch (e: Exception) { 38 | logger.w(tag(), "Unable to update widget $widgetId", e) 39 | } 40 | } 41 | } 42 | 43 | companion object { 44 | 45 | internal val EXTRA_WIDGET_ID = "widgetId" 46 | 47 | fun update(context: Context, widgetId: Int) { 48 | val intent = Intent(context, StationWidgetService::class.java) 49 | intent.putExtra(EXTRA_WIDGET_ID, widgetId) 50 | context.startService(intent) 51 | } 52 | 53 | fun updateAll(context: Context) { 54 | context.appWidgetManager().getAppWidgetIds(ComponentName(context, StationWidget::class.java)).forEach { 55 | StationWidgetService.update(context, it) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/res/values/about.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | Jeżeli chcesz nam pomóc podziel się uwagami bądź sugestiami dotyczącymi SmokSmoga dla Androida, na tej stronie

6 | 7 | https://groups.google.com/forum/#!forum/smoksmog-android

8 | 9 | lub zwyczajnie wysłać nam maila na adres

10 | 11 | smoksmog-android@googlegroups.com

12 | 13 | Będziemy wdzięczni za kontakt z nami przed napisaniem opinii na Google Play, nie dlatego, że nie lubimy tych opinii :-) ale dlatego, że często nie możemy, w wypadku problemów, nawiązać kontaktu z jej autorem.

14 | 15 | Oficjalną stronę projektu SmokSmog znajdziesz pod poniższym linkiem

16 | 17 | http://smoksmog.malopolska.pl/

18 | 19 | SmokSmog na Androida jest projektem Open Source, czyli kod źródłowy jest jawny i dostępny dla wszystkich, którzy chcieliby zweryfikować naszą pracę lub współtworzyć nasz projekt

20 | 21 | https://github.com/SmokSmog/smoksmog-android ]]>
22 | 23 |
24 | 25 | Obliczany jest na podstawie najnowszych pomiarów, pochodzących z ostatniej godziny.

26 | 27 | Więcej na temat indeksu jakości powietrza]]>
28 | 29 |
-------------------------------------------------------------------------------- /domain/src/test/kotlin/com/antyzero/smoksmog/SmokSmogTest.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import com.antyzero.smoksmog.location.Location 4 | import com.antyzero.smoksmog.location.Location.Position 5 | import com.antyzero.smoksmog.location.LocationProvider 6 | import com.antyzero.smoksmog.storage.PersistentStorage 7 | import com.nhaarman.mockito_kotlin.doReturn 8 | import com.nhaarman.mockito_kotlin.mock 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.junit.Test 11 | import pl.malopolska.smoksmog.Api 12 | import pl.malopolska.smoksmog.model.Station 13 | import rx.Observable 14 | import rx.observers.TestSubscriber 15 | 16 | class SmokSmogTest { 17 | 18 | @Test 19 | fun nearestStation() { 20 | val latitude = 49.617454 21 | val longitude = 20.715333 22 | val testSubscriber = TestSubscriber() 23 | val locationProvider = mock { 24 | on { location() } doReturn Position(latitude to longitude).observable() 25 | } 26 | val api = mock { 27 | on { stationByLocation(latitude, longitude) } doReturn Station( 28 | 6, "Nowy Sącz", latitude = latitude.toFloat(), longitude = longitude.toFloat()).observable() 29 | } 30 | val smokSmog = SmokSmog(api, mock {}, locationProvider) 31 | 32 | smokSmog.nearestStation().subscribe(testSubscriber) 33 | 34 | testSubscriber.assertNoErrors() 35 | testSubscriber.assertCompleted() 36 | testSubscriber.assertValueCount(1) 37 | assertThat(testSubscriber.onNextEvents[0].id).isEqualTo(6) 38 | } 39 | 40 | @Test 41 | fun locationUnknown() { 42 | val testSubscriber = TestSubscriber() 43 | val locationProvider = mock { 44 | on { location() } doReturn Location.Unknown().observable() 45 | } 46 | val smokSmog = SmokSmog(mock { }, mock { }, locationProvider) 47 | 48 | smokSmog.nearestStation().subscribe(testSubscriber) 49 | 50 | testSubscriber.assertNoErrors() 51 | testSubscriber.assertCompleted() 52 | testSubscriber.assertValueCount(0) 53 | } 54 | } 55 | 56 | private fun T.observable() = Observable.just(this) -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/antyzero/smoksmog/storage/PersistentStorageTest.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.storage 2 | 3 | import com.antyzero.smoksmog.storage.model.Item 4 | import com.antyzero.smoksmog.storage.model.Module 5 | import org.assertj.core.api.Assertions 6 | import org.junit.Test 7 | import java.io.File 8 | 9 | 10 | class PersistentStorageTest { 11 | 12 | private val storageFile = File.createTempFile("pref", "json") 13 | 14 | fun createStorageInstance() = JsonFileStorage(storageFile) 15 | 16 | @Test 17 | fun persistenceNotCleaned() { 18 | val storageOne: PersistentStorage = createStorageInstance() 19 | storageOne.add(Item.Station(1, mutableSetOf( 20 | Module.AirQualityIndex(Module.AirQualityIndex.Type.POLISH), 21 | Module.Measurements() 22 | ))) 23 | storageOne.addStation(2) 24 | storageOne.update(2, Item.Station(modules = mutableSetOf( 25 | Module.Measurements() 26 | ))) 27 | 28 | val storageTwo = createStorageInstance() 29 | 30 | Assertions.assertThat(storageTwo.fetchAll()).hasSize(2) 31 | Assertions.assertThat(storageTwo.fetchAll()[0].id).isEqualTo(1) 32 | Assertions.assertThat(storageTwo.fetchAll()[0].modules).hasSize(2) 33 | Assertions.assertThat(storageTwo.fetchAll()[0].modules.toList()[0]).isInstanceOf(Module.AirQualityIndex::class.java) 34 | Assertions.assertThat(storageTwo.fetchAll()[0].modules.toList()[1]).isInstanceOf(Module.Measurements::class.java) 35 | Assertions.assertThat(storageTwo.fetchAll()[1].id).isEqualTo(2) 36 | Assertions.assertThat(storageTwo.fetchAll()[1].modules).hasSize(1) 37 | Assertions.assertThat(storageTwo.fetchAll()[1].modules.toList()[0]).isInstanceOf(Module.Measurements::class.java) 38 | } 39 | 40 | @Test 41 | fun persistenceCleaned() { 42 | val storageOne: PersistentStorage = createStorageInstance() 43 | storageOne.add(Item.Station(1, mutableSetOf( 44 | Module.AirQualityIndex(Module.AirQualityIndex.Type.POLISH), 45 | Module.Measurements() 46 | ))) 47 | storageOne.clear() 48 | 49 | val storageTwo = createStorageInstance() 50 | 51 | Assertions.assertThat(storageTwo.fetchAll()).hasSize(0) 52 | } 53 | } -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'kotlin' 3 | 4 | sourceCompatibility = JavaVersion.VERSION_1_7 5 | targetCompatibility = JavaVersion.VERSION_1_7 6 | 7 | //noinspection GroovyAssignabilityCheck 8 | sourceSets { 9 | main.java.srcDirs += 'src/main/kotlin' 10 | 11 | integrationTest { 12 | java { 13 | compileClasspath += main.output + test.output 14 | runtimeClasspath += main.output + test.output 15 | srcDir file('src/integrationTest/kotlin') 16 | } 17 | resources.srcDir file('src/integrationTest/resources') 18 | } 19 | } 20 | 21 | configurations { 22 | integrationTestCompile.extendsFrom testCompile 23 | integrationTestRuntime.extendsFrom testRuntime 24 | } 25 | 26 | test { 27 | testLogging { 28 | events "passed", "skipped", "failed", "standardOut" 29 | showExceptions true 30 | exceptionFormat "full" 31 | showCauses true 32 | showStackTraces true 33 | } 34 | } 35 | 36 | task integrationTest(type: Test) { 37 | testClassesDir = sourceSets.integrationTest.output.classesDir 38 | classpath = sourceSets.integrationTest.runtimeClasspath 39 | outputs.upToDateWhen { false } 40 | 41 | testLogging { 42 | events "passed", "skipped", "failed", "standardOut" 43 | showExceptions true 44 | exceptionFormat "full" 45 | showCauses true 46 | showStackTraces true 47 | } 48 | } 49 | 50 | check.dependsOn integrationTest 51 | integrationTest.mustRunAfter test 52 | 53 | tasks.withType(Test) { 54 | reports.html.destination = file("${reporting.baseDir}/${name}") 55 | } 56 | 57 | dependencies { 58 | 59 | compile project(':network') 60 | 61 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 62 | compile "io.reactivex:rxjava:${libVersions.rx.java}" 63 | compile "io.reactivex:rxkotlin:${libVersions.rx.kotlin}" 64 | 65 | testCompile "junit:junit:${libVersions.junit}" 66 | testCompile "org.assertj:assertj-core:${libVersions.assertj}" 67 | testCompile "com.nhaarman:mockito-kotlin:1.1.0" 68 | testCompile "com.squareup.okhttp3:mockwebserver:${libVersions.square.okhttp}" 69 | 70 | integrationTestCompile "junit:junit:${libVersions.junit}" 71 | integrationTestCompile "org.assertj:assertj-core:${libVersions.assertj}" 72 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/antyzero/smoksmog/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog 2 | 3 | import com.antyzero.smoksmog.domain.DomainModule 4 | import com.antyzero.smoksmog.eventbus.EventBusModule 5 | import com.antyzero.smoksmog.fabric.FabricModule 6 | import com.antyzero.smoksmog.firebase.SmokSmogFirebaseInstanceIdService 7 | import com.antyzero.smoksmog.i18n.I18nModule 8 | import com.antyzero.smoksmog.job.JobModule 9 | import com.antyzero.smoksmog.job.SmokSmogJobService 10 | import com.antyzero.smoksmog.logger.LoggerModule 11 | import com.antyzero.smoksmog.settings.SettingsModule 12 | import com.antyzero.smoksmog.ui.dialog.AboutDialog 13 | import com.antyzero.smoksmog.ui.dialog.FacebookDialog 14 | import com.antyzero.smoksmog.ui.screen.ActivityComponent 15 | import com.antyzero.smoksmog.ui.screen.ActivityModule 16 | import com.antyzero.smoksmog.ui.screen.start.item.AirQualityViewHolder 17 | import com.antyzero.smoksmog.ui.screen.start.item.ParticulateViewHolder 18 | import com.antyzero.smoksmog.ui.widget.StationWidget 19 | import com.antyzero.smoksmog.ui.widget.StationWidgetService 20 | import com.antyzero.smoksmog.ui.widget.WidgetModule 21 | import com.antyzero.smoksmog.user.UserModule 22 | import dagger.Component 23 | import javax.inject.Singleton 24 | 25 | @Singleton 26 | @Component(modules = arrayOf( 27 | ApplicationModule::class, 28 | DomainModule::class, 29 | LoggerModule::class, 30 | FabricModule::class, 31 | SettingsModule::class, 32 | EventBusModule::class, 33 | I18nModule::class, 34 | UserModule::class, 35 | WidgetModule::class, 36 | JobModule::class)) 37 | interface ApplicationComponent { 38 | 39 | operator fun plus(activityModule: ActivityModule): ActivityComponent 40 | 41 | fun inject(smokSmogApplication: SmokSmogApplication) 42 | 43 | fun inject(airQualityViewHolder: AirQualityViewHolder) 44 | 45 | fun inject(aboutDialog: AboutDialog) 46 | 47 | fun inject(particulateViewHolder: ParticulateViewHolder) 48 | 49 | fun inject(facebookDialog: FacebookDialog) 50 | 51 | fun inject(stationWidget: StationWidget) 52 | 53 | fun inject(stationWidgetService: StationWidgetService) 54 | 55 | fun inject(smokSmogFirebaseInstanceIdService: SmokSmogFirebaseInstanceIdService) 56 | 57 | fun inject(smokSmogJobService: SmokSmogJobService) 58 | } 59 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/antyzero/smoksmog/rules/RxSchedulerTestRule.java: -------------------------------------------------------------------------------- 1 | package com.antyzero.smoksmog.rules; 2 | 3 | import android.support.test.espresso.Espresso; 4 | 5 | import com.antyzero.smoksmog.rules.rx.CustomExecutorScheduler; 6 | import com.antyzero.smoksmog.rules.rx.SchedulersHook; 7 | import com.antyzero.smoksmog.rules.rx.ThreadPoolIdlingResource; 8 | 9 | import org.junit.rules.TestRule; 10 | import org.junit.runner.Description; 11 | import org.junit.runners.model.Statement; 12 | 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ThreadPoolExecutor; 15 | 16 | import rx.plugins.RxJavaTestPlugins; 17 | 18 | public class RxSchedulerTestRule implements TestRule { 19 | 20 | private final SchedulersHook schedulersHook; 21 | private final ThreadPoolIdlingResource idlingResource; 22 | 23 | public RxSchedulerTestRule() { 24 | ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) Executors.newScheduledThreadPool(16); 25 | CustomExecutorScheduler scheduler = new CustomExecutorScheduler(threadPoolExecutor); 26 | schedulersHook = new SchedulersHook(scheduler); 27 | idlingResource = new ThreadPoolIdlingResource(threadPoolExecutor) { 28 | @Override 29 | public String getName() { 30 | return getClass().getSimpleName(); 31 | } 32 | }; 33 | } 34 | 35 | @Override 36 | public Statement apply(Statement base, Description description) { 37 | return new RxStatement(base, this); 38 | } 39 | 40 | private static class RxStatement extends Statement { 41 | private final Statement base; 42 | private final RxSchedulerTestRule testRule; 43 | 44 | public RxStatement(Statement base, RxSchedulerTestRule schedulersHook) { 45 | this.base = base; 46 | this.testRule = schedulersHook; 47 | } 48 | 49 | @Override 50 | public void evaluate() throws Throwable { 51 | 52 | RxJavaTestPlugins.resetPlugins(); 53 | RxJavaTestPlugins.getInstance().registerSchedulersHook(testRule.schedulersHook); 54 | Espresso.registerIdlingResources(testRule.idlingResource); 55 | 56 | base.evaluate(); 57 | 58 | Espresso.unregisterIdlingResources(testRule.idlingResource); 59 | RxJavaTestPlugins.resetPlugins(); 60 | } 61 | } 62 | } 63 | --------------------------------------------------------------------------------