こんにちは。
Android 8.0(コードネームは「Oreo」、API Levelは26)が2017年8月下旬に公式発表されました。
そこで何か大きな仕様変更があるのではないかとチラッと見てみたら、、、
という記事を見て焦りました。
アプリがブロードキャストを受信するように登録されている場合、ブロードキャストが送信されるたびにアプリのレシーバーがリソースを消費します。 そのため、非常に多くのアプリがシステム イベントに基づくブロードキャストの受信を登録している場合、問題が発生する可能性があります。ブロードキャストをトリガーするシステム イベントより、これらのすべてのアプリが続けざまにリソースを消費して、ユーザー エクスペリエンスに悪影響を与える可能性があります。
なるほど、いろんなブロードキャストされたインテントを受信するために、いろんなブロードキャストレシーバーが起動したままだと、そりゃリソースをじゃぶじゃぶ消費しますよね。特に電力(充電)の減りが早いとムカつきますし。ということで、Android 7.0(Nougat)の時点でバックグラウンド処理の最適化で伏線を張っておいて(布石を打っておいて)、Android 8.0(Oreo)でさらに強化された、とのこと。
ということで、Android 8.0では、
暗黙的なブロードキャスト インテントに対して登録されているブロードキャスト レシーバーをすべて削除する必要があります。
ワオ。仕事増えるなぁ。
Android 8.0 を対象にしているアプリは、暗黙的なブロードキャストに対するブロードキャスト レシーバーをマニフェストで登録できなくなりました。
語調も強めですね。
しかし、抜け道とでも言いましょうか、「Android 8.0 を対象とするアプリで引き続き動作する暗黙的なブロードキャスト」として暗黙的なブロードキャストの例外(Implicit Broadcast Exceptions)があるとのこと。ここで『例外』という用語が使われているのですが、Javaの言語仕様にある用語と混同して混乱しても嫌なので、私のこの投稿上では『除外』と書いておきますね。あ、『特例』とか『ホワイトリスト』と言ってもいいかもしれませんね。
この投稿は、「とりあえず動作確認をしてみたくて」サンプルアプリを作ってみた体験記です。
開発環境
Android Studioは「3.0 CANARY 8」です。このバージョンのAndroid StudioでないとOreoのエミュレータは入手できないので。
Android Studio 3.0 Beta 2
Build #AI-171.4263559, built on August 11, 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
Android Emulatorのリビジョンは26.1.4で、Android 6.0(以下、Marshmallowの「マシュ」)のと、Android 8.0(以下、Oreoの「オレオ」)の2つを用意しました。
一つ目のアプリ
このアプリでやりたいことは、マニフェストにブロードキャストレシーバーを登録したままだと、どうなるか?の検証です。
ブロードキャストインテント | 大丈夫なのか?駄目なのか? |
---|---|
明示的 | マシュでもオレオでも大丈夫 |
暗黙的かつオリジナルのAction | マシュは大丈夫だけど、オレオは機能せず |
暗黙的でシステムのやつで[適用] | マシュは大丈夫だけど、オレオは機能せず |
暗黙的でシステムのやつで[除外] | マシュでもオレオでも大丈夫 |
画面とActivity
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickSendExplicit"
android:text="明示的なインテントをブロードキャスト" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onClickSendImplicit"
android:text="暗黙的なインテントをブロードキャスト" />
</LinearLayout>
「Explicit」が明示的、「Implicit」が暗黙的、という意味の英単語です。
package jp.co.casareal.oreobroadcastsample;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* 明示的ブロードキャストインテントを送出するボタン押下時
*/
public void onClickSendExplicit(View view) {
Intent intent = new Intent(getApplicationContext(), ExplicitIntentReceiver.class);
intent.putExtra("message", "明示的なブロードキャストインテント");
sendBroadcast(intent);
}
/**
* 暗黙的ブロードキャストインテントを送出するボタン押下時
*/
public void onClickSendImplicit(View view) {
Intent intent = new Intent("jp.co.casareal.oreobroadcastsample.ORIGINAL");
intent.putExtra("message", "暗黙的なブロードキャストインテント");
sendBroadcast(intent);
}
}
この投稿では、「ふと我思ふ、ブロードキャストインテントを明示的に送出する意義とは?」については考察しないとします。
ブロードキャストレシーバーたち
明示的ブロードキャストインテントを受信するレシーバー
package jp.co.casareal.oreobroadcastsample;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.widget.Toast;
/**
* 明示的ブロードキャストインテントを受信するレシーバー
*/
public class ExplicitIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String massage = intent.getStringExtra("message");
Toast toast = Toast.makeText(context, massage, Toast.LENGTH_LONG);
toast.getView().setBackgroundColor(Color.RED);
toast.show();
}
}
オリジナルの暗黙的ブロードキャストインテントを受信するレシーバー
package jp.co.casareal.oreobroadcastsample;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.widget.Toast;
/**
* オリジナルの暗黙的ブロードキャストインテントを受信するレシーバー
*/
public class ImplicitIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if ("jp.co.casareal.oreobroadcastsample.ORIGINAL".equals(intent.getAction())) {
String massage = intent.getStringExtra("message");
Toast toast = Toast.makeText(context, massage, Toast.LENGTH_LONG);
toast.getView().setBackgroundColor(Color.BLUE);
toast.show();
}
}
}
オレオからの制限適用除外のACTION_LOCALE_CHANGEDのレシーバー
なんでこのActionを特別扱いしているんでしょう?
Only sent when the locale changes, which is not often.
とのことです。ロケールなんて頻繁に変更されるもんじゃないから、許してやろう、というかんじでしょうか。
package jp.co.casareal.oreobroadcastsample;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.widget.Toast;
/**
* Android 8.0からの「Background Execution Limits(ブロードキャストの制限)」適用除外のACTION_LOCALE_CHANGEDのレシーバー
*/
public class LocaleChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
Toast toast = Toast.makeText(context, Intent.ACTION_LOCALE_CHANGED, Toast.LENGTH_LONG);
toast.getView().setBackgroundColor(Color.GREEN);
toast.show();
}
}
}
オレオからの制限が適用されるACTION_AIRPLANE_MODE_CHANGEDのレシーバー
私自身、あまり飛行機に頻繁に乗る生活を送ってはいないのですが、このブロードキャストインテントは許してはくれないようです。
package jp.co.casareal.oreobroadcastsample;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.widget.Toast;
/**
* Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」に適用されるACTION_AIRPLANE_MODE_CHANGEDのレシーバー
*/
public class AirplaneModeChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction())) {
Toast toast = Toast.makeText(context, Intent.ACTION_AIRPLANE_MODE_CHANGED, Toast.LENGTH_LONG);
toast.getView().setBackgroundColor(Color.GREEN);
toast.show();
}
}
}
問題のマニフェスト
上記のブロードキャストレシーバーをすべて登録しておきます。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.casareal.oreobroadcastsample">
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 明示的なブロードキャストインテントを受信するレシーバー -->
<receiver android:name=".ExplicitIntentReceiver" />
<!-- 暗黙的なブロードキャストインテントを受信するレシーバー -->
<receiver android:name=".ImplicitIntentReceiver">
<intent-filter>
<action android:name="jp.co.casareal.oreobroadcastsample.ORIGINAL" />
</intent-filter>
</receiver>
<!-- Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」適用外のACTION_LOCALE_CHANGEDのレシーバー -->
<receiver android:name=".LocaleChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
<!-- Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」に適用されるACTION_AIRPLANE_MODE_CHANGEDのレシーバー -->
<receiver android:name=".AirplaneModeChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>
</application>
</manifest>
エミュレータで実行
マシュとオレオのエミュレータで試してみます。なお、以下に掲載する画面ショットは、マシュのエミュレータばかりです。なぜなら、オレオのエミュレータだと、トーストが表示されないからです。
用意した2つのボタンをクリックする編
マシュの画面です。2つのボタンのどちらをクリックしてもトーストが表示されます。
一方、オレオのエミュレータで試してみると、案の定、「明示的なブロードキャストインテントを受信するレシーバー」であるExplicitIntentReceiverは機能する(トーストを表示する)のですが、「暗黙的ブロードキャストインテントを受信するレシーバー」であるImplicitIntentReceiverは機能しません。なお、【暗黙的なインテントをブロードキャスト】ボタンをクリックしても例外は発生しません(ので、アプリが落ちたりはいません、ただただ、だんまりなだけです)。
システムが送出するブロードキャストインテント編
制限適用除外のACTION_LOCALE_CHANGED
マシュはもちろん、オレオでもトーストが表示されました。
制限が適用されるACTION_AIRPLANE_MODE_CHANGEDのレシーバー
マシュしかトースト表示されません。
どうしてもAndroid 8.0端末でも機能させたい!
ほら言わんこっちゃない、Android 8.0からは一部を除く暗黙的ブロードキャストインテントのレシーバーはマニフェストに登録したままだと駄目なんだってばよ。
デモデモダッテ、と我儘を言いたい方もいらっしゃるかと存じます。上記のActivity(のonCreateメソッド内など)に以下のコードのように、registerReceiverでレシーバーを指定する旨を記述すれば、Android 8.0端末でもトーストが表示されますよ。
// Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」をかいくぐる
BroadcastReceiver receiver0 = new ImplicitIntentReceiver();
IntentFilter filter0 = new IntentFilter();
filter0.addAction("jp.co.casareal.oreobroadcastsample.ORIGINAL");
registerReceiver(receiver0, filter0);
BroadcastReceiver receiver1 = new AirplaneModeChangedReceiver();
IntentFilter filter1 = new IntentFilter();
filter1.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver1, filter1);
二つ目のアプリ
例えば、「ユーザが機内モードをON/OFFしたら、ユーザにトーストでその旨を通知するお節介アプリを作りたい」ということで、「Activityが無くて、BroadcastReceiverとServiceだけで構成されているアプリ」を作ってみます。
ここでちょっと主旨から逸れて、「Activity(画面)の無いアプリ」の話題に入る
「Activityの無いアプリ」を作りたい、という要件は、Android 3.1 (HONEYCOMB_MR1) からは不可能となりました。Android 3.1 APIsの「Launch controls on stopped applications」の項目を参照してください。
仕方ない、Activityは要らないけど作るか。
package jp.co.casareal.onlyreceiverapp;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
/**
* Android3.1からは、初期起動Activityのないアプリでは
* BroadcastReceiverは起動できないので、仕方なく用意したランチャActivity。
*
* マニフェストにてandroid:theme属性に
* "@android:style/Theme.Translucent.NoTitleBar"(またはTheme.Translucent)
* を設定して背景を透明化させてユーザに認識させない。
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」をかいくぐる
BroadcastReceiver receiver = new AirplaneModeChangedReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
registerReceiver(receiver, filter);
}
}
お馴染みのsetContentViewメソッドを書かない!というか、レイアウトリソースファイルも作らない!もはや『画面』に非ずのActivity。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.casareal.onlyreceiverapp">
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<!-- Android 8.0からの「Background Execution Limits(ブロードキャストの制限事項)」に適用されるACTION_AIRPLANE_MODE_CHANGEDのレシーバー -->
<receiver android:name=".AirplaneModeChangedReceiver">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE" />
</intent-filter>
</receiver>
<!-- むしろこのActivityは脇役 -->
<activity
android:name=".MainActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
</intent-filter>
</activity>
</application>
</manifest>
ユーザに見せたくもないActivityの設定のポイントは、以下の2点を施します。
<category android:name="android.intent.category.HOME" />
android:theme="@android:style/Theme.Translucent.NoTitleBar"
アプリ一覧(ランチャー)にアイコンすら表示させたくないので、お馴染みのLAUNCHER
ではなくHOME
にしました。ただし、アプリ実行の際には、以下の手間がかかります(Instant Runで簡便に実行できるものではなさそうです)。
そして念には念を、ということで、Activityの背景を透明にするテーマTheme.Translucent.NoTitleBar
を適用しました。
まだ主旨から逸れていて、「特定のActivityを指定して実行」の話題に入る
LAUNCHER
ではなくHOME
にしたActivityを実行したくとも、Instant Runでは実行できません。
Android Studioのメニュー[Run]→[Edit Configurations]をクリックします。
「Launch Options」の「Launch:」で「Specified Activity」を選択し、「Activity:」に起動させるActivityを指定します。
主旨に戻って、オレオで暗黙的ブロードキャストインテントのレシーバーが機能するのを確認する
これにていざ実行!なわけですが、このアプリは機内モードをON/OFFさせてこその動作確認です。オレオのエミュレータで試してみます。
おお、出ました。
まとめ
registerReceiverメソッドで、暗黙的ブロードキャストインテントを受信するブロードキャストレシーバーを明示的に指定することで、Android 8.0(Oreo)から強化されたブロードキャストの制限の適用をかいくぐることができました。
『暗黙的なものを、明示的にやればよい』というアベコベさが面白いな。
以上です。