読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

ぴよぴよエンジニアの日記です 技術系のことや日常のことをつぶやきます

API Level 19以降でAlarmManagerが辛かった話

Android

Androidの話です.

API Level 19以降でAlamManager(AlarmManager | Android Developers)が辛かった話になります.先に自分なりの結論を示しておきます.

  • set, setReapitingはAPI Level 19以降では不正確
  • setExactも秒単位の処理ではかなり微妙
  • java.util.Timerで秒単位の処理を一応実現できた


開発環境


作りたかったもの

f:id:Santea:20151223201731p:plain

バスの発着時間をカウントダウンするウィジェットを作りました.やりたいことは、

  1. 先発のバスの発着時間をカウントダウンすること
  2. 次発、次次発の時刻を表示すること
  3. バッテリー消費を考えて、カウントダウンはボタンのタップで開始すること

以上の3点が主な内容になります.

ウィジェットを作るのは初めてだったのですが、ざっと調べたところAppWidgetProvider(AppWidgetProvider | Android Developers)を使えば結構簡単に作れる感じ.繰り返し処理はAlarmManagerを使っている例が多かったので、AppWidgetProvider+AlarmManagerで作ることにしました.

AlarmMangerが1秒ごとに動いてくれない

最初は以下のような実装でした.

package jp.santea.apps.bustimetablewidget;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;

public class SampleWidgetProvider extends AppWidgetProvider {

    private static final String COUNTDOWN_OPERATION = "CountdownOperation";
    private static final long INTERVAL_COUNTDOWN = 1000;
    private static int COUNTER;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                         int[] appWidgetIds) {

        Logger.init(context);
        Logger.d("onUpdate ...");

        COUNTER = 0;

        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.bustimetable_widget);

        ComponentName thisWidget = new ComponentName(context, BusTimetableWidget.class);
        AppWidgetManager manager = AppWidgetManager.getInstance(context);
        manager.updateAppWidget(thisWidget, remoteViews);

        setCountdownAlarm(context);
    }

    private void setCountdownAlarm(Context context) {
        PendingIntent operation = getPendingIntent(context, COUNTDOWN_OPERATION);
        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
        long firstTime = System.currentTimeMillis() + 1000 * 3;
        alarmManager.setRepeating(AlarmManager.RTC, firstTime, INTERVAL_COUNTDOWN, operation);
    }

    protected PendingIntent getPendingIntent(Context context, String action) {
        Intent intent = new Intent(context, getClass());
        intent.setAction(action);

        return PendingIntent.getBroadcast(context, 0, intent, 0);
    }

    @Override
    public void onReceive(Context context, Intent intent) {

        super.onReceive(context, intent);

        if(intent.getAction().equals(COUNTDOWN_OPERATION)){

            Logger.d("Countdown operation ...");

            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.sample_widget);
            ComponentName watchWidget = new ComponentName(context, SampleWidgetProvider.class);

            remoteViews.setTextViewText(R.id.textView_counter, "COUNT: " + COUNTER++);

            appWidgetManager.updateAppWidget(watchWidget, remoteViews);
        }
    }
}

しかしながら1000[ms]でリピート間隔を設定したはずなのにリピートはほぼ1分間隔...
f:id:Santea:20151223223807p:plain

API Lebel 19以降でAlarmManagerの仕様が変わったとのこと.公式にもアナウンスされていました.

Note: Beginning with API 19 (KITKAT) alarm delivery is inexact: the OS will shift alarms in order to minimize wakeups and battery use. There are new APIs to support applications which need strict delivery guarantees; see setWindow(int, long, long, PendingIntent) and setExact(int, long, PendingIntent). Applications whose targetSdkVersion is earlier than API 19 will continue to see the previous behavior in which all alarms are delivered exactly when requested.

setExact(int, long, PendingIntent)を使えるよと書いてあるので、次にsetExactを試してみます.結果的にはsetExactも1秒間隔のリピートは無理でした.
以下コードです.

private void setCountdownAlarm(Context context) {
    PendingIntent operation = getPendingIntent(context, COUNTDOWN_OPERATION);
    AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    long firstTime = System.currentTimeMillis() + INTERVAL_COUNTDOWN;
    alarmManager.setExact(AlarmManager.RTC, firstTime, operation);
}

setExcactを使用するように変更しています.

@Override
public void onReceive(Context context, Intent intent) {

    super.onReceive(context, intent);

    if(intent.getAction().equals(COUNTDOWN_OPERATION)){

        Logger.d("Countdown operation ...");

        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.sample_widget);
        ComponentName watchWidget = new ComponentName(context, SampleWidgetProvider.class);

        remoteViews.setTextViewText(R.id.textView_counter, "COUNT: " + COUNTER++);

        appWidgetManager.updateAppWidget(watchWidget, remoteViews);

        setCountdownAlarm(context);
    }
}

またonReceive内でsetCountdownAlarmを再度呼び出すことでリピートさせています.
その結果は以下のヒストグラムです.
f:id:Santea:20151223230655p:plain
マジョリティは5秒で3秒台、4秒台がほんの少しありますね.

これらのことからAlarmMangaerでは1秒間隔の処理はできないということになってしまいました….

Timerでの実装

AlarmManagerがダメで途方に暮れていたところ、

単純にストップウォッチみたいな機能でいいならJavaのTimerを使うのはどうでしょう

というコメントを頂きました.

そこで実装したのが以下のコードになります.

@Override
public void onReceive(Context context, Intent intent) {
        
    if (intent.getAction().equals(START_COUNTDOWN_OPERATION)) {

        if (DOES_RUN_COUNTDOWN) {
                
            if (timer != null)
                timer.cancel();

        } else {
                
            timer = new Timer();
            timer.schedule(createTimerTask(context), 0, INTERVAL_COUNTDOWN);
        }

    }
}

private TimerTask createTimerTask(final Context context) {

    return new TimerTask() {
        @Override
        public void run() {

            Message message = new Message();
            createHandler(context).sendMessage(message);
        }
    };
}

private Handler createHandler(final Context context) {
     return new Handler(Looper.getMainLooper()) {
         public void handleMessage(Message msg) {

            updateCountdown(context);
         }
     };
}

ほぼほぼ1秒間隔で更新できました!
ソースコードになります.
github.com

まとめ

今回はAPI Level依存のAlarmManagerの仕様で苦しめられました.最初は自分の書き方が悪いのかと思い、色々試行錯誤したのですが、解決せず….
teratailを頼り質問してみたら色々コメントを頂きまして、結果Timerで解決して良かったです.コメントを頂きました御二方には感謝申し上げます.
[Android]AppWidgetProviderでAlarmManager#setRepeatingがリピートしない(21930)|teratail

また本記事、コードに関してご意見ご感想ありましたらぜひお願いします.特にAlarmManager関連の良い方法があれば教えて頂きたいです.

PS. 技術系の記事には実機とOSとSDKのバージョンを書いて欲しいと思いました.