Androidの通知を自動で強制キャンセルする方法

この記事は
「DeNA IPプラットフォーム事業部 Advent Calendar 2017」
16日目の小ネタ記事です。

#はじめに
しばらく前に○PERIA Z4が送ってくる通知（しかもただの広告）が、設定で無効にしても無視されることに気が付いたので、プログラムで通知を監視して不要な通知を自動でキャンセルできないかとやってみました。

#通知の監視
通知を監視するには`NotificationListenerService`と云うクラスがそのまま利用できます。具体的には継承したクラスを作ってメソッドを一つオーバーライドするだけです。

```kotlin
override fun onNotificationPosted(sbn: StatusBarNotification?) {
    sbn?.let {
        if (it.packageName == "com.kusonymobile.entrance") {
            cancelNotification(it.key)
        }
    }

    super.onNotificationPosted(sbn)
}

```
やっていることは通知が来た時に呼ばれるコールバック`onNotificationPosted`内でパッケージ名をチェックして不要であればキャンセルするだけの処理です。引数の`StatusBarNotification`から取れる情報を利用してもっと粒度の高いフィルタ（含まれる文字列、画像の有無等）も可能です。

後は、`NotificationListenerService`を継承したサービスをManifestに登録するだけです。これだけですべての通知を監視するアプリが出来上がります。

```xml
<service
    android:name=".NotificationListener"
    android:label="@string/app_name"
    android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
    <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
    </intent-filter>
</service>
```
#Notification access許可
こんなに簡単にすべての通知を覗かれても困るので、このサービスを利用するには *Notification access*の許可をアプリインストール後にユーザから得る必要があります。この許可がないと`NotificationListenerService`へのコールバックが発生しないので通知を監視することができません。アプリをインストールさせるだけで裏でこっそり監視なんてことは出来ない仕組みです。

手動で許可画面に行くのは辿り着くまでが深すぎて面倒なので、以下のコードで許可があるかをチェックして、なければ許可画面を直接開きます。

```kotlin
if (!NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)) {
    Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS).apply {
        startActivity(this)
    }
}
```
![device-2017-12-15-154042.png](https://qiita-image-store.s3.amazonaws.com/0/188470/8a550f1d-3f2d-bc23-c73c-15aaf96cb6f8.png)

ちょっと怖い警告が出ます。実際、SMSで送られてくるコードや通知には表示されていないメールの全文が読めたりします。
#Android O
以上で目的達成できますが、Android O以降だとバックグラウンドのサービスはすぐに破棄されてしまうので、サービスがバインドされると同時に通知を利用してフォアグラウンドサービスになるようにします。

```kotlin
override fun onBind(intent: Intent?): IBinder {
    postNotification()

    return super.onBind(intent)
}
```
```kotlin
private fun postNotification() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(NotificationChannel("notification_monitor", "Monitoring notifications", NotificationManager.IMPORTANCE_MIN))
    }

    NotificationCompat.Builder(this, "notification_monitor")
            .setContentTitle("Notification listener service")
            .setContentText("Hello world!")
            .setSmallIcon(android.R.drawable.ic_menu_delete)
            .setOngoing(true)
            .setShowWhen(false)
            .setContentIntent(PendingIntent.getActivity(this, 1, Intent(), PendingIntent.FLAG_UPDATE_CURRENT))
            .setPriority(Notification.PRIORITY_MIN)
            .build()
            .apply {
                startForeground(1, this)
            }
}
```

#おわりに
こんな感じですごく簡単に出来る訳なんですが、通知の強制キャンセルを利用するシナリオはほぼ皆無です。しかも完璧ではなくて、通知に優先度高めで音や振動が付けられていると一瞬だけ表示されます。通知の優先度や、チャンネルの設定を書き換えではどうかと試してみましたが、前者はセキュリティ例外、後者はそもそも自身のパッケージ以外のチャンネルは取得できないので無理っぽいです。また、通知が大量に来ている時はキャンセルされるまで少しラグがある時もあります。`NotificationListenerService`はインストール後の許可が必要で、許可時の警告と合わせて一般向けのアプリでの利用は難しいですが、通知周りの操作（タップやらキャンセルやら）をコールバックで受け取ることが出来るので結構いろんなことが出来ます。無効化できないアプリ、無効化できない通知で広告を送ってくるとても常識のある企業が作った端末を持っている方は個人用に使ってみてはいかがでしょうか。

[サンプルコード](https://github.com/mightyfrog/Notification-Auto-Cancel-Sample)をgithubにあげています。