実現したいこと
正直行きたくないなあ…でも断りの文面考えるのめんどくさいなあ…って誘いあるじゃないですか。
そういうときに、文章を選択すると「断る」という選択肢が表示されて、
飲み会以外にも、仕事の依頼やデートの誘いなど様々なシーンで重宝しそうです。
実装方法
今回のアプリはFlutterで開発します。理由は、私がFlutterしかできないから。
メニューへの追加
こちらのライブラリを使用すると、テキスト選択時のメニューに好きな項目を追加できるようです。
ただ、Androidしか対応していません。そもそもiOSではテキスト選択メニューをカスタマイズできないかも?一旦あきらめてAndroidだけ作ります。
AndroidManifest.xmlを編集
android/app/src/main/配下の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を新規作成する。
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クラスを編集
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に書きます。
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風トーク画面メーカーというサイト上で実験しました。
うーーーん、アプリ遷移せずに、他のアプリ上に表示したいんだけどな…。が、力尽きた。
反省
- Google翻訳みたいに他のアプリの上に表示する方法が分からなかった
- Androidでしか動かない
- iOSでメニューのカスタマイズってどうやるの?
- ShareExtensionを使えばできそうな予感はあったが、LINE上でうまく動かなかった
- アプリ作るよりLINEアカウント作る方が綺麗な解法だったかもしれない