Sticky Intents & acquiring System Service status

As mentioned in the Broadcast Receiver post, updates will only be applied when the app is visible (between onResume and onPause). Therefore system changes which occur outside this window could leave the app with outdated information on resume. Therefore we should also check at onResume what the current system status is.

Depending on the version of Android the device is running you may be able to acquire a status directly from e.g. BatteryManager:

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // If using a modern enough version of Android...
            BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE); // ...ask the appropriate service for a status check
            doSomething(batteryManager.isCharging()); // Returns a boolean you can feed back into your own methods
        } else // Otherwise use sticky intents (see below)

Certain system broadcast intents do not disappear the moment they have fired and are processed. Instead they remain available to the system until superceded by a more up-to-date intent. These are known as sticky intents, and are the way of determining system status in older Android versions:

   {
            IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); // Create a new IntentFilter for the appropriate sticky Intent
            Intent chargingStatus = registerReceiver(null, intentFilter); // The same code you would use to register a Broadcast Receiver, but leave the first parameter null
            int batteryChargingStatus = chargingStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); // Get the info you want from the sticky Intent, with a default value
            boolean isCharging = batteryChargingStatus == BatteryManager.BATTERY_STATUS_CHARGING || batteryChargingStatus == BatteryManager.BATTERY_STATUS_FULL; // This will return a boolean of true if the phone is charging or fully charged
            doSomething(isCharging); // Feed the boolean into your own methods
   }

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.06

Broadcast Receiver

Listens out for designated System Broadcast Intents via an IntentFilter (declared in the AndroidManifest.xml) so that your app can respond to changes in the system. It can be triggered even when the app is not running. There are two types:

  • Static (Manifest-declared) – triggered whenever the broadcast intent occurs, even if the app is offline
  • Dynamic (Context-registered) – tied to the app’s lifecycle

It is preferred that dynamic broadcast receivers or job scheduling is used over static broadcast receivers, as abuse of statics could result in multiple apps responding to a system event. For this reason, some broadcast intents will not let you create a corresponding static receiver. These intents have FLAG_RECEIVER_REGISTERED_ONLY flag set.

For a static broadcast receiver you will register the receiver in the AndroidManifest.xml:

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>

Then implement the receiver like so:

public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        @Override
        public void onReceive(Context context, Intent intent) {
            // Do something
        }
    }

For a dynamic receiver you register and un-register the receiver in the onResume() and onPause method overrides respectively:

public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        BroadcastReceiver br = new MyBroadcastReceiver();
        @Override
        public void onReceive(Context context, Intent intent) {
            //Do something
        }
    
    @Override
    protected void onResume() {
    super.onResume();
    IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
    filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    this.registerReceiver(br, filter);
    @Override
    protected void onPause() {
    super.onPause();
    this.unRegisterReceiver(br);
    }
}

The potential issue with the above example of a Dynamic receiver is that it will only check for changes once onResume has initiated, and so if the system status changes while the app is not visible, it may hold the incorrect status when the app starts. Adding the code to the onCreate and onDestroy methods would result in unnecessary actions occurring in the background when the app is not visible. To overcome this, check for the current system status on onResume in addition to registering the broadcast receiver. See an example here.

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.05

System Broadcast Intent

A special Intent sent by the system when events occur on the phone. Examples include:

  • android.intent.action.SCREEN_ON
  • android.intent.action.MEDIA_MOUNTED
  • android.intent.action.HEADSET_PLUG
  • android.intent.action.BATTERY_LOW
  • android.intent.action.DOWNLOAD_COMPLETE
  • android.media.AUDIO_BECOMING_NOISY

In order for your app to listen out for any of these System Broadcast Intents you will needs to set up a Broadcast Receiver.

PendingIntent

This configures the permissions for services, private activities and broadcast-protected Intents to be started via Intent from e.g. a notification or another app, even if your app is not currently running. Hence the PendingIntent static methods: getService(), getActivity() or getActivities() and getBroadcast(). Each method requires 4 parameters e.g. for getActivity():

  • context: The Context in which this PendingIntent should start the activity
  • requestCode: Private request code for the sender, unique to this Intent
  • intent: Intent of the activity to be launched
  • flags: May be FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT, or any of the flags as supported by Intent.fillIn() to control which unspecified parts of the intent that can be supplied when the actual send happens

A PendingIntent can be st up as follows:

    private static final int PENDING_INTENT_ID = 58; // Provide unique ID no. for the PendingIntent
    private static PendingIntent contentIntent (Context context) { // In this case we've set up a helper class
        Intent intent = new Intent(context, MainActivity.class); // Create the Intent - this opens the MainActivity
        return PendingIntent.getActivity(context, PENDING_INTENT_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT); // Creates the PendingIntent
    }

Intent Service

A Service which runs off a completely separate thread to the main. All IntentService requests are handled on a single background thread and are issued in order. Therefore IntentServices are good for tasks that need to happen in order.

Services must be registered in the AndroidManifest.xml:

        <service
            android:name=".sync.myIntentService"
            android:exported="false"
            ></service>

An Intent Service can be started in a very similar way to an Activity:

Intent myIntent = new Intent(this, myIntentService.class);
startService(myIntent);

Extra data can be attached to the Intent when starting the Service, as with Activities:

Intent myIntent = new Intent(this, myIntentService.class);
myIntent.setAction("Some specific action");
startService(myIntent);

To create the Service, extend IntentService. Override the onHandle Intent method to tell it what to do in the background:

public class MyIntentService extends IntentService {

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getAction(); //Add this line if extra data attached
        //Do background work here
    }
}

The IntentService will then stop itself when it is finished.

ud851-Exercises-student\Lesson10-Hydration-Reminder\T10.01

Explicit Intent with URL

Open a third party app to view a webpage using implicit Intent.

    private void openWebPage(String url) {
        /*
         * We wanted to demonstrate the Uri.parse method because its usage occurs frequently. You
         * could have just as easily passed in a Uri as the parameter of this method.
         */
        Uri webpage = Uri.parse(url);

        /*
         * Here, we create the Intent with the action of ACTION_VIEW. This action allows the user
         * to view particular content. In this case, our webpage URL.
         */
        Intent intent = new Intent(Intent.ACTION_VIEW, webpage);

        /*
         * This is a check we perform with every implicit Intent that we launch. In some cases,
         * the device where this code is running might not have an Activity to perform the action
         * with the data we've specified. Without this check, in those cases your app would crash.
         */
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivity(intent);
        }
    }

Adding An Activity, Up Navigation, Accessing With Explicit Intents, Passing Data

How to add an Activity to your app and navigate to it using an explicit Intent.

Create an empty Activity through the IDE. IDE will automatically add the essential code to the AndroidManifests.xml file. Make any desirable additions.

        <activity android:name="com.example.android.explicitintent.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.example.android.explicitintent.ChildActivity"
            android:label="@string/action_settings" // Name at top of screen for Activity
            android:parentActivityName=".MainActivity"> // Make sure the back arrow functions properly by declaring parent activity
            <meta-data // To support Android 4.0 and lower
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".MainActivity" />
        </activity>

In new Activity, enable Up navigation.

getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Or getActionBar() on older versions before support library

In Parent Activity, set up an explicit Intent.

        private Button doSomethingCoolButton = (Button) findViewById(R.id.b_do_something_cool);
        private String text = "Send me!"; // Text to be passed to new Activity
        mDoSomethingCoolButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Context context = MainActivity.this;

                /* This is the class that we want to start (and open) when the button is clicked. */
                Class destinationActivity = ChildActivity.class;

                /*
                 * Here, we create the Intent that will start the Activity we specified above in
                 * the destinationActivity variable. The constructor for an Intent also requires a
                 * context, which we stored in the variable named "context".
                 */
                Intent startChildActivityIntent = new Intent(context, destinationActivity);

                startChildActivityIntent.putExtra(Intent.EXTRA_TEXT, text); // Add text as extra data

                /*
                 * Once the Intent has been created, we can use Activity's method, "startActivity"
                 * to start the ChildActivity.
                 */
                startActivity(startChildActivityIntent);
            }
        });

In new Activity, access and use the extra data.

        mDisplayText = (TextView) findViewById(R.id.tv_display); // Find a TextView

        // Use the getIntent method to store the Intent that started this Activity in a variable
        Intent intent = getIntent();

        // Check if this Intent has the extra text we passed from MainActivity
        if (intent.hasExtra(Intent.EXTRA_TEXT)) {
            String enteredText = intent.getStringExtra(Intent.EXTRA_TEXT);
            mDisplayText.setText(enteredText); // Assign text to TextView
        }