Repo created
5
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/build
|
||||
/src/main/res/values-in/
|
||||
/src/main/res/values-iw/
|
||||
# /src/main/res/raw/ne_50m_admin_0_countries.json
|
||||
locales_config.xml
|
||||
408
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
@file:Suppress("ChromeOsAbiSupport")
|
||||
|
||||
import breezy.buildlogic.getCommitCount
|
||||
import breezy.buildlogic.getGitSha
|
||||
import breezy.buildlogic.registerLocalesConfigTask
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("breezy.android.application")
|
||||
id("breezy.android.application.compose")
|
||||
id("com.android.application")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.google.dagger.hilt.android")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.mikepenz.aboutlibraries.plugin.android")
|
||||
}
|
||||
|
||||
val supportedAbi = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "org.breezyweather"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.breezyweather"
|
||||
versionCode = 60012
|
||||
versionName = "6.0.12"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters += supportedAbi
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*supportedAbi.toTypedArray())
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
named("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-r${getCommitCount()}"
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
isDebuggable = false
|
||||
isCrunchPngs = false // No need to do that, we already optimized them
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
val properties = Properties()
|
||||
if (project.rootProject.file("local.properties").canRead()) {
|
||||
properties.load(project.rootProject.file("local.properties").inputStream())
|
||||
}
|
||||
buildTypes.forEach {
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"DEFAULT_LOCATION_SOURCE",
|
||||
"\"${properties.getProperty("breezy.source.default_location") ?: "native"}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"DEFAULT_LOCATION_SEARCH_SOURCE",
|
||||
"\"${properties.getProperty("breezy.source.default_location_search") ?: "openmeteo"}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"DEFAULT_GEOCODING_SOURCE",
|
||||
"\"${properties.getProperty("breezy.source.default_geocoding") ?: "naturalearth"}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"DEFAULT_FORECAST_SOURCE",
|
||||
"\"${properties.getProperty("breezy.source.default_weather") ?: "auto"}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ACCU_WEATHER_KEY",
|
||||
"\"${properties.getProperty("breezy.accu.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"AEMET_KEY",
|
||||
"\"${properties.getProperty("breezy.aemet.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ATMO_AURA_KEY",
|
||||
"\"${properties.getProperty("breezy.atmoaura.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ATMO_FRANCE_KEY",
|
||||
"\"${properties.getProperty("breezy.atmofrance.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ATMO_GRAND_EST_KEY",
|
||||
"\"${properties.getProperty("breezy.atmograndest.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ATMO_HDF_KEY",
|
||||
"\"${properties.getProperty("breezy.atmohdf.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ATMO_SUD_KEY",
|
||||
"\"${properties.getProperty("breezy.atmosud.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"BAIDU_IP_LOCATION_AK",
|
||||
"\"${properties.getProperty("breezy.baiduip.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"BMKG_KEY",
|
||||
"\"${properties.getProperty("breezy.bmkg.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"CWA_KEY",
|
||||
"\"${properties.getProperty("breezy.cwa.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"ECCC_KEY",
|
||||
"\"${properties.getProperty("breezy.eccc.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"GEO_NAMES_KEY",
|
||||
"\"${properties.getProperty("breezy.geonames.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"MET_IE_KEY",
|
||||
"\"${properties.getProperty("breezy.metie.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"MET_OFFICE_KEY",
|
||||
"\"${properties.getProperty("breezy.metoffice.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"MF_WSFT_JWT_KEY",
|
||||
"\"${properties.getProperty("breezy.mf.jwtKey") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"MF_WSFT_KEY",
|
||||
"\"${properties.getProperty("breezy.mf.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"OPEN_WEATHER_KEY",
|
||||
"\"${properties.getProperty("breezy.openweather.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"PIRATE_WEATHER_KEY",
|
||||
"\"${properties.getProperty("breezy.pirateweather.key") ?: ""}\""
|
||||
)
|
||||
it.buildConfigField(
|
||||
"String",
|
||||
"POLLENINFO_KEY",
|
||||
"\"${properties.getProperty("breezy.polleninfo.key") ?: ""}\""
|
||||
)
|
||||
}
|
||||
|
||||
flavorDimensions.add("default")
|
||||
|
||||
productFlavors {
|
||||
create("basic") {
|
||||
dimension = "default"
|
||||
}
|
||||
create("freenet") {
|
||||
dimension = "default"
|
||||
versionNameSuffix = "_freenet"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("basic") {
|
||||
java.srcDirs("src/src_nonfreenet")
|
||||
res.srcDirs("src/res_nonfreenet")
|
||||
}
|
||||
getByName("freenet") {
|
||||
java.srcDirs("src/src_freenet")
|
||||
res.srcDirs("src/res_freenet")
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"kotlin-tooling-metadata.json",
|
||||
"LICENSE.txt",
|
||||
"META-INF/versions/9/OSGI-INF/MANIFEST.MF",
|
||||
"META-INF/**/*.properties",
|
||||
"META-INF/**/LICENSE.txt",
|
||||
"META-INF/*.properties",
|
||||
"META-INF/*.version",
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/README.md"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
includeInApk = false
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
renderScript = false
|
||||
shaders = false
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
offlineMode = true
|
||||
|
||||
collect {
|
||||
// Define the path configuration files are located in. E.g. additional libraries, licenses to add to the target .json
|
||||
// Warning: Please do not use the parent folder of a module as path, as this can result in issues. More details: https://github.com/mikepenz/AboutLibraries/issues/936
|
||||
// The path provided is relative to the modules path (not project root)
|
||||
configPath = file("../config")
|
||||
}
|
||||
|
||||
export {
|
||||
// Remove the "generated" timestamp to allow for reproducible builds
|
||||
excludeFields.add("generated")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.data)
|
||||
implementation(projects.domain)
|
||||
implementation(projects.mapsUtils)
|
||||
implementation(projects.uiWeatherView)
|
||||
implementation(projects.weatherUnit)
|
||||
implementation(libs.breezy.datasharing.lib)
|
||||
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
implementation(libs.core.splashscreen)
|
||||
|
||||
implementation(libs.cardview)
|
||||
implementation(libs.swiperefreshlayout)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.compose.material.ripple)
|
||||
implementation(libs.compose.animation)
|
||||
implementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.util)
|
||||
implementation(libs.compose.foundation)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.material.icons)
|
||||
implementation(libs.navigation.compose)
|
||||
lintChecks(libs.compose.lint.checks)
|
||||
|
||||
implementation(libs.accompanist.permissions)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
|
||||
// preference.
|
||||
implementation(libs.preference.ktx)
|
||||
|
||||
// db
|
||||
implementation(libs.bundles.sqlite)
|
||||
|
||||
// work.
|
||||
implementation(libs.work.runtime)
|
||||
|
||||
// lifecycle.
|
||||
implementation(libs.bundles.lifecycle)
|
||||
implementation(libs.recyclerview)
|
||||
|
||||
// hilt.
|
||||
implementation(libs.dagger.hilt.core)
|
||||
ksp(libs.dagger.hilt.compiler)
|
||||
implementation(libs.hilt.work)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// HTTP
|
||||
implementation(libs.bundles.retrofit)
|
||||
implementation(libs.bundles.okhttp)
|
||||
// implementation(libs.kotlinx.serialization.csv) // Can be reenabled if needed (see also HttpModule.kt)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.serialization.xml.core)
|
||||
implementation(libs.kotlinx.serialization.xml)
|
||||
|
||||
// data store
|
||||
// implementation(libs.datastore)
|
||||
|
||||
// jwt - Only used by MF at the moment
|
||||
"basicImplementation"(libs.jjwt.api)
|
||||
"basicRuntimeOnly"(libs.jjwt.impl)
|
||||
"basicRuntimeOnly"(libs.jjwt.orgjson) {
|
||||
exclude("org.json", "json") // provided by Android natively
|
||||
}
|
||||
|
||||
// rx java.
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
implementation(libs.kotlinx.coroutines.rx3)
|
||||
|
||||
// ui.
|
||||
implementation(libs.vico.compose.m3)
|
||||
implementation(libs.vico.views)
|
||||
implementation(libs.adaptiveiconview)
|
||||
implementation(libs.activity)
|
||||
|
||||
// utils.
|
||||
implementation(libs.suncalc)
|
||||
implementation(libs.aboutLibraries)
|
||||
|
||||
// Allows reflection of the relative time class to pass Locale as parameter
|
||||
implementation(libs.restrictionBypass)
|
||||
|
||||
// debugImplementation because LeakCanary should only run in debug builds.
|
||||
// debugImplementation(libs.leakcanary)
|
||||
}
|
||||
|
||||
tasks {
|
||||
// May be too heavy to run, so let’s keep the generated file in Git
|
||||
// val naturalEarthConfigTask = registerNaturalEarthConfigTask(project)
|
||||
val localesConfigTask = registerLocalesConfigTask(project)
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings by registering(Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
}
|
||||
|
||||
// Duplicating Indonesian string assets due to some locale code issues on different devices
|
||||
val copyIndonesianStrings by registering(Copy::class) {
|
||||
from("./src/main/res/values-id")
|
||||
into("./src/main/res/values-in")
|
||||
include("**/*")
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(
|
||||
// naturalEarthConfigTask,
|
||||
copyHebrewStrings,
|
||||
copyIndonesianStrings,
|
||||
localesConfigTask
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(libs.kotlin.gradle)
|
||||
}
|
||||
}
|
||||
73
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class org.breezyweather.common.activities.models.** { *; }
|
||||
-keep class org.breezyweather.db.entities.** { *; }
|
||||
-keep interface org.breezyweather.sources.**.* { *; }
|
||||
-keep class org.breezyweather.sources.**.json.** { *; }
|
||||
|
||||
-keep public class * extends android.app.Service
|
||||
-keep public class * extends android.content.BroadcastReceiver
|
||||
|
||||
-keep class androidx.lifecycle.** {*;}
|
||||
-keep class android.arch.lifecycle.** {*;}
|
||||
|
||||
-keep class **.R$* {*;}
|
||||
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
!static !transient <fields>;
|
||||
!private <fields>;
|
||||
!private <methods>;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
-keepclassmembers class * {
|
||||
void *(**On*Event);
|
||||
void *(**On*Listener);
|
||||
}
|
||||
|
||||
-assumenosideeffects class android.util.Log {
|
||||
public static int v(...);
|
||||
public static int i(...);
|
||||
public static int w(...);
|
||||
public static int d(...);
|
||||
public static int e(...);
|
||||
}
|
||||
|
||||
# suncalc
|
||||
-dontwarn edu.umd.cs.findbugs.annotations.Nullable
|
||||
|
||||
# RestrictionBypass
|
||||
-keep class org.chickenhook.restrictionbypass.** { *; }
|
||||
|
||||
# Jwt
|
||||
-keep class io.jsonwebtoken.impl.** { *; }
|
||||
6
app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/debug/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 952 B |
BIN
app/src/debug/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/debug/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 9 KiB |
7
app/src/debug/res/values-night-v31/colors.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- theme -->
|
||||
<color name="colorSplashScreen">#400000</color>
|
||||
|
||||
</resources>
|
||||
7
app/src/debug/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- theme -->
|
||||
<color name="colorSplashScreen">#800000</color>
|
||||
|
||||
</resources>
|
||||
4
app/src/debug/res/values/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#800000</color>
|
||||
</resources>
|
||||
576
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- location. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- request for some location SDKs and reading wallpaper in widget config activities. -->
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
<!-- background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<!-- query internet state. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- widgets. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||
|
||||
<!-- tiles. -->
|
||||
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
|
||||
android:minSdkVersion="34" />
|
||||
|
||||
<!-- notification. -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- weather update in background -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<!-- Data sharing -->
|
||||
<permission
|
||||
android:description="@string/content_provider_permission_description"
|
||||
android:icon="@drawable/ic_launcher_foreground"
|
||||
android:label="@string/content_provider_permission_label"
|
||||
android:name="${applicationId}.READ_PROVIDER"
|
||||
android:protectionLevel="dangerous" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.live_wallpaper"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.network"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<!-- Breezy Weather Icon Packs -->
|
||||
<intent>
|
||||
<action android:name="org.breezyweather.ICON_PROVIDER" />
|
||||
</intent>
|
||||
<!-- Geometric Weather Icon Packs -->
|
||||
<intent>
|
||||
<action android:name="wangdaye.com.geometricweather.ICON_PROVIDER" />
|
||||
</intent>
|
||||
<!-- Chronus Icon Packs -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
</intent>
|
||||
<!-- GadgetBridge WeatherSpec -->
|
||||
<intent>
|
||||
<action android:name="nodomain.freeyourgadget.gadgetbridge.ACTION_GENERIC_WEATHER" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/breezy_weather"
|
||||
android:name=".BreezyWeather"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:allowCrossUidActivitySwitchFromBelow="false"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,ManifestResource,RtlEnabled,UnusedAttribute"
|
||||
tools:targetApi="n">
|
||||
|
||||
<meta-data
|
||||
android:name="org.breezyweather.PROVIDER_CONFIG"
|
||||
android:resource="@xml/icon_provider_config" />
|
||||
<meta-data
|
||||
android:name="org.breezyweather.DRAWABLE_FILTER"
|
||||
android:resource="@xml/icon_provider_drawable_filter" />
|
||||
<meta-data
|
||||
android:name="org.breezyweather.ANIMATOR_FILTER"
|
||||
android:resource="@xml/icon_provider_animator_filter" />
|
||||
<meta-data
|
||||
android:name="org.breezyweather.SHORTCUT_FILTER"
|
||||
android:resource="@xml/icon_provider_shortcut_filter" />
|
||||
<meta-data
|
||||
android:name="org.breezyweather.SUN_MOON_FILTER"
|
||||
android:resource="@xml/icon_provider_sun_moon_filter" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/BreezyWeatherTheme.Main"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.APP_WEATHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<!--<action android:name="org.breezyweather.ICON_PROVIDER" />-->
|
||||
<action android:name="${applicationId}.Main" />
|
||||
<action android:name="${applicationId}.ACTION_SHOW_ALERTS" />
|
||||
<action android:name="${applicationId}.ACTION_SHOW_DAILY_FORECAST" />
|
||||
<action android:name="${applicationId}.ACTION_MANAGEMENT" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/action_add_as_location">
|
||||
<data android:scheme="geo" />
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.search.SearchActivity"
|
||||
android:label="@string/action_search"
|
||||
android:theme="@style/BreezyWeatherTheme.Search"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.details.DetailsActivity"
|
||||
android:label="@string/daily_forecast"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.alert.AlertActivity"
|
||||
android:label="@string/alerts"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/action_settings"
|
||||
android:theme="@style/BreezyWeatherTheme">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.CardDisplayManageActivity"
|
||||
android:label="@string/settings_main_cards_title"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.DailyTrendDisplayManageActivity"
|
||||
android:label="@string/settings_main_daily_trends_title"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.HourlyTrendDisplayManageActivity"
|
||||
android:label="@string/settings_main_hourly_trends_title"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.PreviewIconActivity"
|
||||
android:label="@string/action_preview"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.WorkerInfoActivity"
|
||||
android:label="@string/settings_background_updates_worker_info_title"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.about.AboutActivity"
|
||||
android:label="@string/action_about"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.DependenciesActivity"
|
||||
android:label="@string/action_dependencies"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.settings.activities.PrivacyPolicyActivity"
|
||||
android:label="@string/about_privacy_policy"
|
||||
android:theme="@style/BreezyWeatherTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".wallpaper.LiveWallpaperConfigActivity"
|
||||
android:label="@string/settings_modules_live_wallpaper_title"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true" />
|
||||
|
||||
<!-- widget -->
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.DayWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.WeekWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.DayWeekWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.ClockDayHorizontalWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.ClockDayDetailsWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.ClockDayVerticalWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.ClockDayWeekWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.TextWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.DailyTrendWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.HourlyTrendWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".remoteviews.config.MultiCityWidgetConfigActivity"
|
||||
android:theme="@style/BreezyWeatherTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- service -->
|
||||
<service
|
||||
android:name=".background.interfaces.TileService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:label="@string/breezy_weather"
|
||||
android:icon="@drawable/weather_clear_day_mini_light"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="tile" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".wallpaper.MaterialLiveWallpaperService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:permission="android.permission.BIND_WALLPAPER"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.wallpaper.WallpaperService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.service.wallpaper"
|
||||
android:resource="@xml/live_wallpaper" />
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="wallpaper" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- widget -->
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetMaterialYouForecastProvider"
|
||||
android:label="@string/widget_material_you_forecast"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_material_you_forecast" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetMaterialYouCurrentProvider"
|
||||
android:label="@string/widget_material_you_current"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_material_you_current" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_OPTIONS_CHANGED" />
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE_OPTIONS" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetDayProvider"
|
||||
android:label="@string/widget_day"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_day" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetWeekProvider"
|
||||
android:label="@string/widget_week"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_week" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetDayWeekProvider"
|
||||
android:label="@string/widget_day_week"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_day_week" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetClockDayHorizontalProvider"
|
||||
android:label="@string/widget_clock_day_horizontal"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_clock_day_horizontal" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetClockDayDetailsProvider"
|
||||
android:label="@string/widget_clock_day_details"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_clock_day_details" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetClockDayVerticalProvider"
|
||||
android:label="@string/widget_clock_day_vertical"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_clock_day_vertical" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetClockDayWeekProvider"
|
||||
android:label="@string/widget_clock_day_week"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_clock_day_week" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetTextProvider"
|
||||
android:label="@string/widget_text"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_text" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetTrendDailyProvider"
|
||||
android:label="@string/widget_trend_daily"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_trend_daily" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetTrendHourlyProvider"
|
||||
android:label="@string/widget_trend_hourly"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_trend_hourly" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".background.receiver.widget.WidgetMultiCityProvider"
|
||||
android:label="@string/widget_multi_city"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_multi_city" />
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="android.appwidget.action.ACTION_APPWIDGET_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync|shortService"
|
||||
tools:node="merge" />
|
||||
|
||||
<!--<provider
|
||||
android:name=".background.provider.WeatherContentProvider"
|
||||
android:authorities="${applicationId}.provider.weather"
|
||||
android:exported="true"
|
||||
android:readPermission="${applicationId}.READ_PROVIDER" />-->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
155
app/src/main/java/org/breezyweather/BreezyWeather.kt
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather
|
||||
|
||||
import android.app.Application
|
||||
import android.app.UiModeManager
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.breezyweather.common.activities.BreezyActivity
|
||||
import org.breezyweather.common.extensions.uiModeManager
|
||||
import org.breezyweather.common.extensions.workManager
|
||||
import org.breezyweather.common.utils.helpers.LogHelper
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class BreezyWeather : Application(), Configuration.Provider {
|
||||
|
||||
companion object {
|
||||
|
||||
lateinit var instance: BreezyWeather
|
||||
private set
|
||||
|
||||
fun getProcessName() = try {
|
||||
val file = File("/proc/" + Process.myPid() + "/" + "cmdline")
|
||||
val mBufferedReader = BufferedReader(FileReader(file))
|
||||
val processName = mBufferedReader.readLine().trim {
|
||||
it <= ' '
|
||||
}
|
||||
mBufferedReader.close()
|
||||
|
||||
processName
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val activitySet: MutableSet<BreezyActivity> by lazy {
|
||||
HashSet()
|
||||
}
|
||||
var topActivity: BreezyActivity? = null
|
||||
private set
|
||||
|
||||
val debugMode: Boolean by lazy {
|
||||
applicationInfo != null && applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
||||
}
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
instance = this
|
||||
|
||||
setupNotificationChannels()
|
||||
|
||||
if (getProcessName().equals(packageName)) {
|
||||
// Sets and persists the night mode setting for this app. This allows the system to know
|
||||
// if the app wants to be displayed in dark mode before it launches so that the splash
|
||||
// screen can be displayed accordingly.
|
||||
setDayNightMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* We don’t use the return value, but querying the work manager might help bringing back
|
||||
* scheduled workers after the app has been killed/shutdown on some devices
|
||||
*/
|
||||
this.workManager.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
|
||||
}
|
||||
|
||||
fun addActivity(a: BreezyActivity) {
|
||||
activitySet.add(a)
|
||||
}
|
||||
|
||||
fun removeActivity(a: BreezyActivity) {
|
||||
activitySet.remove(a)
|
||||
}
|
||||
|
||||
fun setTopActivity(a: BreezyActivity) {
|
||||
topActivity = a
|
||||
}
|
||||
|
||||
fun checkToCleanTopActivity(a: BreezyActivity) {
|
||||
if (topActivity === a) {
|
||||
topActivity = null
|
||||
}
|
||||
}
|
||||
|
||||
fun recreateAllActivities() {
|
||||
val topA = topActivity
|
||||
for (a in activitySet) {
|
||||
if (a != topA) a.recreate()
|
||||
}
|
||||
// ensure that top activity stays on top by recreating it last
|
||||
topA?.recreate()
|
||||
}
|
||||
|
||||
private fun setDayNightMode() {
|
||||
updateDayNightMode(SettingsManager.getInstance(this).darkMode.value)
|
||||
}
|
||||
|
||||
fun updateDayNightMode(dayNightMode: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
uiModeManager?.setApplicationNightMode(
|
||||
when (dayNightMode) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> UiModeManager.MODE_NIGHT_NO
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> UiModeManager.MODE_NIGHT_YES
|
||||
else -> UiModeManager.MODE_NIGHT_AUTO
|
||||
}
|
||||
)
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(dayNightMode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
try {
|
||||
Notifications.createChannels(this)
|
||||
} catch (e: Exception) {
|
||||
LogHelper.log(msg = "Failed to setup notification channels")
|
||||
}
|
||||
}
|
||||
|
||||
override val workManagerConfiguration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
}
|
||||
307
app/src/main/java/org/breezyweather/Migrations.kt
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import breezyweather.domain.source.SourceFeature
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.breezyweather.background.forecast.TodayForecastNotificationJob
|
||||
import org.breezyweather.background.forecast.TomorrowForecastNotificationJob
|
||||
import org.breezyweather.background.weather.WeatherUpdateJob
|
||||
import org.breezyweather.common.options.appearance.DailyTrendDisplay
|
||||
import org.breezyweather.common.options.appearance.HourlyTrendDisplay
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import org.breezyweather.ui.main.utils.StatementManager
|
||||
import java.io.File
|
||||
|
||||
object Migrations {
|
||||
|
||||
/**
|
||||
* Performs a migration when the application is updated.
|
||||
*
|
||||
* @return true if a migration is performed, false otherwise.
|
||||
*/
|
||||
fun upgrade(
|
||||
context: Context,
|
||||
sourceManager: SourceManager,
|
||||
locationRepository: LocationRepository,
|
||||
weatherRepository: WeatherRepository,
|
||||
): Boolean {
|
||||
val lastVersionCode = SettingsManager.getInstance(context).lastVersionCode
|
||||
val oldVersion = lastVersionCode
|
||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||
if (oldVersion > 0) { // Not fresh install
|
||||
if (oldVersion < 50000) {
|
||||
// V5.0.0 adds many new charts
|
||||
// Adding it to people who customized their hourly trends tabs so they don't miss
|
||||
// this new feature. This can still be removed by user from settings
|
||||
// as this code is only executed once, after migrating from a version < 5.0.0
|
||||
try {
|
||||
val curHourlyTrendDisplayList = HourlyTrendDisplay.toValue(
|
||||
SettingsManager.getInstance(context).hourlyTrendDisplayList
|
||||
)
|
||||
if (curHourlyTrendDisplayList != SettingsManager.DEFAULT_HOURLY_TREND_DISPLAY) {
|
||||
SettingsManager.getInstance(context).hourlyTrendDisplayList =
|
||||
HourlyTrendDisplay.toHourlyTrendDisplayList(
|
||||
"$curHourlyTrendDisplayList&feels_like&humidity&pressure&cloud_cover&visibility"
|
||||
)
|
||||
}
|
||||
val curDailyTrendDisplayList = DailyTrendDisplay.toValue(
|
||||
SettingsManager.getInstance(context).dailyTrendDisplayList
|
||||
)
|
||||
if (curDailyTrendDisplayList != SettingsManager.DEFAULT_DAILY_TREND_DISPLAY) {
|
||||
SettingsManager.getInstance(context).dailyTrendDisplayList =
|
||||
DailyTrendDisplay.toDailyTrendDisplayList("$curDailyTrendDisplayList&feels_like")
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
// Delete old ObjectBox database
|
||||
context.applicationInfo?.dataDir?.let {
|
||||
val file = File("$it/files/objectbox/")
|
||||
if (file.exists() && file.isDirectory) {
|
||||
file.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 50102) {
|
||||
// V5.1.2 adds daily sunshine chart
|
||||
try {
|
||||
val curDailyTrendDisplayList =
|
||||
DailyTrendDisplay.toValue(SettingsManager.getInstance(context).dailyTrendDisplayList)
|
||||
if (curDailyTrendDisplayList != SettingsManager.DEFAULT_DAILY_TREND_DISPLAY) {
|
||||
SettingsManager.getInstance(context).dailyTrendDisplayList =
|
||||
DailyTrendDisplay.toDailyTrendDisplayList("$curDailyTrendDisplayList&sunshine")
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 50400) {
|
||||
// V5.4.0 changes the way empty source value work on locations
|
||||
runBlocking {
|
||||
locationRepository.getAllLocations(withParameters = false)
|
||||
.forEach {
|
||||
val source = sourceManager.getWeatherSource(it.forecastSource)
|
||||
if (source != null) {
|
||||
locationRepository.update(
|
||||
it.copy(
|
||||
currentSource = if (it.currentSource.isNullOrEmpty() &&
|
||||
SourceFeature.CURRENT in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.CURRENT)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.currentSource
|
||||
},
|
||||
airQualitySource = if (it.airQualitySource.isNullOrEmpty() &&
|
||||
SourceFeature.AIR_QUALITY in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.AIR_QUALITY)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.airQualitySource
|
||||
},
|
||||
pollenSource = if (it.pollenSource.isNullOrEmpty() &&
|
||||
SourceFeature.POLLEN in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.POLLEN)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.pollenSource
|
||||
},
|
||||
minutelySource = if (it.minutelySource.isNullOrEmpty() &&
|
||||
SourceFeature.MINUTELY in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.MINUTELY)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.minutelySource
|
||||
},
|
||||
alertSource = if (it.alertSource.isNullOrEmpty() &&
|
||||
SourceFeature.ALERT in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.ALERT)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.alertSource
|
||||
},
|
||||
normalsSource = if (it.normalsSource.isNullOrEmpty() &&
|
||||
SourceFeature.NORMALS in source.supportedFeatures &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.NORMALS)
|
||||
) {
|
||||
source.id
|
||||
} else {
|
||||
it.normalsSource
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 50402) {
|
||||
try {
|
||||
// We cannot determine if the permission was permanently denied in the past. That is why we
|
||||
// need to update the state for all users updating from an older version.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
StatementManager(context).setPermissionDenied(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
} catch (ignored: Throwable) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 50403) {
|
||||
// V5.4.3 no longer uses forecastSource as reverseGeocodingSource. Migrates current location
|
||||
runBlocking {
|
||||
locationRepository.getAllLocations(withParameters = false)
|
||||
.forEach {
|
||||
if (it.isCurrentPosition) {
|
||||
val source = sourceManager.getReverseGeocodingSource(it.forecastSource)
|
||||
if (source != null &&
|
||||
source.isFeatureSupportedForLocation(it, SourceFeature.REVERSE_GEOCODING)
|
||||
) {
|
||||
locationRepository.update(
|
||||
it.copy(
|
||||
reverseGeocodingSource = source.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 50407) {
|
||||
runBlocking {
|
||||
// V5.4.7 removes incorrect INSEE code for Paris, Marseille, Lyon with Atmo France
|
||||
locationRepository.updateParameters(
|
||||
source = "atmofrance",
|
||||
parameter = "citycode",
|
||||
values = mapOf(
|
||||
"75101" to "75056", // Paris
|
||||
"75102" to "75056", // Paris
|
||||
"75103" to "75056", // Paris
|
||||
"75104" to "75056", // Paris
|
||||
"75105" to "75056", // Paris
|
||||
"75106" to "75056", // Paris
|
||||
"75107" to "75056", // Paris
|
||||
"75108" to "75056", // Paris
|
||||
"75109" to "75056", // Paris
|
||||
"75110" to "75056", // Paris
|
||||
"75111" to "75056", // Paris
|
||||
"75112" to "75056", // Paris
|
||||
"75113" to "75056", // Paris
|
||||
"75114" to "75056", // Paris
|
||||
"75115" to "75056", // Paris
|
||||
"75116" to "75056", // Paris
|
||||
"75117" to "75056", // Paris
|
||||
"75118" to "75056", // Paris
|
||||
"75119" to "75056", // Paris
|
||||
"75120" to "75056", // Paris
|
||||
"13201" to "13055", // Marseille
|
||||
"13202" to "13055", // Marseille
|
||||
"13203" to "13055", // Marseille
|
||||
"13204" to "13055", // Marseille
|
||||
"13205" to "13055", // Marseille
|
||||
"13206" to "13055", // Marseille
|
||||
"13207" to "13055", // Marseille
|
||||
"13208" to "13055", // Marseille
|
||||
"13209" to "13055", // Marseille
|
||||
"13210" to "13055", // Marseille
|
||||
"13211" to "13055", // Marseille
|
||||
"13212" to "13055", // Marseille
|
||||
"13213" to "13055", // Marseille
|
||||
"13214" to "13055", // Marseille
|
||||
"13215" to "13055", // Marseille
|
||||
"13216" to "13055", // Marseille
|
||||
"69381" to "69123", // Lyon
|
||||
"69382" to "69123", // Lyon
|
||||
"69383" to "69123", // Lyon
|
||||
"69384" to "69123", // Lyon
|
||||
"69385" to "69123", // Lyon
|
||||
"69386" to "69123", // Lyon
|
||||
"69387" to "69123", // Lyon
|
||||
"69388" to "69123", // Lyon
|
||||
"69389" to "69123" // Lyon
|
||||
)
|
||||
)
|
||||
|
||||
// V5.4.7 migrates some Open-Meteo weather models
|
||||
locationRepository.updateParameters(
|
||||
source = "openmeteo",
|
||||
parameter = "weatherModels",
|
||||
values = mapOf(
|
||||
"ecmwf_ifs04" to "ecmwf_ifs025",
|
||||
"ecmwf_aifs025" to "ecmwf_aifs025_single",
|
||||
"arpae_cosmo_seamless" to "italia_meteo_arpae_icon_2i",
|
||||
"arpae_cosmo_2i" to "italia_meteo_arpae_icon_2i",
|
||||
"arpae_cosmo_5m" to "italia_meteo_arpae_icon_2i"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 60005) {
|
||||
runBlocking {
|
||||
// V6.0.5 makes so many database migrations that the data from previous versions is unusable
|
||||
// so let’s force a refresh
|
||||
weatherRepository.deleteAllWeathers()
|
||||
|
||||
// V6.0.5 restricts Open-Meteo pollen to Europe, and Accu to US/Europe
|
||||
locationRepository.getAllLocations(withParameters = false)
|
||||
.forEach {
|
||||
if (it.pollenSource in arrayOf("openmeteo", "accu")) {
|
||||
val source = sourceManager.getWeatherSource(it.pollenSource!!)
|
||||
if (source == null ||
|
||||
!source.isFeatureSupportedForLocation(it, SourceFeature.POLLEN)
|
||||
) {
|
||||
locationRepository.update(
|
||||
it.copy(
|
||||
pollenSource = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsManager.getInstance(context).lastVersionCode = BuildConfig.VERSION_CODE
|
||||
|
||||
// Always set up background tasks to ensure they're running
|
||||
WeatherUpdateJob.setupTask(context) // This will also refresh data immediately
|
||||
TodayForecastNotificationJob.setupTask(context, false)
|
||||
TomorrowForecastNotificationJob.setupTask(context, false)
|
||||
|
||||
return oldVersion != 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.forecast
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import breezyweather.domain.location.model.Location
|
||||
import breezyweather.domain.weather.model.Daily
|
||||
import breezyweather.domain.weather.reference.WeatherCode
|
||||
import org.breezyweather.R
|
||||
import org.breezyweather.common.extensions.cancelNotification
|
||||
import org.breezyweather.common.extensions.formatMeasure
|
||||
import org.breezyweather.common.extensions.notificationBuilder
|
||||
import org.breezyweather.common.extensions.notify
|
||||
import org.breezyweather.common.extensions.toBitmap
|
||||
import org.breezyweather.domain.location.model.isDaylight
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
import org.breezyweather.remoteviews.presenters.AbstractRemoteViewsPresenter
|
||||
import org.breezyweather.ui.theme.resource.ResourceHelper
|
||||
import org.breezyweather.ui.theme.resource.ResourcesProviderFactory
|
||||
import org.breezyweather.unit.formatting.UnitWidth
|
||||
import org.breezyweather.unit.temperature.TemperatureUnit
|
||||
|
||||
class ForecastNotificationNotifier(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val progressNotificationBuilder = context
|
||||
.notificationBuilder(Notifications.CHANNEL_FORECAST) {
|
||||
setSmallIcon(R.drawable.ic_running_in_background)
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
}
|
||||
|
||||
private val completeNotificationBuilder = context
|
||||
.notificationBuilder(Notifications.CHANNEL_FORECAST) {
|
||||
setAutoCancel(false)
|
||||
}
|
||||
|
||||
fun showProgress(): Notification {
|
||||
return progressNotificationBuilder
|
||||
// prevent Android from muting notifications ('muting recently noisy')
|
||||
// and only play a sound for the actual forecast notification
|
||||
.setSilent(true)
|
||||
.setContentTitle(context.getString(R.string.notification_running_in_background))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun showComplete(location: Location, today: Boolean) {
|
||||
context.cancelNotification(
|
||||
if (today) {
|
||||
Notifications.ID_UPDATING_TODAY_FORECAST
|
||||
} else {
|
||||
Notifications.ID_UPDATING_TOMORROW_FORECAST
|
||||
}
|
||||
)
|
||||
|
||||
val weather = location.weather ?: return
|
||||
val daily = (if (today) weather.today else weather.tomorrow) ?: return
|
||||
|
||||
val provider = ResourcesProviderFactory.newInstance
|
||||
|
||||
val daytime: Boolean = if (today) location.isDaylight else true
|
||||
val weatherCode: WeatherCode? = if (today) {
|
||||
if (daytime) daily.day?.weatherCode else daily.night?.weatherCode
|
||||
} else {
|
||||
daily.day?.weatherCode
|
||||
}
|
||||
val temperatureUnit = SettingsManager.getInstance(context).getTemperatureUnit(context)
|
||||
|
||||
val notification: Notification = with(completeNotificationBuilder) {
|
||||
priority = NotificationCompat.PRIORITY_MAX
|
||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
setSubText(
|
||||
if (today) {
|
||||
context.getString(R.string.daily_today_short)
|
||||
} else {
|
||||
context.getString(R.string.daily_tomorrow_short)
|
||||
}
|
||||
)
|
||||
setDefaults(Notification.DEFAULT_SOUND or Notification.DEFAULT_VIBRATE)
|
||||
setAutoCancel(true)
|
||||
setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
|
||||
setSmallIcon(ResourceHelper.getDefaultMinimalXmlIconId(weatherCode, daytime))
|
||||
weatherCode?.let {
|
||||
setLargeIcon(ResourceHelper.getWeatherIcon(provider, it, daytime).toBitmap())
|
||||
}
|
||||
|
||||
setContentTitle(getDayString(daily, temperatureUnit))
|
||||
setContentText(getNightString(daily, temperatureUnit))
|
||||
setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(
|
||||
getDayString(daily, temperatureUnit) +
|
||||
"\n\n" +
|
||||
getNightString(daily, temperatureUnit)
|
||||
)
|
||||
// do not show any title when expanding the notification
|
||||
.setBigContentTitle("")
|
||||
)
|
||||
setContentIntent(
|
||||
AbstractRemoteViewsPresenter.getWeatherPendingIntent(
|
||||
context,
|
||||
null,
|
||||
if (today) {
|
||||
Notifications.ID_TODAY_FORECAST
|
||||
} else {
|
||||
Notifications.ID_TOMORROW_FORECAST
|
||||
}
|
||||
)
|
||||
)
|
||||
}.build()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
weather.current?.weatherCode != null
|
||||
) {
|
||||
try {
|
||||
notification.javaClass
|
||||
.getMethod("setSmallIcon", Icon::class.java)
|
||||
.invoke(
|
||||
notification,
|
||||
ResourceHelper.getMinimalIcon(
|
||||
provider,
|
||||
weather.current!!.weatherCode!!,
|
||||
daytime
|
||||
)
|
||||
)
|
||||
} catch (ignore: Exception) {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
context.notify(
|
||||
if (today) Notifications.ID_TODAY_FORECAST else Notifications.ID_TOMORROW_FORECAST,
|
||||
notification
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDayString(daily: Daily, temperatureUnit: TemperatureUnit) =
|
||||
context.getString(R.string.daytime) +
|
||||
context.getString(R.string.colon_separator) +
|
||||
daily.day?.temperature?.temperature?.formatMeasure(
|
||||
context,
|
||||
temperatureUnit,
|
||||
valueWidth = UnitWidth.NARROW
|
||||
) +
|
||||
context.getString(R.string.dot_separator) +
|
||||
daily.day?.weatherText
|
||||
|
||||
private fun getNightString(daily: Daily, temperatureUnit: TemperatureUnit) =
|
||||
context.getString(R.string.nighttime) +
|
||||
context.getString(R.string.colon_separator) +
|
||||
daily.night?.temperature?.temperature?.formatMeasure(
|
||||
context,
|
||||
temperatureUnit,
|
||||
valueWidth = UnitWidth.NARROW
|
||||
) +
|
||||
context.getString(R.string.dot_separator) +
|
||||
daily.night?.weatherText
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.forecast
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.breezyweather.common.extensions.cancelNotification
|
||||
import org.breezyweather.common.extensions.hasNotificationPermission
|
||||
import org.breezyweather.common.extensions.isRunning
|
||||
import org.breezyweather.common.extensions.setForegroundSafely
|
||||
import org.breezyweather.common.extensions.workManager
|
||||
import org.breezyweather.common.utils.helpers.LogHelper
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
@HiltWorker
|
||||
class TodayForecastNotificationJob @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val locationRepository: LocationRepository,
|
||||
private val weatherRepository: WeatherRepository,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val notifier = ForecastNotificationNotifier(context)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
setForegroundSafely()
|
||||
|
||||
return try {
|
||||
if (SettingsManager.getInstance(context).isTodayForecastEnabled) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
if (location != null) {
|
||||
notifier.showComplete(
|
||||
location.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
today = true
|
||||
)
|
||||
}
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { LogHelper.log(msg = it) }
|
||||
Result.failure()
|
||||
} finally {
|
||||
context.cancelNotification(Notifications.ID_UPDATING_TODAY_FORECAST)
|
||||
|
||||
// Add a new job in 24 hours
|
||||
setupTask(context, nextDay = true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_UPDATING_TODAY_FORECAST,
|
||||
notifier.showProgress(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ForecastNotificationToday"
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.workManager.isRunning(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(context: Context, nextDay: Boolean) {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
if (settings.isTodayForecastEnabled) {
|
||||
if (context.hasNotificationPermission) {
|
||||
val request = OneTimeWorkRequestBuilder<TodayForecastNotificationJob>()
|
||||
.setInitialDelay(
|
||||
getForecastAlarmDelayInMinutes(settings.todayForecastTime, nextDay),
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
return
|
||||
} else {
|
||||
settings.isTodayForecastEnabled = false
|
||||
}
|
||||
}
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
private fun getForecastAlarmDelayInMinutes(time: String, nextDay: Boolean): Long {
|
||||
val realTimes = intArrayOf(
|
||||
Calendar.getInstance()[Calendar.HOUR_OF_DAY],
|
||||
Calendar.getInstance()[Calendar.MINUTE]
|
||||
)
|
||||
val setTimes = intArrayOf(
|
||||
time.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].toInt(),
|
||||
time.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toInt()
|
||||
)
|
||||
var delay = (setTimes[0] - realTimes[0]).hours.inWholeMinutes + (setTimes[1] - realTimes[1])
|
||||
if (delay <= 0 || nextDay) {
|
||||
delay += 1.days.inWholeMinutes
|
||||
}
|
||||
return delay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.forecast
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkerParameters
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.breezyweather.common.extensions.cancelNotification
|
||||
import org.breezyweather.common.extensions.hasNotificationPermission
|
||||
import org.breezyweather.common.extensions.isRunning
|
||||
import org.breezyweather.common.extensions.setForegroundSafely
|
||||
import org.breezyweather.common.extensions.workManager
|
||||
import org.breezyweather.common.utils.helpers.LogHelper
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
@HiltWorker
|
||||
class TomorrowForecastNotificationJob @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val locationRepository: LocationRepository,
|
||||
private val weatherRepository: WeatherRepository,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val notifier = ForecastNotificationNotifier(context)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
setForegroundSafely()
|
||||
|
||||
return try {
|
||||
if (SettingsManager.getInstance(context).isTomorrowForecastEnabled) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
if (location != null) {
|
||||
notifier.showComplete(
|
||||
location.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
today = false
|
||||
)
|
||||
}
|
||||
}
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
e.message?.let { LogHelper.log(msg = it) }
|
||||
Result.failure()
|
||||
} finally {
|
||||
context.cancelNotification(Notifications.ID_UPDATING_TOMORROW_FORECAST)
|
||||
|
||||
// Add a new job in 24 hours
|
||||
setupTask(context, nextDay = true)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_UPDATING_TOMORROW_FORECAST,
|
||||
notifier.showProgress(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ForecastNotificationTomorrow"
|
||||
|
||||
fun isRunning(context: Context): Boolean {
|
||||
return context.workManager.isRunning(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(context: Context, nextDay: Boolean) {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
if (settings.isTomorrowForecastEnabled) {
|
||||
if (context.hasNotificationPermission) {
|
||||
val request = OneTimeWorkRequestBuilder<TomorrowForecastNotificationJob>()
|
||||
.setInitialDelay(
|
||||
getForecastAlarmDelayInMinutes(settings.tomorrowForecastTime, nextDay),
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
return
|
||||
} else {
|
||||
settings.isTomorrowForecastEnabled = false
|
||||
}
|
||||
}
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
context.workManager.cancelUniqueWork(TAG)
|
||||
}
|
||||
|
||||
private fun getForecastAlarmDelayInMinutes(time: String, nextDay: Boolean): Long {
|
||||
val realTimes = intArrayOf(
|
||||
Calendar.getInstance()[Calendar.HOUR_OF_DAY],
|
||||
Calendar.getInstance()[Calendar.MINUTE]
|
||||
)
|
||||
val setTimes = intArrayOf(
|
||||
time.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0].toInt(),
|
||||
time.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toInt()
|
||||
)
|
||||
var delay = (setTimes[0] - realTimes[0]).hours.inWholeMinutes + (setTimes[1] - realTimes[1])
|
||||
if (delay <= 0 || nextDay) {
|
||||
delay += 1.days.inWholeMinutes
|
||||
}
|
||||
return delay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.interfaces
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.annotation.RequiresApi
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.common.extensions.formatMeasure
|
||||
import org.breezyweather.domain.location.model.isDaylight
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.ui.main.MainActivity
|
||||
import org.breezyweather.ui.theme.resource.ResourceHelper
|
||||
import org.breezyweather.ui.theme.resource.ResourcesProviderFactory
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Tile service.
|
||||
* TODO: Memory leak
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class TileService : TileService(), CoroutineScope {
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
override fun onTileAdded() {
|
||||
refreshTile(this, qsTile)
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
refreshTile(this, qsTile)
|
||||
}
|
||||
|
||||
override fun onStopListening() {
|
||||
refreshTile(this, qsTile)
|
||||
}
|
||||
|
||||
@SuppressLint("StartActivityAndCollapseDeprecated")
|
||||
override fun onClick() {
|
||||
val intent = Intent(MainActivity.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
this.startActivityAndCollapse(
|
||||
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
this.startActivityAndCollapse(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshTile(context: Context, tile: Tile?) {
|
||||
if (tile == null) return
|
||||
launch {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false) ?: return@launch
|
||||
val locationRefreshed = location.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true, // isDaylight
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
locationRefreshed.weather?.current?.let { current ->
|
||||
tile.apply {
|
||||
current.weatherCode?.let {
|
||||
icon = ResourceHelper.getMinimalIcon(
|
||||
ResourcesProviderFactory.newInstance,
|
||||
it,
|
||||
locationRefreshed.isDaylight
|
||||
)
|
||||
}
|
||||
tile.label = current.temperature?.temperature?.formatMeasure(
|
||||
context,
|
||||
SettingsManager.getInstance(context).getTemperatureUnit(context)
|
||||
)
|
||||
state = Tile.STATE_INACTIVE
|
||||
}
|
||||
tile.updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.common.extensions.workManager
|
||||
import org.breezyweather.remoteviews.presenters.notification.WidgetNotificationIMP
|
||||
import org.breezyweather.sources.RefreshHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Receiver to force app to autostart on boot
|
||||
* Does nothing, it’s just that some OEM do not respect Android policy to keep scheduled workers
|
||||
* regardless of if the app is started or not
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
lateinit var refreshHelper: RefreshHelper
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
if (action.isNullOrEmpty()) return
|
||||
when (action) {
|
||||
Intent.ACTION_BOOT_COMPLETED -> {
|
||||
/**
|
||||
* We don’t use the return value, but querying the work manager might help bringing back
|
||||
* scheduled workers after the app has been killed/shutdown on some devices
|
||||
*/
|
||||
context.workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED))
|
||||
|
||||
// Bring back notification-widget if necessary
|
||||
if (WidgetNotificationIMP.isEnabled(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
refreshHelper.updateNotificationIfNecessary(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import org.breezyweather.background.weather.WeatherUpdateJob
|
||||
import org.breezyweather.common.extensions.cancelNotification
|
||||
import org.breezyweather.common.extensions.notificationManager
|
||||
import org.breezyweather.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
/**
|
||||
* Taken partially from Mihon
|
||||
* License Apache, Version 2.0
|
||||
* https://github.com/mihonapp/mihon/blob/aa498360db90350f2642e6320dc55e7d474df1fd/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Global [BroadcastReceiver] that runs on UI thread
|
||||
* Pending Broadcasts should be made from here.
|
||||
* NOTE: Use local broadcasts if possible.
|
||||
*/
|
||||
class NotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
// Cancel weather update and dismiss notification
|
||||
ACTION_CANCEL_WEATHER_UPDATE -> cancelWeatherUpdate(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the notification
|
||||
*
|
||||
* @param notificationId the id of the notification
|
||||
*/
|
||||
private fun dismissNotification(context: Context, notificationId: Int) {
|
||||
context.cancelNotification(notificationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to stop a weather update
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
private fun cancelWeatherUpdate(context: Context) {
|
||||
WeatherUpdateJob.stop(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NAME = "NotificationReceiver"
|
||||
|
||||
private const val ACTION_CANCEL_WEATHER_UPDATE = "$ID.$NAME.CANCEL_WEATHER_UPDATE"
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the weather update
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelWeatherUpdatePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_WEATHER_UPDATE
|
||||
}
|
||||
return PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which dismissed the notification
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun dismissNotification(context: Context, notificationId: Int, groupId: Int? = null) {
|
||||
/*
|
||||
Group notifications always have at least 2 notifications:
|
||||
- Group summary notification
|
||||
- Single manga notification
|
||||
|
||||
If the single notification is dismissed by the system, ie by a user swipe or tapping on the notification,
|
||||
it will auto dismiss the group notification if there's no other single updates.
|
||||
|
||||
When programmatically dismissing this notification, the group notification is not automatically dismissed.
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val groupKey = context.notificationManager.activeNotifications.find {
|
||||
it.id == notificationId
|
||||
}?.groupKey
|
||||
|
||||
if (groupId != null && groupId != 0 && !groupKey.isNullOrEmpty()) {
|
||||
val notifications = context.notificationManager.activeNotifications.filter {
|
||||
it.groupKey == groupKey
|
||||
}
|
||||
|
||||
if (notifications.size == 2) {
|
||||
context.cancelNotification(groupId)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.cancelNotification(notificationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that opens the error log file in an external viewer
|
||||
*
|
||||
* @param context context of application
|
||||
* @param uri uri of error log file
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun openErrorLogPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
setDataAndType(uri, "text/plain")
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.ClockDayDetailsWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget clock day details provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetClockDayDetailsProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (ClockDayDetailsWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
ClockDayDetailsWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.ClockDayHorizontalWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget clock day horizontal provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetClockDayHorizontalProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (ClockDayHorizontalWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
ClockDayHorizontalWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true, // isDaylight
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.ClockDayVerticalWidgetIMP
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget clock day vertical provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetClockDayVerticalProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@Inject
|
||||
lateinit var sourceManager: SourceManager
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (ClockDayVerticalWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
ClockDayVerticalWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = true, // Custom subtitle
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
location?.let { locationNow ->
|
||||
sourceManager.getPollenIndexSource(
|
||||
(locationNow.pollenSource ?: "").ifEmpty { locationNow.forecastSource }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.ClockDayWeekWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget clock day week provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetClockDayWeekProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (ClockDayWeekWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
ClockDayWeekWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.DayWidgetIMP
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget day provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetDayProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@Inject
|
||||
lateinit var sourceManager: SourceManager
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (DayWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
DayWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = true, // Custom subtitle
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
location?.let { locationNow ->
|
||||
sourceManager.getPollenIndexSource(
|
||||
(locationNow.pollenSource ?: "").ifEmpty { locationNow.forecastSource }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.DayWeekWidgetIMP
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget day week provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetDayWeekProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@Inject
|
||||
lateinit var sourceManager: SourceManager
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (DayWeekWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
DayWeekWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = true, // Custom subtitle
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
location?.let { locationNow ->
|
||||
sourceManager.getPollenIndexSource(
|
||||
(locationNow.pollenSource ?: "").ifEmpty { locationNow.forecastSource }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.MaterialYouCurrentWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WidgetMaterialYouCurrentProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (MaterialYouCurrentWidgetIMP.isEnabled(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
MaterialYouCurrentWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true, // isDaylight
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle,
|
||||
) {
|
||||
onUpdate(context, appWidgetManager, intArrayOf(appWidgetId))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.MaterialYouForecastWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WidgetMaterialYouForecastProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (MaterialYouForecastWidgetIMP.isEnabled(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
MaterialYouForecastWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = true,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.MultiCityWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget multi city provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetMultiCityProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (MultiCityWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val locationList = locationRepository.getXLocations(3, withParameters = false).toMutableList()
|
||||
for (i in locationList.indices) {
|
||||
locationList[i] = locationList[i].copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
locationList[i].formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
}
|
||||
MultiCityWidgetIMP.updateWidgetView(context, locationList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.TextWidgetIMP
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget text provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetTextProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@Inject
|
||||
lateinit var sourceManager: SourceManager
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (TextWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
TextWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true, // isDaylight
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = true, // Custom subtitle
|
||||
withNormals = false
|
||||
)
|
||||
),
|
||||
location?.let { locationNow ->
|
||||
sourceManager.getPollenIndexSource(
|
||||
(locationNow.pollenSource ?: "").ifEmpty { locationNow.forecastSource }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.DailyTrendWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget trend daily provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetTrendDailyProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (DailyTrendWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
DailyTrendWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = true // Threshold lines
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.HourlyTrendWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget trend hourly provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetTrendHourlyProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (HourlyTrendWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
HourlyTrendWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true, // isDaylight
|
||||
withHourly = true,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = true // Threshold lines
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.receiver.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.breezyweather.remoteviews.presenters.WeekWidgetIMP
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Widget week provider.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class WidgetWeekProvider : AppWidgetProvider() {
|
||||
|
||||
@Inject
|
||||
lateinit var locationRepository: LocationRepository
|
||||
|
||||
@Inject
|
||||
lateinit var weatherRepository: WeatherRepository
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
if (WeekWidgetIMP.isInUse(context)) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val location = locationRepository.getFirstLocation(withParameters = false)
|
||||
WeekWidgetIMP.updateWidgetView(
|
||||
context,
|
||||
location?.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(
|
||||
location.formattedId,
|
||||
withDaily = true,
|
||||
withHourly = false,
|
||||
withMinutely = false,
|
||||
withAlerts = false,
|
||||
withNormals = false
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import org.breezyweather.BuildConfig
|
||||
import org.breezyweather.background.updater.interactor.GetApplicationRelease
|
||||
import org.breezyweather.common.extensions.withIOContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/aa498360db90350f2642e6320dc55e7d474df1fd/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt
|
||||
*/
|
||||
class AppUpdateChecker @Inject constructor(
|
||||
private val getApplicationRelease: GetApplicationRelease,
|
||||
) {
|
||||
|
||||
suspend fun checkForUpdate(
|
||||
context: Context,
|
||||
forceCheck: Boolean = false,
|
||||
): GetApplicationRelease.Result {
|
||||
// Disable app update checks for older Android versions that we're going to drop support for
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
AppUpdateNotifier(context).promptOldAndroidVersion()
|
||||
return GetApplicationRelease.Result.OsTooOld
|
||||
}
|
||||
|
||||
return withIOContext {
|
||||
val result = getApplicationRelease.await(
|
||||
GetApplicationRelease.Arguments(
|
||||
BuildConfig.VERSION_NAME,
|
||||
GITHUB_ORG,
|
||||
GITHUB_REPO,
|
||||
forceCheck
|
||||
)
|
||||
)
|
||||
|
||||
when (result) {
|
||||
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val GITHUB_ORG = "breezy-weather"
|
||||
val GITHUB_REPO = "breezy-weather"
|
||||
|
||||
val RELEASE_URL = "https://github.com/${GITHUB_REPO}/releases/tag/v${BuildConfig.VERSION_NAME}"
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.net.toUri
|
||||
import org.breezyweather.R
|
||||
import org.breezyweather.background.receiver.NotificationReceiver
|
||||
import org.breezyweather.background.updater.model.Release
|
||||
import org.breezyweather.common.extensions.notificationBuilder
|
||||
import org.breezyweather.common.extensions.notify
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
|
||||
internal class AppUpdateNotifier(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE)
|
||||
|
||||
/**
|
||||
* Call to show notification.
|
||||
*
|
||||
* @param id id of the notification channel.
|
||||
*/
|
||||
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_APP_UPDATER) {
|
||||
context.notify(id, build())
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
|
||||
}
|
||||
|
||||
fun promptOldAndroidVersion() {
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.about_update_check_eol))
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
clearActions()
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
fun promptUpdate(release: Release) {
|
||||
/*val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
|
||||
context,
|
||||
release.getDownloadLink(),
|
||||
release.version,
|
||||
)*/
|
||||
|
||||
val releaseIntent = Intent(Intent.ACTION_VIEW, release.releaseLink.toUri()).run {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
release.hashCode(),
|
||||
this,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
with(notificationBuilder) {
|
||||
setContentTitle(context.getString(R.string.notification_app_update_available))
|
||||
setContentText(release.version)
|
||||
setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
// setContentIntent(updateIntent)
|
||||
setContentIntent(releaseIntent)
|
||||
|
||||
clearActions()
|
||||
addAction(
|
||||
android.R.drawable.stat_sys_download_done,
|
||||
context.getString(R.string.action_download),
|
||||
// updateIntent,
|
||||
releaseIntent
|
||||
)
|
||||
/*addAction(
|
||||
R.drawable.ic_info_24dp,
|
||||
context.getString(R.string.whats_new),
|
||||
releaseIntent,
|
||||
)*/
|
||||
}
|
||||
notificationBuilder.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater.data
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
/**
|
||||
* Open-Meteo API
|
||||
*/
|
||||
interface GithubApi {
|
||||
@GET("repos/{org}/{repository}/releases/latest")
|
||||
suspend fun getLatest(
|
||||
@Path("org") org: String,
|
||||
@Path("repository") repository: String,
|
||||
): GithubRelease
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater.data
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/d29b7c4e5735dc137d578d3bcb3da1f0a02573e8/data/src/main/java/tachiyomi/data/release/GithubRelease.kt
|
||||
*/
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.breezyweather.background.updater.model.Release
|
||||
|
||||
/**
|
||||
* Contains information about the latest release from GitHub.
|
||||
*/
|
||||
@Serializable
|
||||
data class GithubRelease(
|
||||
@SerialName("tag_name") val version: String,
|
||||
@SerialName("body") val info: String,
|
||||
@SerialName("html_url") val releaseLink: String,
|
||||
@SerialName("assets") val assets: List<GitHubAssets>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Assets class containing download url.
|
||||
*/
|
||||
@Serializable
|
||||
data class GitHubAssets(
|
||||
@SerialName("browser_download_url") val downloadLink: String,
|
||||
)
|
||||
|
||||
val releaseMapper: (GithubRelease) -> Release = {
|
||||
Release(
|
||||
it.version,
|
||||
it.info,
|
||||
it.releaseLink,
|
||||
it.assets.map(GitHubAssets::downloadLink)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater.data
|
||||
|
||||
import org.breezyweather.background.updater.model.Release
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/02864ebd60ac9eb974a1b54b06368d20b0ca3ce5/data/src/main/java/tachiyomi/data/release/ReleaseServiceImpl.kt
|
||||
*/
|
||||
class ReleaseService @Inject constructor(
|
||||
@Named("JsonClient") val client: Retrofit.Builder,
|
||||
) {
|
||||
|
||||
suspend fun latest(org: String, repository: String): Release {
|
||||
return client
|
||||
.baseUrl("https://api.github.com/")
|
||||
.build()
|
||||
.create(GithubApi::class.java)
|
||||
.getLatest(org, repository)
|
||||
.let(releaseMapper)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater.interactor
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.breezyweather.background.updater.data.ReleaseService
|
||||
import org.breezyweather.background.updater.model.Release
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/aa498360db90350f2642e6320dc55e7d474df1fd/domain/src/main/java/tachiyomi/domain/release/interactor/GetApplicationRelease.kt
|
||||
*/
|
||||
class GetApplicationRelease @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
val service: ReleaseService,
|
||||
) {
|
||||
|
||||
suspend fun await(
|
||||
arguments: Arguments,
|
||||
): Result {
|
||||
val now = Date().time
|
||||
|
||||
val lastChecked = SettingsManager.getInstance(context).appUpdateCheckLastTimestamp
|
||||
|
||||
// Limit checks to once every day at most
|
||||
if (!arguments.forceCheck && now < lastChecked + 1.days.inWholeMilliseconds) {
|
||||
return Result.NoNewUpdate
|
||||
}
|
||||
|
||||
val release = service.latest(arguments.org, arguments.repository)
|
||||
|
||||
SettingsManager.getInstance(context).appUpdateCheckLastTimestamp = now
|
||||
|
||||
// Check if latest version is different from current version
|
||||
val isNewVersion = isNewVersion(
|
||||
arguments.versionName,
|
||||
release.version
|
||||
)
|
||||
return when {
|
||||
isNewVersion -> Result.NewUpdate(release)
|
||||
else -> Result.NoNewUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNewVersion(
|
||||
versionName: String,
|
||||
versionTag: String,
|
||||
): Boolean {
|
||||
// Removes "v" prefixes
|
||||
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
|
||||
val oldVersion = versionName.replace("[^\\d.]".toRegex(), "")
|
||||
|
||||
val newSemVer = newVersion.split(".").map { it.toInt() }
|
||||
val oldSemVer = oldVersion.split(".").map { it.toInt() }
|
||||
|
||||
oldSemVer.mapIndexed { index, i ->
|
||||
// Useful in case of pre-releases, where the newer stable version is older than the pre-release
|
||||
if (newSemVer[index] < i) {
|
||||
return false
|
||||
}
|
||||
if (newSemVer[index] > i) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
data class Arguments(
|
||||
val versionName: String,
|
||||
val org: String,
|
||||
val repository: String,
|
||||
val forceCheck: Boolean = false,
|
||||
)
|
||||
|
||||
sealed interface Result {
|
||||
data class NewUpdate(val release: Release) : Result
|
||||
data object NoNewUpdate : Result
|
||||
data object OsTooOld : Result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.updater.model
|
||||
|
||||
import android.os.Build
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/c83037eeab3b180c7b82355331131df6950f5d45/domain/src/main/java/tachiyomi/domain/release/model/Release.kt
|
||||
*/
|
||||
/**
|
||||
* Contains information about the latest release.
|
||||
*/
|
||||
data class Release(
|
||||
val version: String,
|
||||
val info: String,
|
||||
val releaseLink: String,
|
||||
private val assets: List<String>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Get download link of latest release from the assets.
|
||||
* @return download link of latest release.
|
||||
*/
|
||||
fun getDownloadLink(): String {
|
||||
val apkVariant = when (Build.SUPPORTED_ABIS[0]) {
|
||||
"arm64-v8a" -> "-arm64-v8a"
|
||||
"armeabi-v7a" -> "-armeabi-v7a"
|
||||
"x86" -> "-x86"
|
||||
"x86_64" -> "-x86_64"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
return assets.find {
|
||||
it.startsWith("breezy-weather$apkVariant-") && !it.contains("freenet")
|
||||
} ?: assets[0] // FIXME
|
||||
}
|
||||
|
||||
/**
|
||||
* Assets class containing download url.
|
||||
*/
|
||||
data class Assets(val downloadLink: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,530 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.weather
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import breezyweather.domain.location.model.Location
|
||||
import com.google.maps.android.SphericalUtil
|
||||
import com.google.maps.android.model.LatLng
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import org.breezyweather.BuildConfig
|
||||
import org.breezyweather.background.updater.AppUpdateChecker
|
||||
import org.breezyweather.common.bus.EventBus
|
||||
import org.breezyweather.common.extensions.createFileInCacheDir
|
||||
import org.breezyweather.common.extensions.getFormattedDate
|
||||
import org.breezyweather.common.extensions.getIsoFormattedDate
|
||||
import org.breezyweather.common.extensions.getUriCompat
|
||||
import org.breezyweather.common.extensions.isOnline
|
||||
import org.breezyweather.common.extensions.isRunning
|
||||
import org.breezyweather.common.extensions.setForegroundSafely
|
||||
import org.breezyweather.common.extensions.withIOContext
|
||||
import org.breezyweather.common.extensions.workManager
|
||||
import org.breezyweather.common.options.NotificationStyle
|
||||
import org.breezyweather.common.source.LocationResult
|
||||
import org.breezyweather.common.source.RefreshError
|
||||
import org.breezyweather.common.source.WeatherResult
|
||||
import org.breezyweather.domain.location.model.getPlace
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
import org.breezyweather.remoteviews.presenters.MultiCityWidgetIMP
|
||||
import org.breezyweather.sources.RefreshHelper
|
||||
import org.breezyweather.sources.SourceManager
|
||||
import org.breezyweather.ui.main.utils.RefreshErrorType
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Based on Mihon LibraryUpdateJob
|
||||
* Licensed under Apache License, Version 2.0
|
||||
* https://github.com/mihonapp/mihon/blob/88e9fefa59b3f7f77ab3ddcab1b039f81534c83e/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt
|
||||
*/
|
||||
@HiltWorker
|
||||
class WeatherUpdateJob @AssistedInject constructor(
|
||||
@Assisted private val context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
private val refreshHelper: RefreshHelper,
|
||||
private val sourceManager: SourceManager,
|
||||
private val locationRepository: LocationRepository,
|
||||
private val weatherRepository: WeatherRepository,
|
||||
private val updateChecker: AppUpdateChecker,
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
private val notifier = WeatherUpdateNotifier(context)
|
||||
|
||||
private var locationsToUpdate: List<Location> = mutableListOf()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
if (tags.contains(WORK_NAME_AUTO)) {
|
||||
// Find a running manual worker. If exists, try again later
|
||||
if (context.workManager.isRunning(WORK_NAME_MANUAL)) {
|
||||
return Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
// Exit early in case there is no network and Android still executes the job
|
||||
if (!context.isOnline()) {
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
// Set the last update time to now
|
||||
SettingsManager.getInstance(context).weatherUpdateLastTimestamp = Date().time
|
||||
|
||||
val locationFormattedId = inputData.getString(KEY_LOCATION)
|
||||
addLocationToQueue(locationFormattedId)
|
||||
|
||||
return withIOContext {
|
||||
try {
|
||||
updateWeatherData()
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
// Assume success although cancelled
|
||||
Result.success()
|
||||
} else {
|
||||
e.printStackTrace()
|
||||
Result.failure()
|
||||
}
|
||||
} finally {
|
||||
notifier.cancelProgressNotification()
|
||||
// if (BuildConfig.FLAVOR != "freenet" && SettingsManager.getInstance(context).isAppUpdateCheckEnabled) {
|
||||
if ((BuildConfig.FLAVOR != "freenet" && SettingsManager.getInstance(context).isAppUpdateCheckEnabled) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|
||||
) {
|
||||
try {
|
||||
updateChecker.checkForUpdate(context, forceCheck = false)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val notifier = WeatherUpdateNotifier(context)
|
||||
return ForegroundInfo(
|
||||
Notifications.ID_WEATHER_PROGRESS,
|
||||
notifier.progressNotificationBuilder.build(),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds list of locations to be updated.
|
||||
*
|
||||
* @param locationFormattedId the ID of the location to update, or null if automatic detection.
|
||||
*/
|
||||
private suspend fun addLocationToQueue(locationFormattedId: String?) {
|
||||
locationsToUpdate = if (locationFormattedId != null) {
|
||||
val location = locationRepository.getLocation(locationFormattedId)
|
||||
if (location != null) {
|
||||
listOf(
|
||||
location.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(location.formattedId)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
val locationList = when {
|
||||
// Should be getAllLocations(), but some rare users have 100+ locations. No need to refresh all of them
|
||||
// in that case, they don't actually use them every day, they just add them as "bookmarks"
|
||||
refreshHelper.isBroadcastSourcesEnabled(context) -> locationRepository.getXLocations(5)
|
||||
SettingsManager.getInstance(context).isWidgetNotificationEnabled &&
|
||||
SettingsManager.getInstance(context).widgetNotificationStyle == NotificationStyle.CITIES ->
|
||||
locationRepository.getXLocations(4)
|
||||
MultiCityWidgetIMP.isInUse(context) -> locationRepository.getXLocations(3)
|
||||
else -> locationRepository.getXLocations(1)
|
||||
}
|
||||
|
||||
locationList
|
||||
.map {
|
||||
it.copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(it.formattedId)
|
||||
)
|
||||
}
|
||||
.filterIndexed { i, location ->
|
||||
// Only refresh secondary locations once a day as we only need daily info
|
||||
i == 0 ||
|
||||
location.weather?.base?.refreshTime == null ||
|
||||
location.weather!!.base.refreshTime!!.getIsoFormattedDate(location) <
|
||||
Date().getFormattedDate("yyyy-MM-dd")
|
||||
}
|
||||
.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that updates weather in [locationsToUpdate]. It's called in a background thread, so it's safe
|
||||
* to do heavy operations or network calls here.
|
||||
* For each weather it calls [updateLocation] and updates the notification showing the current
|
||||
* progress.
|
||||
*
|
||||
* @return an observable delivering the progress of each update.
|
||||
*/
|
||||
private suspend fun updateWeatherData() {
|
||||
val progressCount = AtomicInteger(0)
|
||||
val currentlyUpdatingLocation = CopyOnWriteArrayList<Location>()
|
||||
val newUpdates = CopyOnWriteArrayList<Pair<Location, Location>>()
|
||||
val skippedUpdates = CopyOnWriteArrayList<Pair<Location, String?>>()
|
||||
val failedUpdates = CopyOnWriteArrayList<Pair<Location, String?>>()
|
||||
|
||||
/**
|
||||
* Update coordinates if locations to update contains a current location
|
||||
*/
|
||||
val updateCoordinatesErrors = if (locationsToUpdate.any { it.isCurrentPosition }) {
|
||||
updateCoordinates()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
locationsToUpdate.forEach { location ->
|
||||
withUpdateNotification(
|
||||
currentlyUpdatingLocation,
|
||||
progressCount,
|
||||
location
|
||||
) {
|
||||
// TODO: Implement this, it’s a good idea
|
||||
/*if (location.updateStrategy != UpdateStrategy.ALWAYS_UPDATE) {
|
||||
skippedUpdates.add(location to context.getString(R.string.skipped_reason_not_always_update))
|
||||
} else {*/
|
||||
try {
|
||||
val locationResult = updateLocation(location)
|
||||
locationResult.errors.forEach {
|
||||
val shortMessage = it.getMessage(context, sourceManager)
|
||||
if (it.error != RefreshErrorType.NETWORK_UNAVAILABLE &&
|
||||
it.error != RefreshErrorType.SERVER_TIMEOUT
|
||||
) {
|
||||
failedUpdates.add(locationResult.location to shortMessage)
|
||||
} else {
|
||||
skippedUpdates.add(locationResult.location to shortMessage)
|
||||
}
|
||||
}
|
||||
if (!locationResult.location.isUsable) {
|
||||
// Report coordinate update errors only if we can’t re-use last known coordinates
|
||||
updateCoordinatesErrors.forEach {
|
||||
val shortMessage = it.getMessage(context, sourceManager)
|
||||
failedUpdates.add(locationResult.location to shortMessage)
|
||||
}
|
||||
}
|
||||
if (locationResult.location.isUsable && !locationResult.location.needsGeocodeRefresh) {
|
||||
val ignoreCaching = SphericalUtil.computeDistanceBetween(
|
||||
LatLng(locationResult.location.latitude, locationResult.location.longitude),
|
||||
LatLng(location.latitude, location.longitude)
|
||||
) > RefreshHelper.CACHING_DISTANCE_LIMIT
|
||||
val weatherResult = updateWeather(
|
||||
locationResult.location,
|
||||
location.longitude != locationResult.location.longitude ||
|
||||
location.latitude != locationResult.location.latitude,
|
||||
ignoreCaching
|
||||
)
|
||||
newUpdates.add(
|
||||
location to locationResult.location.copy(weather = weatherResult.weather)
|
||||
)
|
||||
weatherResult.errors.forEach {
|
||||
failedUpdates.add(location to it.getMessage(context, sourceManager))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
val errorMessage = if (e.message.isNullOrEmpty()) {
|
||||
context.getString(RefreshErrorType.DATA_REFRESH_FAILED.shortMessage)
|
||||
} else {
|
||||
e.message
|
||||
}
|
||||
failedUpdates.add(location to errorMessage)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
notifier.cancelProgressNotification()
|
||||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
// We updated at least one location, so we need to reload location list and make some post-actions
|
||||
val locationList = locationRepository.getAllLocations().toMutableList()
|
||||
for (i in locationList.indices) {
|
||||
locationList[i] = locationList[i].copy(
|
||||
weather = weatherRepository.getWeatherByLocationId(locationList[i].formattedId)
|
||||
)
|
||||
}
|
||||
|
||||
// Update widgets and notification-widget
|
||||
refreshHelper.updateWidgetIfNecessary(context, locationList)
|
||||
refreshHelper.updateNotificationIfNecessary(context, locationList)
|
||||
|
||||
// Update shortcuts
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
refreshHelper.refreshShortcuts(applicationContext, locationList)
|
||||
}
|
||||
|
||||
val location = locationList[0]
|
||||
val indexOfFirstLocation = newUpdates.firstOrNull { it.first.formattedId == location.formattedId }
|
||||
|
||||
// Send alert and precipitation for the first location
|
||||
if (indexOfFirstLocation != null) {
|
||||
Notifications.checkAndSendAlert(
|
||||
applicationContext,
|
||||
location,
|
||||
locationsToUpdate.firstOrNull { it.formattedId == location.formattedId }?.weather
|
||||
)
|
||||
Notifications.checkAndSendPrecipitation(applicationContext, location)
|
||||
}
|
||||
|
||||
refreshHelper.broadcastDataIfNecessary(
|
||||
context,
|
||||
locationList,
|
||||
newUpdates.map { it.first.formattedId }.toTypedArray()
|
||||
)
|
||||
|
||||
// Inform main activity that we updated location
|
||||
newUpdates.forEach {
|
||||
EventBus.instance
|
||||
.with(Location::class.java)
|
||||
.postValue(it.second)
|
||||
}
|
||||
}
|
||||
|
||||
if (failedUpdates.isNotEmpty()) {
|
||||
val errorFile = writeErrorFile(failedUpdates)
|
||||
notifier.showUpdateErrorNotification(
|
||||
failedUpdates.groupBy { it.first }.size,
|
||||
errorFile.getUriCompat(context)
|
||||
)
|
||||
}
|
||||
/*if (skippedUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current location coordinates.
|
||||
*
|
||||
* @return errors if any
|
||||
*/
|
||||
private suspend fun updateCoordinates(): List<RefreshError> {
|
||||
return refreshHelper.updateCurrentCoordinates(context, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the location with updated coordinates and reverse geocoding.
|
||||
*
|
||||
* @param location the location to update.
|
||||
* @return location updated.
|
||||
*/
|
||||
private suspend fun updateLocation(location: Location): LocationResult {
|
||||
return refreshHelper.getLocation(context, location)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the weather for the given location and adds them to the database.
|
||||
*
|
||||
* @param location the location to update.
|
||||
* @return weather.
|
||||
*/
|
||||
private suspend fun updateWeather(
|
||||
location: Location,
|
||||
coordinatesChanged: Boolean,
|
||||
ignoreCaching: Boolean,
|
||||
): WeatherResult {
|
||||
return refreshHelper.getWeather(
|
||||
context,
|
||||
location,
|
||||
coordinatesChanged,
|
||||
ignoreCaching
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun withUpdateNotification(
|
||||
updatingLocation: CopyOnWriteArrayList<Location>,
|
||||
completed: AtomicInteger,
|
||||
location: Location,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
coroutineScope {
|
||||
ensureActive()
|
||||
|
||||
updatingLocation.add(location)
|
||||
notifier.showProgressNotification(
|
||||
updatingLocation,
|
||||
completed.get(),
|
||||
locationsToUpdate.size
|
||||
)
|
||||
|
||||
block()
|
||||
|
||||
ensureActive()
|
||||
|
||||
updatingLocation.remove(location)
|
||||
completed.getAndIncrement()
|
||||
notifier.showProgressNotification(
|
||||
updatingLocation,
|
||||
completed.get(),
|
||||
locationsToUpdate.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes basic file of update errors to cache dir.
|
||||
*/
|
||||
private fun writeErrorFile(errors: List<Pair<Location, String?>>): File {
|
||||
try {
|
||||
if (errors.isNotEmpty()) {
|
||||
val file = context.createFileInCacheDir("breezyweather_update_errors.txt")
|
||||
file.bufferedWriter().use { out ->
|
||||
out.write("Errors during refresh\n\n")
|
||||
// Error file format:
|
||||
// ! Location
|
||||
// - Error
|
||||
errors.groupBy({ it.first }, { it.second }).forEach { (location, errors) ->
|
||||
out.write("\n! ${location.getPlace(context, showCurrentPositionInPriority = true)}\n")
|
||||
errors.forEach {
|
||||
out.write(" - $it\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return File("")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WeatherUpdate"
|
||||
private const val WORK_NAME_AUTO = "WeatherUpdate-auto"
|
||||
private const val WORK_NAME_MANUAL = "WeatherUpdate-manual"
|
||||
|
||||
/**
|
||||
* Key for location to update.
|
||||
*/
|
||||
private const val KEY_LOCATION = "location"
|
||||
|
||||
private const val MINUTES_PER_HOUR: Long = 60
|
||||
private const val BACKOFF_DELAY_MINUTES: Long = 10
|
||||
|
||||
fun cancelAllWorks(context: Context) {
|
||||
context.workManager.cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(
|
||||
context: Context,
|
||||
) {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
val pollingRate = settings.updateInterval.interval
|
||||
if (pollingRate != null && pollingRate > 15.minutes) {
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = NetworkType.CONNECTED,
|
||||
requiresBatteryNotLow = settings.ignoreUpdatesWhenBatteryLow
|
||||
)
|
||||
|
||||
val request = PeriodicWorkRequestBuilder<WeatherUpdateJob>(
|
||||
pollingRate.inWholeMinutes,
|
||||
TimeUnit.MINUTES,
|
||||
BACKOFF_DELAY_MINUTES,
|
||||
TimeUnit.MINUTES
|
||||
)
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_NAME_AUTO)
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
context.workManager.enqueueUniquePeriodicWork(
|
||||
WORK_NAME_AUTO,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
} else {
|
||||
context.workManager.cancelUniqueWork(WORK_NAME_AUTO)
|
||||
}
|
||||
}
|
||||
|
||||
fun startNow(
|
||||
context: Context,
|
||||
location: Location? = null,
|
||||
): Boolean {
|
||||
val wm = context.workManager
|
||||
if (wm.isRunning(TAG)) {
|
||||
// Already running either as a scheduled or manual job
|
||||
return false
|
||||
}
|
||||
|
||||
val inputData = workDataOf(
|
||||
KEY_LOCATION to location?.formattedId
|
||||
)
|
||||
val request = OneTimeWorkRequestBuilder<WeatherUpdateJob>()
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_NAME_MANUAL)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
wm.enqueueUniqueWork(WORK_NAME_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun stop(context: Context) {
|
||||
val wm = context.workManager
|
||||
val workQuery = WorkQuery.Builder.fromTags(listOf(TAG))
|
||||
.addStates(listOf(WorkInfo.State.RUNNING))
|
||||
.build()
|
||||
wm.getWorkInfos(workQuery).get()
|
||||
// Should only return one work but just in case
|
||||
.forEach {
|
||||
wm.cancelWorkById(it.id)
|
||||
|
||||
// Re-enqueue cancelled scheduled work
|
||||
if (it.tags.contains(WORK_NAME_AUTO)) {
|
||||
setupTask(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.background.weather
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.app.NotificationCompat
|
||||
import breezyweather.domain.location.model.Location
|
||||
import org.breezyweather.R
|
||||
import org.breezyweather.background.receiver.NotificationReceiver
|
||||
import org.breezyweather.common.extensions.cancelNotification
|
||||
import org.breezyweather.common.extensions.chop
|
||||
import org.breezyweather.common.extensions.notificationBuilder
|
||||
import org.breezyweather.common.extensions.notify
|
||||
import org.breezyweather.remoteviews.Notifications
|
||||
|
||||
/**
|
||||
* Based on Mihon
|
||||
* Apache License, Version 2.0
|
||||
* https://github.com/mihonapp/mihon/blob/aa498360db90350f2642e6320dc55e7d474df1fd/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
|
||||
*/
|
||||
class WeatherUpdateNotifier(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the weather update
|
||||
*/
|
||||
private val cancelIntent by lazy {
|
||||
NotificationReceiver.cancelWeatherUpdatePendingBroadcast(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached progress notification to avoid creating a lot.
|
||||
*/
|
||||
val progressNotificationBuilder by lazy {
|
||||
context.notificationBuilder(Notifications.CHANNEL_BACKGROUND) {
|
||||
setContentTitle(context.getString(R.string.app_name))
|
||||
setSmallIcon(R.drawable.ic_running_in_background)
|
||||
setOngoing(true)
|
||||
setOnlyAlertOnce(true)
|
||||
addAction(R.drawable.ic_close, context.getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param locations the manga that are being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
fun showProgressNotification(locations: List<Location>, current: Int, total: Int) {
|
||||
val updatingText = locations.joinToString("\n") { it.city.chop(40) }
|
||||
progressNotificationBuilder
|
||||
.setContentTitle(
|
||||
context.getString(R.string.notification_updating_weather_data, current, total)
|
||||
)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||
|
||||
context.notify(
|
||||
Notifications.ID_WEATHER_PROGRESS,
|
||||
progressNotificationBuilder
|
||||
.setProgress(total, current, false)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows notification containing update entries that failed with action to open full log.
|
||||
*
|
||||
* @param failed Number of entries that failed to update.
|
||||
* @param uri Uri for error log file containing all titles that failed.
|
||||
*/
|
||||
fun showUpdateErrorNotification(failed: Int, uri: Uri) {
|
||||
if (failed == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
context.notify(
|
||||
Notifications.ID_WEATHER_ERROR,
|
||||
Notifications.CHANNEL_BACKGROUND
|
||||
) {
|
||||
setContentTitle(context.resources.getString(R.string.notification_update_error, failed))
|
||||
setContentText(context.getString(R.string.action_show_errors))
|
||||
setSmallIcon(R.drawable.ic_running_in_background)
|
||||
|
||||
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the progress notification.
|
||||
*/
|
||||
fun cancelProgressNotification() {
|
||||
context.cancelNotification(Notifications.ID_WEATHER_PROGRESS)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.actionmodecallback
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
internal class BreezyFloatingTextActionModeCallback(
|
||||
private val callback: BreezyTextActionModeCallback,
|
||||
) : ActionMode.Callback2() {
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
return callback.onActionItemClicked(mode, item)
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return callback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return callback.onPrepareActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
callback.onDestroyActionMode(mode)
|
||||
}
|
||||
|
||||
override fun onGetContentRect(mode: ActionMode?, view: View?, outRect: Rect?) {
|
||||
val rect = callback.rect
|
||||
outRect?.set(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.actionmodecallback
|
||||
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
||||
internal class BreezyPrimaryTextActionModeCallback(
|
||||
private val callback: BreezyTextActionModeCallback,
|
||||
) : ActionMode.Callback {
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
return callback.onActionItemClicked(mode, item)
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return callback.onCreateActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return callback.onPrepareActionMode(mode, menu)
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
callback.onDestroyActionMode(mode)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.actionmodecallback
|
||||
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
|
||||
/**
|
||||
* A selection container with the following options:
|
||||
* - Copy
|
||||
* - Select all
|
||||
* - Translate
|
||||
* - Share
|
||||
*/
|
||||
@Composable
|
||||
fun BreezySelectionContainer(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val breezyTextToolbar = remember { BreezyTextToolbar(view = view) }
|
||||
|
||||
CompositionLocalProvider(LocalTextToolbar provides breezyTextToolbar) {
|
||||
SelectionContainer {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.actionmodecallback
|
||||
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import org.breezyweather.R
|
||||
|
||||
internal class BreezyTextActionModeCallback(
|
||||
val onActionModeDestroy: ((mode: ActionMode?) -> Unit)? = null,
|
||||
var rect: Rect = Rect.Zero,
|
||||
var onCopyRequested: (() -> Unit)? = null,
|
||||
var onSelectAllRequested: (() -> Unit)? = null,
|
||||
var onTranslateRequested: (() -> Unit)? = null,
|
||||
var onShareRequested: (() -> Unit)? = null,
|
||||
) : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
requireNotNull(menu) { "onCreateActionMode requires a non-null menu" }
|
||||
requireNotNull(mode) { "onCreateActionMode requires a non-null mode" }
|
||||
|
||||
onCopyRequested?.let { addMenuItem(menu, MenuItemOption.Copy) }
|
||||
onSelectAllRequested?.let { addMenuItem(menu, MenuItemOption.SelectAll) }
|
||||
onTranslateRequested?.let { addMenuItem(menu, MenuItemOption.Translate) }
|
||||
onShareRequested?.let { addMenuItem(menu, MenuItemOption.Share) }
|
||||
return true
|
||||
}
|
||||
|
||||
// this method is called to populate new menu items when the actionMode was invalidated
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
if (mode == null || menu == null) return false
|
||||
updateMenuItems(menu)
|
||||
// should return true so that new menu items are populated
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
when (item!!.itemId) {
|
||||
MenuItemOption.Copy.id -> onCopyRequested?.invoke()
|
||||
MenuItemOption.SelectAll.id -> onSelectAllRequested?.invoke()
|
||||
MenuItemOption.Translate.id -> onTranslateRequested?.invoke()
|
||||
MenuItemOption.Share.id -> onShareRequested?.invoke()
|
||||
else -> return false
|
||||
}
|
||||
mode?.finish()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
onActionModeDestroy?.invoke(mode)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun updateMenuItems(menu: Menu) {
|
||||
addOrRemoveMenuItem(menu, MenuItemOption.Copy, onCopyRequested)
|
||||
addOrRemoveMenuItem(menu, MenuItemOption.SelectAll, onSelectAllRequested)
|
||||
addOrRemoveMenuItem(menu, MenuItemOption.Translate, onTranslateRequested)
|
||||
addOrRemoveMenuItem(menu, MenuItemOption.Share, onShareRequested)
|
||||
}
|
||||
|
||||
internal fun addMenuItem(menu: Menu, item: MenuItemOption) {
|
||||
menu
|
||||
.add(0, item.id, item.order, item.titleResource)
|
||||
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
|
||||
}
|
||||
|
||||
private fun addOrRemoveMenuItem(menu: Menu, item: MenuItemOption, callback: (() -> Unit)?) {
|
||||
when {
|
||||
callback != null && menu.findItem(item.id) == null -> addMenuItem(menu, item)
|
||||
callback == null && menu.findItem(item.id) != null -> menu.removeItem(item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class MenuItemOption(val id: Int) {
|
||||
Copy(0),
|
||||
SelectAll(1),
|
||||
Translate(2),
|
||||
Share(3),
|
||||
;
|
||||
|
||||
val titleResource: Int
|
||||
get() =
|
||||
when (this) {
|
||||
Copy -> android.R.string.copy
|
||||
SelectAll -> android.R.string.selectAll
|
||||
Translate -> R.string.action_translate
|
||||
Share -> R.string.action_share
|
||||
}
|
||||
|
||||
/** This item will be shown before all items that have order greater than this value. */
|
||||
val order = id
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.actionmodecallback
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.ActionMode
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.platform.TextToolbar
|
||||
import androidx.compose.ui.platform.TextToolbarStatus
|
||||
import org.breezyweather.R
|
||||
import org.breezyweather.common.extensions.clipboardManager
|
||||
import org.breezyweather.common.utils.helpers.SnackbarHelper
|
||||
|
||||
internal class BreezyTextToolbar(
|
||||
private val view: View,
|
||||
) : TextToolbar {
|
||||
private var actionMode: ActionMode? = null
|
||||
private val textActionModeCallback: BreezyTextActionModeCallback =
|
||||
BreezyTextActionModeCallback(onActionModeDestroy = { actionMode = null })
|
||||
override var status: TextToolbarStatus = TextToolbarStatus.Hidden
|
||||
private set
|
||||
|
||||
override fun showMenu(
|
||||
rect: Rect,
|
||||
onCopyRequested: (() -> Unit)?,
|
||||
onPasteRequested: (() -> Unit)?,
|
||||
onCutRequested: (() -> Unit)?,
|
||||
onSelectAllRequested: (() -> Unit)?,
|
||||
onAutofillRequested: (() -> Unit)?,
|
||||
) {
|
||||
textActionModeCallback.rect = rect
|
||||
textActionModeCallback.onCopyRequested = onCopyRequested
|
||||
textActionModeCallback.onSelectAllRequested = onSelectAllRequested
|
||||
textActionModeCallback.onTranslateRequested = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
{
|
||||
// Get selected text by copying it, then restore the previous clip
|
||||
val clipboardManager = view.context.clipboardManager
|
||||
val previousClipboard = clipboardManager.primaryClip
|
||||
onCopyRequested?.invoke()
|
||||
val text = clipboardManager.text
|
||||
if (previousClipboard != null) {
|
||||
clipboardManager.setPrimaryClip(previousClipboard)
|
||||
} else {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
|
||||
}
|
||||
|
||||
val intent = Intent().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
action = Intent.ACTION_TRANSLATE
|
||||
putExtra(Intent.EXTRA_TEXT, text.trim())
|
||||
} else {
|
||||
action = Intent.ACTION_PROCESS_TEXT
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT, text.trim())
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
}
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
|
||||
try {
|
||||
view.context.startActivity(Intent.createChooser(intent, ""))
|
||||
} catch (e: Exception) {
|
||||
SnackbarHelper.showSnackbar(view.context.getString(R.string.action_translate_no_app))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
textActionModeCallback.onShareRequested = {
|
||||
// Get selected text by copying it, then restore the previous clip
|
||||
val clipboardManager = view.context.clipboardManager
|
||||
val previousClipboard = clipboardManager.primaryClip
|
||||
onCopyRequested?.invoke()
|
||||
val text = clipboardManager.text
|
||||
if (previousClipboard != null) {
|
||||
clipboardManager.setPrimaryClip(previousClipboard)
|
||||
} else {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, " "))
|
||||
}
|
||||
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_SUBJECT, view.context.getString(R.string.app_name))
|
||||
putExtra(Intent.EXTRA_TEXT, text.trim())
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
|
||||
try {
|
||||
view.context.startActivity(Intent.createChooser(intent, ""))
|
||||
} catch (e: Exception) {
|
||||
SnackbarHelper.showSnackbar(view.context.getString(R.string.action_share_no_app))
|
||||
}
|
||||
}
|
||||
if (actionMode == null) {
|
||||
status = TextToolbarStatus.Shown
|
||||
actionMode =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
TextToolbarHelperMethods.startActionMode(
|
||||
view,
|
||||
BreezyFloatingTextActionModeCallback(textActionModeCallback),
|
||||
ActionMode.TYPE_FLOATING
|
||||
)
|
||||
} else {
|
||||
view.startActionMode(BreezyPrimaryTextActionModeCallback(textActionModeCallback))
|
||||
}
|
||||
} else {
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showMenu(
|
||||
rect: Rect,
|
||||
onCopyRequested: (() -> Unit)?,
|
||||
onPasteRequested: (() -> Unit)?,
|
||||
onCutRequested: (() -> Unit)?,
|
||||
onSelectAllRequested: (() -> Unit)?,
|
||||
) {
|
||||
showMenu(
|
||||
rect = rect,
|
||||
onCopyRequested = onCopyRequested,
|
||||
onPasteRequested = onPasteRequested,
|
||||
onCutRequested = onCutRequested,
|
||||
onSelectAllRequested = onSelectAllRequested
|
||||
)
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
status = TextToolbarStatus.Hidden
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is here to ensure that the classes that use this API will get verified and can be AOT
|
||||
* compiled. It is expected that this class will soft-fail verification, but the classes which use
|
||||
* this method will pass.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
internal object TextToolbarHelperMethods {
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun startActionMode(
|
||||
view: View,
|
||||
actionModeCallback: ActionMode.Callback,
|
||||
type: Int,
|
||||
): ActionMode? {
|
||||
return view.startActionMode(actionModeCallback, type)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun invalidateContentRect(actionMode: ActionMode) {
|
||||
actionMode.invalidateContentRect()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import org.breezyweather.BreezyWeather
|
||||
import org.breezyweather.common.extensions.isDarkMode
|
||||
import org.breezyweather.common.extensions.setSystemBarStyle
|
||||
import org.breezyweather.common.snackbar.SnackbarContainer
|
||||
|
||||
abstract class BreezyActivity : AppCompatActivity() {
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
window.setSystemBarStyle(!isDarkMode)
|
||||
}
|
||||
|
||||
BreezyWeather.instance.addActivity(this)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
BreezyWeather.instance.setTopActivity(this)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
BreezyWeather.instance.setTopActivity(this)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
BreezyWeather.instance.checkToCleanTopActivity(this)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
BreezyWeather.instance.removeActivity(this)
|
||||
}
|
||||
|
||||
fun updateLocalNightMode(expectedLightTheme: Boolean) {
|
||||
getDelegate().localNightMode = if (expectedLightTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
}
|
||||
}
|
||||
|
||||
open val snackbarContainer: SnackbarContainer
|
||||
get() = SnackbarContainer(
|
||||
this,
|
||||
findViewById<ViewGroup>(android.R.id.content).getChildAt(0) as ViewGroup,
|
||||
true
|
||||
)
|
||||
|
||||
fun provideSnackbarContainer(): SnackbarContainer = snackbarContainer
|
||||
|
||||
val isActivityCreated: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
|
||||
val isActivityStarted: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
val isActivityResumed: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import org.breezyweather.common.snackbar.SnackbarContainer
|
||||
|
||||
open class BreezyFragment : Fragment() {
|
||||
var isFragmentViewCreated = false
|
||||
private set
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
isFragmentViewCreated = true
|
||||
}
|
||||
|
||||
val isFragmentCreated: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
|
||||
val isFragmentStarted: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
val isFragmentResumed: Boolean
|
||||
get() = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
|
||||
val snackbarContainer: SnackbarContainer
|
||||
get() = SnackbarContainer(this, (requireView() as ViewGroup), true)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.activities
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
|
||||
// TODO: Issue with getter on application when converted to Kotlin
|
||||
open class BreezyViewModel(
|
||||
application: Application,
|
||||
) : AndroidViewModel(application) {
|
||||
private var mNewInstance = true
|
||||
fun checkIsNewInstance(): Boolean {
|
||||
val result = mNewInstance
|
||||
mNewInstance = false
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.activities.livedata
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import org.breezyweather.common.bus.MyObserverWrapper
|
||||
|
||||
class BusLiveData<T>(
|
||||
private val mainHandler: Handler,
|
||||
) : MutableLiveData<T>() {
|
||||
|
||||
companion object {
|
||||
const val START_VERSION = -1
|
||||
}
|
||||
|
||||
private val wrapperMap = HashMap<Observer<in T>, MyObserverWrapper<T>>()
|
||||
internal var version = START_VERSION
|
||||
|
||||
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
innerObserver(owner, MyObserverWrapper(this, observer, version))
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAutoRemove(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
owner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
removeObserver(observer)
|
||||
}
|
||||
})
|
||||
innerObserver(owner, MyObserverWrapper(this, observer, version))
|
||||
}
|
||||
}
|
||||
|
||||
fun observeStickily(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
innerObserver(owner, MyObserverWrapper(this, observer, START_VERSION))
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerObserver(owner: LifecycleOwner, wrapper: MyObserverWrapper<T>) {
|
||||
wrapperMap[wrapper.observer] = wrapper
|
||||
super.observe(owner, wrapper)
|
||||
}
|
||||
|
||||
override fun observeForever(observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
innerObserverForever(MyObserverWrapper(this, observer, version))
|
||||
}
|
||||
}
|
||||
|
||||
fun observeStickilyForever(observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
innerObserverForever(MyObserverWrapper(this, observer, START_VERSION))
|
||||
}
|
||||
}
|
||||
|
||||
private fun innerObserverForever(wrapper: MyObserverWrapper<T>) {
|
||||
wrapperMap[wrapper.observer] = wrapper
|
||||
super.observeForever(wrapper)
|
||||
}
|
||||
|
||||
override fun removeObserver(observer: Observer<in T>) {
|
||||
runOnMainThread {
|
||||
val wrapper = wrapperMap.remove(observer)
|
||||
if (wrapper != null) {
|
||||
super.removeObserver(wrapper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setValue(value: T) {
|
||||
++version
|
||||
super.setValue(value)
|
||||
}
|
||||
|
||||
override fun postValue(value: T) {
|
||||
runOnMainThread { setValue(value) }
|
||||
}
|
||||
|
||||
private fun runOnMainThread(r: Runnable) {
|
||||
if (Looper.getMainLooper().thread === Thread.currentThread()) {
|
||||
r.run()
|
||||
} else {
|
||||
mainHandler.post(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.activities.livedata
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
class EqualtableLiveData<T>(
|
||||
value: T? = null,
|
||||
) : MutableLiveData<T>(value) {
|
||||
|
||||
override fun setValue(value: T) {
|
||||
if (value == this.value) {
|
||||
return
|
||||
}
|
||||
super.setValue(value)
|
||||
}
|
||||
|
||||
override fun postValue(value: T) {
|
||||
// this.value is a volatile value.
|
||||
if (value == this.value) {
|
||||
return
|
||||
}
|
||||
super.postValue(value)
|
||||
}
|
||||
}
|
||||
49
app/src/main/java/org/breezyweather/common/bus/EventBus.kt
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.bus
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import org.breezyweather.common.activities.livedata.BusLiveData
|
||||
|
||||
class EventBus private constructor() {
|
||||
|
||||
companion object {
|
||||
|
||||
val instance: EventBus by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
EventBus()
|
||||
}
|
||||
}
|
||||
|
||||
private val liveDataMap = HashMap<String, BusLiveData<Any>>()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun <T> with(type: Class<T>): BusLiveData<T> {
|
||||
val key = key(type = type)
|
||||
|
||||
if (!liveDataMap.containsKey(key)) {
|
||||
liveDataMap[key] = BusLiveData(mainHandler)
|
||||
}
|
||||
return liveDataMap[key] as BusLiveData<T>
|
||||
}
|
||||
|
||||
fun remove(type: Class<*>) {
|
||||
liveDataMap.remove(key(type))
|
||||
}
|
||||
|
||||
private fun <T> key(type: Class<T>) = type.name
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.bus
|
||||
|
||||
import androidx.lifecycle.Observer
|
||||
import org.breezyweather.common.activities.livedata.BusLiveData
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
internal class MyObserverWrapper<T> internal constructor(
|
||||
host: BusLiveData<T>,
|
||||
internal val observer: Observer<in T>,
|
||||
private var version: Int,
|
||||
) : Observer<T> {
|
||||
|
||||
private val host = WeakReference(host)
|
||||
|
||||
override fun onChanged(value: T) {
|
||||
host.get()?.let {
|
||||
if (version >= it.version) {
|
||||
return
|
||||
}
|
||||
version = it.version
|
||||
observer.onChanged(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
275
app/src/main/java/org/breezyweather/common/di/DbModule.kt
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.di
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import breezyweather.data.AlertSeverityColumnAdapter
|
||||
import breezyweather.data.Alerts
|
||||
import breezyweather.data.AndroidDatabaseHandler
|
||||
import breezyweather.data.Dailys
|
||||
import breezyweather.data.Database
|
||||
import breezyweather.data.DatabaseHandler
|
||||
import breezyweather.data.DistanceColumnAdapter
|
||||
import breezyweather.data.DurationColumnAdapter
|
||||
import breezyweather.data.Hourlys
|
||||
import breezyweather.data.Locations
|
||||
import breezyweather.data.Minutelys
|
||||
import breezyweather.data.Normals
|
||||
import breezyweather.data.PollenConcentrationColumnAdapter
|
||||
import breezyweather.data.PollutantConcentrationColumnAdapter
|
||||
import breezyweather.data.PrecipitationColumnAdapter
|
||||
import breezyweather.data.PressureColumnAdapter
|
||||
import breezyweather.data.RatioColumnAdapter
|
||||
import breezyweather.data.SpeedColumnAdapter
|
||||
import breezyweather.data.TemperatureColumnAdapter
|
||||
import breezyweather.data.TimeZoneColumnAdapter
|
||||
import breezyweather.data.WeatherCodeColumnAdapter
|
||||
import breezyweather.data.Weathers
|
||||
import breezyweather.data.location.LocationRepository
|
||||
import breezyweather.data.weather.WeatherRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
import org.breezyweather.BuildConfig
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DbModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSqlDriver(@ApplicationContext context: Context): SqlDriver {
|
||||
return AndroidSqliteDriver(
|
||||
schema = Database.Schema,
|
||||
context = context,
|
||||
name = "breezyweather.db",
|
||||
factory = if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Support database inspector in Android Studio
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
} else {
|
||||
RequerySQLiteOpenHelperFactory()
|
||||
},
|
||||
callback = object : AndroidSqliteDriver.Callback(Database.Schema) {
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
setPragma(db, "foreign_keys = ON")
|
||||
setPragma(db, "journal_mode = WAL")
|
||||
setPragma(db, "synchronous = NORMAL")
|
||||
}
|
||||
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
|
||||
val cursor = db.query("PRAGMA $pragma")
|
||||
cursor.moveToFirst()
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(driver: SqlDriver): Database {
|
||||
return Database(
|
||||
driver,
|
||||
locationsAdapter = Locations.Adapter(
|
||||
timezoneAdapter = TimeZoneColumnAdapter
|
||||
),
|
||||
weathersAdapter = Weathers.Adapter(
|
||||
weather_codeAdapter = WeatherCodeColumnAdapter,
|
||||
temperatureAdapter = TemperatureColumnAdapter,
|
||||
temperature_source_feels_likeAdapter = TemperatureColumnAdapter,
|
||||
temperature_apparentAdapter = TemperatureColumnAdapter,
|
||||
temperature_wind_chillAdapter = TemperatureColumnAdapter,
|
||||
humidexAdapter = TemperatureColumnAdapter,
|
||||
wind_speedAdapter = SpeedColumnAdapter,
|
||||
wind_gustsAdapter = SpeedColumnAdapter,
|
||||
pm25Adapter = PollutantConcentrationColumnAdapter,
|
||||
pm10Adapter = PollutantConcentrationColumnAdapter,
|
||||
so2Adapter = PollutantConcentrationColumnAdapter,
|
||||
no2Adapter = PollutantConcentrationColumnAdapter,
|
||||
o3Adapter = PollutantConcentrationColumnAdapter,
|
||||
coAdapter = PollutantConcentrationColumnAdapter,
|
||||
relative_humidityAdapter = RatioColumnAdapter,
|
||||
dew_pointAdapter = TemperatureColumnAdapter,
|
||||
pressureAdapter = PressureColumnAdapter,
|
||||
visibilityAdapter = DistanceColumnAdapter,
|
||||
cloud_coverAdapter = RatioColumnAdapter,
|
||||
ceilingAdapter = DistanceColumnAdapter
|
||||
),
|
||||
dailysAdapter = Dailys.Adapter(
|
||||
daytime_weather_codeAdapter = WeatherCodeColumnAdapter,
|
||||
daytime_temperatureAdapter = TemperatureColumnAdapter,
|
||||
daytime_temperature_source_feels_likeAdapter = TemperatureColumnAdapter,
|
||||
daytime_temperature_apparentAdapter = TemperatureColumnAdapter,
|
||||
daytime_temperature_wind_chillAdapter = TemperatureColumnAdapter,
|
||||
daytime_humidexAdapter = TemperatureColumnAdapter,
|
||||
daytime_total_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
daytime_thunderstorm_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
daytime_rain_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
daytime_snow_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
daytime_ice_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
daytime_total_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
daytime_thunderstorm_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
daytime_rain_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
daytime_snow_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
daytime_ice_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
daytime_total_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
daytime_thunderstorm_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
daytime_rain_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
daytime_snow_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
daytime_ice_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
daytime_wind_speedAdapter = SpeedColumnAdapter,
|
||||
daytime_wind_gustsAdapter = SpeedColumnAdapter,
|
||||
nighttime_temperatureAdapter = TemperatureColumnAdapter,
|
||||
nighttime_temperature_source_feels_likeAdapter = TemperatureColumnAdapter,
|
||||
nighttime_temperature_apparentAdapter = TemperatureColumnAdapter,
|
||||
nighttime_temperature_wind_chillAdapter = TemperatureColumnAdapter,
|
||||
nighttime_humidexAdapter = TemperatureColumnAdapter,
|
||||
nighttime_weather_codeAdapter = WeatherCodeColumnAdapter,
|
||||
nighttime_total_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
nighttime_thunderstorm_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
nighttime_rain_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
nighttime_snow_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
nighttime_ice_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
nighttime_total_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
nighttime_thunderstorm_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
nighttime_rain_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
nighttime_snow_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
nighttime_ice_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
nighttime_total_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
nighttime_thunderstorm_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
nighttime_rain_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
nighttime_snow_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
nighttime_ice_precipitation_durationAdapter = DurationColumnAdapter,
|
||||
nighttime_wind_speedAdapter = SpeedColumnAdapter,
|
||||
nighttime_wind_gustsAdapter = SpeedColumnAdapter,
|
||||
degree_day_heatingAdapter = TemperatureColumnAdapter,
|
||||
degree_day_coolingAdapter = TemperatureColumnAdapter,
|
||||
pm25Adapter = PollutantConcentrationColumnAdapter,
|
||||
pm10Adapter = PollutantConcentrationColumnAdapter,
|
||||
so2Adapter = PollutantConcentrationColumnAdapter,
|
||||
no2Adapter = PollutantConcentrationColumnAdapter,
|
||||
o3Adapter = PollutantConcentrationColumnAdapter,
|
||||
coAdapter = PollutantConcentrationColumnAdapter,
|
||||
alderAdapter = PollenConcentrationColumnAdapter,
|
||||
ashAdapter = PollenConcentrationColumnAdapter,
|
||||
birchAdapter = PollenConcentrationColumnAdapter,
|
||||
chestnutAdapter = PollenConcentrationColumnAdapter,
|
||||
cypressAdapter = PollenConcentrationColumnAdapter,
|
||||
grassAdapter = PollenConcentrationColumnAdapter,
|
||||
hazelAdapter = PollenConcentrationColumnAdapter,
|
||||
hornbeamAdapter = PollenConcentrationColumnAdapter,
|
||||
lindenAdapter = PollenConcentrationColumnAdapter,
|
||||
moldAdapter = PollenConcentrationColumnAdapter,
|
||||
mugwortAdapter = PollenConcentrationColumnAdapter,
|
||||
oakAdapter = PollenConcentrationColumnAdapter,
|
||||
oliveAdapter = PollenConcentrationColumnAdapter,
|
||||
planeAdapter = PollenConcentrationColumnAdapter,
|
||||
plantainAdapter = PollenConcentrationColumnAdapter,
|
||||
poplarAdapter = PollenConcentrationColumnAdapter,
|
||||
ragweedAdapter = PollenConcentrationColumnAdapter,
|
||||
sorrelAdapter = PollenConcentrationColumnAdapter,
|
||||
treeAdapter = PollenConcentrationColumnAdapter,
|
||||
urticaceaeAdapter = PollenConcentrationColumnAdapter,
|
||||
willowAdapter = PollenConcentrationColumnAdapter,
|
||||
sunshine_durationAdapter = DurationColumnAdapter,
|
||||
relative_humidity_averageAdapter = RatioColumnAdapter,
|
||||
relative_humidity_minAdapter = RatioColumnAdapter,
|
||||
relative_humidity_maxAdapter = RatioColumnAdapter,
|
||||
dewpoint_averageAdapter = TemperatureColumnAdapter,
|
||||
dewpoint_minAdapter = TemperatureColumnAdapter,
|
||||
dewpoint_maxAdapter = TemperatureColumnAdapter,
|
||||
pressure_averageAdapter = PressureColumnAdapter,
|
||||
pressure_maxAdapter = PressureColumnAdapter,
|
||||
pressure_minAdapter = PressureColumnAdapter,
|
||||
cloud_cover_averageAdapter = RatioColumnAdapter,
|
||||
cloud_cover_minAdapter = RatioColumnAdapter,
|
||||
cloud_cover_maxAdapter = RatioColumnAdapter,
|
||||
visibility_averageAdapter = DistanceColumnAdapter,
|
||||
visibility_maxAdapter = DistanceColumnAdapter,
|
||||
visibility_minAdapter = DistanceColumnAdapter
|
||||
),
|
||||
hourlysAdapter = Hourlys.Adapter(
|
||||
weather_codeAdapter = WeatherCodeColumnAdapter,
|
||||
temperatureAdapter = TemperatureColumnAdapter,
|
||||
temperature_source_feels_likeAdapter = TemperatureColumnAdapter,
|
||||
temperature_apparentAdapter = TemperatureColumnAdapter,
|
||||
temperature_wind_chillAdapter = TemperatureColumnAdapter,
|
||||
humidexAdapter = TemperatureColumnAdapter,
|
||||
total_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
thunderstorm_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
rain_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
snow_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
ice_precipitationAdapter = PrecipitationColumnAdapter,
|
||||
total_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
thunderstorm_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
rain_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
snow_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
ice_precipitation_probabilityAdapter = RatioColumnAdapter,
|
||||
wind_speedAdapter = SpeedColumnAdapter,
|
||||
wind_gustsAdapter = SpeedColumnAdapter,
|
||||
pm25Adapter = PollutantConcentrationColumnAdapter,
|
||||
pm10Adapter = PollutantConcentrationColumnAdapter,
|
||||
so2Adapter = PollutantConcentrationColumnAdapter,
|
||||
no2Adapter = PollutantConcentrationColumnAdapter,
|
||||
o3Adapter = PollutantConcentrationColumnAdapter,
|
||||
coAdapter = PollutantConcentrationColumnAdapter,
|
||||
relative_humidityAdapter = RatioColumnAdapter,
|
||||
dew_pointAdapter = TemperatureColumnAdapter,
|
||||
pressureAdapter = PressureColumnAdapter,
|
||||
cloud_coverAdapter = RatioColumnAdapter,
|
||||
visibilityAdapter = DistanceColumnAdapter
|
||||
),
|
||||
minutelysAdapter = Minutelys.Adapter(
|
||||
precipitation_intensityAdapter = PrecipitationColumnAdapter
|
||||
),
|
||||
alertsAdapter = Alerts.Adapter(
|
||||
severityAdapter = AlertSeverityColumnAdapter
|
||||
),
|
||||
normalsAdapter = Normals.Adapter(
|
||||
temperature_max_averageAdapter = TemperatureColumnAdapter,
|
||||
temperature_min_averageAdapter = TemperatureColumnAdapter
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabaseHandler(db: Database, driver: SqlDriver): DatabaseHandler {
|
||||
return AndroidDatabaseHandler(db, driver)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLocationRepository(databaseHandler: DatabaseHandler): LocationRepository {
|
||||
return LocationRepository(databaseHandler)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWeatherRepository(databaseHandler: DatabaseHandler): WeatherRepository {
|
||||
return WeatherRepository(databaseHandler)
|
||||
}
|
||||
}
|
||||
195
app/src/main/java/org/breezyweather/common/di/HttpModule.kt
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.di
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Cache
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import okhttp3.tls.HandshakeCertificates
|
||||
import org.breezyweather.BreezyWeather
|
||||
import org.breezyweather.R
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import java.io.File
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class HttpModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(app: Application, loggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
|
||||
val client = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
/**
|
||||
* Add support for Let’s encrypt certificate authority on Android < 7.0
|
||||
*/
|
||||
try {
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certificateIsrgRootX1 = certificateFactory
|
||||
.generateCertificates(app.resources.openRawResource(R.raw.isrg_root_x1))
|
||||
.single() as X509Certificate
|
||||
val certificateIsrgRootX2 = certificateFactory
|
||||
.generateCertificates(app.resources.openRawResource(R.raw.isrg_root_x2))
|
||||
.single() as X509Certificate
|
||||
val certificates = HandshakeCertificates.Builder()
|
||||
.addTrustedCertificate(certificateIsrgRootX1)
|
||||
.addTrustedCertificate(certificateIsrgRootX2)
|
||||
.addPlatformTrustedCertificates()
|
||||
.build()
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager)
|
||||
} catch (ignored: Exception) {
|
||||
OkHttpClient.Builder()
|
||||
}
|
||||
} else {
|
||||
OkHttpClient.Builder()
|
||||
}
|
||||
|
||||
return client
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(45, TimeUnit.SECONDS)
|
||||
.cache(
|
||||
Cache(
|
||||
File(app.cacheDir, "http_cache"), // $0.05 worth of phone storage in 2020
|
||||
50L * 1024L * 1024L // 50 MiB
|
||||
)
|
||||
)
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRxJava3CallAdapterFactory(): RxJava3CallAdapterFactory {
|
||||
return RxJava3CallAdapterFactory.create()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
return HttpLoggingInterceptor().apply {
|
||||
level = if (BreezyWeather.instance.debugMode) {
|
||||
HttpLoggingInterceptor.Level.BODY
|
||||
} else {
|
||||
HttpLoggingInterceptor.Level.NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("JsonSerializer")
|
||||
fun provideKotlinxJsonSerializationConverterFactory(): Converter.Factory {
|
||||
val contentType = "application/json".toMediaType()
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
isLenient = !BreezyWeather.instance.debugMode
|
||||
}
|
||||
return json.asConverterFactory(contentType)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("JsonClient")
|
||||
fun provideJsonRetrofitBuilder(
|
||||
client: OkHttpClient,
|
||||
@Named("JsonSerializer") jsonConverterFactory: Converter.Factory,
|
||||
callAdapterFactory: RxJava3CallAdapterFactory,
|
||||
): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.addConverterFactory(jsonConverterFactory)
|
||||
// TODO: We should probably migrate to suspend
|
||||
// https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("XmlSerializer")
|
||||
fun provideKotlinxXmlSerializationConverterFactory(): Converter.Factory {
|
||||
val contentType = "application/xml".toMediaType()
|
||||
return XML {
|
||||
defaultPolicy {
|
||||
pedantic = false
|
||||
ignoreUnknownChildren()
|
||||
}
|
||||
autoPolymorphic = true
|
||||
}.asConverterFactory(contentType)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("XmlClient")
|
||||
fun provideXmlRetrofitBuilder(
|
||||
client: OkHttpClient,
|
||||
@Named("XmlSerializer") xmlConverterFactory: Converter.Factory,
|
||||
callAdapterFactory: RxJava3CallAdapterFactory,
|
||||
): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.addConverterFactory(xmlConverterFactory)
|
||||
// TODO: We should probably migrate to suspend
|
||||
// https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
}
|
||||
|
||||
/*@Provides
|
||||
@Singleton
|
||||
@Named("CsvSerializer")
|
||||
fun provideKotlinxCsvSerializationConverterFactory(): Converter.Factory {
|
||||
val contentType = "text/csv".toMediaType() // RFC 7111
|
||||
val csv = Csv {
|
||||
hasHeaderRecord = true
|
||||
delimiter = ';'
|
||||
recordSeparator = "\r\n"
|
||||
ignoreUnknownColumns = true
|
||||
}
|
||||
return csv.asConverterFactory(contentType)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("CsvClient")
|
||||
fun provideCsvRetrofitBuilder(
|
||||
client: OkHttpClient,
|
||||
@Named("CsvSerializer") csvConverterFactory: Converter.Factory,
|
||||
callAdapterFactory: RxJava3CallAdapterFactory,
|
||||
): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.client(client)
|
||||
.addConverterFactory(csvConverterFactory)
|
||||
// TODO: We should probably migrate to suspend
|
||||
// https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
}*/
|
||||
}
|
||||
32
app/src/main/java/org/breezyweather/common/di/RxModule.kt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class RxModule {
|
||||
@Provides
|
||||
fun provideCompositeDisposable(): CompositeDisposable {
|
||||
return CompositeDisposable()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class ApiKeyMissingException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class ApiLimitReachedException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class ApiUnauthorizedException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class InvalidLocationException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class InvalidOrIncompleteDataException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class LocationAccessOffException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class LocationException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class LocationSearchException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class MissingPermissionLocationBackgroundException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class MissingPermissionLocationException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class NoNetworkException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class NonFreeNetSourceException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class OutdatedServerDataException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class ParsingException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class ReverseGeocodingException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class SourceNotInstalledException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class UnsupportedFeatureException : Exception()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.exceptions
|
||||
|
||||
class WeatherException : Exception()
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.Manifest
|
||||
import android.app.UiModeManager
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.hardware.SensorManager
|
||||
import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.RawRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import com.google.maps.android.data.geojson.GeoJsonParser
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/162b6397050e1577c113a88e7b7cfe9f98e6a45c/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if the give permission is granted.
|
||||
*
|
||||
* @param permission the permission to check.
|
||||
* @return true if it has permissions.
|
||||
*/
|
||||
fun Context.hasPermission(
|
||||
permission: String,
|
||||
) = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
/**
|
||||
* Checks if the notification permission is granted.
|
||||
*
|
||||
* @return true if the permission is granted. Always returns true on Android 12 and lower.
|
||||
*/
|
||||
val Context.hasNotificationPermission
|
||||
get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
hasPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
val Context.clipboardManager: ClipboardManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
val Context.inputMethodManager: InputMethodManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
val Context.locationManager: LocationManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
val Context.powerManager: PowerManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
val Context.sensorManager: SensorManager?
|
||||
get() = if (SettingsManager.getInstance(this).isGravitySensorEnabled) {
|
||||
getSystemService()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val Context.windowManager: WindowManager?
|
||||
get() = getSystemService()
|
||||
|
||||
val Context.shortcutManager: ShortcutManager?
|
||||
get() = getSystemService()
|
||||
|
||||
val Context.uiModeManager: UiModeManager?
|
||||
get() = getSystemService()
|
||||
|
||||
fun Context.createFileInCacheDir(name: String): File {
|
||||
val file = File(externalCacheDir, name)
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
file.createNewFile()
|
||||
return file
|
||||
}
|
||||
|
||||
fun Context.parseRawGeoJson(@RawRes rawFile: Int): GeoJsonParser {
|
||||
val text = resources.openRawResource(rawFile).bufferedReader().use { it.readText() }
|
||||
return GeoJsonParser(JSONObject(text))
|
||||
}
|
||||
|
||||
fun Context.openApplicationDetailsSettings() {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
|
||||
Uri.fromParts("package", packageName, null)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
|
||||
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.IO, block = block)
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
|
||||
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
|
||||
withContext(NonCancellable, block)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.zip.GZIPOutputStream
|
||||
|
||||
val Bundle.sizeInBytes: Int
|
||||
get() {
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.writeBundle(this)
|
||||
|
||||
return parcel.dataSize().also {
|
||||
parcel.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compress a string using GZIP.
|
||||
*
|
||||
* @return an UTF-8 encoded byte array.
|
||||
*/
|
||||
fun String.gzipCompress(): ByteArray {
|
||||
val bos = ByteArrayOutputStream()
|
||||
GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(this) }
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.icu.text.DateTimePatternGenerator
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.icu.util.TimeZone
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Build
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import androidx.annotation.RequiresApi
|
||||
import breezyweather.domain.location.model.Location
|
||||
import breezyweather.domain.weather.reference.Month
|
||||
import org.breezyweather.BreezyWeather
|
||||
import org.breezyweather.common.options.appearance.CalendarHelper
|
||||
import org.breezyweather.common.utils.helpers.LogHelper
|
||||
import org.chickenhook.restrictionbypass.RestrictionBypass
|
||||
import java.lang.reflect.Method
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
val Context.is12Hour: Boolean
|
||||
get() = !DateFormat.is24HourFormat(this)
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun Date.getRelativeTime(context: Context): String {
|
||||
try {
|
||||
// Reflection allows us to specify the locale
|
||||
// If we don't, we always have system locale instead of per-app language preference
|
||||
val getRelativeTimeSpanStringMethod: Method = RestrictionBypass.getMethod(
|
||||
Class.forName("android.text.format.RelativeDateTimeFormatter"),
|
||||
"getRelativeTimeSpanString",
|
||||
Locale::class.java,
|
||||
java.util.TimeZone::class.java,
|
||||
Long::class.javaPrimitiveType,
|
||||
Long::class.javaPrimitiveType,
|
||||
Long::class.javaPrimitiveType,
|
||||
Int::class.javaPrimitiveType
|
||||
)
|
||||
return getRelativeTimeSpanStringMethod.invoke(
|
||||
null,
|
||||
context.currentLocale,
|
||||
java.util.TimeZone.getDefault(),
|
||||
time,
|
||||
Date().time,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
) as String
|
||||
} catch (_: Exception) {
|
||||
if (BreezyWeather.instance.debugMode) {
|
||||
LogHelper.log(msg = "Reflection of relative time failed")
|
||||
}
|
||||
return DateUtils.getRelativeTimeSpanString(
|
||||
time,
|
||||
Date().time,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE
|
||||
) as String
|
||||
}
|
||||
}
|
||||
|
||||
// Makes the code more readable by not having to do a null check condition
|
||||
fun Long.toDate(): Date {
|
||||
return Date(this)
|
||||
}
|
||||
|
||||
fun Date.getFormattedDate(
|
||||
pattern: String,
|
||||
location: Location? = null,
|
||||
context: Context? = null,
|
||||
withBestPattern: Boolean = false,
|
||||
): String {
|
||||
val locale = context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
SimpleDateFormat(
|
||||
if (withBestPattern) {
|
||||
DateTimePatternGenerator.getInstance(locale).getBestPattern(pattern)
|
||||
} else {
|
||||
pattern
|
||||
},
|
||||
locale
|
||||
).apply {
|
||||
timeZone = location?.timeZone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault()
|
||||
}.format(this)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getFormattedDate(pattern, location?.timeZone, locale)
|
||||
}
|
||||
}
|
||||
|
||||
fun Date.getFormattedTime(
|
||||
location: Location? = null,
|
||||
context: Context?,
|
||||
twelveHour: Boolean,
|
||||
): String {
|
||||
return if (twelveHour) {
|
||||
getFormattedDate("h:mm a", location, context, withBestPattern = true)
|
||||
} else {
|
||||
getFormattedDate("HH:mm", location, context)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun LocalTime.getFormattedTime(
|
||||
locale: Locale = Locale.Builder().setLanguage("en").setRegion("001").build(),
|
||||
twelveHour: Boolean,
|
||||
): String {
|
||||
return if (twelveHour) {
|
||||
format(
|
||||
DateTimeFormatter.ofPattern(
|
||||
DateTimePatternGenerator.getInstance(locale).getBestPattern("h:mm a")
|
||||
).withLocale(locale)
|
||||
)
|
||||
} else {
|
||||
format(DateTimeFormatter.ofPattern("HH:mm").withLocale(locale))
|
||||
}
|
||||
}
|
||||
|
||||
fun Date.getFormattedShortDayAndMonth(
|
||||
location: Location,
|
||||
context: Context?,
|
||||
): String {
|
||||
return getFormattedDate("MM-dd", location, context, withBestPattern = true)
|
||||
}
|
||||
|
||||
fun Date.getFormattedDayOfTheMonth(
|
||||
location: Location,
|
||||
context: Context?,
|
||||
): String {
|
||||
return getFormattedDate("dd", location, context, withBestPattern = true)
|
||||
}
|
||||
|
||||
fun Date.getFormattedMediumDayAndMonth(
|
||||
location: Location,
|
||||
context: Context?,
|
||||
): String {
|
||||
val locale = context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
return getFormattedDate("d MMM", location, context, withBestPattern = true).capitalize(locale)
|
||||
}
|
||||
|
||||
fun Date.getFormattedFullDayAndMonth(
|
||||
location: Location,
|
||||
context: Context?,
|
||||
): String {
|
||||
val locale = context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
return getFormattedDate("d MMMM", location, context, withBestPattern = true).capitalize(locale)
|
||||
}
|
||||
|
||||
fun getShortWeekdayDayMonth(
|
||||
context: Context?,
|
||||
): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
DateTimePatternGenerator.getInstance(
|
||||
context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
).getBestPattern("EEE d MMM")
|
||||
} else {
|
||||
"EEE d MMM"
|
||||
}
|
||||
}
|
||||
|
||||
fun getLongWeekdayDayMonth(
|
||||
context: Context?,
|
||||
): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
DateTimePatternGenerator.getInstance(
|
||||
context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
).getBestPattern("EEEE d MMMM")
|
||||
} else {
|
||||
"EEEE d MMMM"
|
||||
}
|
||||
}
|
||||
|
||||
fun Date.getWeek(location: Location, context: Context?, full: Boolean = false): String {
|
||||
val locale = context?.currentLocale ?: Locale.Builder().setLanguage("en").setRegion("001").build()
|
||||
return getFormattedDate(if (full) "EEEE" else "E", location, context).capitalize(locale)
|
||||
}
|
||||
|
||||
fun Date.getHour(location: Location, context: Context): String {
|
||||
return getFormattedDate(
|
||||
if (context.is12Hour) "h a" else "H:mm",
|
||||
location,
|
||||
context,
|
||||
withBestPattern = context.is12Hour
|
||||
)
|
||||
}
|
||||
|
||||
fun Date.getHourIn24Format(location: Location): String {
|
||||
return getFormattedDate("H", location)
|
||||
}
|
||||
|
||||
/**
|
||||
* See CalendarHelper.supportedCalendars for full list of supported calendars
|
||||
*/
|
||||
fun Date.getFormattedMediumDayAndMonthInAdditionalCalendar(
|
||||
location: Location? = null,
|
||||
context: Context,
|
||||
): String? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val calendarId = CalendarHelper.getAlternateCalendarSetting(context)
|
||||
if (calendarId != null) {
|
||||
val alternateCalendar = CalendarHelper.getCalendars(context).firstOrNull { it.id == calendarId }
|
||||
if (alternateCalendar != null) {
|
||||
val locale = context.currentLocale
|
||||
val uLocale = ULocale.Builder().apply {
|
||||
setLanguageTag(locale.toLanguageTag())
|
||||
setUnicodeLocaleKeyword(CalendarHelper.CALENDAR_EXTENSION_TYPE, calendarId)
|
||||
alternateCalendar.additionalParams?.forEach {
|
||||
setUnicodeLocaleKeyword(it.key, it.value)
|
||||
}
|
||||
}.build()
|
||||
SimpleDateFormat(
|
||||
if (!alternateCalendar.specificPattern.isNullOrEmpty()) {
|
||||
alternateCalendar.specificPattern
|
||||
} else {
|
||||
DateTimePatternGenerator.getInstance(uLocale).getBestPattern("d MMM")
|
||||
},
|
||||
uLocale
|
||||
).apply {
|
||||
timeZone = location?.timeZone?.let { TimeZone.getTimeZone(it.id) } ?: TimeZone.getDefault()
|
||||
}.format(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Date.toCalendar(location: Location): Calendar {
|
||||
return Calendar.getInstance().also {
|
||||
it.time = this
|
||||
it.timeZone = location.timeZone
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized function to get yyyy-MM-dd formatted date
|
||||
* Takes 0 ms on my device compared to 2-3 ms for getFormattedDate() (which uses SimpleDateFormat)
|
||||
* Saves about 1 second when looping through 24 hourly over a 16 day period
|
||||
*/
|
||||
fun Calendar.getIsoFormattedDate(): String {
|
||||
return "${this[Calendar.YEAR]}-${getMonth(twoDigits = true)}-${getDayOfMonth(twoDigits = true)}"
|
||||
}
|
||||
|
||||
fun Calendar.getMonth(twoDigits: Boolean = false): String {
|
||||
return "${(this[Calendar.MONTH] + 1).let { month ->
|
||||
if (twoDigits && month.toString().length < 2) "0$month" else month
|
||||
}}"
|
||||
}
|
||||
|
||||
fun Calendar.getDayOfMonth(twoDigits: Boolean = false): String {
|
||||
return "${this[Calendar.DAY_OF_MONTH].let { day ->
|
||||
if (twoDigits && day.toString().length < 2) "0$day" else day
|
||||
}}"
|
||||
}
|
||||
|
||||
fun Date.getIsoFormattedDate(location: Location): String {
|
||||
return toCalendar(location).getIsoFormattedDate()
|
||||
}
|
||||
|
||||
fun Date.getCalendarMonth(location: Location): Month {
|
||||
return Month.fromCalendarMonth(toCalendarWithTimeZone(location.timeZone)[Calendar.MONTH])
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* The functions below make use of old java.util.* that should be replaced with android.icu
|
||||
* counterparts, introduced in Android SDK 24
|
||||
*/
|
||||
|
||||
fun Date.toCalendarWithTimeZone(zone: TimeZone): Calendar {
|
||||
return Calendar.getInstance().also {
|
||||
it.time = this
|
||||
it.timeZone = zone
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a date at midnight on a specific timezone from a formatted date
|
||||
* @this formattedDate in yyyy-MM-dd format
|
||||
* @param timeZoneP
|
||||
* @return Date
|
||||
*/
|
||||
fun String.toDateNoHour(timeZoneP: TimeZone = TimeZone.getDefault()): Date? {
|
||||
if (isEmpty() || length < 10) return null
|
||||
return Calendar.getInstance().also {
|
||||
it.timeZone = timeZoneP
|
||||
it.set(Calendar.YEAR, substring(0, 4).toInt())
|
||||
it.set(Calendar.MONTH, substring(5, 7).toInt() - 1)
|
||||
it.set(Calendar.DAY_OF_MONTH, substring(8, 10).toInt())
|
||||
it.set(Calendar.HOUR_OF_DAY, 0)
|
||||
it.set(Calendar.MINUTE, 0)
|
||||
it.set(Calendar.SECOND, 0)
|
||||
it.set(Calendar.MILLISECOND, 0)
|
||||
}.time
|
||||
}
|
||||
|
||||
@Deprecated("Makes no sense, must be replaced")
|
||||
fun Date.toTimezone(timeZone: TimeZone = TimeZone.getDefault()): Date {
|
||||
val calendarWithTimeZone = toCalendarWithTimeZone(timeZone)
|
||||
return Date(
|
||||
calendarWithTimeZone[Calendar.YEAR] - 1900,
|
||||
calendarWithTimeZone[Calendar.MONTH],
|
||||
calendarWithTimeZone[Calendar.DAY_OF_MONTH],
|
||||
calendarWithTimeZone[Calendar.HOUR_OF_DAY],
|
||||
calendarWithTimeZone[Calendar.MINUTE],
|
||||
calendarWithTimeZone[Calendar.SECOND]
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use toTimezoneSpecificHour instead")
|
||||
fun Date.toTimezoneNoHour(timeZone: TimeZone = TimeZone.getDefault()): Date {
|
||||
return toTimezoneSpecificHour(timeZone)
|
||||
}
|
||||
|
||||
fun Date.toTimezoneSpecificHour(
|
||||
timeZone: TimeZone = TimeZone.getDefault(),
|
||||
specificHour: Int = 0,
|
||||
): Date {
|
||||
return toCalendarWithTimeZone(timeZone).apply {
|
||||
set(Calendar.YEAR, get(Calendar.YEAR))
|
||||
set(Calendar.MONTH, get(Calendar.MONTH))
|
||||
set(Calendar.DAY_OF_MONTH, get(Calendar.DAY_OF_MONTH))
|
||||
set(Calendar.HOUR_OF_DAY, specificHour)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.time
|
||||
}
|
||||
|
||||
@Deprecated("Use ICU functions instead")
|
||||
fun Date.getFormattedDate(
|
||||
pattern: String,
|
||||
timeZone: TimeZone?,
|
||||
locale: Locale,
|
||||
): String {
|
||||
return SimpleDateFormat(pattern, locale).apply {
|
||||
setTimeZone(timeZone ?: TimeZone.getDefault())
|
||||
}.format(this)
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.SettingNotFoundException
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.Px
|
||||
import androidx.annotation.Size
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.google.android.material.resources.TextAppearance
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val MAX_TABLET_ADAPTIVE_LIST_WIDTH_DIP_PHONE = 512
|
||||
private const val MAX_TABLET_ADAPTIVE_LIST_WIDTH_DIP_TABLET = 600
|
||||
val FLOATING_DECELERATE_INTERPOLATOR: Interpolator = DecelerateInterpolator(1f)
|
||||
const val DEFAULT_CARD_LIST_ITEM_ELEVATION_DP = 2f
|
||||
private const val SQUISHED_BLOCK_FACTOR = 1.1f
|
||||
|
||||
val Context.isTabletDevice: Boolean
|
||||
get() = (
|
||||
resources.configuration.screenLayout
|
||||
and Configuration.SCREENLAYOUT_SIZE_MASK
|
||||
) >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
||||
|
||||
val Context.isLandscape: Boolean
|
||||
get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
/**
|
||||
* Minimum size is adjusted from font scale:
|
||||
* - At 1.0 font scale, minimum size for a block is 1.0 (160 dp)
|
||||
* - At 2.0 font scale, minimum size for a block is 2.0 (320 dp)
|
||||
*/
|
||||
val Context.minBlockWidth: Float
|
||||
get() = 0.5f + fontScale.div(2f)
|
||||
|
||||
/**
|
||||
* @param widthInDp available width in which blocks will be displayed
|
||||
* @return a number of blocks between 1 and 5 that can fit
|
||||
*/
|
||||
fun Context.getBlocksPerRow(
|
||||
widthInDp: Float = windowWidth.toFloat().div(density),
|
||||
): Int {
|
||||
val potentialResult = floor(widthInDp.div(minBlockWidth)).roundToInt().coerceIn(1..5)
|
||||
return if (potentialResult > 2) {
|
||||
// if more than 2 blocks can fit, we prefer displaying less blocks and have a bit more room
|
||||
// rather than having squished blocks
|
||||
floor(widthInDp.div(minBlockWidth * SQUISHED_BLOCK_FACTOR)).roundToInt().coerceIn(1..5)
|
||||
} else {
|
||||
potentialResult
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified estimation by taking into account more than 2 blocks are never squished,
|
||||
* and that devices with drawer layout always have space for at least 2 non-squished blocks
|
||||
*/
|
||||
val Context.areBlocksSquished: Boolean
|
||||
get() = getBlocksPerRow().let { blocksPerRow ->
|
||||
if (blocksPerRow > 2) {
|
||||
false
|
||||
} else {
|
||||
windowWidth.toFloat().div(density).div(minBlockWidth * SQUISHED_BLOCK_FACTOR) < blocksPerRow
|
||||
}
|
||||
}
|
||||
|
||||
val Context.isRtl: Boolean
|
||||
get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
val Context.isDarkMode: Boolean
|
||||
get() = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
val Context.isMotionReduced: Boolean
|
||||
get() {
|
||||
return try {
|
||||
Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == 0f
|
||||
} catch (e: SettingNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val Context.density: Int
|
||||
get() {
|
||||
return resources.displayMetrics.densityDpi
|
||||
}
|
||||
|
||||
val Context.fontScale: Float
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
resources.configuration.fontScale *
|
||||
resources.displayMetrics.densityDpi.div(android.util.DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat())
|
||||
} else {
|
||||
1f // Let’s just ignore it on old Android versions
|
||||
}
|
||||
}
|
||||
|
||||
// Take into account font scale, but not as much
|
||||
// For example a font scale of 1.6 makes the width 1.3 times larger
|
||||
val Context.fontScaleToApply: Float
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
fontScale.let {
|
||||
if (it != 1f) 1f + abs(it - 1f).div(2f).times(if (it > 1f) 1f else -1f) else it
|
||||
}
|
||||
} else {
|
||||
1f // Let’s just ignore it on old Android versions
|
||||
}
|
||||
}
|
||||
|
||||
val Context.windowHeightInDp: Float
|
||||
get() {
|
||||
return pxToDp(resources.displayMetrics.heightPixels)
|
||||
}
|
||||
|
||||
val Context.windowWidthInDp: Float
|
||||
get() {
|
||||
return pxToDp(resources.displayMetrics.widthPixels)
|
||||
}
|
||||
|
||||
val Context.windowWidth: Int
|
||||
@Px
|
||||
get() {
|
||||
return resources.displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
fun Context.dpToPx(dp: Float): Float {
|
||||
return dp * (resources.displayMetrics.densityDpi / 160f)
|
||||
}
|
||||
|
||||
fun Context.spToPx(sp: Int): Float {
|
||||
return sp * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1.0f, resources.displayMetrics)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun Context.pxToDp(@Px px: Int): Float {
|
||||
return px / (resources.displayMetrics.densityDpi / 160f)
|
||||
}
|
||||
|
||||
@Px
|
||||
fun Context.getTabletListAdaptiveWidth(@Px width: Int): Int {
|
||||
return if (!isTabletDevice && !isLandscape) {
|
||||
width
|
||||
} else {
|
||||
min(
|
||||
width.toFloat(),
|
||||
dpToPx(
|
||||
if (isTabletDevice) {
|
||||
MAX_TABLET_ADAPTIVE_LIST_WIDTH_DIP_TABLET
|
||||
} else {
|
||||
MAX_TABLET_ADAPTIVE_LIST_WIDTH_DIP_PHONE
|
||||
}.toFloat()
|
||||
)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi", "VisibleForTests")
|
||||
fun Context.getTypefaceFromTextAppearance(
|
||||
@StyleRes textAppearanceId: Int,
|
||||
): Typeface {
|
||||
return TextAppearance(this, textAppearanceId).getFont(this)
|
||||
}
|
||||
|
||||
fun Context.getThemeColor(
|
||||
@AttrRes id: Int,
|
||||
): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(id, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
fun Context.getColorResource(@ColorRes id: Int): androidx.compose.ui.graphics.Color {
|
||||
return androidx.compose.ui.graphics.Color(ResourcesCompat.getColor(resources, id, theme))
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Window.setSystemBarStyle(
|
||||
lightStatus: Boolean,
|
||||
) {
|
||||
var newLightStatus = lightStatus
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// Use default dark and light platform colors from EdgeToEdge
|
||||
val colorSystemBarDark = Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
||||
val colorSystemBarLight = Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
// Always apply a dark shader as a light or transparent status bar is not supported
|
||||
newLightStatus = false
|
||||
}
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
|
||||
navigationBarColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && lightStatus) {
|
||||
colorSystemBarLight
|
||||
} else {
|
||||
colorSystemBarDark
|
||||
}
|
||||
} else {
|
||||
isStatusBarContrastEnforced = false
|
||||
isNavigationBarContrastEnforced = true
|
||||
}
|
||||
|
||||
// Contrary to the documentation FALSE applies a light foreground color and TRUE a dark foreground color
|
||||
WindowInsetsControllerCompat(this, decorView).run {
|
||||
isAppearanceLightStatusBars = newLightStatus
|
||||
isAppearanceLightNavigationBars = lightStatus
|
||||
}
|
||||
}
|
||||
|
||||
fun Drawable.toBitmap(): Bitmap {
|
||||
val bitmap = createBitmap(intrinsicWidth, intrinsicHeight)
|
||||
val canvas = Canvas(bitmap)
|
||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
// translationY, scaleX, scaleY
|
||||
@Size(3)
|
||||
fun View.getFloatingOvershotEnterAnimators(): Array<Animator> {
|
||||
return getFloatingOvershotEnterAnimators(1.5f)
|
||||
}
|
||||
|
||||
@Size(3)
|
||||
fun View.getFloatingOvershotEnterAnimators(overshootFactor: Float): Array<Animator> {
|
||||
return getFloatingOvershotEnterAnimators(overshootFactor, translationY, scaleX, scaleY)
|
||||
}
|
||||
|
||||
@Size(3)
|
||||
fun View.getFloatingOvershotEnterAnimators(
|
||||
overshootFactor: Float,
|
||||
translationYFrom: Float,
|
||||
scaleXFrom: Float,
|
||||
scaleYFrom: Float,
|
||||
): Array<Animator> {
|
||||
val translation: Animator = ObjectAnimator.ofFloat(this, "translationY", translationYFrom, 0f)
|
||||
translation.interpolator = OvershootInterpolator(overshootFactor)
|
||||
val scaleX: Animator = ObjectAnimator.ofFloat(this, "scaleX", scaleXFrom, 1f)
|
||||
scaleX.interpolator = FLOATING_DECELERATE_INTERPOLATOR
|
||||
val scaleY: Animator = ObjectAnimator.ofFloat(this, "scaleY", scaleYFrom, 1f)
|
||||
scaleY.interpolator = FLOATING_DECELERATE_INTERPOLATOR
|
||||
return arrayOf(translation, scaleX, scaleY)
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import org.breezyweather.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Returns the uri of a file
|
||||
*
|
||||
* @param context context of application
|
||||
*/
|
||||
fun File.getUriCompat(context: Context): Uri {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", this)
|
||||
} else {
|
||||
toUri()
|
||||
}
|
||||
}
|
||||
|
||||
fun fileFromAsset(resource: Int, context: Context): File =
|
||||
File("${context.cacheDir}/$resource").apply {
|
||||
writeBytes(context.resources.openRawResource(resource).readBytes())
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import java.util.Locale
|
||||
|
||||
val Context.currentLocale: Locale
|
||||
get() {
|
||||
return AppCompatDelegate.getApplicationLocales().get(0)
|
||||
?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
resources.configuration.locales[0]
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.configuration.locale
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Review this use vs toLanguageTag()
|
||||
val Locale.code: String
|
||||
get() {
|
||||
val language = language
|
||||
val country = country
|
||||
return if (isTraditionalChinese) {
|
||||
language.lowercase() + "-" + country.lowercase()
|
||||
} else {
|
||||
language.lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Review this use vs toLanguageTag()
|
||||
val Locale.codeWithCountry: String
|
||||
get() {
|
||||
val language = language
|
||||
val country = country
|
||||
return if (!country.isNullOrEmpty()) {
|
||||
language.lowercase() + "-" + country.lowercase()
|
||||
} else {
|
||||
language.lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts "Hant" for traditional chinese but no country code otherwise
|
||||
val Locale.codeForGeonames: String
|
||||
get() {
|
||||
return if (isTraditionalChinese) {
|
||||
language.lowercase() + "-Hant"
|
||||
} else {
|
||||
language.lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
// Everything in uppercase + "ZHT" for traditional Chinese
|
||||
val Locale.codeForNaturalEarth: String
|
||||
get() {
|
||||
return if (isTraditionalChinese) {
|
||||
language.uppercase() + "T"
|
||||
} else {
|
||||
language.uppercase()
|
||||
}
|
||||
}
|
||||
|
||||
val Locale.isChinese: Boolean
|
||||
get() = language.equals("zh", ignoreCase = true)
|
||||
|
||||
// There is no way to access the script used, so assume Taiwan, Hong Kong and Macao
|
||||
val Locale.isTraditionalChinese: Boolean
|
||||
get() = isChinese &&
|
||||
arrayOf("TW", "HK", "MO").any { country.equals(it, ignoreCase = true) }
|
||||
|
||||
fun Locale.getCountryName(countryCode: String): String {
|
||||
return Locale.Builder()
|
||||
.setLanguage(language)
|
||||
.setRegion(countryCode)
|
||||
.build()
|
||||
.displayCountry
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] at its end.
|
||||
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||
*/
|
||||
fun String.chop(count: Int, replacement: String = "…"): String {
|
||||
return if (length > count) {
|
||||
take(count - replacement.length) + replacement
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fun String.capitalize(locale: Locale = Locale.Builder().setLanguage("en").setRegion("001").build()): String {
|
||||
return replaceFirstChar { firstChar ->
|
||||
if (firstChar.isLowerCase()) {
|
||||
firstChar.titlecase(locale)
|
||||
} else {
|
||||
firstChar.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.uncapitalize(locale: Locale = Locale.Builder().setLanguage("en").setRegion("001").build()): String {
|
||||
return replaceFirstChar { firstChar ->
|
||||
if (firstChar.isUpperCase()) {
|
||||
firstChar.lowercase(locale)
|
||||
} else {
|
||||
firstChar.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.getStringByLocale(
|
||||
id: Int,
|
||||
locale: Locale = Locale.Builder().setLanguage("en").setRegion("001").build(),
|
||||
): String {
|
||||
val configuration = Configuration(resources.configuration)
|
||||
configuration.setLocale(locale)
|
||||
return createConfigurationContext(configuration).resources.getString(id)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* Source: ProAndroidDev
|
||||
* https://proandroiddev.com/jetpack-compose-tricks-conditionally-applying-modifiers-for-dynamic-uis-e3fe5a119f45
|
||||
*/
|
||||
inline fun Modifier.conditional(
|
||||
condition: Boolean,
|
||||
ifTrue: Modifier.() -> Modifier,
|
||||
ifFalse: Modifier.() -> Modifier = { this },
|
||||
): Modifier = if (condition) {
|
||||
then(ifTrue(Modifier))
|
||||
} else {
|
||||
then(ifFalse(Modifier))
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/c5e8c9f01fa6b54425675ee3ebdc6f735aee7ba9/app/src/main/java/eu/kanade/tachiyomi/util/system/NetworkExtensions.kt
|
||||
*/
|
||||
val Context.connectivityManager: ConnectivityManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
fun Context.isOnline(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val activeNetwork = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
|
||||
val maxTransport = NetworkCapabilities.TRANSPORT_LOWPAN
|
||||
return if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
|
||||
// If VPN is enabled, but there is no other transport enabled, we are actually offline
|
||||
(NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).count(networkCapabilities::hasTransport) > 1
|
||||
} else {
|
||||
(NetworkCapabilities.TRANSPORT_CELLULAR..maxTransport).any(networkCapabilities::hasTransport)
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
return connectivityManager.activeNetworkInfo?.isConnected ?: false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationChannelGroupCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/953f5fb0253879547a94f88231b36ce81a35b48e/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt
|
||||
*/
|
||||
val Context.notificationManager: NotificationManager
|
||||
get() = getSystemService()!!
|
||||
|
||||
fun Context.notify(
|
||||
id: Int,
|
||||
channelId: String,
|
||||
block: (NotificationCompat.Builder.() -> Unit)? = null,
|
||||
) {
|
||||
val notification = notificationBuilder(channelId, block).build()
|
||||
notify(id, notification)
|
||||
}
|
||||
|
||||
fun Context.notify(id: Int, notification: Notification) {
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
NotificationManagerCompat.from(this).notify(id, notification)
|
||||
}
|
||||
|
||||
fun Context.cancelNotification(id: Int) {
|
||||
NotificationManagerCompat.from(this).cancel(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to create a notification builder.
|
||||
*
|
||||
* @param channelId the channel id.
|
||||
* @param block the function that will execute inside the builder.
|
||||
* @return a notification to be displayed or updated.
|
||||
*/
|
||||
fun Context.notificationBuilder(
|
||||
channelId: String,
|
||||
block: (NotificationCompat.Builder.() -> Unit)? = null,
|
||||
): NotificationCompat.Builder {
|
||||
val builder = NotificationCompat.Builder(this, channelId)
|
||||
if (block != null) {
|
||||
builder.block()
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build a notification channel group.
|
||||
*
|
||||
* @param channelId the channel id.
|
||||
* @param block the function that will execute inside the builder.
|
||||
* @return a notification channel group to be displayed or updated.
|
||||
*/
|
||||
fun buildNotificationChannelGroup(
|
||||
channelId: String,
|
||||
block: (NotificationChannelGroupCompat.Builder.() -> Unit),
|
||||
): NotificationChannelGroupCompat {
|
||||
val builder = NotificationChannelGroupCompat.Builder(channelId)
|
||||
builder.block()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to build a notification channel.
|
||||
*
|
||||
* @param channelId the channel id.
|
||||
* @param channelImportance the channel importance.
|
||||
* @param block the function that will execute inside the builder.
|
||||
* @return a notification channel to be displayed or updated.
|
||||
*/
|
||||
fun buildNotificationChannel(
|
||||
channelId: String,
|
||||
channelImportance: Int,
|
||||
block: (NotificationChannelCompat.Builder.() -> Unit),
|
||||
): NotificationChannelCompat {
|
||||
val builder = NotificationChannelCompat.Builder(channelId, channelImportance)
|
||||
builder.block()
|
||||
return builder.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
operator fun Int?.plus(other: Int?): Int? = if (this != null || other != null) {
|
||||
(this ?: 0) + (other ?: 0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
operator fun Double?.plus(other: Double?): Double? = if (this != null || other != null) {
|
||||
(this ?: 0.0) + (other ?: 0.0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
operator fun Double?.minus(other: Double?): Double? = if (this != null || other != null) {
|
||||
(this ?: 0.0) - (other ?: 0.0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun Double.ensurePositive(): Double? = if (this >= 0.0) this else null
|
||||
|
||||
fun Double.roundUpToNearestMultiplier(multiplier: Double): Double {
|
||||
return ceil(div(multiplier)).times(multiplier)
|
||||
}
|
||||
|
||||
fun Double.roundDownToNearestMultiplier(multiplier: Double): Double {
|
||||
return floor(div(multiplier)).times(multiplier)
|
||||
}
|
||||
|
||||
fun Double.roundDecimals(decimals: Int): Double? {
|
||||
return if (!isNaN()) {
|
||||
BigDecimal(this).setScale(decimals, RoundingMode.HALF_UP).toDouble()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val Array<Double>.median: Double?
|
||||
get() {
|
||||
if (isEmpty()) return null
|
||||
|
||||
sort()
|
||||
|
||||
return if (size % 2 != 0) {
|
||||
this[size / 2]
|
||||
} else {
|
||||
(this[(size - 1) / 2] + this[size / 2]) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/58a0add4f6bd8a5ab1006755035ff1b102355d4a/presentation-core/src/main/java/tachiyomi/presentation/core/util/PaddingValues.kt
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
operator fun PaddingValues.plus(other: PaddingValues): PaddingValues {
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
return PaddingValues(
|
||||
start = calculateStartPadding(layoutDirection) +
|
||||
other.calculateStartPadding(layoutDirection),
|
||||
end = calculateEndPadding(layoutDirection) +
|
||||
other.calculateEndPadding(layoutDirection),
|
||||
top = calculateTopPadding() + other.calculateTopPadding(),
|
||||
bottom = calculateBottomPadding() + other.calculateBottomPadding()
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
/**
|
||||
* Allow to split a string while keeping delimiters
|
||||
*/
|
||||
fun String.splitKeeping(str: String): List<String> {
|
||||
return split(str).flatMap { listOf(it, str) }.dropLast(1).filterNot { it.isEmpty() }
|
||||
}
|
||||
|
||||
fun String.splitKeeping(vararg strs: String): List<String> {
|
||||
var res = listOf(this)
|
||||
strs.forEach { str ->
|
||||
res = res.flatMap { it.splitKeeping(str) }
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.breezyweather.R
|
||||
import org.breezyweather.domain.settings.SettingsManager
|
||||
import org.breezyweather.unit.distance.Distance
|
||||
import org.breezyweather.unit.distance.Distance.Companion.meters
|
||||
import org.breezyweather.unit.duration.format
|
||||
import org.breezyweather.unit.formatting.UnitWidth
|
||||
import org.breezyweather.unit.pollen.PollenConcentrationUnit
|
||||
import org.breezyweather.unit.pollutant.PollutantConcentrationUnit
|
||||
import org.breezyweather.unit.precipitation.Precipitation
|
||||
import org.breezyweather.unit.precipitation.PrecipitationUnit
|
||||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.ratio.Ratio
|
||||
import org.breezyweather.unit.ratio.RatioUnit
|
||||
import org.breezyweather.unit.speed.Speed
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
import org.breezyweather.unit.temperature.TemperatureUnit
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* TODO: Lot of duplicates code in this page
|
||||
* Technically, we can do a <T : WeatherValue> extension, but we need to handle how we are getting the user-preferred
|
||||
* unit
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
* Getting the default unit from Settings is terribly slow, so it must be send as parameter
|
||||
*/
|
||||
fun Temperature.formatMeasure(
|
||||
context: Context,
|
||||
unit: TemperatureUnit,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
showSign: Boolean = false,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = unit,
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
showSign = showSign,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
* Getting the default unit from Settings is terribly slow, so it must be send as parameter
|
||||
*/
|
||||
fun Temperature.formatValue(
|
||||
context: Context,
|
||||
unit: TemperatureUnit,
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = unit,
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Source: https://weather.metoffice.gov.uk/guides/what-does-this-forecast-mean
|
||||
*/
|
||||
const val VISIBILITY_VERY_POOR = 1000.0
|
||||
const val VISIBILITY_POOR = 4000.0
|
||||
const val VISIBILITY_MODERATE = 10000.0
|
||||
const val VISIBILITY_GOOD = 20000.0
|
||||
const val VISIBILITY_CLEAR = 40000.0
|
||||
|
||||
val visibilityScaleThresholds = listOf(
|
||||
0.meters,
|
||||
VISIBILITY_VERY_POOR.meters,
|
||||
VISIBILITY_POOR.meters,
|
||||
VISIBILITY_MODERATE.meters,
|
||||
VISIBILITY_GOOD.meters,
|
||||
VISIBILITY_CLEAR.meters
|
||||
)
|
||||
|
||||
/**
|
||||
* @param context
|
||||
*/
|
||||
fun Distance.getVisibilityDescription(context: Context): String? {
|
||||
return when (inMeters) {
|
||||
in 0.0..<VISIBILITY_VERY_POOR -> context.getString(R.string.visibility_very_poor)
|
||||
in VISIBILITY_VERY_POOR..<VISIBILITY_POOR -> context.getString(R.string.visibility_poor)
|
||||
in VISIBILITY_POOR..<VISIBILITY_MODERATE -> context.getString(R.string.visibility_moderate)
|
||||
in VISIBILITY_MODERATE..<VISIBILITY_GOOD -> context.getString(R.string.visibility_good)
|
||||
in VISIBILITY_GOOD..<VISIBILITY_CLEAR -> context.getString(R.string.visibility_clear)
|
||||
in VISIBILITY_CLEAR..Double.MAX_VALUE -> context.getString(R.string.visibility_perfectly_clear)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Distance.formatMeasure(
|
||||
context: Context,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = settings.getDistanceUnit(context),
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Distance.formatValue(
|
||||
context: Context,
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = settings.getDistanceUnit(context),
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
fun Speed.getBeaufortScaleStrength(context: Context): String? {
|
||||
return context.resources.getStringArray(R.array.wind_strength_descriptions).getOrElse(inBeaufort) { null }
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Speed.getBeaufortScaleColor(context: Context): Int {
|
||||
return context.resources.getIntArray(R.array.wind_strength_colors).getOrNull(inBeaufort) ?: Color.TRANSPARENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Speed.formatMeasure(
|
||||
context: Context,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = settings.getSpeedUnit(context),
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Speed.formatValue(
|
||||
context: Context,
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = settings.getSpeedUnit(context),
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Precipitation.formatMeasure(
|
||||
context: Context,
|
||||
unit: PrecipitationUnit = SettingsManager.getInstance(context).getPrecipitationUnit(context),
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = unit,
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Precipitation.formatValue(
|
||||
context: Context,
|
||||
unit: PrecipitationUnit = SettingsManager.getInstance(context).getPrecipitationUnit(context),
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = unit,
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Precipitation.formatMeasureIntensity(
|
||||
context: Context,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatIntensity(
|
||||
context = context,
|
||||
unit = settings.getPrecipitationUnit(context),
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Pressure.formatMeasure(
|
||||
context: Context,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = settings.getPressureUnit(context),
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Pressure.formatValue(
|
||||
context: Context,
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = settings.getPressureUnit(context),
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun PollutantConcentrationUnit.formatMeasure(
|
||||
context: Context,
|
||||
value: Number,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
value = value,
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun PollenConcentrationUnit.formatMeasure(
|
||||
context: Context,
|
||||
value: Number,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
value = value,
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Duration.formatTime(
|
||||
context: Context,
|
||||
smallestUnit: DurationUnit = DurationUnit.HOURS,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
unitWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = DurationUnit.HOURS,
|
||||
smallestUnit = smallestUnit,
|
||||
valueWidth = valueWidth,
|
||||
unitWidth = unitWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Ratio.formatPercent(
|
||||
context: Context,
|
||||
valueWidth: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return format(
|
||||
context = context,
|
||||
unit = RatioUnit.PERCENT,
|
||||
valueWidth = valueWidth,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient format function with parameters filled for our app
|
||||
*/
|
||||
fun Ratio.formatValue(
|
||||
context: Context,
|
||||
width: UnitWidth = UnitWidth.SHORT,
|
||||
): String {
|
||||
val settings = SettingsManager.getInstance(context)
|
||||
return formatValue(
|
||||
unit = RatioUnit.PERCENT,
|
||||
width = width,
|
||||
locale = context.currentLocale,
|
||||
useNumberFormatter = settings.useNumberFormatter,
|
||||
useMeasureFormat = settings.useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
// We don't need any cloud cover unit, it's just a percent, but we need some helpers
|
||||
|
||||
/**
|
||||
* Source: WMO Cloud distribution for aviation
|
||||
*/
|
||||
const val CLOUD_COVER_SKC = 12.5 // 1 okta
|
||||
const val CLOUD_COVER_FEW = 37.5 // 3 okta
|
||||
const val CLOUD_COVER_SCT = 62.5 // 5 okta
|
||||
const val CLOUD_COVER_BKN = 87.5 // 7 okta
|
||||
const val CLOUD_COVER_OVC = 100.0 // 8 okta
|
||||
|
||||
fun Ratio.getCloudCoverColor(context: Context): Int {
|
||||
return when (inPercent) {
|
||||
in 0.0..<CLOUD_COVER_FEW -> ContextCompat.getColor(context, R.color.colorLevel_1)
|
||||
in CLOUD_COVER_FEW..CLOUD_COVER_SCT -> ContextCompat.getColor(context, R.color.colorLevel_2)
|
||||
in CLOUD_COVER_SCT..100.0 -> ContextCompat.getColor(context, R.color.colorLevel_3)
|
||||
else -> Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
*/
|
||||
fun Ratio.getCloudCoverDescription(context: Context): String? {
|
||||
return when (inPercent) {
|
||||
in 0.0..<CLOUD_COVER_SKC -> context.getString(R.string.common_weather_text_clear_sky)
|
||||
in CLOUD_COVER_SKC..<CLOUD_COVER_FEW -> context.getString(R.string.common_weather_text_mostly_clear)
|
||||
in CLOUD_COVER_FEW..<CLOUD_COVER_SCT -> context.getString(R.string.common_weather_text_partly_cloudy)
|
||||
in CLOUD_COVER_SCT..<CLOUD_COVER_BKN -> context.getString(R.string.common_weather_text_mostly_cloudy)
|
||||
in CLOUD_COVER_BKN..CLOUD_COVER_OVC -> context.getString(R.string.common_weather_text_cloudy)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
/**
|
||||
* Source: Android Developers, Chris Banes
|
||||
* https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
*/
|
||||
|
||||
/**
|
||||
* Apply window insets (system bars and display cutouts) for a view.
|
||||
*/
|
||||
fun View.doOnApplyWindowInsets(f: (View, Insets) -> Unit) {
|
||||
// Set an actual OnApplyWindowInsetsListener which proxies to the given lambda
|
||||
setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
val i = insets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout()
|
||||
)
|
||||
f(v, i)
|
||||
// Always return the insets, so that children can also use them
|
||||
insets
|
||||
}
|
||||
// request some insets
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
|
||||
fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
// We're already attached, just request as normal
|
||||
requestApplyInsets()
|
||||
} else {
|
||||
// We're not attached to the hierarchy, add a listener to request when we are
|
||||
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
v.removeOnAttachStateChangeListener(this)
|
||||
v.requestApplyInsets()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.common.extensions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import kotlinx.coroutines.delay
|
||||
import org.breezyweather.common.utils.helpers.LogHelper
|
||||
|
||||
/**
|
||||
* Taken from Mihon
|
||||
* Apache License, Version 2.0
|
||||
*
|
||||
* https://github.com/mihonapp/mihon/blob/aa498360db90350f2642e6320dc55e7d474df1fd/app/src/main/java/eu/kanade/tachiyomi/util/system/WorkManagerExtensions.kt
|
||||
*/
|
||||
|
||||
val Context.workManager: WorkManager
|
||||
get() = WorkManager.getInstance(this)
|
||||
|
||||
/**
|
||||
* Makes this worker run in the context of a foreground service.
|
||||
*
|
||||
* Note that this function is a no-op if the process is subject to foreground
|
||||
* service restrictions.
|
||||
*
|
||||
* Moving to foreground service context requires the worker to run a bit longer,
|
||||
* allowing Service.startForeground() to be called and avoiding system crash.
|
||||
*/
|
||||
suspend fun CoroutineWorker.setForegroundSafely() {
|
||||
try {
|
||||
setForeground(getForegroundInfo())
|
||||
delay(500)
|
||||
} catch (e: IllegalStateException) {
|
||||
LogHelper.log(msg = "Not allowed to set foreground job")
|
||||
}
|
||||
}
|
||||
fun WorkManager.isRunning(tag: String): Boolean {
|
||||
val list = getWorkInfosByTag(tag).get()
|
||||
return list.any { it.state == WorkInfo.State.RUNNING }
|
||||
}
|
||||