GoogleHomeでスマホ遠隔操作

[Firebase Cloud Messaging](https://firebase.google.com/?hl=ja)を勉強する機会があったので、ついでに周りに転がってるGoogleHomeと[トレースロイド](https://github.com/knoxrd/traceroid)の機能を一部改造してGoogleHomeからスマホを遠隔操作してみました。
家でスマホが少し離れた場所にある場合にGoogleHomeにロック解除の命令を出しつつ取りに行くと多少便利かなとか思い。

#最終形
[![ビデオがありません](http://img.youtube.com/vi/57AW7zyLWo4/0.jpg)](http://www.youtube.com/watch?v=57AW7zyLWo4)

#仕組み
![enkaku_arch.png](https://qiita-image-store.s3.amazonaws.com/0/200860/59242056-ba58-f077-fe0a-9c544d35d6f8.png)

##IFTTTの設定
ここは特別な事はせず「ロック解除」と言ったらWebhookをキックするように設定。

##LambdaとFirebase
[公式サイト](https://firebase.google.com/docs/admin/setup?hl=ja)を参考に
$ npm install firebase-admin --save
等のコマンドをローカル環境で実行しZip圧縮しLambdaへアップロードして実行する。
↓がデータメッセージのPushをトリガするサンプルソースです。

```index.js
exports.handler = (event, context, callback) => {
    var admin = require("firebase-admin");

    // 公式サイトの「SDK を初期化する」で作成したJSONファイルへのパス
    var serviceAccount = require("./XXXXX.json");

    var ret = admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
      projectId: "XXXXXX"
    });

    // 送信する端末のID
    var registrationToken = "XXXXXXXXX";

    var payload = {
      data: {
        execname: "test123",
        test: "12345"
      }
    };

    admin.messaging().sendToDevice(registrationToken, payload)
      .then(function(response) {
        // See the MessagingDevicesResponse reference documentation for
        // the contents of response.
        callback(null, "success");
        admin.app().delete()
      })
      .catch(function(error) {
        callback(null, "error");
        admin.app().delete()
      });
};
```


##トレースロイド改造箇所
トレースロイドは[こちら](https://qiita.com/tanaka512/items/93d77295fc4ecaf2d4ec)で書いたとおりハードイベントを記録して再現する事が出来ます。
なので予め電源ボタンを押してからロックを解除するまでの動作を記録しておき、記録しておいた動作を発火させるトリガをPush通知にしただけです。

最初にmain関数にNOTIFY_WAIT_MODEという監視モードを追加して実行された際にforkして監視プロセスが常駐するような形に修正。
↓★箇所

```Android/kdbserver/src/kdbservier.c
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include "common.h"
 
 #define PLAY_MODE 1
 #define REC_MODE 2
 #define NOTIFY_WAIT_MODE 3   // ★★★Push反応モードを追加★★★
 #define PROCESS_NAME "kdbserverChild"
 
 int main(int argc, char** argv) {
     int getoptResult;
     int mode = 0;
     int runAsDaemon = 0;
     int autoSsMsec = 0;
     char logFile[26 + 7 + MAX_TEST_NAME_LEN + 1];
     char watchDevs[MAX_WATCH_DEV_NUM][MAX_WATCH_DEV_LEN + 1] = {{'\0'}};
     char cmd[15 + 26];
     int portNum = 9374;
 
// ★★★Push反応モードを追加★★★
//    while ((getoptResult = getopt(argc,argv,"r:p:da:n:vw:")) != -1) {
    while ((getoptResult = getopt(argc,argv,"r:p:da:n:vw:t")) != -1) {
         switch(getoptResult) {
         // Play log
         case 'p':
             mode = PLAY_MODE;
             snprintf(logFile, sizeof(logFile), "%sscript/%s", PUSHED_DIR, optarg);
             break;
         // Record log file
         case 'r':
             mode = REC_MODE;
             snprintf(cmd, sizeof(cmd), "rm -rf %sscript/", PUSHED_DIR);

// ・・・省略・・・


             snprintf(watchDevs[0], MAX_WATCH_DEV_LEN + 1, "%s", optarg);
             break;
         // Auto SS mode
         // Take SS automaticly when playing logfile
         case 'a':
             autoSsMsec = atoi(optarg);
             break;
         case 'n':
             portNum = atoi(optarg);
             break;
// ★★★Push反応モードを追加★★★
        case 't':
            printf("t mode!\n");
            mode = NOTIFY_WAIT_MODE;
            break;
// ★★★Push反応モードを追加★★★
         case 'v':
            printf("kdb server version 1.0.0\n");
             return 0;
         default:
             return 1;
         }
     }
 
     if (watchDevs[0][0] == '\0') {
         get_read_dev_list(watchDevs);
     }
 

// ・・・省略・・・



                 // _exit will prevent calling method registerd by atexit()
                 _exit(EXIT_SUCCESS);
             }
             // I am child process.
             // Set process name.
             sprintf(argv[0], PROCESS_NAME);
         }
         start_event_receiver(portNum);
         start_dev_read(watchDevs);
         script_loop(logFile);
// ★★★Push反応モードを追加★★★
    } else if (mode == NOTIFY_WAIT_MODE) {
        int pid = fork();
        if (pid < 0) {
            printf("Fork faild.\n");
            exit(EXIT_FAILURE);
        } else if (pid > 0) {
            // I am parent process.
            // _exit will prevent calling method registerd by atexit()
            _exit(EXIT_SUCCESS);
        }
        // I am child process.
        // Set process name.
        sprintf(argv[0], PROCESS_NAME);
        printf("NOTIFY_WAIT_MODE\n");

        // No way to end so use kill command.
        start_playlog_event_receiver(watchDevs);
// ★★★Push反応モードを追加★★★
     }
 
     return 0;
 }

```



次に実際にAndroidアプリからの通知を監視する処理を追加。
↓★箇所

```Android/kdbserver/src/event_receiver.c

// ・・・省略・・・


//★★★追加★★★
// main thread will block!!
void start_playlog_event_receiver(char devs[MAX_WATCH_DEV_NUM][MAX_WATCH_DEV_LEN + 1]) {
    logcat_event_receiver_loop_with_playloginfo(devs);
}
//★★★追加★★★

 static void *logcat_event_receiver_loop(void *arg) {
     FILE *fp;
     char buf[128];
 
     fp = popen("logcat -c && logcat", "r");
 
     if (fp == NULL) {
         printf("logcat faild\n");
         return 0;
     }
 
     while (fgets(buf, 128, fp) != NULL) {
         if (strstr(buf, "trr001_hbrwfiuahwerdfbjdvaknjiuweahgrt") != NULL) {
             exe_command_if_necessary(KDB_CMD_ORIENTATION);
         }
     }
     pclose(fp);
 }

//★★★追加★★★
static void logcat_event_receiver_loop_with_playloginfo(char devs[MAX_WATCH_DEV_NUM][MAX_WATCH_DEV_LEN + 1]) {
    FILE *fp;
    char buf[128];
    char logFile[26 + 7 + MAX_TEST_NAME_LEN + 1];

    fp = popen("logcat -c && logcat", "r");

    if (fp == NULL) {
        printf("logcat faild\n");
        return;
    }

    snprintf(logFile, sizeof(logFile), "%sscript/%s", PUSHED_DIR, "test123");

    while (fgets(buf, 128, fp) != NULL) {
        if (strstr(buf, "trr001_hbrwfiuahwerdfbjdvaknjiuweahgrt") != NULL) {
            // exe_command_if_necessary(KDB_CMD_ORIENTATION);
            play_log_file(devs, logFile, 0);
        }
    }
    pclose(fp);
}
//★★★追加★★★

// ・・・省略・・・

```
ちなみにトレースロイドの中枢はkdbserverフォルダ配下にあるLinuxバイナリ実行ファイルなのですが、Android端末の画面の回転の検知と画面の回転の再現の為に通常のapkファイルと連携する機能が備わっており(logcat_event_receiver_loop関数)、そして残念ながらAndroidアプリからバイナリへ情報を伝達する手段がadbログへの出力となっています(※)。
今回もそれに便乗してPushを受信するアプリからのログ出力を検知して予め記録しておいたシナリオファイル(test123)を実行する形になっています。
※Androidアプリと上手に連携する方法が他にあったら教えて下さい…。
※プロセス間通信を試そうとしたのですが、ベンダー依存なのか実装ミスなのか動く端末と動かない端末があったりしたので挫折してログ出力での連携方法になっています。
※android_event_receiver_loop()関数あたりに名残が残ってますが。。

トレースロイド側の改造はたったこれだけ。

##Push通知受信アプリ

Push通知受信アプリはデータメッセージを受信したらadbログを出力するだけ。

```MyFirebaseMessagingService.java
public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        Log.d("MessagingService", "onMessageReceived");

        Map<String, String> data = remoteMessage.getData();

        Log.d("test", "trr001_hbrwfiuahwerdfbjdvaknjiuweahgrt");
    }
}
```



#改造バイナリと使い方
一応バイナリと使い方を公開しておきます。
Linux用です。

1. [こちら](https://drive.google.com/file/d/1EsNZdd5ZC3NzW8vFwfwBZxfhf3IbYkK_/view?usp=sharing)からZipファイルを落として解凍
2. trr.configのADB_PATHを自身のadbへのパスに書き換える
3. $ cd TraceRoid/
4. 端末にインストール
    5. $ ./trr -i
6. 操作の記録を開始
    7. $ ./trr -r start test123
8. 端末上で記録したい操作をする(今回は電源ボタンを押してロックを解除するまで)
9. 操作の記録停止
    10. $ ./trr -r stop
11. Push監視プロセス常駐
    12. $ adb shell
    13. $ /data/local/tmp/kdbserver/kdbserver -t
    14. USB抜く
15. Push通知を受信させる
    16. Push受信してadbログを出力するアプリは自身で作成して下さい
17. 用が済んだら監視プロセスをKill
    18. $ adb shell ps | grep kdb
    19. $ adb shell kill XXXX

#残課題
 - test123という一つの動作しか発火させられないので、データメッセージに発火させる動作も設定出来るようにする
 - データメッセージはプロセスが常駐していないと受信できないがAndroidは結構色んなトリガでServiceを殺すので、MyFirebaseMessagingServiceをちゃんとした常駐サービスに仕立てあげる

#感想
少しだけ実運用てみた感想。

- 便利な場合もあった
- Push通知が時間がかかる場合もあるので微妙なシーンも多々…
- Push通知自体の実装は結構簡単に出来る
- 他に自動化する操作を変えると何か便利なものが生まれるような気がする
- セキュリティ的には問題ありまくり
- トレースロイドの導入は一般の人にはハードルが高い

#参考リンク
https://qiita.com/gen0083/items/86479f72cfa31e96a3ba
https://qiita.com/kirimin/items/f16b22625a365f5d64f3
https://firebase.google.com/docs/admin/setup?hl=ja