酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Androidアプリでアイコンバッジを表示させる

既存のAndroidアプリにアイコンバッジをつけることになりました。いままでAndroidアプリでアイコンバッジ数を厳密に管理して表示させたことがなかったので、いざ実装してみると考えていた以上に大変でした。

Playストアアプリに更新待ちが1件存在していることを示すアイコンバッジ

iOSアプリの場合には、アイコンバッジの表示はUIApplication.shared.applicationIconBadgeNumber = 2で完了です。NSNumber型で取得もできるので、通知をタップされるとデクリメントすれば完了です。

Androidアプリの場合には、まず様々なメーカーが作成したホームアプリに対応する必要があります。アイコンバッジの表示はブロードキャストを使って「これこれを表示してください」とホームアプリへ一方通行に投げることになるので、アプリ内で受けた通知をすべて管理して、通知をタップされたりスワイプ等でクリアされた場合にも制御する必要があります。

メーカーごとにホームアプリでのアイコンバッジの表示のさせ方が異なる

メーカーごとにホームアプリでのアイコンバッジの表示のさせ方が異なります。

iOSのようにAPIが用意されているわけではないのでメーカーによって異なるIntentを投げる必要があります。なぜこんな風にバラバラの実装になってしまっているのかはわかりませんが、標準化を進める前に手法が定着してしまったのでしょうか(推測)。

最初は「android - How to display count of notifications in app launcher icon - Stack Overflow」のように実装しました。

public static void setBadge(Context context, int count) {
    String launcherClassName = getLauncherClassName(context);
    if (launcherClassName == null) {
        return;
    }
    Intent intent = new Intent("android.intent.action.BADGE_COUNT_UPDATE");
    intent.putExtra("badge_count", count);
    intent.putExtra("badge_count_package_name", context.getPackageName());
    intent.putExtra("badge_count_class_name", launcherClassName);
    context.sendBroadcast(intent);
}

これで目的を達成したと思ったのですが、手持ちのSony Xperia Z5 Compactでテストしたところ、アイコンバッジがつかないことに気づきました。調べたところソニー製ホームアプリでは以下のパラメータでIntentを投げる必要があることがわかりました。

private static void setBadgeSony(Context context, int count) {
    String launcherClassName = getLauncherClassName(context);
    if (launcherClassName == null) {
        return;
    }

    Intent intent = new Intent();
    intent.setAction("com.sonyericsson.home.action.UPDATE_BADGE");
    intent.putExtra("com.sonyericsson.home.intent.extra.badge.ACTIVITY_NAME", launcherClassName);
    intent.putExtra("com.sonyericsson.home.intent.extra.badge.SHOW_MESSAGE", true);
    intent.putExtra("com.sonyericsson.home.intent.extra.badge.MESSAGE", String.valueOf(count));
    intent.putExtra("com.sonyericsson.home.intent.extra.badge.PACKAGE_NAME", context.getPackageName());
    context.sendBroadcast(intent);
}

サムスン製とソニー製ホームアプリでこんなに差異があるんなら、他のメーカー製のホームアプリでも違う実装をしないといけないのではないか……と考えて、総合的にサポートされているライブラリを使うことにしました。

調査の結果、「ShortcutBadger」というライブラリが、2019年3月時点でもメンテされていることがわかり利用することにした。

github.com

ShortcutBadgerを使うと ShortcutBadger.applyCount(context, badgeCount); でアイコンバッジが表示できて、ShortcutBadger.applyCount(context, 0); でアイコンバッジを削除することができました。iOSのようにバッヂ数を取得することはできませんが、ほぼほぼiOSのように処理を同一化することができるようになりました。

Androidでは将来的に数字付きのアイコンバッジが主流ではなくなってしまうかもしれませんが、ShortcutBadgerはスターも5kを超えているためそうそうメンテナンスが途切れることはなさそうだと安心しています。

おまけ:アプリ内で通知情報を管理する

本題から離れたところになるのであくまでも「おまけ」扱いです。前節ではアイコンバッジをつけるための方法を悩んでいました。こちらの方はアイコンバッジの数字をどうやって管理するか考えた結果です。かなり雑な落書きですが全体像は下記のようにしました。

f:id:ch3cooh393:20190318235320p:plain

MyFirebaseMessagingService

FCMのリモート通知を受信するサービスを作ります。

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onNewToken(String token) {
        super.onNewToken(token);
        sendRegistrationToServer(token);
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        final NotificationInfo notificationInfo = new NotificationInfo(remoteMessage);
        sendNotification(notificationInfo);
    }

    private void sendRegistrationToServer(String token) {
        // デバイストークンをサーバーへ送信する
    }

    private void sendNotification(NotificationInfo notificationInfo) {

        final Context context = getApplicationContext();

        //タップされたとき
        Intent sendShowIntent = new Intent(context, MainActivity.class);
        PendingIntent showIntent = PendingIntent.getActivity(context, 0, sendShowIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        //スワイプ等で削除されたとき
        Intent sendDeleteIntent = new Intent(context, NotificationReceiver.class);
        sendDeleteIntent.setAction(NotificationReceiver.DeleteNotification);
        PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, sendDeleteIntent, 0);

        //通知数を+1する

        //アイコンバッジを更新する処理

        //通知を表示する
        final String channelId = "channel_id";
        final String channelName = "channel_name";

        NotificationManager manager = context.getSystemService(NotificationManager.class);
        manager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT));
        NotificationCompat.Builder builder
                = new NotificationCompat.Builder(context, channelId)
                .setContentIntent(showIntent)
                .setDeleteIntent(deleteIntent)
                .setContentTitle(notificationInfo.getTitle())
                .setContentText(notificationInfo.getBody())
                .setAutoCancel(true)
                .setSmallIcon(R.drawable.ic_notification_icon);
        NotificationManagerCompat.from(context).notify(notificationInfo.getId(), builder.build());
    }
}

通知がタップされた時はMainActivityが呼び出されるので、通知数のデクリメント処理はActivity側で処理すれば良いです。

通知がスワイプ等で削除された場合に、タップされた時と同じように下記のコードのように書いていると、通知をキャンセルしたはずなのにアプリが起動してしまうので気をつけましょう。

//スワイプ等で削除されたとき
Intent sendDeleteIntent = new Intent(context, MainActivity.class);
PendingIntent deleteIntent = PendingIntent.getActivity(context, 0, sendDeleteIntent, PendingIntent.FLAG_UPDATE_CURRENT);

また、Android 8.0(Oreo)以降では、暗黙的ブロードキャストを実行すると怒られが発生するので、対象を定めて明示的ブロードキャストをおこなうようにします。

Intent sendDeleteIntent = new Intent(context, NotificationReceiver.class);
sendDeleteIntent.setAction(NotificationReceiver.DeleteNotification); //アクション名は適当
PendingIntent deleteIntent = PendingIntent.getBroadcast(context, 0, sendDeleteIntent, 0);

NotificationReceiver

通知がスワイプやすべて削除で消された場合に、このレシーバーが呼ばれるので通知数をデクリメントする。

public class NotificationReceiver extends BroadcastReceiver {

    final String TAG = NotificationReceiver.class.getSimpleName();

    public static final String DeleteNotification = "jp.ch3cooh.hogeapp.DeleteNotification";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        switch (action) {
            case DeleteNotification:

                //通知数を-1する

                //アイコンバッジを更新する処理

                break;

            default:
                break;
        }
    }
}

関連記事