diff --git a/app/src/main/java/io/github/chipppppppppp/lime/LimeOptions.java b/app/src/main/java/io/github/chipppppppppp/lime/LimeOptions.java index ce61fa79..bcde8baf 100644 --- a/app/src/main/java/io/github/chipppppppppp/lime/LimeOptions.java +++ b/app/src/main/java/io/github/chipppppppppp/lime/LimeOptions.java @@ -31,12 +31,17 @@ public Option(String name, int id, boolean checked) { public Option preventUnsendMessage = new Option("prevent_unsend_message", R.string.switch_prevent_unsend_message, false); public Option sendMuteMessage = new Option("mute_message", R.string.switch_send_mute_message, false); public Option removeKeepUnread = new Option("remove_keep_unread", R.string.switch_remove_keep_unread, false); + public Option KeepUnreadLSpatch = new Option("Keep_UnreadLSpatch", R.string.switch_KeepUnreadLSpatch, false); public Option blockTracking = new Option("block_tracking", R.string.switch_block_tracking, false); public Option stopVersionCheck = new Option("stop_version_check", R.string.switch_stop_version_check, false); public Option outputCommunication = new Option("output_communication", R.string.switch_output_communication, false); public Option archived = new Option("archived_message", R.string.switch_archived, false); public Option callTone = new Option("call_tone", R.string.call_tone, false); - + public Option NaviColor = new Option("NaviColor", R.string.NaviColor, false); + public Option ReadChecker = new Option("ReadChecker", R.string.ReadChecker, false); + public Option Notif_invalid = new Option("Notif_invalid", R.string.Notif_invalid, false); + + public Option[] options = { removeVoom, removeWallet, @@ -56,10 +61,15 @@ public Option(String name, int id, boolean checked) { preventUnsendMessage, archived, sendMuteMessage, + ReadChecker, removeKeepUnread, + KeepUnreadLSpatch, blockTracking, stopVersionCheck, outputCommunication, - callTone + callTone, + NaviColor, + Notif_invalid + }; } diff --git a/app/src/main/java/io/github/chipppppppppp/lime/Main.java b/app/src/main/java/io/github/chipppppppppp/lime/Main.java index 86113d5a..8834567e 100644 --- a/app/src/main/java/io/github/chipppppppppp/lime/Main.java +++ b/app/src/main/java/io/github/chipppppppppp/lime/Main.java @@ -12,14 +12,18 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage; import io.github.chipppppppppp.lime.hooks.AddRegistrationOptions; import io.github.chipppppppppp.lime.hooks.Archived; +import io.github.chipppppppppp.lime.hooks.AutomaticBackup; import io.github.chipppppppppp.lime.hooks.BlockTracking; import io.github.chipppppppppp.lime.hooks.CheckHookTargetVersion; import io.github.chipppppppppp.lime.hooks.Constants; import io.github.chipppppppppp.lime.hooks.EmbedOptions; import io.github.chipppppppppp.lime.hooks.IHook; import io.github.chipppppppppp.lime.hooks.KeepUnread; +import io.github.chipppppppppp.lime.hooks.KeepUnreadLSpatch; import io.github.chipppppppppp.lime.hooks.ModifyRequest; import io.github.chipppppppppp.lime.hooks.ModifyResponse; +import io.github.chipppppppppp.lime.hooks.NaviColor; +import io.github.chipppppppppp.lime.hooks.Notif_invalid; import io.github.chipppppppppp.lime.hooks.OutputRequest; import io.github.chipppppppppp.lime.hooks.OutputResponse; import io.github.chipppppppppp.lime.hooks.PreventMarkAsRead; @@ -67,7 +71,11 @@ public class Main implements IXposedHookLoadPackage, IXposedHookInitPackageResou new OutputRequest(), new Archived(), new Ringtone(), - new UnsentCap() + new UnsentCap(), + new KeepUnreadLSpatch(), + new NaviColor(), + new AutomaticBackup(), + new Notif_invalid() }; public void handleLoadPackage(@NonNull XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/AutomaticBackup.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/AutomaticBackup.java new file mode 100644 index 00000000..615ada92 --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/AutomaticBackup.java @@ -0,0 +1,138 @@ +package io.github.chipppppppppp.lime.hooks; + + +import static android.content.ContentValues.TAG; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; +import io.github.chipppppppppp.lime.hooks.IHook; + +public class AutomaticBackup implements IHook { + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + + XposedHelpers.findAndHookMethod("jp.naver.line.android.activity.schemeservice.LineSchemeServiceActivity", + loadPackageParam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Intent intent = ((Activity) param.thisObject).getIntent(); + XposedBridge.log("onCreate Intent action: " + intent.getAction()); + handleIntent(intent, param.thisObject); + } + }); + } + private void handleIntent(Intent intent, Object activity) { + if (intent != null) { + String text = intent.getStringExtra(Intent.EXTRA_TEXT); + if ("トーク履歴のバックアップを開始".equals(text)) { + backupChatHistory(((Activity) activity).getApplicationContext()); + } + if ("トーク画像フォルダのバックアップを開始".equals(text)) { + backupChatsFolder(((Activity) activity).getApplicationContext()); + } + if ("バックアップを開始".equals(text)) { + backupChatHistory(((Activity) activity).getApplicationContext()); + backupChatsFolder(((Activity) activity).getApplicationContext()); + } + } + } + + + + private void backupChatHistory(Context appContext) { + File originalDbFile = appContext.getDatabasePath("naver_line"); + File backupDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "LimeBackup"); + if (!backupDir.exists() && !backupDir.mkdirs()) { + return; + } + File backupFileWithTimestamp = new File(backupDir, "naver_line_backup" + ".db"); + + try (FileChannel source = new FileInputStream(originalDbFile).getChannel()) { + try (FileChannel destinationWithTimestamp = new FileOutputStream(backupFileWithTimestamp).getChannel()) { + destinationWithTimestamp.transferFrom(source, 0, source.size()); + } + showToast(appContext, "自動バックアップが成功しました"); // トーストをUIスレッドで表示 + + } catch (IOException e) { + showToast(appContext, "自動バックアップ中にエラーが発生しました: " + e.getMessage()); + } + } + private void backupChatsFolder(Context context) { + File originalChatsDir = new File(Environment.getExternalStorageDirectory(), "Android/data/jp.naver.line.android/files/chats"); + File backupDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "LimeBackup"); + + if (!backupDir.exists() && !backupDir.mkdirs()) { + return; + } + + File backupChatsDir = new File(backupDir, "chats_backup"); + if (!backupChatsDir.exists() && !backupChatsDir.mkdirs()) { + return; + } + try { + copyDirectory(originalChatsDir, backupChatsDir); + Toast.makeText(context, "トーク画像フォルダのバックアップが成功しました", Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Toast.makeText(context, "トーク画像フォルダのバックアップ中にエラーが発生しました", Toast.LENGTH_SHORT).show(); + } + } + + private void copyDirectory(File sourceDir, File destDir) throws IOException { + if (!sourceDir.exists()) { + throw new IOException("Source directory does not exist: " + sourceDir.getAbsolutePath()); + } + + if (!destDir.exists()) { + destDir.mkdirs(); + } + + File[] files = sourceDir.listFiles(); + if (files != null) { + for (File file : files) { + File destFile = new File(destDir, file.getName()); + if (file.isDirectory()) { + copyDirectory(file, destFile); + } else { + copyFile(file, destFile); + } + } + } + } + + private void copyFile(File sourceFile, File destFile) throws IOException { + + if (destFile.exists()) { + destFile.delete(); + } + + try (FileChannel sourceChannel = new FileInputStream(sourceFile).getChannel(); + FileChannel destChannel = new FileOutputStream(destFile).getChannel()) { + destChannel.transferFrom(sourceChannel, 0, sourceChannel.size()); + } + } + + private void showToast(final Context context, final String message) { + new android.os.Handler(context.getMainLooper()).post(() -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + ); + } +} diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/Constants.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/Constants.java index b7868f9a..edf02a59 100644 --- a/app/src/main/java/io/github/chipppppppppp/lime/hooks/Constants.java +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/Constants.java @@ -18,6 +18,10 @@ public HookTarget(String className, String methodName) { static final HookTarget WEBVIEW_CLIENT_HOOK = new HookTarget("qH0.m", "onPageFinished"); static final HookTarget MUTE_MESSAGE_HOOK = new HookTarget("u71.b", "H"); static final HookTarget MARK_AS_READ_HOOK = new HookTarget("JL.e$d", "run"); + + //以下のクラス名はtestメゾットで出力させることが出来ます、(14.17.0になっています) + static final HookTarget NOTIFICATION_READ_HOOK = new HookTarget("H91.c", "invokeSuspend"); + static final HookTarget REQUEST_HOOK = new HookTarget("org.apache.thrift.k", "b"); static final HookTarget RESPONSE_HOOK = new HookTarget("org.apache.thrift.k", "a"); } diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/EmbedOptions.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/EmbedOptions.java index 5f6370dd..d398c702 100644 --- a/app/src/main/java/io/github/chipppppppppp/lime/hooks/EmbedOptions.java +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/EmbedOptions.java @@ -23,6 +23,13 @@ import android.widget.Switch; import android.widget.Toast; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.callbacks.XC_LoadPackage; @@ -249,6 +256,19 @@ public void onClick(View v) { } }); + + Button MuteGroups_Button = new Button(context); + MuteGroups_Button.setLayoutParams(buttonParams); + MuteGroups_Button.setText("通知を無効にしているグループ"); + MuteGroups_Button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + MuteGroups_Button(context); + } + }); + layout.addView(MuteGroups_Button); + + buttonLayout.addView(copyButton); Button pasteButton = new Button(context); @@ -302,6 +322,10 @@ public void onDismiss(DialogInterface dialog) { } }); + + + + AlertDialog dialog = builder.create(); Button button = new Button(context); @@ -394,4 +418,67 @@ public void onClick(View view) { } ); } + + + private void MuteGroups_Button(Context context) { + // ファイルパスを指定 + File dir = context.getFilesDir(); // 例えば内部ストレージのファイルディレクトリ + File file = new File(dir, "Notification.txt"); + + // ファイルが存在する場合、内容を読み込む + StringBuilder fileContent = new StringBuilder(); + if (file.exists()) { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + fileContent.append(line).append("\n"); + } + } catch (IOException e) { + XposedBridge.log("Error reading the file: " + e.getMessage()); + } + } + + // 新しい内容を編集できるようにEditTextを表示する + final EditText editText = new EditText(context); + editText.setText(fileContent.toString()); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setMinLines(10); // 適切な行数を設定 + editText.setGravity(Gravity.TOP); // 上から入力されるように設定 + + // ボタン用のレイアウトパラメータを設定 + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + buttonParams.setMargins(16, 16, 16, 16); // 任意のマージン設定 + + // 保存ボタンを作成 + Button saveButton = new Button(context); + saveButton.setText("Save"); + saveButton.setLayoutParams(buttonParams); // レイアウトパラメータを設定 + saveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + // 編集した内容をファイルに保存 + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + writer.write(editText.getText().toString()); + XposedBridge.log("File saved successfully."); + } catch (IOException e) { + XposedBridge.log("Error saving the file: " + e.getMessage()); + } + } + }); + + // 編集画面を表示するためのLayoutに追加 + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + layout.addView(editText); + layout.addView(saveButton); + + // ダイアログを表示して編集画面を表示する + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle("通知を無効にしているグループ"); + builder.setView(layout); + builder.setNegativeButton("キャンセル", null); + builder.show(); + } + } diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnread.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnread.java index b54363ef..5e7c2d42 100644 --- a/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnread.java +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnread.java @@ -23,52 +23,98 @@ import io.github.chipppppppppp.lime.R; public class KeepUnread implements IHook { + static boolean keepUnread = false; @Override public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { if (limeOptions.removeKeepUnread.checked) return; - Class hookTarget; - hookTarget = loadPackageParam.classLoader.loadClass("jp.naver.line.android.common.view.listview.PopupListView"); - XposedBridge.hookAllConstructors(hookTarget, new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.hookAllConstructors( + loadPackageParam.classLoader.loadClass("jp.naver.line.android.common.view.listview.PopupListView"), + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + ViewGroup viewGroup = (ViewGroup) param.thisObject; + Context context = viewGroup.getContext(); + context.getApplicationContext().createPackageContext(Constants.MODULE_NAME, Context.CONTEXT_IGNORE_SECURITY); - ViewGroup viewGroup = (ViewGroup) param.thisObject; - Context context = viewGroup.getContext(); - Context moduleContext = context.getApplicationContext().createPackageContext(Constants.MODULE_NAME, Context.CONTEXT_IGNORE_SECURITY); - String textKeepUnread = moduleContext.getResources().getString(R.string.switch_keep_unread); - RelativeLayout layout = new RelativeLayout(context); - RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); - layout.setLayoutParams(layoutParams); + Context moduleContext = context.getApplicationContext().createPackageContext(Constants.MODULE_NAME, Context.CONTEXT_IGNORE_SECURITY); + String textKeepUnread = moduleContext.getResources().getString(R.string.switch_keep_unread); - Switch switchView = new Switch(context); - switchView.setText(textKeepUnread); - RelativeLayout.LayoutParams switchParams = new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); - switchParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE); + keepUnread = readStateFromFile(context); - switchView.setChecked(false); - switchView.setOnCheckedChangeListener((buttonView, isChecked) -> { - }); + RelativeLayout container = new RelativeLayout(context); + RelativeLayout.LayoutParams containerParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + container.setLayoutParams(containerParams); - layout.addView(switchView, switchParams); + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setColor(Color.parseColor("#06C755")); + background.setCornerRadii(new float[]{100, 100, 80, 30, 100, 100, 80, 30}); + container.setBackground(background); - ((ListView) viewGroup.getChildAt(0)).addFooterView(layout); - } - }); - + TextView label = new TextView(context); + label.setText(textKeepUnread); + label.setTextSize(18); + label.setTextColor(Color.WHITE); + label.setId(View.generateViewId()); + RelativeLayout.LayoutParams labelParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + labelParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + labelParams.setMargins(40, 0, 0, 0); + container.addView(label, labelParams); + + Switch switchView = new Switch(context); + RelativeLayout.LayoutParams switchParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + switchParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + switchParams.setMargins(0, 0, 40, 0); + switchView.setChecked(keepUnread); + switchView.setOnCheckedChangeListener((buttonView, isChecked) -> { + keepUnread = isChecked; + saveStateToFile(context, isChecked); + }); + + container.addView(switchView, switchParams); + + ((ListView) viewGroup.getChildAt(0)).addFooterView(container); + } + } + ); XposedHelpers.findAndHookMethod( loadPackageParam.classLoader.loadClass(Constants.MARK_AS_READ_HOOK.className), Constants.MARK_AS_READ_HOOK.methodName, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + if (keepUnread) { param.setResult(null); + } } } ); } - -} + private void saveStateToFile(Context context, boolean state) { + String filename = "keep_unread_state.txt"; + try (FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE)) { + fos.write((state ? "1" : "0").getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } + private boolean readStateFromFile(Context context) { + String filename = "keep_unread_state.txt"; + try (FileInputStream fis = context.openFileInput(filename)) { + int c; + StringBuilder sb = new StringBuilder(); + while ((c = fis.read()) != -1) { + sb.append((char) c); + } + return "1".equals(sb.toString()); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnreadLSpatch.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnreadLSpatch.java new file mode 100644 index 00000000..3c208dad --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/KeepUnreadLSpatch.java @@ -0,0 +1,91 @@ +package io.github.chipppppppppp.lime.hooks; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.Switch; +import android.widget.TextView; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; +import io.github.chipppppppppp.lime.R; + +public class KeepUnreadLSpatch implements IHook { + static boolean keepUnread = false; + + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + if (!limeOptions.KeepUnreadLSpatch.checked) return; + + XposedBridge.hookAllConstructors( + loadPackageParam.classLoader.loadClass("jp.naver.line.android.common.view.listview.PopupListView"), + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + ViewGroup viewGroup = (ViewGroup) param.thisObject; + Context context = viewGroup.getContext(); + + Context moduleContext = context.getApplicationContext().createPackageContext(Constants.MODULE_NAME, Context.CONTEXT_IGNORE_SECURITY); + String textKeepUnread = moduleContext.getResources().getString(R.string.switch_keep_unread); + + RelativeLayout container = new RelativeLayout(context); + RelativeLayout.LayoutParams containerParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + container.setLayoutParams(containerParams); + + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setColor(Color.parseColor("#06C755")); + background.setCornerRadii(new float[]{100, 100, 80, 30, 100, 100, 80, 30}); + + container.setBackground(background); + + TextView label = new TextView(context); + label.setText(textKeepUnread); + label.setTextSize(18); + label.setTextColor(Color.WHITE); + label.setId(View.generateViewId()); + RelativeLayout.LayoutParams labelParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + labelParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + labelParams.setMargins(40, 0, 0, 0); + container.addView(label, labelParams); + + Switch switchView = new Switch(context); + RelativeLayout.LayoutParams switchParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + switchParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + switchParams.setMargins(0, 0, 40, 0); + switchView.setChecked(false); + switchView.setOnCheckedChangeListener((buttonView, isChecked) -> { + keepUnread = isChecked; + }); + + container.addView(switchView, switchParams); + + ((ListView) viewGroup.getChildAt(0)).addFooterView(container); + } + } + ); + + XposedHelpers.findAndHookMethod( + loadPackageParam.classLoader.loadClass(Constants.MARK_AS_READ_HOOK.className), + Constants.MARK_AS_READ_HOOK.methodName, + new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + if (keepUnread) { + param.setResult(null); + } + } + } + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/NaviColor.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/NaviColor.java new file mode 100644 index 00000000..3ebffe21 --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/NaviColor.java @@ -0,0 +1,138 @@ +package io.github.chipppppppppp.lime.hooks; + +import android.app.Activity; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.view.Window; +import android.view.WindowManager; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; + + +public class NaviColor implements IHook { + + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + if (!limeOptions.NaviColor.checked) return; + XposedBridge.hookAllMethods(Activity.class, "onCreate", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + Activity activity = (Activity) param.thisObject; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Window window = activity.getWindow(); + window.setNavigationBarColor(Color.BLACK); + } + } + }); + XposedHelpers.findAndHookMethod("android.app.Activity", loadPackageParam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Activity activity = (Activity) param.thisObject; + Window window = activity.getWindow(); + + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.BLACK); + } + }); + } +} + /* + + XposedHelpers.findAndHookMethod( + View.class, + "onAttachedToWindow", + new XC_MethodHook() { + View view; + + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + view = (View) param.thisObject; + + + int viewId = view.getId(); + String resourceName = getResourceName(view.getContext(), viewId); + XposedBridge.log("View ID: " + viewId + ", Resource Name: " + resourceName); + + } + } + ); + } + private int getIdByName(Context context, String resourceName) { + return context.getResources().getIdentifier(resourceName, "id", context.getPackageName()); + } + + private String getResourceName(Context context, int resourceId) { + return context.getResources().getResourceEntryName(resourceId); + } +} + XposedHelpers.findAndHookMethod("android.view.View", loadPackageParam.classLoader, "onAttachedToWindow", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + View view = (View) param.thisObject; + + try { + + String resourceName = view.getResources().getResourceEntryName(view.getId()); + + + Drawable background = view.getBackground(); + if (background instanceof ColorDrawable) { + int bgColor = ((ColorDrawable) background).getColor(); + String hexColor = String.format("#%06X", (0xFFFFFF & bgColor)); // 色を16進数形式に変換 + + + if (hexColor.equals("#111111") || hexColor.equals("#1F1F1F")) { + ((ColorDrawable) background).setColor(Color.parseColor("#000000")); + XposedBridge.log("Changed Background Color of View Resource Name: " + resourceName + " from " + hexColor + " to #000000"); + } else { + XposedBridge.log("View Resource Name: " + resourceName + " Background Color: " + hexColor); + } + } + + if (view instanceof TextView) { + int textColor = ((TextView) view).getCurrentTextColor(); + String hexTextColor = String.format("#%06X", (0xFFFFFF & textColor)); // 色を16進数形式に変換 + + + if (hexTextColor.equals("#111111")) { + ((TextView) view).setTextColor(Color.parseColor("#000000")); + XposedBridge.log("Changed Text Color of View Resource Name: " + resourceName + " from " + hexTextColor + " to #000000"); + } else { + XposedBridge.log("View Resource Name: " + resourceName + " Text Color: " + hexTextColor); + } + } + + if (view instanceof Button) { + Drawable buttonBackground = view.getBackground(); + if (buttonBackground instanceof ColorDrawable) { + int buttonBgColor = ((ColorDrawable) buttonBackground).getColor(); + String hexButtonColor = String.format("#%06X", (0xFFFFFF & buttonBgColor)); // 色を16進数形式に変換 + + + if (hexButtonColor.equals("#111111") || hexButtonColor.equals("#1F1F1F")) { + ((ColorDrawable) buttonBackground).setColor(Color.parseColor("#000000")); + XposedBridge.log("Changed Button Background Color of Resource Name: " + resourceName + " from " + hexButtonColor + " to #000000"); + } else { + XposedBridge.log("Button Resource Name: " + resourceName + " Background Color: " + hexButtonColor); + } + } + } + + } catch (Resources.NotFoundException e) { + + XposedBridge.log("View ID: " + view.getId() + " - Resource name not found."); + } + } + }); +*/ + + + + diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/Notif_invalid.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/Notif_invalid.java new file mode 100644 index 00000000..c2b59e3d --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/Notif_invalid.java @@ -0,0 +1,192 @@ +package io.github.chipppppppppp.lime.hooks; + +import android.app.AndroidAppHelper; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; + +public class Notif_invalid implements IHook { + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + if (!limeOptions.Notif_invalid.checked) return; + XposedBridge.hookAllMethods( + loadPackageParam.classLoader.loadClass(Constants.RESPONSE_HOOK.className), + Constants.RESPONSE_HOOK.methodName, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + String paramValue = param.args[1].toString(); + if (paramValue.contains("chatName:") && paramValue.contains("getChats_result")) { + String chatName = extractChatName(paramValue); + if (chatName != null) { + // XposedBridge.log("Extracted chatName: " + chatName); + Context moduleContext = AndroidAppHelper.currentApplication(); + File dir = moduleContext.getFilesDir(); + saveChatNameToFile(chatName, dir); + } + } + } + + private String extractChatName(String paramValue) { + String marker = "chatName:"; + int startIndex = paramValue.indexOf(marker); + if (startIndex != -1) { + startIndex += marker.length(); + int endIndex = paramValue.indexOf(',', startIndex); + if (endIndex != -1) { + return paramValue.substring(startIndex, endIndex).trim(); + } + } + return null; + } + + private void saveChatNameToFile(String chatName, File dir) { + if (!dir.exists() && !dir.mkdirs()) { + XposedBridge.log("Failed to create directory: " + dir.getPath()); + return; + } + + File file = new File(dir, "Notification.txt"); + + try { + if (!file.exists() && !file.createNewFile()) { + XposedBridge.log("Failed to create file: " + file.getPath()); + return; + } + + List existingChatNames = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + existingChatNames.add(line.trim()); + } + } catch (IOException e) { + XposedBridge.log("Error reading file: " + e.getMessage()); + } + + if (!existingChatNames.contains(chatName.trim())) { + try (FileWriter writer = new FileWriter(file, true)) { + writer.write(chatName + "\n"); + XposedBridge.log("Saved chatName: " + chatName); + } catch (IOException e) { + XposedBridge.log("Error writing to file: " + e.getMessage()); + } + } else { + XposedBridge.log("Chat name already exists: " + chatName); + } + } catch (IOException e) { + XposedBridge.log("Error accessing file: " + e.getMessage()); + } + } + }); + + XposedHelpers.findAndHookMethod(NotificationManager.class, "notify", + String.class, int.class, Notification.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + + int id = (int) param.args[1]; + Notification notification = (Notification) param.args[2]; + + logNotificationDetails("NotificationManager.notify (with tag)", id, notification); + String subText = notification.extras.getString(Notification.EXTRA_SUB_TEXT); + List chatNamesFromFile = loadNamesFromFile(); + for (String chatName : chatNamesFromFile) { + if (subText != null && subText.contains(chatName)) { + param.setResult(null); + return; + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = notification.getChannelId(); + NotificationManager manager = (NotificationManager) AndroidAppHelper.currentApplication().getSystemService(Context.NOTIFICATION_SERVICE); + NotificationChannel channel = manager.getNotificationChannel(channelId); + + if (channel != null) { + String channelName = channel.getName().toString(); + // XposedBridge.log("Notification Channel Name: " + channelName); + } + } + } + }); + + XposedHelpers.findAndHookMethod(Notification.Builder.class, "setContentTitle", + CharSequence.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + CharSequence title = (CharSequence) param.args[0]; + + List chatNamesFromFile = loadNamesFromFile(); + + for (String chatName : chatNamesFromFile) { + if (title != null && title.toString().contains(chatName)) { + //XposedBridge.log("Notification title contains chatName from file. Title: " + title); + param.setResult(null); // nullを返して通知を表示しない + return; + } + } + //XposedBridge.log("Notification title does not contain any chatName from file. Title: " + title); + } + }); + } + + // 通知の詳細をログに記録するヘルパーメソッド + private void logNotificationDetails(String method, int id, Notification notification) { + XposedBridge.log(method + " called. ID: " + id); + + if (notification.extras != null) { + + String title = notification.extras.getString(Notification.EXTRA_TITLE); // タイトル + String text = notification.extras.getString(Notification.EXTRA_TEXT); // メインのテキスト + String subText = notification.extras.getString(Notification.EXTRA_SUB_TEXT); // サブテキスト + + /* + XposedBridge.log("Notification Title: " + (title != null ? title : "No Title")); + XposedBridge.log("Notification Text: " + (text != null ? text : "No Text")); + XposedBridge.log("Notification SubText: " + (subText != null ? subText : "No SubText")); + + */ + } else { + // XposedBridge.log("Notification extras is null."); + } + //XposedBridge.log("Notification Icon: " + notification.icon); + } + + private List loadNamesFromFile() { + List names = new ArrayList<>(); + Context moduleContext = AndroidAppHelper.currentApplication(); + File dir = moduleContext.getFilesDir(); + File file = new File(dir, "Notification.txt"); + if (!file.exists()) { + return names; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + names.add(line.trim()); + } + } catch (IOException e) { + // XposedBridge.log("Error reading names from file: " + e.getMessage()); + } + + return names; + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/ReadChecker.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/ReadChecker.java new file mode 100644 index 00000000..2af29729 --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/ReadChecker.java @@ -0,0 +1,628 @@ +package io.github.chipppppppppp.lime.hooks; + + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Application; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; +import io.github.chipppppppppp.lime.R; + +public class ReadChecker implements IHook { + private SQLiteDatabase limeDatabase; + private SQLiteDatabase db3 = null; + private SQLiteDatabase db4 = null; + private boolean shouldHookOnCreate = false; + private String currentGroupId = null; + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + if (!limeOptions.ReadChecker.checked) return; + XposedHelpers.findAndHookMethod(Application.class, "onCreate", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Application appContext = (Application) param.thisObject; + + + if (appContext == null) { + return; + } + + + File dbFile3 = appContext.getDatabasePath("naver_line"); + File dbFile4 = appContext.getDatabasePath("contact"); + + + if (dbFile3.exists() && dbFile4.exists()) { + SQLiteDatabase.OpenParams.Builder builder1 = new SQLiteDatabase.OpenParams.Builder(); + builder1.addOpenFlags(SQLiteDatabase.OPEN_READWRITE); + SQLiteDatabase.OpenParams dbParams1 = builder1.build(); + + + SQLiteDatabase.OpenParams.Builder builder2 = new SQLiteDatabase.OpenParams.Builder(); + builder2.addOpenFlags(SQLiteDatabase.OPEN_READWRITE); + SQLiteDatabase.OpenParams dbParams2 = builder2.build(); + + + db3 = SQLiteDatabase.openDatabase(dbFile3, dbParams1); + db4 = SQLiteDatabase.openDatabase(dbFile4, dbParams2); + + + initializeLimeDatabase(appContext); + catchNotification(loadPackageParam, db3, db4, appContext); + } + } + }); + + + Class chatHistoryRequestClass = XposedHelpers.findClass("com.linecorp.line.chat.request.ChatHistoryRequest", loadPackageParam.classLoader); + XposedHelpers.findAndHookMethod(chatHistoryRequestClass, "getChatId", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + String chatId = (String) param.getResult(); + XposedBridge.log(chatId); + if (isGroupExists(chatId)) { + shouldHookOnCreate = true; + currentGroupId = chatId; + } else { + shouldHookOnCreate = false; + currentGroupId = null; + } + } + }); + + + Class chatHistoryActivityClass = XposedHelpers.findClass("jp.naver.line.android.activity.chathistory.ChatHistoryActivity", loadPackageParam.classLoader); + XposedHelpers.findAndHookMethod(chatHistoryActivityClass, "onCreate", Bundle.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + if (shouldHookOnCreate && currentGroupId != null) { + + if (!isNoGroup(currentGroupId)) { + Activity activity = (Activity) param.thisObject; + addButton(activity); + } + } + } + }); + + + } + + private boolean isGroupExists(String groupId) { + if (limeDatabase == null) { + // XposedBridge.log("Database is not initialized."); + return false; + } + + + String query = "SELECT 1 FROM group_messages WHERE group_id = ?"; + Cursor cursor = limeDatabase.rawQuery(query, new String[]{groupId}); + boolean exists = cursor.moveToFirst(); + cursor.close(); + + + return exists; + } + + private boolean isNoGroup(String groupId) { + if (limeDatabase == null) { + // XposedBridge.log("Database is not initialized."); + return true; + } + + + String query = "SELECT group_name FROM group_messages WHERE group_id = ?"; + Cursor cursor = limeDatabase.rawQuery(query, new String[]{groupId}); + + boolean noGroup = true; + + if (cursor.moveToFirst()) { + String groupName = cursor.getString(cursor.getColumnIndex("group_name")); + noGroup = groupName == null || groupName.isEmpty(); + } + + cursor.close(); + return noGroup; + } + + private void addButton(Activity activity) { + Button button = new Button(activity); + button.setText("R"); + + + button.setBackgroundColor(Color.BLACK); + + button.setTextColor(Color.WHITE); + + FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ); + frameParams.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; + frameParams.topMargin = 150; + button.setLayoutParams(frameParams); + + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentGroupId != null) { + showDataForGroupId(activity, currentGroupId); + } + } + }); + + ViewGroup layout = activity.findViewById(android.R.id.content); + layout.addView(button); + } + + + private void showDataForGroupId(Activity activity, String groupId) { + if (limeDatabase == null) { + return; + } + + String query = "SELECT server_id, content, created_time FROM group_messages WHERE group_id=? ORDER BY created_time ASC"; + Cursor cursor = limeDatabase.rawQuery(query, new String[]{groupId}); + + Map dataItemMap = new HashMap<>(); + + while (cursor.moveToNext()) { + String serverId = cursor.getString(0); + String content = cursor.getString(1); + String createdTime = cursor.getString(2); + + List user_nameList = getuser_namesForServerId(serverId); + + if (dataItemMap.containsKey(serverId)) { + DataItem existingItem = dataItemMap.get(serverId); + existingItem.user_names.addAll(user_nameList); + } else { + DataItem dataItem = new DataItem(serverId, content, createdTime); + dataItem.user_names.addAll(user_nameList); + dataItemMap.put(serverId, dataItem); + } + } + cursor.close(); + + List sortedDataItems = new ArrayList<>(dataItemMap.values()); + Collections.sort(sortedDataItems, Comparator.comparing(item -> item.createdTime)); + + StringBuilder resultBuilder = new StringBuilder(); + for (DataItem item : sortedDataItems) { + resultBuilder.append("Content: ").append(item.content != null ? item.content : "Media").append("\n"); + resultBuilder.append("Created Time: ").append(item.createdTime).append("\n"); + + if (!item.user_names.isEmpty()) { + resultBuilder.append("既読者 (").append(item.user_names.size()).append("):\n"); + for (String user_name : item.user_names) { + resultBuilder.append("- ").append(user_name).append("\n"); + } + } else { + resultBuilder.append("No talk names found.\n"); + } + resultBuilder.append("\n"); + } + + TextView textView = new TextView(activity); + textView.setText(resultBuilder.toString()); + textView.setPadding(20, 20, 20, 20); + + ScrollView scrollView = new ScrollView(activity); + scrollView.addView(textView); + + // ダイアログ作成 + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("READ Data"); + builder.setView(scrollView); + + // OKボタン + builder.setPositiveButton("OK", null); + + // 削除ボタン + builder.setNegativeButton("削除", (dialog, which) -> { + // 削除の確認ダイアログを表示 + new AlertDialog.Builder(activity) + .setTitle("確認") + .setMessage("本当に削除しますか?") + .setPositiveButton("はい", (confirmDialog, confirmWhich) -> deleteGroupData(groupId,activity)) + .setNegativeButton("いいえ", null) + .show(); + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + } + + private void deleteGroupData(String groupId,Activity activity) { + if (limeDatabase == null) { + return; + } + + String deleteQuery = "DELETE FROM group_messages WHERE group_id=?"; + limeDatabase.execSQL(deleteQuery, new String[]{groupId}); + Toast.makeText(activity, "データが削除されました。", Toast.LENGTH_SHORT).show(); + } + + private List getuser_namesForServerId(String serverId) { + if (limeDatabase == null) { + return Collections.emptyList(); + } + + String query = "SELECT user_name FROM group_messages WHERE server_id=?"; + Cursor cursor = limeDatabase.rawQuery(query, new String[]{serverId}); + List user_names = new ArrayList<>(); + + if (cursor.moveToFirst()) { + String userNameStr = cursor.getString(0); + if (userNameStr != null) { + // 改行で区切ってユーザー名を取得 + String[] names = userNameStr.split("\n"); + Collections.addAll(user_names, names); + } + } + cursor.close(); + return user_names; + } + + private static class DataItem { + String serverId; + String content; + String createdTime; + Set user_names; + + DataItem(String serverId, String content, String createdTime) { + this.serverId = serverId; + this.content = content; + this.createdTime = createdTime; + this.user_names = new HashSet<>(); + } + } + + private void catchNotification(XC_LoadPackage.LoadPackageParam loadPackageParam, SQLiteDatabase db3, SQLiteDatabase db4, Context appContext) { + try { + XposedBridge.hookAllMethods( + loadPackageParam.classLoader.loadClass(Constants.NOTIFICATION_READ_HOOK.className), + "invokeSuspend", + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + String paramValue = param.args[0].toString(); + XposedBridge.log(paramValue); + if (appContext == null) { + XposedBridge.log("appContext is null!"); + return; + } + + Context moduleContext; + try { + moduleContext = appContext.createPackageContext( + "io.github.chipppppppppp.lime", Context.CONTEXT_IGNORE_SECURITY); + } catch (PackageManager.NameNotFoundException e) { + // XposedBridge.log("Failed to create package context: " + e.getMessage()); + return; + } + + if (paramValue != null && paramValue.contains("type:NOTIFIED_READ_MESSAGE")) { + List messages = extractMessages(paramValue); + for (String message : messages) { + fetchDataAndSave(db3, db4, message, appContext, moduleContext); + } + } + } + } + ); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private List extractMessages(String paramValue) { + List messages = new ArrayList<>(); + Pattern pattern = Pattern.compile("type:NOTIFIED_READ_MESSAGE.*?(?=type:|$)"); + Matcher matcher = pattern.matcher(paramValue); + + while (matcher.find()) { + messages.add(matcher.group().trim()); + } + + return messages; + } + + private void fetchDataAndSave(SQLiteDatabase db3, SQLiteDatabase db4, String paramValue, Context context, Context moduleContext) { + File dbFile = new File(context.getFilesDir(), "data_log.txt"); + + try { + String serverId = extractServerId(paramValue, context); + String checkedUser = extractCheckedUser(paramValue); + + if (serverId == null || checkedUser == null) { + writeToFile(dbFile, "Missing parameters: serverId=" + serverId + ", checkedUser=" + checkedUser); + return; + } + + String groupId = queryDatabase(db3, "SELECT chat_id FROM chat_history WHERE server_id=?", serverId); + String groupName = queryDatabase(db3, "SELECT name FROM groups WHERE id=?", groupId); + String content = queryDatabase(db3, "SELECT content FROM chat_history WHERE server_id=?", serverId); + String user_name = queryDatabase(db4, "SELECT profile_name FROM contacts WHERE mid=?", checkedUser); + String timeEpochStr = queryDatabase(db3, "SELECT created_time FROM chat_history WHERE server_id=?", serverId); + String timeFormatted = formatMessageTime(timeEpochStr); + + String media = queryDatabase(db3, "SELECT attachement_type FROM chat_history WHERE server_id=?", serverId); + String mediaDescription = ""; + + if (media != null) { + switch (media) { + case "7": + mediaDescription = moduleContext.getResources().getString(R.string.sticker); + break; + case "1": + mediaDescription = moduleContext.getResources().getString(R.string.picture); + break; + case "2": + mediaDescription = moduleContext.getResources().getString(R.string.video); + break; + default: + mediaDescription = ""; + break; + } + } + + String finalContent = (content != null && !content.isEmpty()) ? content : (!mediaDescription.isEmpty() ? mediaDescription : "No content:" + serverId); + + saveData(groupId, serverId, checkedUser, groupName, finalContent, user_name, timeFormatted, context); + markPreviousMessagesAsRead(groupId, checkedUser, timeEpochStr, context); + } catch (Exception e) { + Log.e("fetchDataAndSave", "Unexpected error:", e); + } + } + + private void markPreviousMessagesAsRead(String groupId, String checkedUser, String timeEpochStr, Context context) { + initializeLimeDatabase(context); + + try { + String query = "SELECT server_id, content, created_time, user_name FROM group_messages " + + "WHERE group_id=? AND created_time 0) { + // `user_name`にすでに同じ名前がないかチェックし、ない場合のみ追加する + if (!existingUserName.contains(user_name)) { + String updatedUserName = existingUserName + (existingUserName.isEmpty() ? "" : "\n") + "-" + user_name; + ContentValues values = new ContentValues(); + values.put("user_name", updatedUserName); + limeDatabase.update("group_messages", values, "server_id=? AND checked_user=?", new String[]{serverId, checkedUser}); + // XposedBridge.log("User name updated for server_id: " + serverId + ", checked_user: " + checkedUser); + } + } else { + // 新しいレコードを挿入 + insertNewRecord(groupId, serverId, checkedUser, groupName, content, "-" + user_name, createdTime); + } + + // 同じ groupId 内の他のレコードの user_name カラムを更新 + updateOtherRecordsUserNames(groupId, user_name); + } + + } catch (Exception e) { + Log.e("saveData", "Error during data existence check or update:", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private void updateOtherRecordsUserNames(String groupId, String user_name) { + Cursor cursor = null; + try { + String selectOtherQuery = "SELECT server_id, user_name FROM group_messages WHERE group_id=? AND user_name NOT LIKE ?"; + cursor = limeDatabase.rawQuery(selectOtherQuery, new String[]{groupId, "%-" + user_name + "%"}); + + while (cursor.moveToNext()) { + String serverId = cursor.getString(cursor.getColumnIndex("server_id")); + String existingUserName = cursor.getString(cursor.getColumnIndex("user_name")); + + // `user_name`にすでに同じ名前がないかチェック + if (!existingUserName.contains(user_name)) { + String updatedUserName = existingUserName + (existingUserName.isEmpty() ? "" : "\n") + "-" + user_name; + ContentValues values = new ContentValues(); + values.put("user_name", updatedUserName); + + limeDatabase.update("group_messages", values, "group_id=? AND server_id=?", new String[]{groupId, serverId}); + // XposedBridge.log("Updated user_name for other records in group_id: " + groupId + ", server_id: " + serverId); + } + } + } catch (Exception e) { + Log.e("updateOtherRecordsUserNames", "Error updating other records' user names:", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + private void insertNewRecord(String groupId, String serverId, String checkedUser, String groupName, String content, String user_name, String createdTime) { + try { + String insertQuery = "INSERT INTO group_messages(group_id, server_id, checked_user, group_name, content, user_name, created_time)" + + "VALUES(?, ?, ?, ?, ?, ?, ?);"; + limeDatabase.execSQL(insertQuery, new Object[]{groupId, serverId, checkedUser, groupName, content, user_name, createdTime}); + + XposedBridge.log("Saved to DB: Group_Id: " + groupId + ", Server_id: " + serverId + ", Checked_user: " + checkedUser + + ", Group_Name: " + groupName + ", Content: " + content + ", user_name: " + user_name + ", Created_Time: " + createdTime); + } catch (Exception e) { + Log.e("insertNewRecord", "Error saving data to database:", e); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/github/chipppppppppp/lime/hooks/test.java b/app/src/main/java/io/github/chipppppppppp/lime/hooks/test.java new file mode 100644 index 00000000..1257bb82 --- /dev/null +++ b/app/src/main/java/io/github/chipppppppppp/lime/hooks/test.java @@ -0,0 +1,421 @@ +package io.github.chipppppppppp.lime.hooks; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +//import androidx.constraintlayout.widget.ConstraintLayout; +//import androidx.constraintlayout.widget.ConstraintSet; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.Enumeration; + +import dalvik.system.DexFile; +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.callbacks.XC_LoadPackage; +import io.github.chipppppppppp.lime.LimeOptions; + + +//このコードで呼び出しメゾットをログに出力させます、ダークモードを真っ黒にする機能の途中です、 + +public class test implements IHook { + private boolean isButtonAdded = false; // ボタンが追加されたかどうかを追跡するフラグ + @Override + public void hook(LimeOptions limeOptions, XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { + String packageName = loadPackageParam.packageName; + + XposedBridge.log("Hooking package: " + packageName); + // hookOnViewAdded(loadPackageParam.classLoader); + hookAllClassesInPackage(loadPackageParam.classLoader, loadPackageParam); + hookFragmentOnCreateView(loadPackageParam.classLoader); + //hookChatHistoryActivity(loadPackageParam.classLoader); // ChatHistoryActivityのフック + //hookLongClickListeners(loadPackageParam.classLoader); // 長押しリスナーのフック + + + + } + + + private void hookAllClassesInPackage(ClassLoader classLoader, XC_LoadPackage.LoadPackageParam loadPackageParam) { + try { + String apkPath = loadPackageParam.appInfo.sourceDir; + if (apkPath == null) { + XposedBridge.log("Could not get APK path."); + return; + } + + DexFile dexFile = new DexFile(new File(apkPath)); + Enumeration classNames = dexFile.entries(); + while (classNames.hasMoreElements()) { + String className = classNames.nextElement(); + + // 指定されたパッケージで始まるクラスのみをフック + // if (className.startsWith("com.linecorp.line") || className.startsWith("jp.naver.line.android")) { + try { + Class clazz = Class.forName(className, false, classLoader); + hookAllMethods(clazz); + } catch (ClassNotFoundException e) { + XposedBridge.log("Class not found: " + className); + } catch (Throwable e) { + XposedBridge.log("Error loading class " + className + ": " + e.getMessage()); + } + // } + } + } catch (Throwable e) { + XposedBridge.log("Error while hooking classes: " + e.getMessage()); + } + } + + private void hookAllClasses(ClassLoader classLoader, XC_LoadPackage.LoadPackageParam loadPackageParam) { + try { + String apkPath = loadPackageParam.appInfo.sourceDir; + if (apkPath == null) { + XposedBridge.log("Could not get APK path."); + return; + } + + DexFile dexFile = new DexFile(new File(apkPath)); + Enumeration classNames = dexFile.entries(); + while (classNames.hasMoreElements()) { + String className = classNames.nextElement(); + try { + Class clazz = Class.forName(className, false, classLoader); + hookAllMethods(clazz); + } catch (ClassNotFoundException e) { + XposedBridge.log("Class not found: " + className); + } catch (Throwable e) { + XposedBridge.log("Error loading class " + className + ": " + e.getMessage()); + } + } + } catch (Throwable e) { + XposedBridge.log("Error while hooking classes: " + e.getMessage()); + } + } + + + private void hookFragmentOnCreateView(ClassLoader classLoader) { + try { + Class fragmentClass = Class.forName("androidx.fragment.app.Fragment", false, classLoader); + Method onCreateViewMethod = fragmentClass.getDeclaredMethod("onCreateView", LayoutInflater.class, ViewGroup.class, Bundle.class); + + XposedBridge.hookMethod(onCreateViewMethod, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("Before calling: " + fragmentClass.getName() + ".onCreateView"); + } + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + ViewGroup rootView = (ViewGroup) param.getResult(); // ルートのViewGroupを取得 + Context context = rootView.getContext(); + + // IDによるビューの取得 + int messageContainerId = getIdByName(context, "message_context_menu_content_container"); + View messageContainer = rootView.findViewById(messageContainerId); + + if (messageContainer != null) { + XposedBridge.log("messageContainer found: " + messageContainer.toString()); + } else { + XposedBridge.log("messageContainer not found"); + } + + // 新しいビューの追加を監視 + rootView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { + @Override + public void onChildViewAdded(View parent, View child) { + XposedBridge.log("Child view added: " + child.toString()); + } + + @Override + public void onChildViewRemoved(View parent, View child) { + XposedBridge.log("Child view removed: " + child.toString()); + } + }); + } + }); + } catch (ClassNotFoundException e) { + XposedBridge.log("Class not found: androidx.fragment.app.Fragment"); + } catch (NoSuchMethodException e) { + XposedBridge.log("Method not found: onCreateView in Fragment"); + } catch (Throwable e) { + XposedBridge.log("Error hooking onCreateView: " + e.getMessage()); + } + } + +/* + private void hookOnViewAdded(ClassLoader classLoader) { + try { + Class constraintLayoutClass = Class.forName("androidx.constraintlayout.widget.ConstraintLayout", false, classLoader); + Method onViewAddedMethod = constraintLayoutClass.getDeclaredMethod("onViewAdded", View.class); + + XposedBridge.hookMethod(onViewAddedMethod, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + View addedView = (View) param.args[0]; + XposedBridge.log(addedView.toString()); + } + + private boolean isButtonAdded = false; // ボタンが追加されたかどうかを追跡するフラグ + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + View addedView = (View) param.args[0]; + XposedBridge.log("Called: " + constraintLayoutClass.getName() + ".onViewAdded"); + + // 追加されたビューの ID を取得 + int addedViewId = addedView.getId(); + + // 親ビューを取得 + ViewGroup parent = (ViewGroup) param.thisObject; + + // 追加されたビューのリソース名を取得 + String resourceName = parent.getContext().getResources().getResourceEntryName(addedViewId); + + // リソース名が chat_ui_message_context_menu_row_container の場合 + if ("chat_ui_message_context_menu_row_container".equals(resourceName) && !isButtonAdded) { + // ボタンを作成 + + + } + + + } + + + private void createAndAddButton(ConstraintLayout parent, View referenceView) { + // ボタンを作成 + Button newButton = new Button(parent.getContext()); + newButton.setText("新しいボタン"); + + // ボタンの ID を設定 + newButton.setId(View.generateViewId()); + + // ボタンのレイアウトパラメータを設定 + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ); + + // ボタンを親ビューに追加 + parent.addView(newButton, params); + + // 制約を設定 + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(parent); + + // ボタンに対する制約を設定 + constraintSet.connect(newButton.getId(), ConstraintSet.TOP, referenceView.getId(), ConstraintSet.BOTTOM); // 上に参照ビューを設定 + constraintSet.connect(newButton.getId(), ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START); // 左端に設定 + constraintSet.connect(newButton.getId(), ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END); // 右端に設定 + constraintSet.setHorizontalBias(newButton.getId(), 0.5f); // 中央に配置 + + // 制約を適用 + constraintSet.applyTo(parent); + } + + private void createAndAddButton(ViewGroup parent) { + // ボタンを作成 + Button newButton = new Button(parent.getContext()); + newButton.setText("新しいボタン"); // ボタンのテキストを設定 + + // ボタンのレイアウトパラメータを設定 + ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ); + + // 親の一番右に追加するために、適切な位置を設定 + params.startToEnd = parent.getChildAt(parent.getChildCount() - 1).getId(); // 最後のビューの右側に配置 + params.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; // 上部を親の上部に固定 + + newButton.setLayoutParams(params); + + // 親ビューにボタンを追加 + parent.addView(newButton); + } + + + }); + } catch (ClassNotFoundException e) { + XposedBridge.log("Class not found: androidx.constraintlayout.widget.ConstraintLayout"); + } catch (NoSuchMethodException e) { + XposedBridge.log("Method not found: onViewAdded in ConstraintLayout"); + } catch (Throwable e) { + XposedBridge.log("Error hooking onViewAdded: " + e.getMessage()); + } + } +*/ + + + private int getIdByName(Context context, String resourceName) { + return context.getResources().getIdentifier(resourceName, "id", context.getPackageName()); + } + + private void hookAllMethods(Class clazz) { + // クラス内のすべてのメソッドを取得 + Method[] methods = clazz.getDeclaredMethods(); + + for (Method method : methods) { + // 抽象メソッドをスキップ + if (java.lang.reflect.Modifier.isAbstract(method.getModifiers())) { + continue; + } + + // 対象メソッドが特定のビュー関連メソッドであるか確認 + if (!"invokeSuspend".equals(method.getName()) && + !"setOnLongClickListener".equals(method.getName()) && + !"setOnTouchListener".equals(method.getName()) && + !"setVisibility".equals(method.getName()) && + !"setAlpha".equals(method.getName()) && + !"setEnabled".equals(method.getName()) && + !"setFocusable".equals(method.getName()) && + !"setOnClickListener".equals(method.getName()) && + !"setBackgroundColor".equals(method.getName()) && + !"setPadding".equals(method.getName()) && + !"setLayoutParams".equals(method.getName()) && + !"requestLayout".equals(method.getName()) && + !"invalidate".equals(method.getName()) && + !"setText".equals(method.getName()) && // 新しく追加されたメソッド + !"setTextColor".equals(method.getName()) && // 新しく追加されたメソッド + !"setHint".equals(method.getName()) && // 新しく追加されたメソッド + !"setHintTextColor".equals(method.getName()) && // 新しく追加されたメソッド + !"onStart".equals(method.getName()) && + !"setCompoundDrawables".equals(method.getName()) && + !"getActivity".equals(method.getName()) && // PendingIntent method + !"onViewAdded".equals(method.getName()) && // PendingIntent method + !"setState".equals(method.getName())) { // PendingIntent method + continue; + } + + method.setAccessible(true); // アクセス可能に設定 + + try { + // メソッドをフックする + XposedBridge.hookMethod(method, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + StringBuilder argsString = new StringBuilder("Args: "); + + // 引数が複数の場合、すべてを追加 + for (int i = 0; i < param.args.length; i++) { + Object arg = param.args[i]; + argsString.append("Arg[").append(i).append("]: ") + .append(arg != null ? arg.toString() : "null") + .append(", "); + } + +// メソッドに応じたログ出力 + if ("invokeSuspend".equals(method.getName())) { + XposedBridge.log("Before calling invokeSuspend in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setVisibility".equals(method.getName())) { + XposedBridge.log("Before calling setVisibility in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setAlpha".equals(method.getName())) { + XposedBridge.log("Before calling setAlpha in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setEnabled".equals(method.getName())) { + XposedBridge.log("Before calling setEnabled in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setFocusable".equals(method.getName())) { + XposedBridge.log("Before calling setFocusable in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setOnClickListener".equals(method.getName())) { + XposedBridge.log("Before calling setOnClickListener in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setBackgroundColor".equals(method.getName())) { + XposedBridge.log("Before calling setBackgroundColor in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setPadding".equals(method.getName())) { + XposedBridge.log("Before calling setPadding in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setLayoutParams".equals(method.getName())) { + XposedBridge.log("Before calling setLayoutParams in class: " + clazz.getName() + " with args: " + argsString); + } else if ("requestLayout".equals(method.getName())) { + XposedBridge.log("Before calling requestLayout in class: " + clazz.getName() + " with args: " + argsString); + } else if ("invalidate".equals(method.getName())) { + XposedBridge.log("Before calling invalidate in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setText".equals(method.getName())) { + XposedBridge.log("Before calling setText in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setTextColor".equals(method.getName())) { + XposedBridge.log("Before calling setTextColor in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setHint".equals(method.getName())) { + XposedBridge.log("Before calling setHint in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setHintTextColor".equals(method.getName())) { + XposedBridge.log("Before calling setHintTextColor in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setCompoundDrawables".equals(method.getName())) { + XposedBridge.log("Before calling setCompoundDrawables in class: " + clazz.getName() + " with args: " + argsString); + } else if ("onStart".equals(method.getName())) { + XposedBridge.log("Before calling onStart in class: " + clazz.getName() + " with args: " + argsString); + } else if ("getActivity".equals(method.getName())) { + XposedBridge.log("Before calling getActivity in class: " + clazz.getName() + " with args: " + argsString); + } else if ("onViewAdded".equals(method.getName())) { + XposedBridge.log("Before calling onViewAdded in class: " + clazz.getName() + " with args: " + argsString); + } else if ("getService".equals(method.getName())) { + XposedBridge.log("Before calling getService in class: " + clazz.getName() + " with args: " + argsString); + } else if ("setState".equals(method.getName())) { + XposedBridge.log("Before setState invoke in class: " + clazz.getName() + " with args: " + argsString); + } + } + + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Object result = param.getResult(); + if ("invokeSuspend".equals(method.getName())) { + XposedBridge.log("Before calling invokeSuspend in class: " + clazz.getName() + (result != null ? result.toString() : "null")); + } else if ("setVisibility".equals(method.getName())) { + XposedBridge.log("After calling setVisibility in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setAlpha".equals(method.getName())) { + XposedBridge.log("After calling setAlpha in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setEnabled".equals(method.getName())) { + XposedBridge.log("After calling setEnabled in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setFocusable".equals(method.getName())) { + XposedBridge.log("After calling setFocusable in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setOnClickListener".equals(method.getName())) { + XposedBridge.log("After calling setOnClickListener in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setBackgroundColor".equals(method.getName())) { + XposedBridge.log("After calling setBackgroundColor in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setPadding".equals(method.getName())) { + XposedBridge.log("After calling setPadding in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setLayoutParams".equals(method.getName())) { + XposedBridge.log("After calling setLayoutParams in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("requestLayout".equals(method.getName())) { + XposedBridge.log("After calling requestLayout in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("invalidate".equals(method.getName())) { + XposedBridge.log("After calling invalidate in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setText".equals(method.getName())) { + XposedBridge.log("After calling setText in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setTextColor".equals(method.getName())) { + XposedBridge.log("After calling setTextColor in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setHint".equals(method.getName())) { + XposedBridge.log("After calling setHint in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setHintTextColor".equals(method.getName())) { + XposedBridge.log("After calling setHintTextColor in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setCompoundDrawables".equals(method.getName())) { + XposedBridge.log("After calling setCompoundDrawables in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("onStart".equals(method.getName())) { + XposedBridge.log("Before calling onStart in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("getActivity".equals(method.getName())) { + XposedBridge.log("After calling getActivity in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("onViewAdded".equals(method.getName())) { + XposedBridge.log("After calling onViewAdded in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("getService".equals(method.getName())) { + XposedBridge.log("After calling getService in class: " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } else if ("setState".equals(method.getName())) { + XposedBridge.log("setState " + clazz.getName() + " with result: " + (result != null ? result.toString() : "null")); + } + } + }); + } catch (IllegalArgumentException e) { + XposedBridge.log("Error hooking method " + method.getName() + " in class " + clazz.getName() + " : " + e.getMessage()); + } catch (Throwable e) { + XposedBridge.log("Unexpected error hooking method " + method.getName() + " in class " + clazz.getName() + " : " + Log.getStackTraceString(e)); + } + } + } + + private boolean isViewCreationMethod(Method method) { + // View作成に関連するメソッドを検出 + String methodName = method.getName().toLowerCase(); + + return methodName.contains("inflate") || methodName.contains("new") || methodName.contains("create"); + } +} \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index af81cf2a..e7f4f757 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,75 +1,82 @@  - - OK - キャンセル - OK - はい - いいえ - キャンセル - スタンプ - 動画 - 写真 - 再取得 - - LINE をクリーンに。 - - エラー - モジュールが有効化されていません! - 設定 - アプリを再起動しています... - 強制停止とキャッシュの削除が必要です - Android ID を偽装 - Android ID の偽装は自己責任です - バージョン不適合のため一部機能が利用できません - 最大バージョンに偽装 - 非表示にしたチャットの再表示を無効化 - 設定を LINE に埋め込まない (設定は同期されません) - VOOM アイコンを削除 - ウォレットアイコンを削除 - ニュースまたは通話アイコンを削除 - ボトムバーのアイコンを均等に配置 - ボトムバーのアイコンの当たり判定を拡張する - ボトムバーのアイコンのラベルを削除 - 広告を削除 - おすすめを削除 - LYP プレミアムのおすすめを削除 - サービスのラベルを削除 - サービスの項目を削除 - 通知から \"通知をオフ\" アクションを削除 - WebView をデフォルトのブラウザにリダイレクト - ブラウザアプリで開く - 常に既読をつけない - 常にミュートメッセージとして送信 - 送信取り消しを拒否 - トーク画面右上のメニューにある「未読のまま閲覧」スイッチを削除 - トラッキング通信をブロック - LINE バージョンの確認を停止 - 通信内容をログに出力 (開発者用) - リクエストを改変 - レスポンスを改変 - コピー - 貼り付け - 着信音を鳴らす(LSPatch用) - - 未読のまま閲覧 - - バックアップ - バックアップの作成に失敗しました - 確認 - クラスが見つかりません - 確認済みのメッセージ - メッセージを削除 - 削除されたメッセージ - 内容がバックアップファイルに移動されました - ファイルの内容が削除されました - ファイルを作成できませんでした - ファイルの削除に失敗しました - ファイルの移動に失敗しました - ファイルの保存に失敗しました - ファイルが見つかりません - %s の読み取りに失敗しました - 本当に削除しますか? - 何もバックアップされていません - 正しく取得できませんでした。\nアプリを再起動してください + + OK + キャンセル + OK + はい + いいえ + キャンセル + スタンプ + 動画 + 写真 + 再取得 + + + LINE をクリーンに。 + + + エラー + モジュールが有効化されていません! + 設定 + アプリを再起動しています... + 強制停止とキャッシュの削除が必要です + Android ID を偽装 + Android ID の偽装は自己責任です + バージョン不適合のため一部機能が利用できません + 最大バージョンに偽装 + 非表示にしたチャットの再表示を無効化 + 設定を LINE に埋め込まない (設定は同期されません) + VOOM アイコンを削除 + ウォレットアイコンを削除 + ニュースまたは通話アイコンを削除 + ボトムバーのアイコンを均等に配置 + ボトムバーのアイコンの当たり判定を拡張する + ボトムバーのアイコンのラベルを削除 + 広告を削除 + おすすめを削除 + LYP プレミアムのおすすめを削除 + サービスのラベルを削除 + サービスの項目を削除 + 通知から \"通知をオフ\" アクションを削除 + WebView を既定のブラウザにリダイレクト + ブラウザアプリで開く + 常に既読をつけない + 常にミュートメッセージとして送信 + 送信取り消しを拒否 + トーク画面右上のメニューにある「未読のまま閲覧」スイッチを削除 + トラッキング通信をブロック + LINE バージョンの確認を停止 + 通信内容をログに出力 (開発者用) + リクエストを改変 + レスポンスを改変 + コピー + ペースト + 着信音を鳴らす(LSPatch用) + + + 未読のまま閲覧 + LsPatch用「未読のまま閲覧」スイッチを表示 + + + バックアップ + バックアップの作成に失敗しました + 確認 + クラスが見つかりません + 確認済みのメッセージ + メッセージを削除 + 削除されたメッセージ + 内容がバックアップファイルに移動されました + ファイルの内容が削除されました + ファイルを作成できませんでした + ファイルの削除に失敗しました + ファイルの移動に失敗しました + ファイルの保存に失敗しました + ファイルが見つかりません + %s の読み取りに失敗しました + 本当に削除しますか? + 何もバックアップされていません + 正しく取得できませんでした。\nアプリを再起動してください + 送信したメッセージの既読者の確認 + 登録しているグループの通知をオフに \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6a1ea2ec..67049fad 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -42,4 +42,5 @@ 貼上 保持未讀 + 登録しているグループの通知をオフに \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 16eaa18d..b654055e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,4 +73,10 @@ Really want to delete it? Nothing backed up Could not get properly.\nPlease restart the app - \ No newline at end of file + + + Show \"Read as unread\" switch for LSPatch + Set nav color to Black + Check read friends + 登録しているグループの通知をオフに +