DbTradeAlert for Android: Schedule Quote Updates – Part 2

First post in this series: Introduction to DbTradeAlert

Previous post: DbTradeAlert for Android: Schedule Quote Updates – Part 1


1.6 Handling Marshmallow’s Doze and App Standby Modes

Now let’s have a look at the addditional requirements Android Marshmallow creates with Doze mode and App Standby mode. They both aim at saving battery power and affect all apps runnning on Marshmallow irrespective of their target SDK.

Because there is enough documentation – and speculation – on the Internet I’ll skip the theory. Fast facts:

  • Both modes defer regular AlarmManager alarms and JobScheduler jobs for inactive devices (Doze) and apps (App Standby)
  • Alarms created by setAndAllowWhileIdle() or setExactAndAllowWhileIdle() – methods added with Marshmallow – continue to go off
  • Both modes take away network access and wake locks from affected apps
  • Users can exempt an app from most of both modes’ restrictions in Settings | Battery | Overflow | Battery optimization
  • Users can manually toggle an app between active and standby in Settings | Developer Options | Inactive Apps

Let’s see how those battery optimizations affect DbTradeAlert. First change the schedue to 1 minute:

alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 1 * 60 * 1000, 1 * 60 * 1000, pendingIntent);

Then create the new schedule by deleting the app from the device before running it from Android Studio. Its logcat window should then show a message for the schedule creation followed by:

... D/QuoteRefreshAlarmRec.: onReceive(): quote refresh alarm received; starting QuoteRefresherService
... ...
... D/QuoteRefresherService: downloadQuotes(): got 485 characters
... ...
... D/DbHelper: updateOrCreateQuotes(): success!
... D/QuoteRefresherService: onHandleIntent(): quotes updated - initiating screen refresh
... D/BroadcastReceiver: quotesRefreshedBroadcastReceiver triggered UI update

Those entries repeat about every minute. After a while the screen shuts off resulting in:
… D/WatchlistListActivity: onPause(): quoteRefresherMessageReceiver unregistered

From then on quotesRefreshedBroadcastReceiver doesn’t run anymore but the app still receives quote refresh alarms about once a minute, gets data from the internet, and writes to and reads from the database.

Now let’s see how the app behaves in Doze mode. To force the device into Doze mode execute this line in Android Studio’s Terminal window:

C:\Users\<AccountName>\AppData\Local\Android\sdk\platform-tools\adb shell dumpsys deviceidle force-idle

The answer should be “Now forced in to idle mode”. If the answer is “Unable to go idle; not enabled” execute this line:

C:\Users\<AccountName>\AppData\Local\Android\sdk\platform-tools\adb shell dumpsys deviceidle enable

The answer should be “Idle mode enabled”. Now you can force the device into Doze mode.

After that switch to the logcat window. On my device the alarms still reach the app every minute (they shouldn’t, more on this later) but QuoteRefresherService reports an exception like this:

java.net.SocketTimeoutException: failed to connect to download.finance.yahoo.com/66.196.66.213 (port 80) after 15000ms

That means the app has no network access when the device is in Doze mode.

To fix that exempt the app from battery optimization:

  1. On the device go to Settings | Battery | Overflow | Battery optimization
  2. Select All Apps
  3. Find and tap DbTradeAlert
  4. In the dialog select “Don’t optimize” and tap “Finish”

After that go back to the logcat window. The app can load quotes from the internet again.

You can check the device’s state via this command:

C:\Users\<AccountName>\AppData\Local\Android\sdk\platform-tools\adb shell dumpsys deviceidle > %TEMP%\dumpsys_deviceidle1.txt

Open “dumpsys_deviceidle1.txt” (path on Windows 8.1 is “C:\Users\<AccountName>\AppData\Local\Temp\”). Near the bottom will be a line like this: “mState=IDLE”

To get the device back to normal just turn the screen on. If you then create the file again like above the line will read “mState=ACTIVE”.

Of course your users won’t know that they need to exempt DbTradeAlert from battery optimization. Here is how to ask them for it:

public class WatchlistListActivity extends AppCompatActivity
        implements WatchlistFragment.OnListFragmentInteractionListener {
    // ...

    @SuppressWarnings("NewApi")
    private void ensureExemptionFromBatteryOptimizations() {
        if (Utils.isAndroidBeforeMarshmallow() == false) {
            String packageName = getPackageName();
            PowerManager powerManager = getSystemService(PowerManager.class);
            if (powerManager.isIgnoringBatteryOptimizations(packageName) == false) {
                String explanation = "DbTradeAlert needs to download quotes even when in background!";
                Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
                Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
                        .setData(Uri.parse("package:" + packageName));
                startActivity(intent);
            }
        }
    } // ensureExemptionFromBatteryOptimizations()

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_watchlist_list);

        this.dbHelper = new DbHelper(this);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        // Set up the ViewPager with the watchlist adapter.
        WatchlistListPagerAdapter watchlistListPagerAdapter
                = new WatchlistListPagerAdapter(getSupportFragmentManager(), dbHelper);
        mViewPager = (ViewPager) findViewById(R.id.container);
        mViewPager.setAdapter(watchlistListPagerAdapter);
        // Request user to whitelist app from Doze and App Standby
        ensureExemptionFromBatteryOptimizations();
        // Create initial quote refresh schedule (just overwrite existing ones)
        Log.d(CLASS_NAME, "onCreate(): creating quote refresh schedule");
        createQuoteRefreshSchedule();
    } // onCreate()

    // ...
}
package de.dbremes.dbtradealert;

import android.database.Cursor;
import android.os.Build;

public class Utils {
    public static  boolean isAndroidBeforeMarshmallow() {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.M;
    } // isAndroidBeforeMarshmallow()

    // ...
} // class Utils
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.dbremes.dbtradealert">

    <!-- other uses-permission elements -->
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

    <!-- application element -->

</manifest>

WatchlistListActivity.ensureExemptionFromBatteryOptimizations() shows a Toast explaining the app’s needs while asking for exemption from battery optimization. Asking for exemption from battery optimization requires android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS but the Toast is optional.

Without the “@SuppressWarnings(“NewApi”)” annotation Android Studio would complain that the overload of Context.getSystemService() taking a class and PowerManager.isIgnoringBatteryOptimizations() require API level 23.

Be aware that quite a few apps got banned from the Play Store for asking users to exempt them from battery optimization – or maybe even for just asking for android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS in the manifest. Be sure to check Google’s “Acceptable Use Cases for Whitelisting” before you implement this.

Asking for exemption

Asking for exemption

That said first delete the app from the device. Only then run it from Android Studio so onCreate() gets trigggered. When the app starts you’ll see a dialog and an explaining Toast. Tap “Yes” to give DbTradeAlert network access even when the device is in Doze mode or the app is in App Standby mode.

Optionally check if the app is now exempt from battery optimization:
C:\Users\<AccountName>\AppData\Local\Android\sdk\platform-tools\adb shell dumpsys deviceidle > %TEMP%\dumpsys_deviceidle2.txt

Under “Whitelist user apps:” you should see “de.dbremes.dbtradealert”.

Surprisingly the alarms kept going off every minute even when my device was in Doze mode. It turns out that my Moto G 2nd Gen. doesn’t support the new battery optimization even though it runs Marshmallow and the guys from Cyanogen Mod got it working for that device. My experience with emulators – both Google’s and Visual Studio’s – was even worse. So here is what should work even on a device that properly supports Doze and App Standby:

package de.dbremes.dbtradealert;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.util.Log;

public class QuoteRefreshScheduler extends BroadcastReceiver {
    final static String CLASS_NAME = "QuoteRefreshScheduler";

    @Override
    @SuppressWarnings("NewApi")
    public void onReceive(Context context, Intent intent) {
        // Create schedule for quote refresh
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        int requestCode = 0;
        Intent newIntent = new Intent(context, QuoteRefreshAlarmReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode,
                newIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // Create schedule for quote refresh (every hour, starting 1 hour from now)
        if (Utils.isAndroidBeforeMarshmallow()) {
                alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                        SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR,
                        AlarmManager.INTERVAL_HOUR, pendingIntent);
            } else {
                alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                        SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR, pendingIntent);
        }
        // Use only for testing Doze and App Standby modes!
//        alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
//                1 * 60 * 1000, 1 * 60 * 1000, pendingIntent);
        // Log what was done
        String scheduleCreationType = "";
        if (intent.getAction() != null) {
            if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
                scheduleCreationType = "after reboot";
            } else {
                scheduleCreationType = "by " + intent.getAction();
            }
        }
        Log.d(CLASS_NAME,
                "onReceive(): quote refresh schedule created " + scheduleCreationType);
    } // onReceive()
} // class QuoteRefreshScheduler

AlarmManager.setAndAllowWhileIdle() cannot create repeating alarms. If you want to see how the result of “adb shell dumpsys alarm” reflects this:

  1. Reinstall the app now in Android Marshmallow or a later version and take a quick snapshot of its alarms – repeatInterval will be 0
  2. Wait for the alarm to go off and take another snapshot – the alarm definition is gone and only the statistics part is left
package de.dbremes.dbtradealert;

import android.content.Context;
import android.content.Intent;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;

public class QuoteRefreshAlarmReceiver extends WakefulBroadcastReceiver {
    // Logging tag can have at most 23 characters
    final static String CLASS_NAME = "QuoteRefreshAlarmRec.";

    @Override
    public void onReceive(Context context, Intent intent) {
        scheduleNextQuoteRefresh(context);
        Log.d(CLASS_NAME,
                "onReceive(): quote refresh alarm received; starting QuoteRefresherService");
        Intent service = new Intent(context, QuoteRefresherService.class);
        startWakefulService(context, service);
    } // onReceive()

    private void scheduleNextQuoteRefresh(Context context) {
        // Starting with Android Marshmallow only
        // AlarmManager.setAndAllowWhileIdle() works in Doze / App Standby mode
        // and unlike setInexactRepeating() cannot set repeating alarms. So apps
        // need to set the next alarm themselves each time an alarm goes off.
        if (Utils.isAndroidBeforeMarshmallow() == false) {
            Log.d(CLASS_NAME,
                    "scheduleNextQuoteRefresh(): ");
            Intent intent = new Intent(context, QuoteRefreshScheduler.class);
            intent.setAction("QuoteRefreshAlarmReceiver.scheduleNextQuoteRefresh()");
            context.sendBroadcast(intent);
        }
    } // scheduleNextQuoteRefresh()

} // class QuoteRefreshAlarmReceiver

QuoteRefreshAlarmReceiver simply tells QuoteRefreshScheduler to create another alarm. For logging purposes it also sets an action and WatchlistListActivity.createQuoteRefreshSchedule() will do this too.

As I have no device supporting battery optimization properly I didn’t bother to test DbTradeAlert with App Standby. And hey: It works on my machine! 🙂

Next post: DbTradeAlert for Android: Schedule Quote Updates – Part 3

Additional Resources

  • Why No Doze for Moto G 2014: http://forum.xda-developers.com/moto-g-2014/help/insomnia-doze-t3262507
  • Optimizing for Doze and App Standby (contains Google’s “Acceptable Use Cases for Whitelisting”): https://developer.android.com/training/monitoring-device-state/doze-standby.html
  • Behavior Changes with Android N: https://developer.android.com/preview/behavior-changes.html
  • Diving Into Android ‘M’ Doze: https://newcircle.com/s/post/1739/2015/06/12/diving-into-android-m-doze
  • When do you absolutely need WakefulBroadcastReceiver: http://porcupineprogrammer.blogspot.de/2014/02/when-do-you-absolutely-need.html
  • Advertisements
    This entry was posted in Uncategorized and tagged , . Bookmark the permalink.

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s