From 0684ad5d18b0c86d1e42412bc52fdee58df4b86b Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 15:50:26 +0900 Subject: [PATCH 01/13] Implement avatar mode --- .../com/aqua_ix/youbimiku/MainActivity.kt | 94 +++++++++++++++++++ app/src/main/res/drawable/ic_cube.xml | 17 ++++ app/src/main/res/layout/activity_main.xml | 16 +++- app/src/main/res/values-ja/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + secrets.defaults.properties | 3 +- 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/ic_cube.xml diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index 5c3a144..734a870 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -1,8 +1,10 @@ package com.aqua_ix.youbimiku +import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Intent +import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle @@ -13,6 +15,10 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.FrameLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -93,6 +99,9 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { private lateinit var navMenu: Menu private lateinit var ironSourceBannerLayout: IronSourceBannerLayout + private var isAvatarMode = false + private lateinit var webView: WebView + private var openAIPreviousResponse = "" private val job = SupervisorJob() @@ -117,6 +126,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { showInAppReviewIfNeeded() setupChat() + setupWebView() setupAdNetwork() } @@ -487,6 +497,52 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } } + private fun setupWebView() { + webView = binding.webView + webView.visibility = View.GONE + + webView.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + allowFileAccess = true + } + + // Add WebView client to handle page loading + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + binding.progressBar.visibility = View.VISIBLE + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + binding.progressBar.visibility = View.GONE + } + + @SuppressLint("NewApi") + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + super.onReceivedError(view, request, error) + // Handle loading errors + binding.progressBar.visibility = View.GONE + Toast.makeText(this@MainActivity, R.string.avatar_mode_error, Toast.LENGTH_SHORT) + .show() + toggleAvatarMode() + } + } + } + + private fun loadAvatarView() { + // Get user name and encode it for URL + val userName = getUserName(this)?.let { Uri.encode(it) } ?: "" + // Construct URL with query parameter + val url = "${BuildConfig.AVATAR_BASE_URL}?userName=$userName" + webView.loadUrl(url) + } + private fun showUserNameDialog(cancelable: Boolean = true) { val dialog = UserNameDialogFragment() val args = Bundle() @@ -498,6 +554,9 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { override fun doPositiveClick() { userAccount.setName(getUserName(this).toString()) + if (isAvatarMode) { + loadAvatarView() + } } private fun showAIModelDialog(cancelable: Boolean = true) { @@ -596,12 +655,27 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { getAIModel(this)?.let { menu.findItem(R.id.setting_language).isVisible = it == AIModelConfig.DIALOG_FLOW.name } + + menu.findItem(R.id.setting_ai_model).isVisible = !isAvatarMode + menu.findItem(R.id.setting_language).isVisible = !isAvatarMode + menu.findItem(R.id.setting_font_size).isVisible = !isAvatarMode + menu.findItem(R.id.clear_message_history).isVisible = !isAvatarMode + + menu.add(Menu.NONE, 1, Menu.NONE, R.string.avatar_mode) + .setIcon(R.drawable.ic_cube) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + navMenu = menu return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { + 1 -> { + toggleAvatarMode() + true + } + R.id.setting_user_name -> { showUserNameDialog() true @@ -636,6 +710,26 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } } + private fun toggleAvatarMode() { + isAvatarMode = !isAvatarMode + invalidateOptionsMenu() + + if (isAvatarMode) { + // Switch to Avatar mode + binding.chatView.visibility = View.GONE + webView.visibility = View.VISIBLE + binding.progressBar.visibility = View.VISIBLE + loadAvatarView() + } else { + // Switch back to chat mode + binding.chatView.visibility = View.VISIBLE + webView.visibility = View.GONE + webView.loadUrl("about:blank") + binding.progressBar.visibility = View.GONE + } + } + + override fun onClick(v: View) { if (binding.chatView.inputText.isEmpty()) { return diff --git a/app/src/main/res/drawable/ic_cube.xml b/app/src/main/res/drawable/ic_cube.xml new file mode 100644 index 0000000..a2be35e --- /dev/null +++ b/app/src/main/res/drawable/ic_cube.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a19e8d8..2a23800 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,6 +13,20 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + - \ 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 c9062da..100e51f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -54,4 +54,8 @@ キャンセル 問題を報告しました 報告に失敗しました + + アバターモード + アバター画面の読み込みに失敗しました + 3Dアバターモードが追加されました!右上のアイコンをタップしてお楽しみください。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 597a711..2e0408b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,4 +55,8 @@ Cancel Reported Report failed + + Avatar Mode + Failed to load avatar page + You can now talk to Hatsune Miku in 3D avatar mode!\n\nYou can change to avatar mode from the button on the top right of the screen. diff --git a/secrets.defaults.properties b/secrets.defaults.properties index daa523d..6bf1928 100644 --- a/secrets.defaults.properties +++ b/secrets.defaults.properties @@ -5,4 +5,5 @@ IMOBILE_PID=IMOBILE_PID IMOBILE_MID=IMOBILE_MID IMOBILE_BANNER_SID=IMOBILE_BANNER_SID IMOBILE_INTERSTITIAL_SID=IMOBILE_INTERSTITIAL_SID -IRONSOURCE_APP_KEY=IRONSOURCE_APP_KEY \ No newline at end of file +IRONSOURCE_APP_KEY=IRONSOURCE_APP_KEY +AVATAR_BASE_URL=AVATAR_BASE_URL \ No newline at end of file From 60abad65e3f8fefe68739680bd7803a27abd5ff9 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 16:25:33 +0900 Subject: [PATCH 02/13] Don't sent user name to webview --- .../main/java/com/aqua_ix/youbimiku/MainActivity.kt | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index 734a870..0e4fffd 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -535,14 +535,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } } - private fun loadAvatarView() { - // Get user name and encode it for URL - val userName = getUserName(this)?.let { Uri.encode(it) } ?: "" - // Construct URL with query parameter - val url = "${BuildConfig.AVATAR_BASE_URL}?userName=$userName" - webView.loadUrl(url) - } - private fun showUserNameDialog(cancelable: Boolean = true) { val dialog = UserNameDialogFragment() val args = Bundle() @@ -555,7 +547,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { override fun doPositiveClick() { userAccount.setName(getUserName(this).toString()) if (isAvatarMode) { - loadAvatarView() + webView.loadUrl(BuildConfig.AVATAR_BASE_URL) } } @@ -656,6 +648,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { menu.findItem(R.id.setting_language).isVisible = it == AIModelConfig.DIALOG_FLOW.name } + menu.findItem(R.id.setting_user_name).isVisible = !isAvatarMode menu.findItem(R.id.setting_ai_model).isVisible = !isAvatarMode menu.findItem(R.id.setting_language).isVisible = !isAvatarMode menu.findItem(R.id.setting_font_size).isVisible = !isAvatarMode @@ -719,7 +712,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { binding.chatView.visibility = View.GONE webView.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE - loadAvatarView() + webView.loadUrl(BuildConfig.AVATAR_BASE_URL) } else { // Switch back to chat mode binding.chatView.visibility = View.VISIBLE From a726a4400be55a77c3e7a95f39f93357e72c71a5 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 16:39:43 +0900 Subject: [PATCH 03/13] Change menu icon depends on mode selection --- .../main/java/com/aqua_ix/youbimiku/MainActivity.kt | 11 ++++++++++- app/src/main/res/drawable/ic_chat.xml | 11 +++++++++++ app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/ic_chat.xml diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index 0e4fffd..9b83c98 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -655,9 +655,18 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { menu.findItem(R.id.clear_message_history).isVisible = !isAvatarMode menu.add(Menu.NONE, 1, Menu.NONE, R.string.avatar_mode) - .setIcon(R.drawable.ic_cube) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + if (isAvatarMode) { + menu.findItem(1) + .setTitle(R.string.chat_mode) + .setIcon(R.drawable.ic_chat) + } else { + menu.findItem(1) + .setTitle(R.string.avatar_mode) + .setIcon(R.drawable.ic_cube) + } + navMenu = menu return true } diff --git a/app/src/main/res/drawable/ic_chat.xml b/app/src/main/res/drawable/ic_chat.xml new file mode 100644 index 0000000..95a0804 --- /dev/null +++ b/app/src/main/res/drawable/ic_chat.xml @@ -0,0 +1,11 @@ + + + + \ 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 100e51f..2658ab1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -55,6 +55,7 @@ 問題を報告しました 報告に失敗しました + チャットモード アバターモード アバター画面の読み込みに失敗しました 3Dアバターモードが追加されました!右上のアイコンをタップしてお楽しみください。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e0408b..b74b943 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ Reported Report failed + Chat Mode Avatar Mode Failed to load avatar page You can now talk to Hatsune Miku in 3D avatar mode!\n\nYou can change to avatar mode from the button on the top right of the screen. From 807ae03b5f6b851f7e00f3ce5a5744374cb565b3 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 16:52:55 +0900 Subject: [PATCH 04/13] Remove unnecesary anotation --- app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index 9b83c98..33a9aa2 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -519,7 +519,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { binding.progressBar.visibility = View.GONE } - @SuppressLint("NewApi") override fun onReceivedError( view: WebView?, request: WebResourceRequest?, From c3ff538c8eca4b9da558b87b45b312d6762a5a71 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 16:58:21 +0900 Subject: [PATCH 05/13] Grant microphone permission --- app/src/main/AndroidManifest.xml | 2 + .../com/aqua_ix/youbimiku/MainActivity.kt | 43 ++++++++++++++++++- app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 844c040..077734d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + , + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + PERMISSIONS_REQUEST_RECORD_AUDIO -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + webView.reload() + } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + Toast.makeText(this, R.string.avatar_mode_error, Toast.LENGTH_SHORT).show() + } + } + } + } + public override fun onPause() { when (remoteConfig.getString(RemoteConfigKey.AD_NETWORK)) { RemoteConfigKey.AdNetwork.IMOBILE -> { diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2658ab1..ceceb17 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -57,6 +57,7 @@ チャットモード アバターモード + 音声入力の許可が必要です アバター画面の読み込みに失敗しました 3Dアバターモードが追加されました!右上のアイコンをタップしてお楽しみください。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b74b943..69bf144 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ Chat Mode Avatar Mode + Please allow permission to talk by voice Failed to load avatar page You can now talk to Hatsune Miku in 3D avatar mode!\n\nYou can change to avatar mode from the button on the top right of the screen. From cc9ed449a9e301dcec4e189601f4c1d871c5ca54 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 16:58:30 +0900 Subject: [PATCH 06/13] Update min SDK version --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5cae8a9..caf38a3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ android { compileSdk = 35 defaultConfig { applicationId = "comviewaquahp.google.sites.youbimiku" - minSdk = 21 + minSdk = 23 targetSdk = 35 versionCode = 36 versionName = "8.5" From bb622afe562a5f18bb3431dc15c732049c1aa251 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Wed, 27 Nov 2024 18:11:00 +0900 Subject: [PATCH 07/13] Implement information dialog --- .../com/aqua_ix/youbimiku/MainActivity.kt | 49 ++++++++++++++++--- .../config/SharedPreferenceManager.kt | 3 +- .../aqua_ix/youbimiku/config/UIModeConfig.kt | 24 +++++++++ app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- 5 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/aqua_ix/youbimiku/config/UIModeConfig.kt diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index f4ee889..7979df1 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -8,6 +8,8 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.Log import android.view.Gravity import android.view.Menu @@ -45,13 +47,16 @@ import com.aqua_ix.youbimiku.config.FontSizeConfig import com.aqua_ix.youbimiku.config.Key import com.aqua_ix.youbimiku.config.LanguageConfig import com.aqua_ix.youbimiku.config.SharedPreferenceManager +import com.aqua_ix.youbimiku.config.UIModeConfig import com.aqua_ix.youbimiku.config.getAIModel import com.aqua_ix.youbimiku.config.getFontSizeType import com.aqua_ix.youbimiku.config.getLanguage import com.aqua_ix.youbimiku.config.getOpenAIRequestCount +import com.aqua_ix.youbimiku.config.getUIMode import com.aqua_ix.youbimiku.config.setAIModel import com.aqua_ix.youbimiku.config.setFontSize import com.aqua_ix.youbimiku.config.setOpenAIRequestCount +import com.aqua_ix.youbimiku.config.setUIMode import com.aqua_ix.youbimiku.database.AppDatabase import com.aqua_ix.youbimiku.database.entityToMessage import com.aqua_ix.youbimiku.database.messageToEntity @@ -496,11 +501,40 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { restoreMessages() } - if (getAIModel(this).equals("")) { - showAIModelDialog(false) + + if (getUIMode(this) == "") { + showAvatarModeInfoDialog() + } else { + isAvatarMode = getUIMode(this) == UIModeConfig.AVATAR.name + Handler(Looper.getMainLooper()).postDelayed({ + toggleAvatarMode(isAvatarMode) + }, 300) // 起動直後にWebViewをロードするとクラッシュするため遅延させる } } + private fun showAvatarModeInfoDialog() { + val builder = AlertDialog.Builder(this) + .setTitle(getString(R.string.avatar_mode_message_title)) + .setMessage(getString(R.string.avatar_mode_message_text)) + .setPositiveButton(R.string.avatar_mode_message_accept) { _, _ -> + toggleAvatarMode(true) + + // 初回起動時にアバターモードを選択した場合はチャットモードをOpenAIに設定 + setAIModel(this, AIModelConfig.OPEN_AI) + } + .setNegativeButton(R.string.avatar_mode_message_cancel) { _, _ -> + setUIMode(this, UIModeConfig.CHAT) + + // モデル選択ダイアログを表示 + if (getAIModel(this).equals("")) { + showAIModelDialog(false) + } + } + + val dialog = builder.create() + dialog.show() + } + private fun setupWebView() { webView = binding.webView webView.visibility = View.GONE @@ -533,7 +567,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { binding.progressBar.visibility = View.GONE Toast.makeText(this@MainActivity, R.string.avatar_mode_error, Toast.LENGTH_SHORT) .show() - toggleAvatarMode() + toggleAvatarMode(false) } } @@ -735,22 +769,23 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } } - private fun toggleAvatarMode() { - isAvatarMode = !isAvatarMode + private fun toggleAvatarMode(enable: Boolean = !isAvatarMode) { + isAvatarMode = enable + setUIMode(this, if (isAvatarMode) UIModeConfig.AVATAR else UIModeConfig.CHAT) invalidateOptionsMenu() if (isAvatarMode) { // Switch to Avatar mode binding.chatView.visibility = View.GONE - webView.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE + webView.visibility = View.VISIBLE webView.loadUrl(BuildConfig.AVATAR_BASE_URL) } else { // Switch back to chat mode binding.chatView.visibility = View.VISIBLE + binding.progressBar.visibility = View.GONE webView.visibility = View.GONE webView.loadUrl("about:blank") - binding.progressBar.visibility = View.GONE } } diff --git a/app/src/main/java/com/aqua_ix/youbimiku/config/SharedPreferenceManager.kt b/app/src/main/java/com/aqua_ix/youbimiku/config/SharedPreferenceManager.kt index c4e3f6b..3423ed0 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/config/SharedPreferenceManager.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/config/SharedPreferenceManager.kt @@ -36,5 +36,6 @@ enum class Key { LANGUAGE, LAUNCH_COUNT, AI_MODEL, - OPENAI_REQUEST_COUNT + OPENAI_REQUEST_COUNT, + UI_MODE } \ No newline at end of file diff --git a/app/src/main/java/com/aqua_ix/youbimiku/config/UIModeConfig.kt b/app/src/main/java/com/aqua_ix/youbimiku/config/UIModeConfig.kt new file mode 100644 index 0000000..3c0dcf4 --- /dev/null +++ b/app/src/main/java/com/aqua_ix/youbimiku/config/UIModeConfig.kt @@ -0,0 +1,24 @@ +package com.aqua_ix.youbimiku.config + +import android.content.Context + +enum class UIModeConfig { + CHAT, + AVATAR +} + +fun getUIMode(context: Context): String? { + return SharedPreferenceManager.get( + context, + Key.UI_MODE.name, + "" + ) +} + +fun setUIMode(context: Context, mode: UIModeConfig) { + return SharedPreferenceManager.put( + context, + Key.UI_MODE.name, + mode.name + ) +} \ 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 ceceb17..2660cb3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -59,5 +59,8 @@ アバターモード 音声入力の許可が必要です アバター画面の読み込みに失敗しました - 3Dアバターモードが追加されました!右上のアイコンをタップしてお楽しみください。 + アバターモードが追加されました + 3Dアバターの初音ミクと会話できる機能が追加されました!(右上のアイコンをタップしてモードを切り替えることができます。) + 使ってみる + 閉じる diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 69bf144..8d6b8e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,5 +60,8 @@ Avatar Mode Please allow permission to talk by voice Failed to load avatar page - You can now talk to Hatsune Miku in 3D avatar mode!\n\nYou can change to avatar mode from the button on the top right of the screen. + About Avatar Mode + You can now talk to Hatsune Miku in 3D avatar mode! (You can change the mode from the button on the top right of the screen.) + Try Avatar Mode + Not Now From e972ebed5cd2f595ab0654745dc42a1ebbf4963f Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 00:40:15 +0900 Subject: [PATCH 08/13] Implement cloudflare service auth --- .../com/aqua_ix/youbimiku/MainActivity.kt | 84 ++++++++++++++++--- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index 7979df1..ec125a2 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -1,5 +1,6 @@ package com.aqua_ix.youbimiku +import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Intent @@ -8,8 +9,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.util.Log import android.view.Gravity import android.view.Menu @@ -21,6 +20,7 @@ import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout @@ -91,6 +91,8 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.net.HttpURLConnection +import java.net.URL class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { @@ -108,6 +110,8 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { private var isAvatarMode = false private lateinit var webView: WebView + private lateinit var avatarClientId: String + private lateinit var avatarClientSecret: String private var openAIPreviousResponse = "" @@ -502,14 +506,25 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } - if (getUIMode(this) == "") { - showAvatarModeInfoDialog() - } else { - isAvatarMode = getUIMode(this) == UIModeConfig.AVATAR.name - Handler(Looper.getMainLooper()).postDelayed({ - toggleAvatarMode(isAvatarMode) - }, 300) // 起動直後にWebViewをロードするとクラッシュするため遅延させる - } + val cfReference = firebaseDatabase.getReference("secrets/cloudflare") + cfReference.addValueEventListener(object : ValueEventListener { + override fun onDataChange(dataSnapshot: DataSnapshot) { + avatarClientId = dataSnapshot.child("clientId").getValue(String::class.java) ?: "" + avatarClientSecret = + dataSnapshot.child("clientSecret").getValue(String::class.java) ?: "" + + if (getUIMode(this@MainActivity) == "") { + showAvatarModeInfoDialog() + } else { + isAvatarMode = getUIMode(this@MainActivity) == UIModeConfig.AVATAR.name + toggleAvatarMode(isAvatarMode) + } + } + + override fun onCancelled(databaseError: DatabaseError) { + Log.e(TAG, "Database error: ${databaseError.message}") + } + }) } private fun showAvatarModeInfoDialog() { @@ -535,6 +550,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { dialog.show() } + @SuppressLint("SetJavaScriptEnabled") private fun setupWebView() { webView = binding.webView webView.visibility = View.GONE @@ -542,11 +558,45 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true - allowFileAccess = true } // Add WebView client to handle page loading webView.webViewClient = object : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + val url = request?.url.toString() + if (url == BuildConfig.AVATAR_BASE_URL || !url.startsWith(BuildConfig.AVATAR_BASE_URL)) { + return super.shouldInterceptRequest(view, request) + } + + val headers = mapOf( + "CF-Access-Client-Id" to avatarClientId, + "CF-Access-Client-Secret" to avatarClientSecret + ) + + return try { + val connection = URL(url).openConnection() as HttpURLConnection + headers.forEach { connection.setRequestProperty(it.key, it.value) } + connection.connect() + + Log.d( + TAG, + "WebResourceResponse: ${connection.contentType}, ${connection.contentEncoding}, ${connection.inputStream}, ${connection.headerFields}" + ) + + WebResourceResponse( + connection.contentType, + connection.contentEncoding ?: "utf-8", + connection.inputStream + ) + } catch (e: Exception) { + Log.e(TAG, "WebResourceResponse error: $e") + return super.shouldInterceptRequest(view, request) + } + } + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) binding.progressBar.visibility = View.VISIBLE @@ -592,6 +642,14 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } } + private fun loadAvatarPage() { + val headers = mapOf( + "CF-Access-Client-Id" to avatarClientId, + "CF-Access-Client-Secret" to avatarClientSecret + ) + webView.loadUrl(BuildConfig.AVATAR_BASE_URL, headers) + } + private fun showUserNameDialog(cancelable: Boolean = true) { val dialog = UserNameDialogFragment() val args = Bundle() @@ -604,7 +662,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { override fun doPositiveClick() { userAccount.setName(getUserName(this).toString()) if (isAvatarMode) { - webView.loadUrl(BuildConfig.AVATAR_BASE_URL) + loadAvatarPage() } } @@ -779,7 +837,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { binding.chatView.visibility = View.GONE binding.progressBar.visibility = View.VISIBLE webView.visibility = View.VISIBLE - webView.loadUrl(BuildConfig.AVATAR_BASE_URL) + loadAvatarPage() } else { // Switch back to chat mode binding.chatView.visibility = View.VISIBLE From a1072e7980b3cbac7c85aa026efb24a90c3a7449 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 00:48:36 +0900 Subject: [PATCH 09/13] Add reload button --- app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt | 7 +++++++ app/src/main/res/menu/menu.xml | 3 +++ app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 12 insertions(+) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index ec125a2..fca7ba7 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -769,6 +769,8 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { menu.findItem(R.id.setting_font_size).isVisible = !isAvatarMode menu.findItem(R.id.clear_message_history).isVisible = !isAvatarMode + menu.findItem(R.id.avatar_mode_reload).isVisible = isAvatarMode + menu.add(Menu.NONE, 1, Menu.NONE, R.string.avatar_mode) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) @@ -818,6 +820,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { true } + R.id.avatar_mode_reload -> { + loadAvatarPage() + true + } + R.id.setting_official_account -> { openOfficialAccountIntent() true diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml index a6dd9cf..afadc38 100644 --- a/app/src/main/res/menu/menu.xml +++ b/app/src/main/res/menu/menu.xml @@ -10,6 +10,9 @@ android:title="@string/setting_language"/> + \ 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 2660cb3..d005362 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -63,4 +63,5 @@ 3Dアバターの初音ミクと会話できる機能が追加されました!(右上のアイコンをタップしてモードを切り替えることができます。) 使ってみる 閉じる + リロード diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d6b8e4..0892812 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,4 +64,5 @@ You can now talk to Hatsune Miku in 3D avatar mode! (You can change the mode from the button on the top right of the screen.) Try Avatar Mode Not Now + Reload From d2de75e1f984f32ae3745eb8e215365b3d596046 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 01:52:29 +0900 Subject: [PATCH 10/13] Change the style of AI model dialog --- .../com/aqua_ix/youbimiku/MainActivity.kt | 55 +++++++++++-------- .../aqua_ix/youbimiku/config/AIModelConfig.kt | 8 +++ app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values/strings.xml | 5 +- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index fca7ba7..a69e9ab 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -49,6 +49,7 @@ import com.aqua_ix.youbimiku.config.LanguageConfig import com.aqua_ix.youbimiku.config.SharedPreferenceManager import com.aqua_ix.youbimiku.config.UIModeConfig import com.aqua_ix.youbimiku.config.getAIModel +import com.aqua_ix.youbimiku.config.getDisplayName import com.aqua_ix.youbimiku.config.getFontSizeType import com.aqua_ix.youbimiku.config.getLanguage import com.aqua_ix.youbimiku.config.getOpenAIRequestCount @@ -503,8 +504,15 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { } else { userAccount.setName(getUserName(this).toString()) restoreMessages() - } + if (getUIMode(this) == "") { + // 非初回起動ユーザー向けのアバターモード案内ダイアログ + showAvatarModeInfoDialog() + } else if (getAIModel(this) == "") { + // 非初回起動ユーザー向けのAIモデル選択ダイアログ + showAIModelDialog(false) + } + } val cfReference = firebaseDatabase.getReference("secrets/cloudflare") cfReference.addValueEventListener(object : ValueEventListener { @@ -513,9 +521,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { avatarClientSecret = dataSnapshot.child("clientSecret").getValue(String::class.java) ?: "" - if (getUIMode(this@MainActivity) == "") { - showAvatarModeInfoDialog() - } else { + if (getUIMode(this@MainActivity) != "") { isAvatarMode = getUIMode(this@MainActivity) == UIModeConfig.AVATAR.name toggleAvatarMode(isAvatarMode) } @@ -540,11 +546,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { .setNegativeButton(R.string.avatar_mode_message_cancel) { _, _ -> setUIMode(this, UIModeConfig.CHAT) - // モデル選択ダイアログを表示 + // 初回起動時にアバターモードを選択しなかった場合はAIモデル選択ダイアログを表示 if (getAIModel(this).equals("")) { showAIModelDialog(false) } } + .setCancelable(false) val dialog = builder.create() dialog.show() @@ -661,31 +668,31 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { override fun doPositiveClick() { userAccount.setName(getUserName(this).toString()) - if (isAvatarMode) { - loadAvatarPage() + + if (getUIMode(this) == "") { + showAvatarModeInfoDialog() } } private fun showAIModelDialog(cancelable: Boolean = true) { - AlertDialog.Builder(this) + val aiModels = AIModelConfig.entries.toTypedArray() + val aiModelNames = aiModels.map { getDisplayName(this, it) }.toTypedArray() + val currentIndex = aiModels.indexOfFirst { it.name == getAIModel(this) } + + val builder = AlertDialog.Builder(this) .setTitle(getString(R.string.setting_ai_model)) - .setMessage(getString(R.string.setting_ai_model_message)) - .setPositiveButton(getString(R.string.setting_ai_model_openai)) { _, _ -> - setAIModel(this, AIModelConfig.OPEN_AI) + .setSingleChoiceItems(aiModelNames, currentIndex) { dialog, which -> + setAIModel(this, aiModels[which]) mikuAccount = getMikuAccountFromAIModel() - if (::navMenu.isInitialized) { - navMenu.findItem(R.id.setting_language).isVisible = false - } - } - .setNegativeButton(getString(R.string.setting_ai_model_dialogflow)) { _, _ -> - setAIModel(this, AIModelConfig.DIALOG_FLOW) - mikuAccount = getMikuAccountFromAIModel() - if (::navMenu.isInitialized) { - navMenu.findItem(R.id.setting_language).isVisible = true - } + (dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true + invalidateOptionsMenu() } + .setPositiveButton(R.string.setting_dialog_accept, null) .setCancelable(cancelable) - .show() + + val dialog = builder.create() + dialog.show() + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = cancelable } private fun showFontSizeDialog() { @@ -760,12 +767,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { val inflater = menuInflater inflater.inflate(R.menu.menu, menu) getAIModel(this)?.let { - menu.findItem(R.id.setting_language).isVisible = it == AIModelConfig.DIALOG_FLOW.name + menu.findItem(R.id.setting_language).isVisible = + it == AIModelConfig.DIALOG_FLOW.name && !isAvatarMode } menu.findItem(R.id.setting_user_name).isVisible = !isAvatarMode menu.findItem(R.id.setting_ai_model).isVisible = !isAvatarMode - menu.findItem(R.id.setting_language).isVisible = !isAvatarMode menu.findItem(R.id.setting_font_size).isVisible = !isAvatarMode menu.findItem(R.id.clear_message_history).isVisible = !isAvatarMode diff --git a/app/src/main/java/com/aqua_ix/youbimiku/config/AIModelConfig.kt b/app/src/main/java/com/aqua_ix/youbimiku/config/AIModelConfig.kt index e9bbb41..40f8f88 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/config/AIModelConfig.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/config/AIModelConfig.kt @@ -1,6 +1,7 @@ package com.aqua_ix.youbimiku.config import android.content.Context +import com.aqua_ix.youbimiku.R enum class AIModelConfig { DIALOG_FLOW, @@ -21,4 +22,11 @@ fun setAIModel(context: Context, model: AIModelConfig) { Key.AI_MODEL.name, model.name ) +} + +fun getDisplayName(context: Context, model: AIModelConfig): String { + return when (model) { + AIModelConfig.DIALOG_FLOW -> context.getString(R.string.setting_ai_model_dialogflow) + AIModelConfig.OPEN_AI -> context.getString(R.string.setting_ai_model_openai) + } } \ 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 d005362..5556299 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -19,9 +19,8 @@ ユーザー名を入力してください。 AIモデルの選択 - 高性能なAI(GPT-3.5)を搭載した初音ミクさんと会話できます!\n\n※長時間待っても返答がない場合は、「AIモデルの選択」から標準モデルに切り替えてお楽しみください。 - 標準モデルと話す - GPTモデルと話してみる + 標準モデル + GPTモデル 文字サイズ 極小 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0892812..b2f89de 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,9 +19,8 @@ Cancel AI Model - You can now talk to Hatsune Miku with the GPT model!\n\n*If there is no response after waiting for a long time, please switch to the standard model from "AI model" menu and enjoy. - Talk with standard model - Try GPT model + Standard Model + GPT Model Font Size Extra Small From 5cad18deee8a488d7d79f0e0eaa83e5a448a0e45 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 01:52:59 +0900 Subject: [PATCH 11/13] Update strings --- app/src/main/res/values-ja/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5556299..4917c99 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -22,7 +22,7 @@ 標準モデル GPTモデル - 文字サイズ + 文字サイズの設定 極小 @@ -34,7 +34,7 @@ 履歴を消去 - 公式アカウント + 公式Xアカウント アプリが見つかりません 応援する From 961928b6be5e2d5bbe0007589b3ec30728c771d8 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 02:32:27 +0900 Subject: [PATCH 12/13] Fix permission string --- app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt index a69e9ab..8b5186a 100644 --- a/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt +++ b/app/src/main/java/com/aqua_ix/youbimiku/MainActivity.kt @@ -1011,7 +1011,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, DialogListener { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { webView.reload() } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - Toast.makeText(this, R.string.avatar_mode_error, Toast.LENGTH_SHORT).show() + Toast.makeText( + this, + R.string.avatar_mode_needs_record_audio_permission, + Toast.LENGTH_SHORT + ).show() } } } From 94a728b2f786bb307a0f94b0371e21ef44482e02 Mon Sep 17 00:00:00 2001 From: Soichi Ikebe Date: Thu, 28 Nov 2024 02:32:44 +0900 Subject: [PATCH 13/13] Insert break --- secrets.defaults.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secrets.defaults.properties b/secrets.defaults.properties index 6bf1928..1403b25 100644 --- a/secrets.defaults.properties +++ b/secrets.defaults.properties @@ -6,4 +6,4 @@ IMOBILE_MID=IMOBILE_MID IMOBILE_BANNER_SID=IMOBILE_BANNER_SID IMOBILE_INTERSTITIAL_SID=IMOBILE_INTERSTITIAL_SID IRONSOURCE_APP_KEY=IRONSOURCE_APP_KEY -AVATAR_BASE_URL=AVATAR_BASE_URL \ No newline at end of file +AVATAR_BASE_URL=AVATAR_BASE_URL