LoginSignup
15
9

More than 5 years have passed since last update.

Android 8.0でウィジェットはBroadcastReceiverだから困った

Last updated at Posted at 2018-01-16

こんにちは。

何について困っているのか

AndroidのWidgetだけ開発してみた或る日のことでございます。できあがったウィジェットを自分のAndroid 6.0の端末にインストールしてご満悦。:relaxed:

ところがふと「そういえば、Android 8.0変更点:マニフェスト ファイルからブロードキャスト レシーバーを削除せよってあったなぁ」と思い出し、さすればいかなることにぞと、作ったウィジェットをAndroid 8.0エミュレーターにインストールしてみたのでございます。

残念。動かず。:sob:

その理由は分かっている

私の作ったウィジェットは、こういう要件があって、こういう実装をしているから、Oreoで動かないのは、うすうす気づいていたんです。要は、この記事は、以下の2本の私のQiita記事の続編です。

要件

ウィジェットはこんなかんじのです。

appwidget_preview.png

このブラウザのアイコンをクリックしたら、ブラウザアプリを起動させます。

browser.png

あと、このブラウザのアイコン以外のところをクリックしたら、DBから「申込対象〇月分」と「Web受付期間」を再検索して表示したいです。

開発環境は以下の通りです。Javaオンリーです。

Android Studio 3.0.1
Build #AI-171.4443003, built on November 10, 2017
JRE: 1.8.0_152-release-915-b01 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0

実装

ウィジェットのプログラム(AppWidgetProviderのサブクラス)は以下のように実装しました。

AppWidgetProviderサブクラス(一部)
public class NewAppWidget extends AppWidgetProvider {

private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
  // まずこれが大事!
  RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
  // 自分自身に向けたIntentとなる
  Intent intent = new Intent("jp.co.casareal.toslovewidget.action.BROWSER");
  // ブロードキャストIntentに仕立て上げる
  PendingIntent pi = PendingIntent.getBroadcast(context, appWidgetId, intent , PendingIntent.FLAG_UPDATE_CURRENT);
  // <ImageView>のandroid:id属性を宛に、クリックイベントの設定
  views.setOnClickPendingIntent(R.id.browser, pi);
}

@Override
public void onReceive(Context context, Intent intent) {
  super.onReceive(context, intent); // これがないとダメっぽい
  switch (intent.getAction()) {
    case "jp.co.casareal.toslovewidget.action.BROWSER": // ブラウザー起動
      Uri uri = Uri.parse("http://www.its-kenpo.or.jp/");
      Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
      // Activity以外からstartActivityするときにはこれをしないといけない
      browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      context.startActivity(browserIntent);
      break;
  }
}
// 後略

もちろん、マニフェストにこの独自Action"jp.co.casareal.toslovewidget.action.BROWSER"は以下のように登録しています。

AndroidManifest.xml(一部)
<receiver
    android:name=".TosloveWidget"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        <action android:name="jp.co.casareal.toslovewidget.action.BROWSER" />
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/toslove_widget_info" />
</receiver>

表示はする、けどクリックしても反応なし

LogCatを見ると、

BroadcastQueue: Background execution not allowed

という旨のWarningログが出力されます。アプリ(ウィジェット)は落ちはしませんが、上述の私の作ったウィジェットの、ブラウザのアイコンをクリックしても(処理してくれないので)反応しません。

対策を講じて、Oreoでも動くようになった!:grin:

結論を言えば、Oreoでも動くように改造できました。

マニフェストから独自Actionを除去

Oreoですから、まずはマニフェストから以下の1文を泣く泣く削除します。

独自Actionの登録を除去
<action android:name="jp.co.casareal.toslovewidget.action.BROWSER" />

あ、でも、ウィジェットそのものの登録(要素)は残しておきます。

ウィジェットクラス

前記事のそれを大幅に書き換えました。

書き直したウィジェットクラス(一部)
public class TosloveWidget extends AppWidgetProvider {

    /**
     * ITS保養施設申込受付期間
     */
    public static final String URL_ITS_TOSLOVE_MOUSHIKOMI = "http://www.its-kenpo.or.jp/shisetsu/hoyou/chokuei/moushikomi.html";

    /**
     * DBから「申込対象〇月分」と「Web受付期間」を再検索して表示Action(この独自Actionは、マニフェストには登録しないこと)
     */
    public static final String ACTION_UPDATE = "jp.co.casareal.toslovewidget.action.UPDATE";

    // 複数のメソッドでも使用するのでここで宣言
    private RemoteViews views;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            // Construct the RemoteViews object ウィジェットレイアウトの初期化
            views = new RemoteViews(context.getPackageName(), R.layout.toslove_widget);

            // ▼ クリックイベントを登録 -------------------------------------------- ▼
            // (1)ブラウザー起動
            Uri uri = Uri.parse(URL_ITS_TOSLOVE_MOUSHIKOMI);
            Intent i1 = new Intent(Intent.ACTION_VIEW, uri);
            PendingIntent p1 = PendingIntent.getActivity(context, appWidgetId, i1, PendingIntent.FLAG_UPDATE_CURRENT);
            views.setOnClickPendingIntent(R.id.browser, p1);

            // (2)DBから「申込対象〇月分」と「Web受付期間」を再検索して表示
            Intent i2 = new Intent(context, getClass());
            i2.setAction(ACTION_UPDATE);
            PendingIntent p2 = PendingIntent.getBroadcast(context, appWidgetId, i2, PendingIntent.FLAG_UPDATE_CURRENT);
            views.setOnClickPendingIntent(R.id.layout_main, p2);
            // ▲ クリックイベントを登録 -------------------------------------------- ▲

            // DBから「申込対象〇月分」と「Web受付期間」を再検索して表示
            update(context);
        }
    }

    /**
     * クリックイベントのブロードキャストIntentを受信
     *
     * @param context
     * @param intent
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        switch (intent.getAction()) {
            case Intent.ACTION_BOOT_COMPLETED: // ブート起動時(このActionはホワイトリスト)
            case ACTION_UPDATE: // ITSサイトからデータ再取得
                update(context);
                break;
        }
    }

    /**
     * DBから「申込対象〇月分」と「Web受付期間」を再検索して表示
     */
    private void update(Context context) {
        // RemoteViewsオブジェクトを操作したりする処理など
    }
}

修正点1:クリックしたらブラウザを起動させたい

前回のときは、回りくどいことをしていました。それは、こういう手順を踏んでいたからです。

  1. 独自ActionのIntentを、ブロードキャスト。
  2. それをレシーヴするべくのonReceiveメソッド内で、Intent i1 = new Intent(Intent.ACTION_VIEW, uri);でブラウザ起動をさせていた。

修正点2:クリックしたらなにかしらの処理をしたい

『「申込対象〇月分」と「Web受付期間」を再検索して表示』をさせるためのクリックイベントについてです。
ここは、自クラス内だけで完結するブロードキャストインテントの送受信にするべく、Intent i2 = new Intent(context, getClass());としました。

修正点3:固執していたことを、捨てた

Android Studioのウィジェット作成ウィザードでお膳立てしてくれるAppWidgetProviderサブクラスには、以下の通りです。

お膳立てされたAppWidgetProviderサブクラス
public class NewAppWidget extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    // 後略
}

onUpdateメソッドよりも、「さぁ、ここの中をお書きなさい!」と言わんばかりに先頭立ってupdateAppWidgetという名のメソッドが仁王立ちしています。
私はこのupdateAppWidgetを捨てました。しばらく粘って頑張ったのですが、捨てました。断捨離。

たしかに、ウィジェットは複数ホームに置くことができるので、appWidgetIdsというint配列でIDが割り振られ、その分の対応をコーディングするとなると、ステップ数がかさみそうではあります。
なら、updateAppWidgetメソッドに処理を委譲しようというココロは理解できるのですが、 updateAppWidgetメソッドは、static なんです。となると、フィールド変数もそれにつられてstaticにするということになりかねません。

左右とかく「『ウィジェットクラスの任意のメンバー(フィールドとメソッド)には盲目的にすべてstaticを付けろ』というのもカッコ悪いかなぁ」だなんてヘンにカッコつけてみたりして、でも一方で「誰もやらないことやってる俺カッケー:muscle_tone3:」と言うのもカッコ悪いよなぁ。と煩悶。

ということで、なんでもかんでもstaticを付ける、ということも、捨て(ることができ)ました。

さいごに

ということで、Android 8.0(Oreo)でも、そしてOreo未満でも、動作するウィジェットになりました。当記事の題材はウィジェットを扱っていますが、今後「暗黙的ブロードキャストの禁止」が面倒くさい程度までし掛かってきそうな氣がします。

以上です。

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9