Search code examples
javaandroidalarmmanager

Periodically Updating my app's UI


I want my Android app to periodically update its UI based on the response from a REST service. I can't do this on the main thread because it's not permitted / bad practice to access the network on the main thread. The general wisdom on SO and the internet is to use a combination a BroadcastReceiver and AlarmManager. For example this is the advice here. I've tried two designs, neither of which I can make to work:

  1. Define a class extending BroadcastReceiver as an inner class of my MainActivity.
  2. Define the same class as an outer class.

With (1) I get this runtime error:

java.lang.RuntimeException: Unable to instantiate receiver com.dbs.alarm.MainActivity$AlarmReceiver: java.lang.InstantiationException: java.lang.Class<com.dbs.alarm.MainActivity$AlarmReceiver> has no zero argument constructor

With (2) the problem is I can't figure out how to access the view I want to modify in MainActivity.

Here is an example implementation of (1):

package com.dbs.alarm;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    // I tried making this its own class, but then findViewById isn't accessible.
    public class AlarmReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // I tried wrapping this in runOnUiThread() but it made no difference.
            TextView myTextView = findViewById(R.id.my_text);
            CharSequence myCharSequence = "Set from UpdateReceiver.onReceive()";
            myTextView.setText(myCharSequence);
        }
    }

    private void setRecurringAlarm(Context context) {
        Intent intent = new Intent(context, AlarmReceiver.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        PendingIntent pendingIntent = PendingIntent.getBroadcast(
                context, 0, intent,
                PendingIntent.FLAG_CANCEL_CURRENT);
        AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

        alarmManager.setInexactRepeating(
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                SystemClock.elapsedRealtime() + 1000,
                1000, // Set so short for demo purposes only.
                pendingIntent
        );
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setRecurringAlarm(this);
    }
}

I also added this to my AndroidManifest.xml, and considering that I get an exception it seems to be registered successfully:

<receiver android:name="com.dbs.alarm.MainActivity$AlarmReceiver">
</receiver>

Solution

  • Since you need direct access to your text view, choosing an inner class for your receiver was the right thing to do. However, BroadcastReceivers that are declared as inner classes must be static to be declared in the manifest, which defeats the purpose of making it an inner class in the first place (in your scenario, at least). Because of this, I suggest registering/unregistering your BroadcastReceiver dynamically in the onStart() and onStop() lifecycle methods:

    public class MainActivity extends AppCompatActivity {
    
        private BroadcastReceiver alarmReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // the "MainActivity.this" tells it to use the method from the parent class
                MainActivity.this.updateMyTextView();
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            setRecurringAlarm(this);
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            final IntentFilter filter = new IntentFilter();
            filter.addAction("YOUR_ACTION_NAME"); 
            registerReceiver(alarmReceiver, filter);
        }
    
        @Override
        protected void onStop() {
            unregisterReceiver(alarmReceiver);
            super.onStop();
        }
    
        private void updateMyTextView(){
            final TextView myTextView = findViewById(R.id.my_text);
            if (myTextView != null){
                CharSequence myCharSequence = "Set from UpdateReceiver.onReceive()";
                myTextView.post(()-> myTextView.setText(myCharSequence));
            }
        }
    
        private void setRecurringAlarm(Context context) {
            Intent intent = new Intent("YOUR_ACTION_NAME");
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    
            PendingIntent pendingIntent = PendingIntent.getBroadcast(
                    context, 0, intent,
                    PendingIntent.FLAG_CANCEL_CURRENT);
            AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    
            alarmManager.setInexactRepeating(
                    AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    SystemClock.elapsedRealtime() + 1000,
                    1000, // Set so short for demo purposes only.
                    pendingIntent
            );
        }
    }
    

    You'll also notice that rather than pass in the receiver class when creating the intent for the alarm, I changed it to use a string ("YOUR_ACTION_NAME") that you can use to define the intent filter your BroadcastReceiver will use to listen for broadcasts.

    As for the issue of running the updates on the UI thread, you can always call post() from a view to run something on the UI thread, or use an activity's runOnUiThread like you attempted to do within the BroadcastReceiver. I made the "update" method belong to the activity rather than the broadcast receiver, since it seemed to make more sense that way in my head.


    EDIT: When writing this answer, I was more focused on solving the issues you were encountering while implementing your solution rather than actually trying to help solve the larger problem of performing periodic UI updates. @Ashikee AbHi's suggestion of using a Handler for this rather than alarm is definitely something you should consider. An alarm/broadcast receiver is great when you have to notify something in a different process, but if everything is contained in a single activity, it would be much cleaner to use Handler.postDelayed.