飲み会の誘いを脳みそ使わず断りたい

# 実現したいこと
正直行きたくないなあ…でも断りの文面考えるのめんどくさいなあ…って誘いあるじゃないですか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/db18a27b-5420-f0ed-253e-1d7628dbfce2.png)

　
そういうときに、文章を選択すると「断る」という選択肢が表示されて、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/bcca217b-b32f-dc70-496b-5dec24c0ffa9.png)

　
断りの文面が生成されたら便利じゃないですか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/2dad4a91-1cf4-7843-dccd-5524ba56c716.png)

飲み会以外にも、仕事の依頼やデートの誘いなど様々なシーンで重宝しそうです。
# 実装方法
今回のアプリはFlutterで開発します。理由は、私がFlutterしかできないから。

## メニューへの追加

こちらのライブラリを使用すると、テキスト選択時のメニューに好きな項目を追加できるようです。

https://pub.dev/packages/flutter_process_text

ただ、Androidしか対応していません。そもそもiOSではテキスト選択メニューをカスタマイズできないかも？一旦あきらめてAndroidだけ作ります。


### AndroidManifest.xmlを編集
android/app/src/main/配下のAndroidManifest.xmlを編集して、下記のコードを追加します。
```AndroidManifest.xml
<activity 
        android:name=".ProcessTextActivity" 
        android:label="断る"
        android:theme="@android:style/Theme.NoDisplay">
          <intent-filter>
              <action android:name="android.intent.action.PROCESS_TEXT" />
              <data android:mimeType="text/plain"/>
              <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
  </activity>
```
android:labelに設定した文言がメニューに表示されるようです。


### Activityクラスを作成
android/app/src/main/java/com/example/{プロジェクト名} 配下にProcessTextActivity.javaを新規作成する。

```ProcessTextActivity.java
package com.example.process_text;

import com.divyanshushekhar.flutter_process_text.FlutterProcessTextPlugin;
import io.flutter.embedding.android.FlutterActivity;
import android.os.Bundle;

public class ProcessTextActivity extends FlutterActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        boolean issAppRunning = MainActivity.getIsAppRunning();
        FlutterProcessTextPlugin.listenProcessTextIntent(issAppRunning);
    }
}
```

### MainActivityクラスを編集

```MainActiviety.java
package com.example.process_text;

import io.flutter.embedding.android.FlutterActivity;

import android.app.ActivityManager;
import android.content.Context;
import android.os.Bundle;
import java.util.List;

public class MainActivity extends FlutterActivity {
   private static boolean isAppRunning;

   public static boolean getIsAppRunning() {
     return isAppRunning;
   }

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
        isAppRunning = isAppRunning(this);
   }

    public static boolean isAppRunning(Context context) {
        final String packageName = context.getPackageName();
        final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        final List<ActivityManager.RunningAppProcessInfo> processInfo = activityManager.getRunningAppProcesses();
        if (processInfo != null)
        {
            for (final ActivityManager.RunningAppProcessInfo info : processInfo) {
                if (info.processName.equals(packageName)) {
                    return true;
                }
            }
        }
        return false;
    }
}
```
正直このあたりのファイルは普段いじらないのでよく意味が分かっていません。サンプルコードをコピペして最低限編集しただけ。
Flutterで作っているのにjava書かされるの勘弁してほしいです。

###  画面を作成
やっとこさDartを触れます。今回は面倒なのですべてmain.dartに書きます。

```main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter_process_text/flutter_process_text.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:math';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late final Stream<String> _processText;

  @override
  void initState() {
    super.initState();
    FlutterProcessText.initialize(
      showConfirmationToast: true,
      showRefreshToast: true,
      showErrorToast: true,
      confirmationMessage: "Text Added",
      refreshMessage: "Got all Text",
      errorMessage: "Some Error",
    );
    _processText = FlutterProcessText.getProcessTextStream;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(''),
      ),
      body: Column(
        children: [
          SizedBox(height: 100),
          Center(
            child: StreamBuilder<String?>(
              stream: _processText,
              builder: (context, snapshot) {
                String refuseText = '';
                if (snapshot.data != null) {
                  // 受け取った文面に応じて、断り文章を作成する
                  refuseText = _getRefuseText(snapshot.data!);
                }

                return Container(
                  padding: const EdgeInsets.all(10),
                  width: 350,
                  decoration: BoxDecoration(
                    color: Colors.black12,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Text(
                    refuseText,
                    style: TextStyle(fontSize: 20),
                  ),
                );
              },
            ),
          ),
          const SizedBox(height: 30),
          ElevatedButton.icon(
            icon: const Icon(Icons.copy),
            onPressed: () {},
            label: const Text('コピー'),
          ),
          const SizedBox(height: 10),
          ElevatedButton.icon(
            icon: const Icon(Icons.refresh),
            onPressed: _launchUrl,
            label: const Text('再生成'),
          ),
        ],
      ),
    );
  }
}

String _getRefuseText(String text) {
  const List<String> candidates = [
    'あー、その日は友達の結婚式があるんだよね',
    '悪い、その週は帰省予定で参加できなそう…',
    'ごめん、その日はカンボジア行くから参加できないわ🙏',
  ];
  // ランダムに選択
  final random = Random();
  return candidates[random.nextInt(candidates.length)];
}

```
断りの文面は、あらかじめセットした候補文からランダムに返しています。
相手のメッセージを分類するファンクションを挟み、カテゴリ（飲み会, デート, 仕事など）に応じた断りの文面を用意すれば、より実用的になりそう。

AIで生成する方法もあり得ますが、飲み会断るために課金はしないよねと思い、無料で運用できる方法を採用しました。


# 出来上がったもの
本当はLINEを使って検証したかったのですが、エミュレーターにLINEを入れるのは大変なので、LINE風トーク画面メーカーというサイト上で実験しました。

https://hi0a.com/demo/-js/js-talk-line/


こういうアレな感じの誘いが来た時に、
![スクリーンショット 2024-12-14 14.16.31 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/edf8fb50-d5fa-bdb2-d1b2-f71914a06a65.png)

　
「断る」を選択すると、
![スクリーンショット 2024-12-14 14.19.28 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/4d0d8c5f-6ec1-416b-5513-85d6df5eac84.png)

　
こうです！！
![スクリーンショット 2024-12-14 15.00.25 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254014/4022f819-a19f-9a95-2768-d24fefec56f4.png)

うーーーん、アプリ遷移せずに、LINE上に表示したいんだけどな…。が、力尽きた。


# 反省
- Google翻訳みたいに他のアプリの上に表示する方法が分からなかった
- Androidでしか動かない
    - iOSでメニューのカスタマイズってどうやるの？
    - ShareExtensionを使えばできそうな予感はあったが、LINEでそれを起動する方法が分らなかった
- アプリ作るよりLINEアカウント作る方が綺麗な解法だったかもしれない