ElixirDesktop: WebViewからAndroid/iOSの通知を送信する方法

# リポジトリ

以下のリポジトリで、記事中に含まれるコードを確認できる。

https://github.com/BlackTanuTanuNinjas/tutorial_notification

# 記事を読むことできるようになること

* ElixirDesktopでAndroid/iOSの通知を送信できる

# 省略する内容

* ElixirDesktopの初期化手順
* webviewからAndroid/iOSの関数を呼び出す方法

# 本題

ElixirDesktopでWebViewからAndroid/iOSの通知を送信実装をまとめる。

## Android

Androidでは以下の5ステップで通知の送信を実装する。

1. 権限を追加する
2. 起動時に権限を要求する
3. 通知のチャネルを作成する
4. 通知の送信処理を実装する
5. 呼び出しを実装する

### 権限を追加する

Androidで通知を送信するためには、通知の権限を追加する必要がある。
`AndroidManifest.xml`に以下を記述して、権限を追加する。

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
```    

### 権限の要求

通知の権限を追加したあと、ユーザーに権限の要求を行う必要がある。
下記の関数を定義し、`onCreate()`内で実行することで、アプリの実行時に権限の要求ができる。

```kotlin:MainActivity.kt
import android.Manifest
    
    // 省略

    private fun requestNotificationPermission() {
        // Android 13以上で権限必要
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val notificationPermission = Manifest.permission.POST_NOTIFICATIONS

            if (ContextCompat.checkSelfPermission(this, notificationPermission) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(notificationPermission),
                    REQUEST_NOTIFICATION_PERMISSION
                )
            } else {
                println("通知権限は許可されている")
            }
        } else {
            println("通知権限の要求は不要")
        }
    }
```

### チャネルの作成

通知を送信する前に、チャネルを作成する必要がある。
下記の関数を定義し、`init()`内に追加することで通知を送信する準備ができる。

```kotlin:Bridge.kt
    fun createNotificationChannel() {
        val channel = NotificationChannel("tutorial_call_channel", "チュートリアル通知", NotificationManager.IMPORTANCE_DEFAULT).apply {
            description = "通知送信チュートリアルの通知チャネル"
        }
        val notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }
```

### 通知の送信処理

通知を送信する処理を以下のように実装する
WebViewから呼び出せるように`@JavascriptInterface`を付与する。

```kotlin:Bridge.kt
    @JavascriptInterface
    fun sendNotification() {
        val intent = Intent(applicationContext, MainActivity::class.java)

        val pendingIntent: PendingIntent = PendingIntent.getActivity(applicationContext, 0, intent,  PendingIntent.FLAG_IMMUTABLE)

        val notification = NotificationCompat.Builder(applicationContext, "tutorial_call_channel")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("tutorial_notificationの通知")
            .setContentText("テキスト")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setDefaults(NotificationCompat.DEFAULT_ALL)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true) 
            .build()

        val notificationManager =
            applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        notificationManager.notify(1, notification)
    }
```

WebViewから呼び出せるように`init()`に下記を追加する

```kotlin:Bridge.kt
webview.addJavascriptInterface(this, "Android")
```

### 呼び出しを実装する

WebView側のJavaScriptで以下のように呼び出す

```javascript
window.Android.sendNotification()
```

## IOS

1. 通知の権限をリクエストする
2. 通知のデリゲートを設定する
3. 通知の送信処理を実装する
4. 呼び出しを実装する 

### 通知の権限をリクエストする

Androidと同様に通知を送信するためには、通知の権限を追加する必要がある。
`アプリ名.swift`に以下のように`AppDelegate`クラスを追加して権限の要求をできる

```swift:アプリ名.swift
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions
                     launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        
        let center = UNUserNotificationCenter.current()
        center.delegate = self
        requestNotificationAuthorization()
        return true
        
    }
    
    private func requestNotificationAuthorization() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in
            if granted {
                print("通知の権限が許可された")
            } else {
                print("通知の権限が拒否された")
            }
        }
    }
}
```

### 通知のデリゲートを設定する

`通知のデリゲート`とは、`通知を受信した時の処理`という意味であり、これを実装することで通知をどのように表示するかを設定する。

以下のように記述することで、フォアグラウンドで画面にアラート表示しつつ、通知音を鳴らし、通知タスクに通知を表示できる。

```swift:アプリ名.swift
extension AppDelegate {
    // フォアグランド
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .sound, .list])
    }

    // バックグランド
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}
```

### 通知の送信処理を実装する

通知を送信する処理は次のように実装できる。

```swift:WebView.swift
    func sendNotification() {
        let content = UNMutableNotificationContent()
        content.title = "tutorial_notificationの通知"
        content.body = "テキスト"
        content.sound = .default
        content.badge = 1
        let identifier = "通知送信チュートリアルの通知チャネル"

        let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)

        UNUserNotificationCenter.current().add(request) { (error: Error?) in
            if error != nil {
                print("通知の送信に失敗した")
            } else {
                print("通知の送信に成功した")
            }
        }
    }
```

WebViewから呼び出せるように`override init()`に下記を追加する

```swift:WebView.swift
configuration.userContentController.add(self, name: "sendNotification")
```

そして`userContentController`に以下を追加

```swift:WebView.swift
case "sendNotification":
    sendNotification()
```

### 呼び出しを実装する

WebView側のJavaScriptで以下のように呼び出す

```javascript
window.webkit.messageHandlers.sendNotification.postMessage("sendNotification");
```

# まとめ

Android/iOSともに以下の手順で通知を実装できる。

1. 権限を追加する
2. 権限を要求する
3. 通知を送信する処理を実装する

ただし、iOSでは`AppDelegate`を記述して通知の表示方法を設定しなければ通知が正しく表示されない点に注意が必要である。

一方で、AndroidにはiOSの`AppDelegate`に相当する仕組みは存在しない。そのため、通知をアラート表示させたい場合は、端末の設定に依存することになる。