LINEの通知から色々な情報をスクレイピングする

# はじめに
2011年6月23日にLINEサービスが開始され今年で9年目になりますが、国内の月間アクティブユーザー数が約8,700万人ということで、身近でもLINEを使っているという方はほとんどだと思います。
普段のやり取りは主にLINEで行うと思いますが、頻繁に使うLINEからデータが取れれば色々活用できると思い、どうにかやりとりの情報を取得できないかと考えました。

# やり方をざっくり説明すると...
LINEでメッセージが届く
↓
通知が表示される
↓
通知内容を、Androidの「通知へのアクセス」機能を使って取得する
↓
必要な情報のみスクレイピングする

こんな感じです。
詳しいやり方はこの後紹介します。
（前置きを飛ばしたい方は[ここ](#スクレイピング方法の目次)をクリック）

# スクレイピングの前に...「通知を取得する方法」
参考になるプロジェクトがあるのでそれを使うと便利です。
こちら→ https://github.com/oggata/NotificationListenerServiceDemo
ダウンロード後にAndroid Studioを開くと、次のようになります。
![000261.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/a03db01e-69e3-c403-2528-79f5502b439e.png)
2015年製のプロジェクトなので、Gradleバージョンが古いため警告が表示されます。
ファイルメニュー→プロジェクト構造...を開き、Android Gradle プラグインバージョンとGradle バージョンを任意のものに変更してOKをクリックしてください。
![000262.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/b807e998-6521-adfb-b92e-e749e21ff4aa.png)
（私はプラグイン4.0.0・Gradle6.1.1を使いました。）

OKをクリックした後、このような画面になったら、「repositories」の部分2箇所を次のように変更します。
![000263.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/8c394d5d-4020-9848-fe16-187f18ae1df8.png)

```gradle:build.gradle
buildscript {
    repositories {
        google()
        jcenter()
    }
    
    ...省略

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url "https://jitpack.io"
        }
    }
}
```

修正が完了したら、左上の「再試行」をクリックして、同期が終わるまで待ちます。

すると再度エラーが出るので、build.gradleの「targetSdkVersion 21」を削除し、リンクになっている「Remove minSdkVersion and sync project」をクリックし、次の記述を削除します。
![000264.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/bd9d5748-da6b-54be-d482-6c9946f8dad0.png)

```xml:AndroidManifest.xml
<uses-sdk
        android:minSdkVersion="18"
        android:targetSdkVersion="18" />

...他の部分は省略
```

削除したら、再度build.gradleを開いて右上の「今すぐ同期」をクリックします。
これでエラーがすべて消えるはずです。

このプロジェクトは、「通知が来たら、その生データをオーバーレイで画面の上から流れるように表示する」というものですが、確認に使うには少し不便なので改良します。

### 「通知へのアクセス」の設定画面を開くボタンを追加する
activity_main.xmlに適当にボタンを配置し、次のプログラムを追加します。

```MainActivity.java
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //通知アクセス許可画面
                Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
                startActivity(intent);
            }
        });
```

### オーバーレイをやめて、EditTextに表示するようにする
コピペできると良いと思ったので、今回はTextViewではなくEditTextを使います。
activity_main.xmlの全画面にEditTextを配置し、次のプログラムを追加します。

```MainActivity.java
public class MainActivity extends Activity {
    EditText editText;
    String log = "";

...省略

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = (EditText) findViewById(R.id.editText);

...省略

    class NotificationReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            String msg = intent.getStringExtra("notification_msg") + "\n";
            
            final DateFormat df = new SimpleDateFormat("MM/dd HH:mm:ss");
            final Date date = new Date(System.currentTimeMillis());
            log=df.format(date)+"\n"+msg+"----------------\n"+log;

            editText.setText(log);
        }
    }
}
```

新しい通知の方が上になるように表示されます。

### リセットボタンを追加する
ログと表示をリセットするためのボタンを付けます。
activity_main.xmlに適当にボタンを配置し、次のプログラムを追加します。

```MainActivity.java
        Button button2 = (Button) findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //リセット
                log="";
                editText.setText("");
            }
        });
```

### 通知の詳細情報を取得する
「sbn.getNotification().extras」を使用すると、実際通知には表示されない情報も取得することができます。
送信者名やメッセージの全文などは、すべてこの中から取得することができます。

```NLService.java
...省略

            i.putExtra("notification_msg", ""
                    + "ID :" + sbn.getId() + "\n"
                    + "Txt :" + sbn.getNotification().tickerText + "\n"
                    + "Pkg :" + sbn.getPackageName() + "\n"
                    + "Tim :" + sbn.getPostTime() + "\n"
                    + "extras :" + sbn.getNotification().extras+"\n"
                    + "\n");

...省略
```

### アプリの実行画面
レイアウト等は変更している箇所があります。
<img width="300" alt="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/6d2f3141-695c-2efc-b2e8-9764cc7f5ad2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409628/6d2f3141-695c-2efc-b2e8-9764cc7f5ad2.png">

# スクレイピング方法の目次
分かりやすいように、スクレイピングしたい情報ごとにまとめています。

<dl>
  <dt>【 判別 】</dt>
  <dd>・ <a href="#lineの通知かどうか">LINEの通知かどうか</a></dd>
  <dd>・ <a href="#メッセージか無料通話か">メッセージか無料通話か</a></dd><br>
  <dt>【 メッセージの場合 】</dt>
  <dd>・ <a href="#送信者名">送信者名</a></dd>
  <dd>・ <a href="#対象グループ名">対象グループ名</a></dd>
  <dd>・ <a href="#メッセージ内容">メッセージ内容</a></dd>
  <dd>・ <a href="#送信されたlineスタンプのurl">送信されたLINEスタンプのURL</a></dd>
  <dd>・ <a href="#送信者のプロフィール画像">送信者のプロフィール画像</a></dd>
  <dd>・ <a href="#チャットid">チャットID</a></dd>
  <dd>・ <a href="#既読にした-or-通知が削除された場合">既読にした or 通知が削除された場合</a></dd><br>
  <dt>【 無料通話の場合 】</dt>
  <dd>・ <a href="#発信開始">発信開始</a></dd>
  <dd>・ <a href="#着信">着信</a></dd>
  <dd>・ <a href="#不在着信">不在着信</a></dd>
  <dd>・ <a href="#通話開始">通話開始</a></dd>
  <dd>・ <a href="#通話終了">通話終了</a></dd>
</dl>

### countStringInString関数について
プログラムの一部に、文字の出現回数を数えるcountStringInString関数が使用されています。

```java
    //文字出現回数取得
    public static int countStringInString(String target, String searchWord) {
        //return (target.length() - target.replaceAll(searchWord, "").length()) / searchWord.length();

        Pattern p = Pattern.compile(searchWord);
        Matcher m = p.matcher(target);
        String result = m.replaceAll("");

        int out = target.length() - result.length();
        return out;
    }
```

## 判別

### LINEの通知かどうか
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if(sbn.getPackageName().equals("jp.naver.line.android")) {
            //ここにLINEの通知を受信した際にしたい処理を記述します
            
        }
```

### メッセージか無料通話か
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880002") != -1 || String.valueOf(sbn.getId()).equals("110000")) {
             //不在着信 or 無料通話系の通知ID の場合→無料通話
             
        } else {
             //違った場合→メッセージ
             
        }
```

記述場所：「NLService.java」の「onNotificationRemoved」内

```NLService.java
        if (String.valueOf(sbn.getId()).equals("110000")) {
             //無料通話が拒否されたり終話ボタンが押されたりして終了した場合→無料通話
             
        }
```

## メッセージの場合

### 送信者名
送信した相手の名前を取得できます。
個別とグループ、両方の場合で取得可能です。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数

        //送信者名を抜き出す
        String Name="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("android.title=")==0){
                //先頭に「android.title=」がきた→送信者名
                Name = extars[a].substring(14);  //先頭から文字削除「android.title=」
                break;
            }
        }
        if(Name.indexOf(" - ")!=-1){
            //Android5・6系ではNameが「[送信者名] - [グループ名]」になる
            Name=Name.split(" - ")[0];
        }
        //Name：送信者名
```

### 対象グループ名
メッセージが送信されたグループの名前を取得できます。
グループではなく個別だった場合はnullが返ります。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数
        
        //グループ名を抜き出す
        String GroupName="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("android.title=")==0){
                //先頭に「android.title=」がきた→送信者名
                GroupName = extars[a].substring(14);  //先頭から文字削除「android.title=」
                break;
            }
        }
        if(GroupName.indexOf(" - ")!=-1){
            //Android5・6系ではNameが「[送信者名] - [グループ名]」になる
            GroupName=Name.split(" - ")[1];
        }else {
            GroupName=sbn.getNotification().extras.getString(Notification.EXTRA_CONVERSATION_TITLE);
            if(GroupName!=null){
                Name=Name.substring(GroupName.length()+2);  //先頭に「: 」が付与されているから削除する
            }
        }
        //GroupName：グループ名
```

### メッセージ内容
送信されたメッセージを取得できます。
通知では「...」と表示されて省略される場合でも、全文取得可能です。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        //内容を抜き出す
        String Text = sbn.getNotification().extras.getString(Notification.EXTRA_TEXT);
        //Text：内容
```

### 送信されたLINEスタンプのURL
「<a href="https://stickershop.line-scdn.net/products/">https://stickershop.line-scdn.net/products/</a>」から始まり「.png」で終わるURLが取得できます。
このURLを元にHTTP GETすればスタンプの画像を表示することも可能です。
（HTTP GETして画像を表示する方法：https://akira-watson.com/android/httpurlconnection-get.html）
スタンプではなかった場合はnullが返ります。
ちなみに、画像サイズはスタンプによりバラバラですが大体120px以上はあります。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        //スタンプの画像URLを抜き出す
        String StampURL="null";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf(" line.sticker.url=")==0){
                //先頭に「line.sticker.url=」がきた→スタンプの画像URL
                StampURL = extars[a].substring(18);  //先頭から18文字削除「line.sticker.url=」
                break;
            }
        }
        //StampURL：スタンプの画像URL
```

### 送信者のプロフィール画像
Bitmap型で取得できるので、ImageViewに表示したりPNGとして保存したりできます。
画像サイズは108px × 108pxです。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        Bitmap LargeIcon = drawableToBitmap(sbn.getNotification().getLargeIcon().loadDrawable(NLService.this));

...省略

    //Drawable型をBitmap型に変換
    public static Bitmap drawableToBitmap (Drawable drawable) {
        Bitmap bitmap = null;

        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if(bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }

        if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
        } else {
            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        }

        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }
```

### チャットID
友達やグループごとに決まっている識別子です。
これを使えば、同じ名前の友達やグループがあったとしても判別することができます。
恐らく、LINEの「トークショートカットを作成」機能はこのIDを使用したURLスキームだと思われますが、検索しても出てきませんでした...
※友達登録時に使うIDではありません。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        //整形
        String ex=String.valueOf(sbn.getNotification().extras);
        ex=ex.substring(8);  //先頭から8文字削除「Bundle[{」
        String[] extars = ex.split(",");
        int dir = countStringInString(ex, ",");  //「,」の数
        
        //チャットIDを抜き出す
        String ChatID="";
        for (int a = 0; a < dir; a++){
            if(extars[a].indexOf("line.chat.id=")==0){
                //先頭に「line.chat.id=」がきた→チャットID
                ChatID = extars[a].substring(13);  //先頭から13文字削除「line.chat.id=」
                break;
            }
        }
        //ChatID：チャットID
```

### 既読にした or 通知が削除された場合
LINEのトーク画面を開いて既読にしたか、メッセージ通知を削除した場合に呼ばれます。
記述場所：「NLService.java」の「onNotificationRemoved」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880000") != -1) {  //15880000or16880000のどちらか？
            //既読にした or 通知が削除された場合
            
        }
```


## 無料通話の場合
次のような流れで処理が呼ばれます。

+ 電話をかけて通話せずに終了したとき：発信開始→通話終了
+ 電話をかけて通話して通話が終わったとき：発信開始→通話開始→通話終了
+ 着信があり相手が切ったとき：着信→通話終了→不在着信
+ 着信があり自分が切ったとき：着信→通話終了
+ 着信があったが反応せず「応答なし」になったとき：（着信があり相手が切ったときと同じ）
+ 着信があり通話して終話ボタンを押したとき：着信→通話開始→通話終了

### 発信開始
自分から電話をかけたときに呼ばれます。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("発信中") != -1) {  //15880002or16880002のどちらか？
            //発信開始
            
        }
```


### 着信
電話がかかってきたときに呼ばれます。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("着信中") != -1) {  //15880002or16880002のどちらか？
            //着信
            
        }
```

### 不在着信
着信があり相手が切った（拒否ボタンを押した or 応答がなかった）場合に呼ばれます。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("880002") != -1 && Name.equals("LINE不在着信")) {  //15880002or16880002のどちらか？
            //不在着信
            
        }
```

### 通話開始
通話が開始されると呼ばれます。
記述場所：「NLService.java」の「onNotificationPosted」内

```NLService.java
        if (String.valueOf(sbn.getId()).indexOf("110000") != -1 && Text.indexOf("通話中") != -1) {  //15880002or16880002のどちらか？
            //通話開始
            
        }
```

### 通話終了
何らかの方法で通話が終了したときに必ず呼ばれます。
記述場所：「NLService.java」の「onNotificationRemoved」内

```NLService.java
        if (String.valueOf(sbn.getId()).equals("110000")) {
            //通話終了
            
        }
```

# 注意
これは実際にLINEの通知を受信してみて、どうやったらうまくスクレイピングできるかを検証して分かったやり方です。
LINEの仕様変更によっては、今後使えなくなる可能性が十分ありますのでご注意ください。
（実際に2018年に大幅な仕様変更があり、2020年にも一部変更されました。）

また、Android 7.0未満（？）では、試しているうちに「onNotificationPosted」が呼ばれなくなる不具合があるようです。
検証に使用する場合は、Android 7.0以上を使ってください。
また、minSdkVersionを24（Android 7.0）以上に設定しておくことをおすすめします。

# 参考サイト

+ [Androidでプッシュ通知が来たタイミングでhogehogeする](https://qiita.com/oggata/items/67f29d8404c655903858)
+ [Drawable型をBitmap型に変換する](https://stackoverflow.com/questions/3035692/how-to-convert-a-drawable-to-a-bitmap)