質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.99%

[Android]AppWidgetProviderでAlarmManager#setRepeatingがリピートしない

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 3,404

Daiki-Kawanuma

score 27

自作ウィジェットを作っております。1秒おきにカウントダウンをしたいのでAppWidgetProvider内でAlarmManager#setRepeatingを呼び出し更新させています。テスト環境はNexus 5X Android 6.0です。以下コードになります。

AppWidgetProvider

public class MyWidget extends AppWidgetProvider {

    public static final String IMAGE_CLICKED = "ImageClicked";
    public static final String COUNTDOWN_OPERATION = "CountdownOperation";
    public static boolean DOES_RUN_COUNTDOWN;
    public static final String UPDATE_TIMETABLE_OPERATION = "UpdateTimeTableOperation";
    private final int INTERVAL_COUNTDOWN = 1000;
    private final int INTERVAL_UPDATE_TIMETABLE = 1000 * 60 * 10;

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

        DOES_RUN_COUNTDOWN = false;

        RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.bustimetable_widget);
        ComponentName watchWidget = new ComponentName(context, BusTimetableWidget.class);

        remoteViews.setOnClickPendingIntent(
                R.id.imageView_bus,
                getPendingIntentBroadcast(context, IMAGE_CLICKED)
        );

        appWidgetManager.updateAppWidget(watchWidget, remoteViews);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        // TODO Auto-generated method stub

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

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

            if(DOES_RUN_COUNTDOWN){
                DOES_RUN_COUNTDOWN = false;
                Toast.makeText(context, "IMAGE_CLICKED: " + DOES_RUN_COUNTDOWN, Toast.LENGTH_LONG).show();
                cancelCountdownAlarm(context);

            } else {
                DOES_RUN_COUNTDOWN = true;
                Toast.makeText(context, "IMAGE_CLICKED: " + DOES_RUN_COUNTDOWN, Toast.LENGTH_LONG).show();
                setCountdownAlarm(context);
            }

        } else if(intent.getAction().equals(UPDATE_TIMETABLE_OPERATION)){

            Toast.makeText(context, "UPDATE_TIMETABLE_OPERATION: " + Calendar.getInstance().getTime(), Toast.LENGTH_LONG).show();

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

            remoteViews.setTextViewText(R.id.textView_fore_count, "n: " + count++);
            // Toast.makeText(context, "COUNTDOWN_OPERATION: " + Calendar.getInstance().getTime(), Toast.LENGTH_SHORT).show();

        }

        appWidgetManager.updateAppWidget(watchWidget, remoteViews);
        super.onReceive(context, intent);
    }

    private void setCountdownAlarm(Context context) {
        PendingIntent operation = getPendingIntentService(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);
    }

    private void cancelCountdownAlarm(Context context){
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        PendingIntent operation = getPendingIntentService(context, COUNTDOWN_OPERATION);
        alarmManager.cancel(operation);
    }

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

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

widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/bustimetable_widget"
    android:initialLayout="@layout/bustimetable_widget"
    android:minHeight="40dp"
    android:minWidth="40dp"
    android:previewImage="@drawable/bus_image"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="0"
    android:widgetCategory="home_screen"/>

Manifest

<intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="CountdownOperation" />
                <action android:name="UpdateTimeTableOperation" />
                <action android:name="ImageClicked"/>
            </intent-filter>

ImageViewのクリックに応じてカウントダウンをスタート、キャンセルする形を取っています。 現状、ImageViewのクリックには即時反応するのですが、カウントダウンは更新が呼ばれていません。おそらくAlarmManagerの使い方が間違っているのではないかと思うのですが、ご指南よろしくお願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

+1

テスト環境はNexus 5X Android 6.0です。

ということは、API Levelは19以上でしょうか?

公式のリファレンスによると、
http://developer.android.com/intl/ja/reference/android/app/AlarmManager.html#setRepeating(int, long, long, android.app.PendingIntent)

API Level 19以降は、全ての繰り返し系アラームは不正確だそうです。

Note: as of API 19, all repeating alarms are inexact.

代替策として
「ワンタイムのアラームを使用し、アラームが起動した時に、次のアラームをセットするように」
と書いてあります(と、思います(^^;))ので、この方法を試してみてはいかがでしょうか?

If your application wants to allow the delivery times to drift in order to guarantee that at least a certain time interval always elapses between alarms, then the approach to take is to use one-time alarms, scheduling the next one yourself when handling each alarm delivery.

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2015/12/10 01:00

    ご回答頂きありがとうございます!
    ご指摘のように、AlarmManager#setに直し(引数はSystem.currentTimeMillis()をそのまま突っ込みました)onReceive内で再度setCountdownAlarmを呼び出したところ、更新が比較的短い単位で継続されました!
    ただ、それでも1秒単位とは程遠く、数秒おきという感じです。
    不正確のレベルがms単位だと思ってたのですが、こんなにも大きなレベルで不正確なものなんでしょうか・・・?

    キャンセル

  • 2015/12/10 01:34 編集

    Daiki-Kawanuma様、返信ありがとうございます。

    > ただ、それでも1秒単位とは程遠く、数秒おきという感じです。
    改めて調べてみたところ、以下のリンクを見つけました。
    https://akira-watson.com/android/setexact-setwindow.html

    set よりは setExact の方が、比較的マシなようですね。

    > 不正確のレベルがms単位だと思ってたのですが、こんなにも大きなレベルで不正確なものなんでしょうか・・・?
    私もよく分かりません。
    デバイスの性能やCPUの負荷などによっても変わってくるかと思いますし。

    setExact で誤差を数十ms程度まで抑えることができれば、例えば
    1. PendingIntent#putExtra に次回の起動予測時刻 (System.currentTimeMillis() + INTERVAL_COUNTDOWN)をセットしておき、
    2. 次回起動時にその値と現在のSystem.currentTimeMillis() の誤差を比較して、
    3. 次々回の起動時刻を調整する
    などとすればさらに精度を上げることができるかもしれませんが、
    「精確に1秒ごと」
    は、なかなか難しいかと思います。

    キャンセル

  • 2015/12/13 05:11

    KiyoshiMotoki 様

    詳細な情報をありがとうございます。AlarmManager#setExactや#setWindowなど色々試したのですが、結局平均して5秒間隔程度までしか間隔を縮められませんでした。最終的にjava.util.Timerとandroid.app.handlerを併用して1秒周期の更新を実現できました!
    現時点での自分の見解になりますが、AlarmMangerは1min単位など比較的長い間隔の更新向けなのではないかという感じです。

    KiyoshiMotoki様にはAlarmManagerの詳細な説明を頂き本当に助かりました。
    ありがとうございました!

    キャンセル

  • 2015/12/13 13:41

    Daiki-Kawanuma様、ご報告ありがとうございます。

    > AlarmMangerは1min単位など比較的長い間隔の更新向けなのではないかという感じです。
    確かに、これはあり得るかもしれませんね。

    改めてAlarmManagerのAPIリファレンスを見てみると、
    "Class Overview"の項に以下のような記載を見つけることができましたので。
    > For normal timing operations (ticks, timeouts, etc) it is easier and much more efficient to use Handler.

    http://developer.android.com/intl/ja/reference/android/app/AlarmManager.html

    こちらこそ、勉強させていただきました。

    キャンセル

checkベストアンサー

0

getPendingIntentService()内の下記のコードですが、Intenを送る先が AppWidgetProviderなので、PendingIntent.getBroadcastに返ると動きそうな気がします。

return PendingIntent.getService(context, 0, intent, 0);

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2015/12/10 11:37

    morisakisan様、横から失礼します。

    > 結局AlarmManagerAPIレベルでの不具合があったのですね。
    不具合ではなく、仕様の変更ですね。

    公式のリファレンスで
    「API Level 19 以降では不正確です」
    と、明言していますので。

    キャンセル

  • 2015/12/11 13:19

    KiyoshiMotoki様、ご指摘ありがとうございました。

    不具合は不適切でした。気をつけます。

    キャンセル

  • 2015/12/13 05:18

    morisakisan 様

    >>単純にストップウォッチみたいな機能でいいならJavaのTimerを使うのはどうでしょう。
    ドンピシャでした!AppWidgetProviderとTimerの併用はあまり例を見つけられず、AlarmManagerを使うのが定石と思っておりましたが、カウントダウンはTimerで実現できました。
    今回はAlarmManagerの仕様に大分振り回されて大変でしたが、Timerという身近な機能で解決できたということで勉強になりました。

    morisakisan様には最初からお世話になったことに加え、実際の解決法を頂きましたのでベストアンサーにさせて頂きます。ありがとうございました!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.99%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる