#1774 Widget remote views factory refactor

This commit is contained in:
Andrei 2023-01-16 14:34:14 -08:00
parent 3e75293c3d
commit d92e666b9e
13 changed files with 351 additions and 370 deletions

View file

@ -1,116 +0,0 @@
package com.newsblur.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import androidx.core.content.ContextCompat;
import android.util.Log;
import com.newsblur.R;
import com.newsblur.activity.AllStoriesItemsList;
import com.newsblur.activity.ItemsList;
import com.newsblur.activity.WidgetConfig;
import com.newsblur.util.FeedSet;
import com.newsblur.util.PendingIntentUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.WidgetBackground;
import java.util.Set;
public class WidgetProvider extends AppWidgetProvider {
private static String TAG = "WidgetProvider";
// Called when the BroadcastReceiver receives an Intent broadcast.
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_STORY)) {
String storyHash = intent.getStringExtra(WidgetUtils.EXTRA_ITEM_ID);
Intent i = new Intent(context, AllStoriesItemsList.class);
i.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds());
i.putExtra(ItemsList.EXTRA_STORY_HASH, storyHash);
i.putExtra(ItemsList.EXTRA_WIDGET_STORY, true);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.getApplicationContext().startActivity(i);
} else if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_CONFIG)) {
Intent i = new Intent(context, WidgetConfig.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.getApplicationContext().startActivity(i);
}
super.onReceive(context, intent);
}
/**
* This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// update each of the app widgets with the remote adapter
Log.d(TAG, "onUpdate");
WidgetUtils.checkWidgetUpdateAlarm(context);
WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(context);
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(context);
for (int appWidgetId : appWidgetIds) {
// Set up the intent that starts the WidgetRemoteViewService, which will
// provide the views for this collection.
Intent intent = new Intent(context, WidgetRemoteViewsService.class);
// Add the app widget ID to the intent extras.
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
// Instantiate the RemoteViews object for the app widget layout.
WidgetRemoteViews rv = new WidgetRemoteViews(context.getPackageName(), R.layout.view_app_widget);
if (widgetBackground == WidgetBackground.DEFAULT) {
rv.setViewBackgroundColor(R.id.container_widget, ContextCompat.getColor(context, R.color.widget_background));
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
rv.setViewBackgroundColor(R.id.container_widget, Color.TRANSPARENT);
}
// Set up the RemoteViews object to use a RemoteViews adapter.
// This adapter connects
// to a RemoteViewsService through the specified intent.
// This is how you populate the data.
rv.setRemoteAdapter(R.id.widget_list, intent);
// The empty view is displayed when the collection has no items.
// It should be in the same layout used to instantiate the RemoteViews
// object above.
rv.setEmptyView(R.id.widget_list, R.id.widget_empty_view);
if (feedIds != null && feedIds.isEmpty()) {
rv.setTextViewText(R.id.widget_empty_view, context.getString(R.string.title_widget_setup));
}
Intent configIntent = new Intent(context, WidgetProvider.class);
configIntent.setAction(WidgetUtils.ACTION_OPEN_CONFIG);
PendingIntent configIntentTemplate = PendingIntentUtils.getImmutableBroadcast(context, WidgetUtils.RC_WIDGET_CONFIG, configIntent, PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_empty_view, configIntentTemplate);
// This section makes it possible for items to have individualized behavior.
// It does this by setting up a pending intent template. Individuals items of a collection
// cannot set up their own pending intents. Instead, the collection as a whole sets
// up a pending intent template, and the individual items set a fillInIntent
// to create unique behavior on an item-by-item basis.
Intent touchIntent = new Intent(context, WidgetProvider.class);
// Set the action for the intent.
// When the user touches a particular view, it will have the effect of
// broadcasting ACTION_OPEN_STORY.
touchIntent.setAction(WidgetUtils.ACTION_OPEN_STORY);
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent touchIntentTemplate = PendingIntentUtils.getImmutableBroadcast(context, WidgetUtils.RC_WIDGET_STORY, touchIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate);
appWidgetManager.updateAppWidget(appWidgetId, rv);
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}

View file

@ -0,0 +1,119 @@
package com.newsblur.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.util.Log
import androidx.core.content.ContextCompat
import com.newsblur.R
import com.newsblur.activity.AllStoriesItemsList
import com.newsblur.activity.ItemsList
import com.newsblur.activity.WidgetConfig
import com.newsblur.util.FeedSet
import com.newsblur.util.PendingIntentUtils.getImmutableBroadcast
import com.newsblur.util.PrefsUtils
import com.newsblur.util.WidgetBackground
import com.newsblur.widget.WidgetUtils.checkWidgetUpdateAlarm
class WidgetProvider : AppWidgetProvider() {
/**
* Called when the BroadcastReceiver receives an Intent broadcast.
*/
override fun onReceive(context: Context, intent: Intent) {
Log.d(this.javaClass.name, "onReceive")
if (intent.action == WidgetUtils.ACTION_OPEN_STORY) {
val storyHash = intent.getStringExtra(WidgetUtils.EXTRA_ITEM_ID)
Intent(context, AllStoriesItemsList::class.java).apply {
putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds())
putExtra(ItemsList.EXTRA_STORY_HASH, storyHash)
putExtra(ItemsList.EXTRA_WIDGET_STORY, true)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
context.applicationContext.startActivity(it)
}
} else if (intent.action == WidgetUtils.ACTION_OPEN_CONFIG) {
Intent(context, WidgetConfig::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}.also {
context.applicationContext.startActivity(it)
}
}
super.onReceive(context, intent)
}
/**
* This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute
*/
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// update each of the app widgets with the remote adapter
Log.d(this.javaClass.name, "onUpdate")
checkWidgetUpdateAlarm(context)
val widgetBackground = PrefsUtils.getWidgetBackground(context)
val feedIds = PrefsUtils.getWidgetFeedIds(context)
for (appWidgetId in appWidgetIds) {
// Set up the intent that starts the WidgetRemoteViewService, which will
// provide the views for this collection.
val intent = Intent(context, WidgetRemoteViewsService::class.java).apply {
// Add the app widget ID to the intent extras.
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
// Instantiate the RemoteViews object for the app widget layout.
val rv = WidgetRemoteViews(context.packageName, R.layout.view_app_widget)
if (widgetBackground == WidgetBackground.DEFAULT) {
rv.setViewBackgroundColor(R.id.container_widget, ContextCompat.getColor(context, R.color.widget_background))
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
rv.setViewBackgroundColor(R.id.container_widget, Color.TRANSPARENT)
}
// Set up the RemoteViews object to use a RemoteViews adapter.
// This adapter connects to a RemoteViewsService through the
// specified intent. This is how you populate the data.
rv.setRemoteAdapter(R.id.widget_list, intent)
// The empty view is displayed when the collection has no items.
// It should be in the same layout used to instantiate the RemoteViews
// object above.
rv.setEmptyView(R.id.widget_list, R.id.widget_empty_view)
if (feedIds != null && feedIds.isEmpty()) {
rv.setTextViewText(R.id.widget_empty_view, context.getString(R.string.title_widget_setup))
}
val configIntent = Intent(context, WidgetProvider::class.java)
configIntent.action = WidgetUtils.ACTION_OPEN_CONFIG
val configIntentTemplate = getImmutableBroadcast(context, WidgetUtils.RC_WIDGET_CONFIG, configIntent, PendingIntent.FLAG_UPDATE_CURRENT)
rv.setOnClickPendingIntent(R.id.widget_empty_view, configIntentTemplate)
// This section makes it possible for items to have individualized behavior.
// It does this by setting up a pending intent template. Individuals items of a collection
// cannot set up their own pending intents. Instead, the collection as a whole sets
// up a pending intent template, and the individual items set a fillInIntent
// to create unique behavior on an item-by-item basis.
val touchIntent = Intent(context, WidgetProvider::class.java).apply {
// Set the action for the intent.
// When the user touches a particular view, it will have the effect of
// broadcasting ACTION_OPEN_STORY.
action = WidgetUtils.ACTION_OPEN_STORY
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
val touchIntentTemplate = getImmutableBroadcast(context, WidgetUtils.RC_WIDGET_STORY, touchIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate)
appWidgetManager.updateAppWidget(appWidgetId, rv)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list)
}
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
}
}

View file

@ -1,15 +0,0 @@
package com.newsblur.widget;
import androidx.annotation.ColorInt;
import android.widget.RemoteViews;
class WidgetRemoteViews extends RemoteViews {
WidgetRemoteViews(String packageName, int layoutId) {
super(packageName, layoutId);
}
void setViewBackgroundColor(int viewId, @ColorInt int color) {
setInt(viewId, "setBackgroundColor", color);
}
}

View file

@ -0,0 +1,14 @@
package com.newsblur.widget
import android.widget.RemoteViews
import androidx.annotation.ColorInt
internal class WidgetRemoteViews(
packageName: String,
layoutId: Int,
) : RemoteViews(packageName, layoutId) {
fun setViewBackgroundColor(viewId: Int, @ColorInt color: Int) {
setInt(viewId, "setBackgroundColor", color)
}
}

View file

@ -12,17 +12,16 @@ import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import com.newsblur.R
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.di.IconLoader
import com.newsblur.di.ThumbnailLoader
import com.newsblur.domain.Feed
import com.newsblur.domain.Story
import com.newsblur.network.APIManager
import com.newsblur.util.*
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import java.util.*
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.min
class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFactory {
@ -32,14 +31,15 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
private val dbHelper: BlurDatabaseHelper
private val iconLoader: ImageLoader
private val thumbnailLoader: ImageLoader
private var fs: FeedSet? = null
private val appWidgetId: Int
private var dataCompleted = false
private val storyItems: MutableList<Story> = ArrayList()
private val storyItems: MutableList<Story> = mutableListOf()
private val cancellationSignal = CancellationSignal()
private val storiesLock = ReentrantLock()
init {
Log.d(TAG, "Constructor")
Log.d(this.javaClass.name, "init")
val hiltEntryPoint = EntryPointAccessors
.fromApplication(context.applicationContext, WidgetRemoteViewsFactoryEntryPoint::class.java)
this.context = context
@ -54,22 +54,14 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
/**
* The system calls onCreate() when creating your factory for the first time.
* This is where you set up any connections and/or cursors to your data source.
*
*
* Heavy lifting,
* for example downloading or creating content etc, should be deferred to onDataSetChanged()
* or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
*/
override fun onCreate() {
Log.d(TAG, "onCreate")
Log.d(this.javaClass.name, "onCreate")
WidgetUtils.enableWidgetUpdate(context)
}
/**
* Allowed to run synchronous calls
*/
override fun getViewAt(position: Int): RemoteViews {
Log.d(TAG, "getViewAt $position")
override fun getViewAt(position: Int): RemoteViews = storiesLock.withLock {
Log.d(this.javaClass.name, "getViewAt $position")
val story = storyItems[position]
val rv = WidgetRemoteViews(context.packageName, R.layout.view_widget_story_item)
rv.setTextViewText(R.id.story_item_title, story.title)
@ -117,39 +109,53 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
* @param position The position of the item within the data set whose row id we want.
* @return The id of the item at the specified position.
*/
override fun getItemId(position: Int): Long = storyItems[position].hashCode().toLong()
override fun getItemId(position: Int): Long = storiesLock.withLock {
storyItems[position].hashCode().toLong()
}
/**
* @return True if the same id always refers to the same object.
*/
override fun hasStableIds(): Boolean = true
override fun onDataSetChanged() {
Log.d(TAG, "onDataSetChanged")
/**
* Heavy lifting like downloading or creating content etc, should be deferred to onDataSetChanged()
*/
override fun onDataSetChanged() = storiesLock.withLock {
Log.d(this.javaClass.name, "onDataSetChanged")
// if user logged out don't try to update widget
if (!WidgetUtils.isLoggedIn(context)) {
Log.d(TAG, "onDataSetChanged - not logged in")
return
Log.d(this.javaClass.name, "onDataSetChanged - not logged in")
return@withLock
}
if (dataCompleted) {
// we have all the stories data, just let the widget redraw
Log.d(TAG, "onDataSetChanged - redraw widget")
dataCompleted = false
} else {
setFeedSet()
if (fs == null) {
Log.d(TAG, "onDataSetChanged - null feed set. Show empty view")
setStories(arrayOf(), HashMap(0))
return
}
Log.d(TAG, "onDataSetChanged - fetch stories")
val response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL)
if (response?.stories == null) {
Log.d(TAG, "Error fetching widget stories")
} else {
Log.d(TAG, "Fetched widget stories")
processStories(response.stories)
dbHelper.insertStories(response, true)
// get fs based on pref widget feed ids
val feedIds = PrefsUtils.getWidgetFeedIds(context)
val fs = if (feedIds == null || feedIds.isNotEmpty()) {
// null feed ids get all feeds
FeedSet.widgetFeeds(feedIds)
} else null // intentionally no feeds selected.
if (fs == null) {
Log.d(this.javaClass.name, "onDataSetChanged - null fs cleared stories")
storyItems.clear()
return@withLock
}
runBlocking {
try {
// Taking more than 20 seconds in this method will result in an ANR.
withTimeout(18000) {
Log.d(this.javaClass.name, "onDataSetChanged - get remote stories")
val response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL)
response.stories?.let {
Log.d(this.javaClass.name, "onDataSetChanged - got ${it.size} remote stories")
processStories(response.stories)
dbHelper.insertStories(response, true)
} ?: Log.d(this.javaClass.name, "onDataSetChanged - null remote stories")
}
} catch (e: TimeoutCancellationException) {
Log.d(this.javaClass.name, "onDataSetChanged - timeout")
}
}
}
@ -159,7 +165,7 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
* unbound.
*/
override fun onDestroy() {
Log.d(TAG, "onDestroy")
Log.d(this.javaClass.name, "onDestroy")
cancellationSignal.cancel()
WidgetUtils.disableWidgetUpdate(context)
PrefsUtils.removeWidgetData(context)
@ -168,81 +174,39 @@ class WidgetRemoteViewsFactory(context: Context, intent: Intent) : RemoteViewsFa
/**
* @return Count of items.
*/
override fun getCount(): Int = min(storyItems.size, WidgetUtils.STORIES_LIMIT)
private fun processStories(stories: Array<Story>) {
Log.d(TAG, "processStories")
val feedMap = HashMap<String, Feed>()
NBScope.executeAsyncTask(
doInBackground = {
dbHelper.getFeedsCursor(cancellationSignal)
},
onPostExecute = {
while (it != null && it.moveToNext()) {
val feed = Feed.fromCursor(it)
if (feed.active) {
feedMap[feed.feedId] = feed
}
}
setStories(stories, feedMap)
}
)
override fun getCount(): Int = storiesLock.withLock {
min(storyItems.size, WidgetUtils.STORIES_LIMIT)
}
private fun setStories(stories: Array<Story>, feedMap: HashMap<String, Feed>) {
Log.d(TAG, "setStories")
/**
* Widget will show tap to config view when
* empty stories and feeds maps are used
*/
private fun processStories(stories: Array<Story>) = storiesLock.withLock {
Log.d(this.javaClass.name, "processStories")
val feedMap = mutableMapOf<String, Feed>()
val cursor = dbHelper.getFeedsCursor(cancellationSignal)
while (cursor != null && cursor.moveToNext()) {
val feed = Feed.fromCursor(cursor)
if (feed.active) {
feedMap[feed.feedId] = feed
}
}
for (story in stories) {
val storyFeed = feedMap[story.feedId]
storyFeed?.let { bindStoryValues(story, it) }
}
storyItems.clear()
storyItems.addAll(mutableListOf(*stories))
// we have the data, notify data set changed
dataCompleted = true
invalidate()
storyItems.addAll(stories.toList())
}
private fun bindStoryValues(story: Story, feed: Feed) {
story.thumbnailUrl = Story.guessStoryThumbnailURL(story)
story.extern_faviconBorderColor = feed.faviconBorder
story.extern_faviconUrl = feed.faviconUrl
story.extern_feedTitle = feed.title
story.extern_feedFade = feed.faviconFade
story.extern_feedColor = feed.faviconColor
}
private fun invalidate() {
Log.d(TAG, "Invalidate app widget with id: $appWidgetId")
val appWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list)
}
private fun setFeedSet() {
val feedIds = PrefsUtils.getWidgetFeedIds(context)
fs = if (feedIds == null || feedIds.isNotEmpty()) {
FeedSet.widgetFeeds(feedIds)
} else {
// no feeds selected. Widget will show tap to config view
null
}
}
companion object {
private const val TAG = "WidgetRemoteViewsFactory"
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetRemoteViewsFactoryEntryPoint {
fun apiManager(): APIManager
fun dbHelper(): BlurDatabaseHelper
@IconLoader
fun iconLoader(): ImageLoader
@ThumbnailLoader
fun thumbnailLoader(): ImageLoader
private fun bindStoryValues(story: Story, feed: Feed) = story.apply {
thumbnailUrl = Story.guessStoryThumbnailURL(story)
extern_faviconBorderColor = feed.faviconBorder
extern_faviconUrl = feed.faviconUrl
extern_feedTitle = feed.title
extern_feedFade = feed.faviconFade
extern_feedColor = feed.faviconColor
}
}

View file

@ -0,0 +1,25 @@
package com.newsblur.widget
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.di.IconLoader
import com.newsblur.di.ThumbnailLoader
import com.newsblur.network.APIManager
import com.newsblur.util.ImageLoader
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@EntryPoint
@InstallIn(SingletonComponent::class)
interface WidgetRemoteViewsFactoryEntryPoint {
fun apiManager(): APIManager
fun dbHelper(): BlurDatabaseHelper
@IconLoader
fun iconLoader(): ImageLoader
@ThumbnailLoader
fun thumbnailLoader(): ImageLoader
}

View file

@ -1,17 +0,0 @@
package com.newsblur.widget;
import android.content.Intent;
import android.widget.RemoteViewsService;
import com.newsblur.util.Log;
public class WidgetRemoteViewsService extends RemoteViewsService {
private static String TAG = "WidgetRemoteViewsFactory";
@Override
public RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent intent) {
Log.d(TAG, "onGetViewFactory");
return new WidgetRemoteViewsFactory(this.getApplicationContext(), intent);
}
}

View file

@ -0,0 +1,13 @@
package com.newsblur.widget
import android.content.Intent
import android.widget.RemoteViewsService
import com.newsblur.util.Log
class WidgetRemoteViewsService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
WidgetRemoteViewsFactory(this.applicationContext, intent).also {
Log.d("WidgetRemoteViewsFactory", "onGetViewFactory")
}
}

View file

@ -1,25 +0,0 @@
package com.newsblur.widget;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.newsblur.R;
import com.newsblur.util.Log;
public class WidgetUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, @Nullable Intent intent) {
if (intent != null && intent.getAction() != null &&
intent.getAction().equals(WidgetUtils.ACTION_UPDATE_WIDGET)) {
Log.d(this.getClass().getName(), "Received " + WidgetUtils.ACTION_UPDATE_WIDGET);
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
}
}

View file

@ -0,0 +1,21 @@
package com.newsblur.widget
import android.appwidget.AppWidgetManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import com.newsblur.R
import com.newsblur.util.Log
class WidgetUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent != null && intent.action != null && intent.action == WidgetUtils.ACTION_UPDATE_WIDGET) {
Log.d(this.javaClass.name, "Received ${WidgetUtils.ACTION_UPDATE_WIDGET}")
val widgetManager = AppWidgetManager.getInstance(context)
val appWidgetIds = widgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list)
}
}
}

View file

@ -1,83 +0,0 @@
package com.newsblur.widget;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import com.newsblur.util.PendingIntentUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
public class WidgetUtils {
private static String TAG = "WidgetUtils";
public static String ACTION_UPDATE_WIDGET = "ACTION_UPDATE_WIDGET";
public static String ACTION_OPEN_STORY = "ACTION_OPEN_STORY";
public static String ACTION_OPEN_CONFIG = "ACTION_OPEN_CONFIG";
public static String EXTRA_ITEM_ID = "EXTRA_ITEM_ID";
public static int RC_WIDGET_UPDATE = 1;
public static int RC_WIDGET_STORY = 2;
public static int RC_WIDGET_CONFIG = 3;
public static int STORIES_LIMIT = 5;
static void enableWidgetUpdate(Context context) {
Log.d(TAG, "enableWidgetUpdate");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = getUpdateIntent(context);
PendingIntent pendingIntent = PendingIntentUtils.getImmutableBroadcast(context, RC_WIDGET_UPDATE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
int widgetUpdateInterval = 1000 * 60 * 5;
long startAlarmAt = SystemClock.currentThreadTimeMillis() + widgetUpdateInterval;
alarmManager.setInexactRepeating(AlarmManager.RTC, startAlarmAt, widgetUpdateInterval, pendingIntent);
}
public static void disableWidgetUpdate(Context context) {
Log.d(TAG, "disableWidgetUpdate");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntentUtils.getImmutableBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(pendingIntent);
}
public static void resetWidgetUpdate(Context context) {
if (hasActiveAppWidgets(context)) {
WidgetUtils.enableWidgetUpdate(context);
}
}
public static boolean hasActiveAppWidgets(Context context) {
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
return appWidgetIds.length > 0;
}
public static boolean isLoggedIn(Context context) {
return PrefsUtils.getUniqueLoginKey(context) != null;
}
public static void updateWidget(Context context) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
Intent intent = new Intent(context, WidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
context.sendBroadcast(intent);
}
public static void checkWidgetUpdateAlarm(Context context) {
boolean hasActiveUpdates = PendingIntentUtils.getImmutableBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_NO_CREATE) != null;
if (!hasActiveUpdates) {
enableWidgetUpdate(context);
}
}
private static Intent getUpdateIntent(Context context) {
Intent intent = new Intent(context, WidgetUpdateReceiver.class);
intent.setAction(ACTION_UPDATE_WIDGET);
return intent;
}
}

View file

@ -0,0 +1,82 @@
package com.newsblur.widget
import android.app.AlarmManager
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import com.newsblur.util.Log
import com.newsblur.util.PendingIntentUtils.getImmutableBroadcast
import com.newsblur.util.PrefsUtils
object WidgetUtils {
private const val RC_WIDGET_UPDATE = 1
const val ACTION_UPDATE_WIDGET = "ACTION_UPDATE_WIDGET"
const val ACTION_OPEN_STORY = "ACTION_OPEN_STORY"
const val ACTION_OPEN_CONFIG = "ACTION_OPEN_CONFIG"
const val EXTRA_ITEM_ID = "EXTRA_ITEM_ID"
const val RC_WIDGET_STORY = 2
const val RC_WIDGET_CONFIG = 3
const val STORIES_LIMIT = 5
fun enableWidgetUpdate(context: Context) {
Log.d(this.javaClass.name, "enableWidgetUpdate")
val alarmManager = context.getSystemService(AlarmManager::class.java)
val intent = getUpdateIntent(context)
val pendingIntent = getImmutableBroadcast(context, RC_WIDGET_UPDATE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val widgetUpdateInterval = 1000 * 60 * 5
val startAlarmAt = SystemClock.currentThreadTimeMillis() + widgetUpdateInterval
alarmManager.setInexactRepeating(AlarmManager.RTC, startAlarmAt, widgetUpdateInterval.toLong(), pendingIntent)
}
@JvmStatic
fun disableWidgetUpdate(context: Context) {
Log.d(this.javaClass.name, "disableWidgetUpdate")
val alarmManager = context.getSystemService(AlarmManager::class.java)
val pendingIntent = getImmutableBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_UPDATE_CURRENT)
alarmManager.cancel(pendingIntent)
}
@JvmStatic
fun resetWidgetUpdate(context: Context) {
if (hasActiveAppWidgets(context)) {
enableWidgetUpdate(context)
}
}
@JvmStatic
fun hasActiveAppWidgets(context: Context): Boolean {
val widgetManager = AppWidgetManager.getInstance(context)
val appWidgetIds = widgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
return appWidgetIds.isNotEmpty()
}
@JvmStatic
fun updateWidget(context: Context) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
val intent = Intent(context, WidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
context.sendBroadcast(intent)
}
@JvmStatic
fun checkWidgetUpdateAlarm(context: Context) {
val hasActiveUpdates = getImmutableBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_NO_CREATE) != null
if (!hasActiveUpdates) {
enableWidgetUpdate(context)
}
}
fun isLoggedIn(context: Context): Boolean = PrefsUtils.getUniqueLoginKey(context) != null
private fun getUpdateIntent(context: Context) = Intent(context, WidgetUpdateReceiver::class.java).apply {
action = ACTION_UPDATE_WIDGET
}
}

View file

@ -1,3 +1,2 @@
android.enableJetifier=true
android.useAndroidX=true
kotlin.code.style=obsolete