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

こんにちは。

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

[AndroidのWidgetだけ開発](https://qiita.com/wakwak/items/1daf3c764c49fc3ffd8c#%E3%81%A8%E3%81%93%E3%82%8D%E3%81%8Coreo)してみた或る日のことでございます。できあがったウィジェットを自分のAndroid 6.0の端末にインストールしてご満悦。:relaxed:

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

残念。動かず。:sob:

## その理由は分かっている

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

- [Android 8.0変更点:マニフェスト ファイルからブロードキャスト レシーバーを削除せよ](https://qiita.com/wakwak/items/eaaaa27881fa79dfc180)
- [AndroidのWidgetだけ開発](https://qiita.com/wakwak/items/1daf3c764c49fc3ffd8c#%E3%81%A8%E3%81%93%E3%82%8D%E3%81%8Coreo)

### 要件

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

![appwidget_preview.png](https://qiita-image-store.s3.amazonaws.com/0/100612/3bcb1e3b-344e-f6ec-18c7-8d83d050c145.png)

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

![browser.png](https://qiita-image-store.s3.amazonaws.com/0/100612/789def74-1d08-48d7-b464-c8b79ec6c816.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のサブクラス）は以下のように実装しました。

```java: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"`は以下のように登録しています。


```xml: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を見ると、

```console:
BroadcastQueue: Background execution not allowed
```

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

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

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

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

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

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

あ、でも、ウィジェットそのものの登録（<receiver>要素）は残しておきます。

## ウィジェットクラス

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

```java:書き直したウィジェットクラス（一部）
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. 独自ActionのIntentを、ブロードキャスト。
2. それをレシーヴするべくのonReceiveメソッド内で、`Intent i1 = new Intent(Intent.ACTION_VIEW, uri);`でブラウザ起動をさせていた。

### 修正点２：クリックしたらなにかしらの処理をしたい

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

### 修正点３：固執していたことを、捨てた

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

```java:お膳立てされた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が割り振られ、その分の対応をコーディングするとなると、ステップ数が<ruby>嵩<rt>かさ</rt></ruby>みそうではあります。
なら、updateAppWidgetメソッドに処理を委譲しようというココロは理解できるのですが、 **updateAppWidgetメソッドは、static** なんです。となると、フィールド変数もそれにつられてstaticにするということになりかねません。

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

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

# さいごに

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

以上です。