Wake screen widget (#8)

* - Update widget validation
* - Cleaned widget folder and code
This commit is contained in:
2022-02-15 00:57:52 +00:00
committed by GitHub
parent ed78d40cc0
commit a1a32e4ceb
11 changed files with 213 additions and 244 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
/local.properties
/.idea/workspace.xml
/.idea/libraries
/.idea/caches
.DS_Store
/build
/captures

Binary file not shown.

View File

@@ -60,13 +60,14 @@ class ServicesHelper(
suspend fun getWidgetWeather(): WidgetData? {
return try {
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
val epoc = System.currentTimeMillis()
result.weather.let {
val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
val temp = it.current?.temp?.toInt().toString()
WidgetData(location, bitmap, temp)
WidgetData(location, bitmap, temp, epoc)
}
} catch (e: Exception) {
null
@@ -102,8 +103,9 @@ class ServicesHelper(
val bitmap = it.current?.icon
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
val temp = it.current?.temp?.toInt().toString()
val epoc = System.currentTimeMillis()
WidgetData(location, bitmap, temp)
WidgetData(location, bitmap, temp, epoc)
}
val list = mutableListOf<InnerWidgetCellData>()
@@ -141,10 +143,9 @@ class ServicesHelper(
}
fun getWidgetBackground(): Int {
return if (settingsRepository.isBlackBackground()) {
return if (settingsRepository.isBlackBackground())
Color.BLACK
} else {
else
Color.TRANSPARENT
}
}
}

View File

@@ -5,7 +5,8 @@ import android.graphics.Bitmap
data class WidgetData(
val location: String?,
val icon: String?,
val currentTemp: String?
val currentTemp: String?,
val timeStamp: Long
)
data class InnerWidgetData(

View File

@@ -133,6 +133,7 @@
android:rowCount="1">
<LinearLayout
android:id="@+id/widget_item_0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
@@ -165,6 +166,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/widget_item_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
@@ -197,6 +199,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/widget_item_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
@@ -229,6 +232,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/widget_item_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
@@ -260,6 +264,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/widget_item_4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"

View File

@@ -5,42 +5,48 @@ import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.core.app.JobIntentService
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
abstract class BaseWidgetServiceIntentClass : JobIntentService(){
abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentService(){
private val kodein = LateInitKodein()
val helper: ServicesHelper by kodein.instance()
lateinit var appWidgetManager: AppWidgetManager
lateinit var appWidgetIds: IntArray
fun setKodein(context: Context){
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
fun initBaseWidget(componentName: ComponentName){
appWidgetManager = AppWidgetManager.getInstance(baseContext)
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
}
fun createRemoteView(context: Context, @LayoutRes id: Int): RemoteViews {
return RemoteViews(context.packageName, id)
fun createRemoteView(@LayoutRes id: Int): RemoteViews {
return RemoteViews(packageName, id)
}
// Create pending intent commonly used for 'click to update' features
fun <T: AppWidgetProvider> createUpdatePendingIntent(
fun createUpdatePendingIntent(
appWidgetProvider: Class<T>,
context: Context,
appWidgetId: Int
): PendingIntent? {
val seconds = (System.currentTimeMillis() / 1000L).toInt()
val intentUpdate = Intent(context.applicationContext, appWidgetProvider)
val intentUpdate = Intent(applicationContext, appWidgetProvider)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return PendingIntent.getBroadcast(
context, seconds, intentUpdate,
this, seconds, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
}
@@ -48,11 +54,26 @@ abstract class BaseWidgetServiceIntentClass : JobIntentService(){
* create a pending intent used to navigate to activity:
* @param activityClass
*/
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(context, activityClass)
fun <T: Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(this, activityClass)
return TaskStackBuilder.create(context)
return TaskStackBuilder.create(this)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
fun setImageView(
path: String?,
views: RemoteViews,
@IdRes viewId: Int,
appWidgetId: Int
){
CoroutineScope(Dispatchers.Main).launch {
Picasso.get().load(path).into(views, viewId, intArrayOf(appWidgetId))
}
}
open fun bindView(widgetId: Int, views: RemoteViews, data: Any?) {}
open fun bindEmptyView(widgetId: Int, views: RemoteViews, data: Any?) {}
open fun bindErrorView(widgetId: Int, views: RemoteViews, data: Any?) {}
}

View File

@@ -1,60 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import kotlinx.coroutines.runBlocking
import org.kodein.di.KodeinAware
import org.kodein.di.android.kodein
import org.kodein.di.generic.instance
class MyWidgetRemoteViewsFactory(
private val context: Context,
val intent: Intent
) : RemoteViewsFactory, KodeinAware{
override val kodein by kodein(context)
private val helper : ServicesHelper by kodein.instance()
private var list: List<InnerWidgetData>? = null
override fun onCreate() {}
override fun onDataSetChanged() {
runBlocking {
list = helper.getWidgetInnerWeather()
}
}
override fun onDestroy() {}
override fun getCount(): Int = list?.size ?: 5
override fun getViewAt(i: Int): RemoteViews {
val rv = RemoteViews(context.packageName, R.layout.widget_item)
if (list.isNullOrEmpty()) return rv
list?.get(i)?.let {
rv.setTextViewText(R.id.widget_item_day, it.date)
rv.setImageViewBitmap(R.id.widget_item_image, it.icon)
rv.setTextViewText(R.id.widget_item_temp_high, it.highTemp)
rv.setOnClickFillInIntent(R.id.widget_item_layout, intent)
}
return rv
}
override fun getLoadingView(): RemoteViews {
return RemoteViews(context.packageName, R.layout.widget_item_loading)
}
override fun getViewTypeCount(): Int = 2
override fun getItemId(i: Int): Long = i.toLong()
override fun hasStableIds(): Boolean = true
}

View File

@@ -3,187 +3,178 @@ package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.Uri
import android.icu.text.SimpleDateFormat
import android.os.PowerManager
import android.widget.RemoteViews
import android.os.Build
import androidx.core.app.ActivityCompat.checkSelfPermission
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetState.*
import com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetState.Companion.getWidgetState
import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
import java.util.*
/**
* Example implementation of a JobIntentService.
* Implementation of a JobIntentService used for home screen widget
*/
class WidgetJobServiceIntent : BaseWidgetServiceIntentClass() {
const val HALF_DAY = 43200000L
class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
private val kodein = LateInitKodein()
private val helper: ServicesHelper by kodein.instance()
override fun onHandleWork(intent: Intent) {
// We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
kodein.baseKodein = (applicationContext as KodeinAware).kodein
executeWidgetUpdate()
}
private fun executeWidgetUpdate() {
setKodein(this)
val componentName = ComponentName(this, NewAppWidget::class.java)
initBaseWidget(componentName)
val appWidgetManager = AppWidgetManager.getInstance(this)
val thisAppWidget = ComponentName(packageName, NewAppWidget::class.java.name)
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
initiateWidgetUpdate(getCurrentWidgetState())
}
validateOperation()?.let {
if (it) updateWidget(appWidgetIds, appWidgetManager)
else updateErrorWidget(appWidgetIds, appWidgetManager)
private fun initiateWidgetUpdate(state: WidgetState) {
when (state) {
NO_LOCATION, SCREEN_ON_CONNECTION_UNAVAILABLE -> updateErrorWidget(state)
SCREEN_ON_CONNECTION_AVAILABLE -> updateWidget(false)
SCREEN_OFF_CONNECTION_AVAILABLE -> updateWidget(true)
SCREEN_OFF_CONNECTION_UNAVAILABLE -> return
}
}
private fun updateWidget(appWidgetIds: IntArray, appWidgetManager: AppWidgetManager){
private fun updateWidget(fromStorage: Boolean) {
CoroutineScope(Dispatchers.IO).launch {
val result = getWidgetWeather()
for (appWidgetId in appWidgetIds) {
bindView(this@WidgetJobServiceIntent, appWidgetManager, appWidgetId, result)
}
}
}
private fun updateErrorWidget(appWidgetIds: IntArray, appWidgetManager: AppWidgetManager){
for (appWidgetId in appWidgetIds) {
setEmptyView(this, appWidgetManager, appWidgetId)
val result = getWidgetWeather(fromStorage)
appWidgetIds.forEach { id -> setupView(id, result) }
}
}
private fun validateOperation(): Boolean? {
private fun updateErrorWidget(state: WidgetState) {
appWidgetIds.forEach { id -> setEmptyView(id, state) }
}
private fun getCurrentWidgetState(): WidgetState {
val pm = getSystemService(POWER_SERVICE) as PowerManager
val isScreenOn = pm.isInteractive
val locationGranted =
checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED
val internetAvailable = isInternetAvailable(this.applicationContext)
// no location return false
if (!locationGranted) return false
// internet is available lets go
if (internetAvailable) return true
// screen is off and no connection, do nothing
if (!isScreenOn && !internetAvailable) return null
return if (isScreenOn && !internetAvailable) false else null
}
private fun createForecastListIntent(
context: Context,
appWidgetId: Int
): Intent {
return Intent(context, WidgetRemoteViewsService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
return getWidgetState(locationGranted, isScreenOn, internetAvailable)
}
@SuppressLint("MissingPermission")
suspend fun getWidgetWeather(): WidgetWeatherCollection? {
suspend fun getWidgetWeather(storageOnly: Boolean): WidgetWeatherCollection? {
return tryOrNullSuspended {
helper.fetchData()
if (!storageOnly) helper.fetchData()
helper.getWidgetWeatherCollection()
}
}
private fun setEmptyView(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
try {
val error = if (checkSelfPermission(context, ACCESS_COARSE_LOCATION)
!= PERMISSION_GRANTED) {
"No Permission"
} else if (!isInternetAvailable(context.applicationContext)) {
"No Connection"
} else {
"No Data"
}
val updatePendingIntent = createUpdatePendingIntent(NewAppWidget::class.java, context, appWidgetId)
val views = createRemoteView(context, R.layout.weather_app_widget)
bindEmptyView(appWidgetManager, appWidgetId, views, updatePendingIntent, error)
} catch (e: Exception) {
e.printStackTrace()
}
private fun setEmptyView(appWidgetId: Int, state: WidgetState) {
val error = when (state) {
NO_LOCATION -> "No Location Permission"
SCREEN_ON_CONNECTION_UNAVAILABLE -> "No network available"
else -> "No data"
}
private fun bindEmptyView(
appWidgetManager: AppWidgetManager,
val views = createRemoteView(R.layout.weather_app_widget)
bindErrorView(appWidgetId, views, error)
}
private fun setupView(
appWidgetId: Int,
views: RemoteViews,
clickingUpdateIntent: PendingIntent?,
warning: String
collection: WidgetWeatherCollection?
) {
setLastUpdated(views)
views.setTextViewText(R.id.widget_current_location, warning)
views.setImageViewResource(R.id.widget_current_icon, R.drawable.ic_baseline_cloud_off_24)
views.setImageViewResource(R.id.location_icon, 0)
views.setTextViewText(R.id.widget_main_temp, "")
views.setTextViewText(R.id.widget_feel_temp, "")
views.setOnClickPendingIntent(R.id.widget_current_icon, clickingUpdateIntent)
views.setOnClickPendingIntent(R.id.widget_current_location, clickingUpdateIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
}
private fun bindView(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
collection: WidgetWeatherCollection?) {
val views = createRemoteView(context, R.layout.weather_app_widget)
setLastUpdated(views)
val views = createRemoteView(R.layout.weather_app_widget)
setLastUpdated(views, collection?.widgetData?.timeStamp)
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
val clickingUpdatePendingIntent = createUpdatePendingIntent(NewAppWidget::class.java, context, appWidgetId)
if (collection != null) {
val clickPendingIntentTemplate =
createClickingPendingIntent(context, MainActivity::class.java)
bindView(appWidgetId, views, collection)
} else {
bindEmptyView(appWidgetId, views, "No weather available")
}
}
override fun bindErrorView(
widgetId: Int,
views: RemoteViews,
data: Any?
) {
bindEmptyView(widgetId, views, data)
}
override fun bindEmptyView(
widgetId: Int,
views: RemoteViews,
data: Any?
) {
val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, widgetId)
views.apply {
val weather = collection.widgetData
setTextViewText(R.id.widget_current_location, data as String)
setImageViewResource(R.id.widget_current_icon, R.drawable.ic_baseline_cloud_off_24)
setImageViewResource(R.id.location_icon, 0)
setTextViewText(R.id.widget_main_temp, "")
setTextViewText(R.id.widget_feel_temp, "")
setOnClickPendingIntent(R.id.widget_current_icon, clickUpdate)
setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
appWidgetManager.updateAppWidget(widgetId, this)
}
}
override fun bindView(widgetId: Int, views: RemoteViews, data: Any?) {
val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, widgetId)
val clickToMain = createClickingPendingIntent(MainActivity::class.java)
val collection = data as WidgetWeatherCollection
val weather = collection.widgetData
views.apply {
setTextViewText(R.id.widget_main_temp, weather.currentTemp)
setTextViewText(R.id.widget_feel_temp, "°C")
setTextViewText(R.id.widget_current_location, weather.location)
setImageViewResource(R.id.location_icon, R.drawable.location_flag)
CoroutineScope(Dispatchers.Main).launch {
Picasso.get().load(weather.icon).into(views, R.id.widget_current_icon, intArrayOf(appWidgetId))
}
setPendingIntentTemplate(R.id.widget_listview, clickPendingIntentTemplate)
setOnClickPendingIntent(R.id.widget_current_icon, clickingUpdatePendingIntent)
setOnClickPendingIntent(R.id.widget_current_location, clickingUpdatePendingIntent)
loadCells(appWidgetId, views, collection.forecast)
// setRemoteAdapter(R.id.widget_listview, forecastListIntent)
}
setImageView(weather.icon, this, R.id.widget_current_icon, widgetId)
setOnClickPendingIntent(R.id.widget_current_icon, clickUpdate)
setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
loadCells(widgetId, this, collection.forecast, clickToMain)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
} else {
bindEmptyView(appWidgetManager, appWidgetId, views, clickingUpdatePendingIntent, "No Connection")
appWidgetManager.updateAppWidget(widgetId, views)
}
}
}
private fun loadCells(appWidgetId: Int, remoteViews: RemoteViews, weather: List<InnerWidgetCellData>){
private fun loadCells(
appWidgetId: Int,
remoteViews: RemoteViews,
weather: List<InnerWidgetCellData>,
clickIntent: PendingIntent
) {
(0..4).forEach { i ->
val containerId: Int = resources.getIdentifier("widget_item_$i", "id", packageName)
val dayId: Int = resources.getIdentifier("widget_item_day_$i", "id", packageName)
val imageId: Int = resources.getIdentifier("widget_item_image_$i", "id", packageName)
val tempId: Int = resources.getIdentifier("widget_item_temp_high_$i", "id", packageName)
@@ -192,18 +183,24 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass() {
remoteViews.setTextViewText(dayId, it.date)
remoteViews.setTextViewText(tempId, it.highTemp)
CoroutineScope(Dispatchers.Main).launch {
Picasso.get().load(it.icon).into(remoteViews, imageId, intArrayOf(appWidgetId))
}
setImageView(it.icon, remoteViews, imageId, appWidgetId)
remoteViews.setOnClickPendingIntent(containerId, clickIntent)
}
}
private fun setLastUpdated(views: RemoteViews){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val current = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("HH:mm")
val formatted = current.format(formatter)
views.setTextViewText(R.id.widget_current_status, "last updated: $formatted")
private fun setLastUpdated(views: RemoteViews, timeStamp: Long?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && timeStamp != null) {
val difference = System.currentTimeMillis().minus(timeStamp)
val status = if (difference > HALF_DAY) {
"12hrs ago"
} else {
val date = Date(timeStamp)
val sdf = SimpleDateFormat("HH:mm", Locale.getDefault())
sdf.format(date)
}
views.setTextViewText(R.id.widget_current_status, "last updated: $status")
}
}

View File

@@ -1,10 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.content.Intent
import android.widget.RemoteViewsService
class WidgetRemoteViewsService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return MyWidgetRemoteViewsFactory(applicationContext, intent)
}
}

View File

@@ -0,0 +1,28 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget
enum class WidgetState {
NO_LOCATION,
SCREEN_ON_CONNECTION_AVAILABLE,
SCREEN_ON_CONNECTION_UNAVAILABLE,
SCREEN_OFF_CONNECTION_AVAILABLE,
SCREEN_OFF_CONNECTION_UNAVAILABLE;
companion object {
fun getWidgetState(
locationAvailable: Boolean,
screenOn: Boolean,
connectionAvailable: Boolean
): WidgetState {
return if (!locationAvailable)
NO_LOCATION
else if (screenOn && connectionAvailable)
SCREEN_ON_CONNECTION_AVAILABLE
else if (screenOn && !connectionAvailable)
SCREEN_ON_CONNECTION_UNAVAILABLE
else if (!screenOn && connectionAvailable)
SCREEN_OFF_CONNECTION_AVAILABLE
else
SCREEN_OFF_CONNECTION_UNAVAILABLE
}
}
}

View File

@@ -1,15 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.widget
import android.app.Activity
import android.app.PendingIntent
import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(context, activityClass)
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}