From 316b755eaddfe9cf53f6c82bfd0368ac461baf91 Mon Sep 17 00:00:00 2001 From: HeCodes2Much Date: Sat, 8 Jun 2024 22:03:39 +0100 Subject: [PATCH] Feat: Added batter status widget. Signed-off-by: HeCodes2Much --- app/build.gradle.kts | 26 +- .../common/StringExtensions.kt | 5 + .../launcher/helper/AppHelper.kt | 10 +- .../helper/weather/WeatherResponse.kt | 4 +- .../launcher/ui/widgets/WidgetFragment.kt | 117 ++++++- .../main/res/drawable/widget_background.xml | 10 +- app/src/main/res/layout/fragment_widgets.xml | 289 ++++++++++++------ app/src/main/res/layout/item_widget.xml | 6 - app/src/main/res/values/nontranslatable.xml | 15 +- app/src/main/res/values/strings.xml | 11 +- app/src/main/res/values/weathericons.xml | 4 +- 11 files changed, 346 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/res/layout/item_widget.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ea40be..a2c13c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,16 +24,6 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" manifestPlaceholders["internetPermission"] = "android.permission.INTERNET" - - val weatherFile = project.rootProject.file("weather.properties") - val properties = Properties() - properties.load(weatherFile.inputStream()) - val apiKey = properties.getProperty("WEATHER_API_KEY") ?: "" - buildConfigField( - type = "String", - name = "API_KEY", - value = "\"$apiKey\"" - ) } buildTypes { @@ -82,14 +72,26 @@ android { productFlavors { create("withInternet") { dimension = "internet" - manifestPlaceholders["hasInternetPermission"] = true manifestPlaceholders["internetPermission"] = "android.permission.INTERNET" + val weatherFile = project.rootProject.file("weather.properties") + val properties = Properties() + properties.load(weatherFile.inputStream()) + val apiKey = properties.getProperty("WEATHER_API_KEY") ?: "" + buildConfigField( + type = "String", + name = "API_KEY", + value = "\"$apiKey\"" + ) } create("withoutInternet") { dimension = "internet" - manifestPlaceholders["hasInternetPermission"] = false manifestPlaceholders["internetPermission"] = "REMOVE" + buildConfigField( + type = "String", + name = "API_KEY", + value = "\"REMOVE\"" + ) } } diff --git a/app/src/main/java/com/github/droidworksstudio/common/StringExtensions.kt b/app/src/main/java/com/github/droidworksstudio/common/StringExtensions.kt index 803f5f6..f78bb43 100644 --- a/app/src/main/java/com/github/droidworksstudio/common/StringExtensions.kt +++ b/app/src/main/java/com/github/droidworksstudio/common/StringExtensions.kt @@ -13,6 +13,7 @@ import android.text.style.StyleSpan import android.view.View import android.widget.Toast import androidx.core.text.getSpans +import java.util.Locale /** * Returns [Spannable] where the term is @@ -101,4 +102,8 @@ fun String.showLongToast(context: Context) { fun String.showShortToast(context: Context) { Toast.makeText(context, this, Toast.LENGTH_SHORT).show() +} + +fun String.capitalizeEachWord(): String { + return this.split(" ").joinToString(" ") { it.replaceFirstChar { str -> if (str.isLowerCase()) str.titlecase(Locale.getDefault()) else str.toString() } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt index e9da375..f6d3dff 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt @@ -257,13 +257,11 @@ class AppHelper @Inject constructor() { ): WeatherResponse { // Check if cached data is available and not expired val cachedWeatherData = context.getWeatherDataFromCache() - if (cachedWeatherData != null && System.currentTimeMillis() - cachedWeatherData.timestamp < TimeUnit.MINUTES.toMillis( - 5 - ) - ) { + if (cachedWeatherData?.let { System.currentTimeMillis() - it.timestamp < TimeUnit.MINUTES.toMillis(15) } == true) { return cachedWeatherData.weatherResponse } + // Fetch weather data from the network val apiKey = BuildConfig.API_KEY val units = "metric" @@ -306,8 +304,8 @@ class AppHelper @Inject constructor() { // Function to retrieve weather data from cache private fun Context.getWeatherDataFromCache(): CachedWeatherData? { - val sharedPreferences = getSharedPreferences("WeatherCache", Context.MODE_PRIVATE) - val timestamp = sharedPreferences.getLong("timestamp", -1) + val sharedPreferences = getSharedPreferences(Constants.WEATHER_PREFS, Context.MODE_PRIVATE) + val timestamp = sharedPreferences.getLong("cachedDataTimestamp", -1) val weatherResponseJson = sharedPreferences.getString("weatherResponse", null) if (timestamp != -1L && weatherResponseJson != null) { val weatherResponse = Gson().fromJson(weatherResponseJson, WeatherResponse::class.java) diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/WeatherResponse.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/WeatherResponse.kt index 1edf901..1e13d8b 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/WeatherResponse.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/WeatherResponse.kt @@ -28,5 +28,7 @@ data class Weather( ) data class Sys( - val country: String + val country: String, + val sunrise: Int, + val sunset: Int ) \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/widgets/WidgetFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/widgets/WidgetFragment.kt index b3f9efd..e6d8256 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/widgets/WidgetFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/widgets/WidgetFragment.kt @@ -1,22 +1,30 @@ package com.github.droidworksstudio.launcher.ui.widgets import android.annotation.SuppressLint +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import android.graphics.drawable.GradientDrawable +import android.os.BatteryManager +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.findNavController +import com.github.droidworksstudio.common.capitalizeEachWord +import com.github.droidworksstudio.common.hasInternetPermission import com.github.droidworksstudio.common.hideKeyboard import com.github.droidworksstudio.launcher.R import com.github.droidworksstudio.launcher.databinding.FragmentWidgetsBinding @@ -27,7 +35,10 @@ import com.github.droidworksstudio.launcher.listener.ScrollEventListener import com.github.droidworksstudio.launcher.utils.Constants import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Date +import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -52,7 +63,6 @@ class WidgetFragment : Fragment(), ): View { // Inflate the layout for this fragment _binding = FragmentWidgetsBinding.inflate(inflater, container, false) - _binding = binding return binding.root } @@ -65,6 +75,7 @@ class WidgetFragment : Fragment(), initializeInjectedDependencies() setupWeatherWidget() + setupBatteryWidget() observeSwipeTouchListener() observeClickListener() } @@ -72,7 +83,7 @@ class WidgetFragment : Fragment(), private fun initializeInjectedDependencies() { context = requireContext() binding.nestScrollView.hideKeyboard() - +// binding.nestScrollView.scrollEventListener = this } @@ -81,29 +92,35 @@ class WidgetFragment : Fragment(), context.getSharedPreferences(Constants.WEATHER_PREFS, Context.MODE_PRIVATE) val latitude = sharedPreferences.getFloat(Constants.LATITUDE, 0f) val longitude = sharedPreferences.getFloat(Constants.LONGITUDE, 0f) + val timestamp = convertTimestampToReadableDate(sharedPreferences.getLong("cachedDataTimestamp", 0)) + lifecycleScope.launch { try { + if (!context.hasInternetPermission()) return@launch binding.weatherRoot.visibility = View.VISIBLE val weatherResponse = appHelper.fetchWeatherData(context, latitude, longitude) Log.d("weatherResponse", "$weatherResponse") - val temperatureScale = if (preferenceHelper.weatherUnits == Constants.Units.Metric) getString(R.string.weather_c) else getString( - R.string.weather_f + val temperatureScale = if (preferenceHelper.weatherUnits == Constants.Units.Metric) getString(R.string.widget_c) else getString( + R.string.widget_f ) - val speedScale = if (preferenceHelper.weatherUnits == Constants.Units.Metric) getString(R.string.weather_mps) else getString(R.string.weather_mph) + val speedScale = if (preferenceHelper.weatherUnits == Constants.Units.Metric) getString(R.string.widget_weather_mps) else getString(R.string.widget_weather_mph) binding.weatherCity.setTextColor(preferenceHelper.widgetTextColor) binding.weatherTemperature.setTextColor(preferenceHelper.widgetTextColor) binding.weatherDescription.setTextColor(preferenceHelper.widgetTextColor) binding.weatherWind.setTextColor(preferenceHelper.widgetTextColor) binding.weatherHumidity.setTextColor(preferenceHelper.widgetTextColor) - binding.weatherPressure.setTextColor(preferenceHelper.widgetTextColor) + binding.weatherRefresh.setTextColor(preferenceHelper.widgetTextColor) + binding.weatherLastRun.setTextColor(preferenceHelper.widgetTextColor) + binding.weatherRefresh.typeface = ResourcesCompat.getFont(requireActivity(), R.font.weather) binding.weatherCity.text = getString(R.string.widget_weather_location, weatherResponse.name, weatherResponse.sys.country) binding.weatherTemperature.text = getString(R.string.widget_weather_temp, weatherResponse.main.temp, temperatureScale) - binding.weatherDescription.text = getString(R.string.widget_weather_description, weatherResponse.weather[0].description) + binding.weatherDescription.text = getString(R.string.widget_weather_description, weatherResponse.weather[0].description).capitalizeEachWord() binding.weatherWind.text = getString(R.string.widget_weather_wind, weatherResponse.wind.speed, speedScale) binding.weatherHumidity.text = getString(R.string.widget_weather_humidity, weatherResponse.main.humidity) - binding.weatherPressure.text = getString(R.string.widget_weather_pressure, weatherResponse.main.pressure) + binding.weatherLastRun.text = timestamp + binding.weatherRefresh.text = getString(R.string.widget_weather_refresh, getString(R.string.refresh_icon)) val weatherIcon = createWeatherIcon(context, setWeatherIcon(context, weatherResponse.weather[0].id)) binding.weatherIcon.setImageBitmap(weatherIcon) @@ -112,10 +129,7 @@ class WidgetFragment : Fragment(), if (weatherWidgetDrawable is GradientDrawable) { weatherWidgetDrawable.setColor(preferenceHelper.widgetBackgroundColor) } - - binding.weatherButtonRefresh.setColorFilter(preferenceHelper.widgetTextColor) } catch (e: Exception) { - binding.weatherRoot.visibility = View.GONE Log.e("Weather", "Failed to fetch weather data: ${e.message}") } } @@ -138,10 +152,10 @@ class WidgetFragment : Fragment(), } private fun setWeatherIcon(context: Context, id: Int): String { - var icon = "" + val icon: String val idDivided = id / 100 + val hourOfDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) if (idDivided * 100 == 800) { - val hourOfDay = Calendar.getInstance().get(Calendar.HOUR_OF_DAY) icon = if (hourOfDay in 7..19) { context.getString(R.string.weather_sunny) } else { @@ -161,6 +175,70 @@ class WidgetFragment : Fragment(), return icon } + private fun convertTimestampToReadableDate(timestamp: Long): String { + val date = Date(timestamp) + val format = SimpleDateFormat("hh:mm aa", Locale.getDefault()) + return format.format(date) + } + + private fun setupBatteryWidget() { + try { + binding.batteryLevel.setTextColor(preferenceHelper.widgetTextColor) + binding.chargingStatus.setTextColor(preferenceHelper.widgetTextColor) + binding.batteryHealth.setTextColor(preferenceHelper.widgetTextColor) + binding.batteryVoltage.setTextColor(preferenceHelper.widgetTextColor) + binding.batteryTemperature.setTextColor(preferenceHelper.widgetTextColor) + + val weatherBatteryDrawable = binding.batteryRoot.background + if (weatherBatteryDrawable is GradientDrawable) { + weatherBatteryDrawable.setColor(preferenceHelper.widgetBackgroundColor) + } + } catch (e: Exception) { + Log.e("Battery", "Failed to fetch battery data: ${e.message}") + } + binding.batteryRoot.visibility = View.VISIBLE + } + + private val batteryReceiver = object : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BATTERY_CHANGED) { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + val isCharging = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) + val health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN) + val count = intent.getIntExtra(BatteryManager.EXTRA_CYCLE_COUNT, 0) + val voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0) + val temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) + + val voltageScale = getString(R.string.widget_mv) + val temperatureScale = getString(R.string.widget_c) + + val batteryPct = (level / scale.toFloat() * 100).toInt() + val chargingStatusText = when (isCharging) { + BatteryManager.BATTERY_PLUGGED_AC, BatteryManager.BATTERY_PLUGGED_USB, BatteryManager.BATTERY_PLUGGED_WIRELESS -> "Charging" + else -> "Not Charging" + } + val healthStatus = when (health) { + BatteryManager.BATTERY_HEALTH_GOOD -> "Good" + BatteryManager.BATTERY_HEALTH_OVERHEAT -> "Overheat" + BatteryManager.BATTERY_HEALTH_DEAD -> "Dead" + BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE -> "Over Voltage" + BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE -> "Unspecified Failure" + BatteryManager.BATTERY_HEALTH_COLD -> "Cold" + else -> "Unknown" + } + + binding.batteryLevel.text = getString(R.string.widgets_battery_level, batteryPct) + binding.batteryCount.text = getString(R.string.widgets_battery_count, count) + binding.chargingStatus.text = getString(R.string.widgets_battery_status, chargingStatusText) + binding.batteryHealth.text = getString(R.string.widgets_battery_health, healthStatus) + binding.batteryVoltage.text = getString(R.string.widgets_battery_voltage, voltage, voltageScale) + binding.batteryTemperature.text = getString(R.string.widgets_battery_temperature, temperature, temperatureScale) + } + } + } + @SuppressLint("ClickableViewAccessibility") private fun observeSwipeTouchListener() { @@ -183,9 +261,20 @@ class WidgetFragment : Fragment(), } private fun observeClickListener() { - binding.weatherButtonRefresh.setOnClickListener { + binding.weatherRefresh.setOnClickListener { setupWeatherWidget() } } + + override fun onResume() { + super.onResume() + context.registerReceiver(batteryReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + setupWeatherWidget() + } + + override fun onPause() { + super.onPause() + context.unregisterReceiver(batteryReceiver) + } } diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml index fd0e43a..47ceeba 100644 --- a/app/src/main/res/drawable/widget_background.xml +++ b/app/src/main/res/drawable/widget_background.xml @@ -1,10 +1,10 @@ - + + android:left="14dp" + android:top="10dp" + android:right="14dp" + android:bottom="10dp" /> diff --git a/app/src/main/res/layout/fragment_widgets.xml b/app/src/main/res/layout/fragment_widgets.xml index 79cfc01..b4c2aff 100644 --- a/app/src/main/res/layout/fragment_widgets.xml +++ b/app/src/main/res/layout/fragment_widgets.xml @@ -19,132 +19,227 @@ android:id="@+id/nestScrollView" android:layout_width="match_parent" android:layout_height="wrap_content" - android:fillViewport="true" - app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toTopOf="parent" android:clipChildren="false" android:clipToPadding="false" android:fadingEdgeLength="48dp" + android:fillViewport="true" android:overScrollMode="never" android:requiresFadingEdge="vertical" - android:scrollbars="none"> + android:scrollbars="none" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="SpeakableTextPresentCheck"> - + android:orientation="vertical"> - - - - - - - - - - - + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="5dp" + android:background="@drawable/widget_background" + android:orientation="vertical" + android:visibility="gone"> + android:layout_alignParentStart="true" + android:orientation="horizontal"> + android:textSize="16sp" + tools:text="London, GB" /> + android:textSize="11sp" + tools:text="lastRun" /> + android:gravity="end" + android:lines="1" + android:textSize="18sp" + tools:text="refresh" /> - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_widget.xml b/app/src/main/res/layout/item_widget.xml deleted file mode 100644 index 0196973..0000000 --- a/app/src/main/res/layout/item_widget.xml +++ /dev/null @@ -1,6 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/values/nontranslatable.xml b/app/src/main/res/values/nontranslatable.xml index cd65187..aae55ce 100644 --- a/app/src/main/res/values/nontranslatable.xml +++ b/app/src/main/res/values/nontranslatable.xml @@ -1,17 +1,18 @@ %s%% + mV + + + °C + °F %1$s %1$s, %2$s - %1$.2f %2$s + %1$.0f %2$s %1$s - m/s - mil/h - mm - - °C - °F + m/s + mil/h Feels Like : %s% Wind : %1$.2f %2$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a233e75..b32b7df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Favorite Apps Hidden Apps - + Search Rename @@ -595,4 +595,13 @@ Bing Brave SwissCows + + Battery Level : %d %% + Battery Charge Count : %1$d + Charging Status : %1$s + Battery Health : %1$s + Battery Voltage : %1$d %2$s + Battery Temperature : %1$d %2$s + + \ No newline at end of file diff --git a/app/src/main/res/values/weathericons.xml b/app/src/main/res/values/weathericons.xml index a375d84..84e5e8a 100644 --- a/app/src/main/res/values/weathericons.xml +++ b/app/src/main/res/values/weathericons.xml @@ -1,5 +1,7 @@ + + @@ -14,6 +16,4 @@ - - \ No newline at end of file