How to call ViewModel functions from AppWidgetProvider? - android

I have an Retrofit2 API:
interface Api {
#POST("/my/url")
suspend fun function()
}
My ViewModel can call this HTTP function:
class MainViewModel : ViewModel() {
private val retrofiClient = APIClient.client!!.create(Api::class.java)
fun test() {
viewModelScope.launch {
retrofitClient.function()
}
}
}
No problem when i call this from my activity.
But my goal is to call this test() function from my app's widget.
My basic AppWidgetProvider looks like this:
class AppWidgetProvider : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// Perform this loop procedure for each App Widget that belongs to this provider
appWidgetIds.forEach { appWidgetId ->
val intent = Intent()
intent.action = "custom-event-name"
intent.setClassName(
MainActivity::class.java.getPackage().name,
MainActivity::class.java.name
)
val pendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
val views: RemoteViews = RemoteViews(
context.packageName,
R.layout.appwidget
).apply {
setOnClickPendingIntent(R.id.test, pendingIntent)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
MainActivity:
class MainActivity : AppCompatActivity() {
private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
println("INTENT RECEIVED")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
...
registerReceiver(receiver, IntentFilter("custom-event-name"))
...
}
The BroadcastReceiver does not receive the intent. Even if it does, can i call my app functions there?
How can i setup my app and/or widget, to call this viewModel.test() method when i click on a button in the widget?

You can use Channel(https://kotlinlang.org/docs/channels.html, for coroutines) or RxRelay (https://github.com/JakeWharton/RxRelay, for rxjava) instead of using broadcast receiver. Just put your Channel or Relay instance on your Application class so you can access it across your activities, fragments, etc.

Related

Android widget OnReceive() being called multiple times when running WorkManager request

I have an app which has a home screen widget via GlanceAppWidget().
I would like to run a worker inside the Content() function of GlanceAppWidget(). I have used enqueue(work) from WorkManager api to successfully execute my worker.
The problem is that onReceive is getting called multiple (infinite) number of times. How can I run the worker once without having onReceive called multiple times?
class MyWidget : GlanceAppWidget() {
#Composable
override fun Content() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance().enqueue(work)
//some composable/ui code that consumes worker output
}
}
class MyWorker(
private val context: Context,
private val workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
override suspend fun doWork(): Result {
return try {
startForegroundService()
//some task here
Result.Success.success(
workDataOf(
"MyKey" to "worker completed successfully"
)
)
} catch (throwable: Throwable) {
Result.failure()
}
}
private suspend fun startForegroundService() {
setForeground(
ForegroundInfo(
Random.nextInt(),
NotificationCompat.Builder(context, "download_channel")
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentText("Downloading...")
.setContentTitle("Download in progress")
.build()
)
)
}
}
class GlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = MyWidget()
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
}
}

How to test the reception of Broadcast Receivers?

I'm writing tests to verify the reception of the transmitting receivers but for some reason, the receiver is never registered or the intent is never sent.
I guess there should be a problem with the Context but, no luck yet finding it.
This is the BroadcastFactory.kt:
object BroadcastFactory {
private lateinit var intent: Intent
fun build(
action: String,
flag: Int? = null,
): BroadcastFactory {
intent = Intent().apply {
this.action = action
this.flags = flag ?: 0
}
return this
}
fun send(
context: Context
): Intent {
context.sendBroadcast(intent)
return intent
}
}
And this is the test file BroadcastTest.kt:
#RunWith(AndroidJUnit4::class)
#SmallTest
class BroadcastTest {
lateinit var intents: MutableList<Intent>
lateinit var latch: CountDownLatch
private lateinit var receiver: BroadcastReceiverTester
inner class BroadcastReceiverTester : BroadcastReceiver() {
override fun onReceive(p0: Context?, intent: Intent?) {
intent?.let {
intents.add(it)
latch.countDown()
}
}
}
private val context: Context = getInstrumentation().targetContext
#Before
fun setUp() {
intents = mutableListOf()
latch = CountDownLatch(1)
receiver = BroadcastReceiverTester()
LocalBroadcastManager.getInstance(context).registerReceiver(
receiver,
IntentFilter.create(
Constants.ACTION, "text/plain"
)
)
}
#Test
fun testBroadcastReception() {
BroadcastFactory
.build(Constants.ACTION, Constants.FLAG)
.send(context)
// assert broadcast reception (NOT WORKING)
latch.await(10, TimeUnit.SECONDS)
assertThat(intents.size).isEqualTo(1)
}
#After
fun tearDown() {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver)
}
}
I'm using a CountDownLatch to wait 10 seconds for the receiver, plus, its value can be asserted. Besides, I set a list of Intents to check the number of registrations/receptions.
There is something I'm missing here? Different context provider? Robolectric runner?
Thanks
Is solved it by changing the receiver with this:
context.registerReceiver(
receiver,
IntentFilter(
Constants.ACTION
)
)
Thanks to #selvin and #mike-m for the help!

How to listen BroadcastReceiver in main activity?

I have broadcast in separated class:
class Receiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {}
}
How to listen this broadcasrt in main activity?
I have tried:
class MainActivity : AppCompatActivity() {
private val receiver = Receiver()
override fun onCreate(savedInstanceState: Bundle?) {
receiver.onReceive(context: Context, intent: Intent) {
//
}
}
}
Now I register events like this:
override fun onStart() {
super.onStart()
registerReceiver(receiver, IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED))
}
I tried this in activity:
private val broadcastReceiver:BroadcastReceiver = (object :BroadcastReceiver(){
override fun onReceive(context: Context, intent: Intent) {
val ltInflater = layoutInflater
val layout: View = ltInflater.inflate(R.layout.custom_toast, findViewById(R.id.toast_layout))
val image = layout.findViewById<ImageView>(R.id.imageView)
}
So, I get error because I can not get access to layout in this step
You need to create a broadCast receiver in the main activity like this
val broadcastReceiver:BroadcastReceiver = (object :BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val layout: View = ltInflater.inflate(R.layout.custom_toast, findViewById(R.id.toast_layout))
val image = layout.findViewById<ImageView>(R.id.imageView)
image.setImageResource(R.drawable.ic_launcher_background)
}
})
and you need to have string that's common between both the class that's going to broadcast the intent and the broadcast receiver let's call it key
val key = keyTelephonyManager.ACTION_PHONE_STATE_CHANGED
you have to register the receiver to start receiving intents like this (do this in onCreate())
var intentFilter = IntentFilter(MainActivity.key);
registerReceiver(broadcastReceiver, intentFilter);

Registering a ContentObserver in AppWidgetProvider to update AppWidget is not working

I have a ContentProvider from a main app. The content will be shared with a consumer app. This consumer app has an app widget. I have tested the ContentProvider and ContentObserver to this consumer app in its Activity and all is well (meaning the RecyclerView of the Activity is updated whenever an update from the main app triggers changes to the database). However, registering the ContentObserver inside my AppWidgetProvider does not work as expected.
My AppWidgetProvider has the following code.
class StackWidgetProvider : AppWidgetProvider() {
override fun onEnabled(context: Context) {
Timber.i("Enabled")
if (favoriteUserProviderObserver == null) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, StackWidgetProvider::class.java)
favoriteUserProviderObserver = FavoriteUserProviderObserver(appWidgetManager, componentName).let {
context.contentResolver.registerContentObserver(CONTENT_URI, true, it)
it
}
}
}
override fun onDisabled(context: Context) {
Timber.i("Disabled")
favoriteUserProviderObserver?.let {
context.contentResolver.unregisterContentObserver(it)
}
favoriteUserProviderObserver = null
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
....
companion object {
private var favoriteUserProviderObserver: FavoriteUserProviderObserver? = null
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
val intent = Intent(context, StackWidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = toUri(Intent.URI_INTENT_SCHEME).toUri()
}
val views = RemoteViews(context.packageName, R.layout.widget_favorite_user_stack).apply {
setRemoteAdapter(R.id.widget_favorite_user_stack_view, intent)
setEmptyView(R.id.widget_favorite_user_stack_view, R.id.widget_favorite_user_empty)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
}
I created a simple custom ContentObserver class like below.
class FavoriteUserProviderObserver(
private val appWidgetManager: AppWidgetManager,
private val componentName: ComponentName
) : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
Timber.i("Provider observer triggered")
appWidgetManager.notifyAppWidgetViewDataChanged(
appWidgetManager.getAppWidgetIds(componentName), R.id.widget_favorite_user_stack_view
)
}
}
The above observer class is never triggered (even when I change the data in my main app). For further clarity, here's the code for my RemoteViewsService and its factory.
class StackWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
StackRemoteViewsFactory(this.applicationContext)
}
class StackRemoteViewsFactory(private val context: Context) :
RemoteViewsService.RemoteViewsFactory {
private var widgetItems = listOf<UserProfileSummary>()
private lateinit var repository: FavoriteUserRepository
override fun onCreate() {
repository = FavoriteUserRepository(
FavoriteUserDataSource(context.contentResolver),
Dispatchers.IO
)
}
override fun onDataSetChanged() {
GlobalScope.launch {
widgetItems = repository.favoriteUsers().toList() // Tested; working on the Activity scope of the consumer app
Timber.i(widgetItems.toString())
}
}
override fun getViewAt(position: Int): RemoteViews =
RemoteViews(context.packageName, R.layout.widget_favorite_user_item).apply {
setTextViewText(R.id.widget_favorite_user_item_text, widgetItems[position].username)
}
override fun getLoadingView(): RemoteViews? = null
override fun getItemId(position: Int): Long = 0
override fun hasStableIds(): Boolean = false
override fun getCount(): Int {
return widgetItems.size
}
override fun getViewTypeCount(): Int = 1
override fun onDestroy() {}
}
So the logic is to ask the ContentObserver to observe changes in the ContentProvider. The observer is registered on the onEnabled and onDisabled part of the AppWidgetProvider. Once the observer notices a change in ContentProvider, it will ask the AppWidgetProvider to update itself, thus calling onDataSetChanged and fetching a new list of data.
However, the observer is never called. What could be the reason it's not working as expected here? (It can't be because of a lack of permission, because the Activity part of the consumer app is able to fetch the data just fine.)
What could be the reason it's not working as expected here?
An AppWidgetProvider is a subclass of BroadcastReceiver. Your instance of AppWidgetProvider will live for (hopefully) a very short time, best measured in milliseconds. Basically, you get one onUpdate() call (or other callback), and that instance is thrown away. The next callback gets a new instance.
As a result, doing anything in an AppWidgetProvider that requires it to be around for a period of time is doomed.
The most likely solution, taking modern versions of Android into account, is to adopt more of a push solution. Bear in mind that any of your code can update the RemoteViews for an app widget, simply by working with AppWidgetManager. So, some code that is already running and knows about the data updates needs to push a new RemoteViews, rather than expecting your AppWidgetProvider to be able to react to changes.

How to send context to BroadcastReceiver?

I want to use my BroadcastReceiver as sender of data into my activity. For this reason I'm using LocalBroadcastManager. This manager is used to register and unregister my receiver. Problem is that Context in onReceive method is different than Context in onStart and onStop method.
I need to pass activity context into my BroadcastReceiver or instance of LocalBroadcastManager initialized inside Activity. Because my receiver is not receiving any data.
Maybe it is not fault of this manager context but I don't know why it doesnt work since I implemented this manager.
class GPSReceiver: BroadcastReceiver(){
companion object{
const val GPS_PAYLOAD = "gps_payload"
}
override fun onReceive(context: Context, intent: Intent) {
try {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val int = Intent(GPS_PAYLOAD)
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
int.putExtra(GPS_PAYLOAD, true)
} else {
int.putExtra(GPS_PAYLOAD, false)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(int)
} catch (ex: Exception) {
}
}
}
Registering receiver inside Activity:
private val gpsStatusReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
App.log("isGpsEnabled: onReceive")
val gpsStatus = intent?.extras?.getBoolean(GPS_PAYLOAD)
if (gpsStatus != null) {
if (gpsStatus){
App.log("isGpsEnabled: true")
hideGpsSnackbar()
} else {
App.log("isGpsEnabled: false")
showGpsSnackbar()
}
} else {
App.log("isGpsEnabled: null")
}
}
}
override fun onStart() {
super.onStart()
LocalBroadcastManager.getInstance(this).apply {
val filter = IntentFilter()
filter.apply {
addAction("android.location.PROVIDERS_CHANGED")
addAction(GPS_PAYLOAD)
}
registerReceiver(gpsStatusReceiver, filter)
}
}
I have seen your code. So there is not issue with context, but in the approach.
Your are registering your reciever with the same strings in which you are getting you data inside the Reciever.
So Send Your broadcast from Fragment/Activity
Send BroadCast Like
private fun sendSuccessfulCheckoutEvent() {
val intent = Intent("successful_checkout_event")
intent.putExtra("cartID", cartId)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
And Listen it in Activity/Fragment like this
1) Create broadcast Reciever
private val checkoutDoneReciever : BroadcastReceiver = object : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val cartNumbers = intent.getIntExtra("cartID", 0)
Log.d("receiver", "Got message: $cartNumbers.toString()")
}
}
2) Register it in onCreate()/onStart()
LocalBroadcastManager.getInstance(this).registerReceiver(cartUpdatedReceiver,IntentFilter("successful_checkout_event"))
3) Unregister it in onDestroy()
LocalBroadcastManager.getInstance(this).unregisterReceiver(cartUpdatedReceiver)

Categories

Resources