API Level 19 以降で AlarmManager が辛かった話
Androidの話です.
API Level 19以降でAlamManager(AlarmManager | Android Developers)が辛かった話になります.先に自分なりの結論を示しておきます.
- set, setReapiting は API Level 19 以降では不正確
- setExact も秒単位の処理ではかなり微妙
- java.util.Timer で秒単位の処理を一応実現できた
開発環境
- IDE:Android Studio 1.3.1
- デバッグ機:Nexus 5X(Android 6.0.1)
- SDK Version:22
作りたかったもの
バスの発着時間をカウントダウンするウィジェットを作りました.やりたいことは、
- 先発のバスの発着時間をカウントダウンすること
- 次発、次次発の時刻を表示すること
- バッテリー消費を考えて、カウントダウンはボタンのタップで開始すること
以上の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分間隔…
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 を再度呼び出すことでリピートさせています. その結果は以下のヒストグラムです.
マジョリティは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秒間隔で更新できました! 全ソースコードになります.
まとめ
今回は API Level 依存の AlarmManager の仕様で苦しめられました.最初は自分の書き方が悪いのかと思い、色々試行錯誤したのですが、解決せず…. teratail を頼り質問してみたら色々コメントを頂きまして、結果 Timer
で解決して良かったです.コメントを頂きました御二方には感謝申し上げます.
[Android]AppWidgetProviderでAlarmManager#setRepeatingがリピートしない(21930)|teratail また本記事、コードに関してご意見ご感想ありましたらぜひお願いします.特にAlarmManager関連の良い方法があれば教えて頂きたいです.
ディスカッション
コメント一覧
まだ、コメントがありません