package kr.co.tadadang.user import android.animation.Animator import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.view.Gravity import android.view.View import android.view.WindowManager import android.webkit.CookieManager import android.webkit.JavascriptInterface import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import kr.co.tadadang.user.native_bridge.ExitAppVCDelegate import kr.co.tadadang.user.native_bridge.HideIntroVCDelegate import kr.co.tadadang.user.sms.SmsRetrieverInterface import kr.co.tadadang.user.util.AnimationUtil class MainActivity : ComponentActivity(), HideIntroVCDelegate, ExitAppVCDelegate { private lateinit var mContext: Context private lateinit var mActivity: MainActivity private lateinit var mWebView: WebView private val mCookieManager by lazy { CookieManager.getInstance() } private var mToast: Toast? = null var mOriUserAgent: String = "" private var uploadMessage: ValueCallback>? = null val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { var results: Array? = WebChromeClient.FileChooserParams.parseResult(it.resultCode, it.data) if (results != null) { uploadMessage?.onReceiveValue(results) } else { val uris = ArrayList() if (intent != null) { val clipData = intent.clipData!! if (clipData.itemCount > 0) { var i = 0 val size = clipData.itemCount while (i < size) { val uri = clipData.getItemAt(i).uri uris.add(uri) i++ } results = uris.toTypedArray() } } if (results != null) { uploadMessage?.onReceiveValue(results) } else { uploadMessage?.onReceiveValue(null) } } } } @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mContext = this mActivity = this mWebView = findViewById(R.id.webview) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) setCookieAllow(mWebView) val webSettings = mWebView.settings webSettings.javaScriptEnabled = true webSettings.allowContentAccess = true webSettings.allowFileAccess = true webSettings.allowFileAccessFromFileURLs = true webSettings.allowUniversalAccessFromFileURLs = true webSettings.builtInZoomControls = false webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH) webSettings.domStorageEnabled = true webSettings.textZoom = 100 webSettings.cacheMode = WebSettings.LOAD_NO_CACHE webSettings.setSupportMultipleWindows(true) webSettings.javaScriptCanOpenWindowsAutomatically = true var versionCode = 0 var versionName = "" try { val pInfo = this.packageManager.getPackageInfo(packageName, 0) versionCode = pInfo.versionCode versionName = pInfo.versionName } catch (e: PackageManager.NameNotFoundException) { } mOriUserAgent = webSettings.userAgentString var newUserAgent = mOriUserAgent newUserAgent += ",type:admin" var isDarkMode = false try { val darkModeCheck = mContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK if (darkModeCheck == Configuration.UI_MODE_NIGHT_YES) { isDarkMode = true } } catch (_: Exception) { } newUserAgent += ",theme:${if (isDarkMode) "dark" else "light"}" newUserAgent += ",os:aos" newUserAgent += ",vercode:$versionCode" newUserAgent += ",vername:$versionName" newUserAgent += ",osver:" + Build.VERSION.SDK_INT newUserAgent += ",:end:" webSettings.userAgentString = newUserAgent mWebView.webChromeClient = object : WebChromeClient() { override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams? ): Boolean { try { if (fileChooserParams != null) { uploadMessage?.onReceiveValue(null) uploadMessage = null uploadMessage = filePathCallback val isMultiSelect = fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE if (fileChooserParams.acceptTypes.indexOf(".png") != -1 || fileChooserParams.acceptTypes.indexOf( "image/*" ) != -1 ) { // 이미지 선택기 val galleryIntent = Intent(Intent.ACTION_PICK) galleryIntent.type = "image/*" if (isMultiSelect) { galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } val intent = Intent.createChooser(galleryIntent, "") resultLauncher.launch(intent) } else if (fileChooserParams.acceptTypes.indexOf(".avi") != -1 || fileChooserParams.acceptTypes.indexOf( "video/*" ) != -1 ) { // 비디오 선택기 val videoIntent = Intent( Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI ) videoIntent.type = "video/*" if (isMultiSelect) { videoIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } resultLauncher.launch(videoIntent) } else { // 파일 선택기 val galleryIntent = Intent(Intent.ACTION_PICK) galleryIntent.type = "video/*, image/*" if (isMultiSelect) { galleryIntent.putExtra( Intent.EXTRA_ALLOW_MULTIPLE, true ) } resultLauncher.launch(galleryIntent) } return true } } catch (e: Exception) { uploadMessage = null e.printStackTrace() } return false } } mWebView.webViewClient = object : WebViewClient() { // 페이지 오류 처리 override fun onReceivedError( view: WebView?, request: WebResourceRequest?, error: WebResourceError? ) { showErrorPage(request?.url) super.onReceivedError(view, request, error) } override fun onReceivedError( view: WebView?, errorCode: Int, description: String?, failingUrl: String? ) { showErrorPage(Uri.parse(failingUrl)) super.onReceivedError(view, errorCode, description, failingUrl) } override fun onPageFinished(view: WebView?, url: String?) { CookieManager.getInstance().flush() pageFinishedAction(Uri.parse(url)) super.onPageFinished(view, url) } // 브릿지 처리 override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? ): Boolean { if (checkUrl(view, request?.url)) { return true } return super.shouldOverrideUrlLoading(view, request) } override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { if (checkUrl(view, Uri.parse(url))) { return true } return super.shouldOverrideUrlLoading(view, url) } } mWebView.setDownloadListener { url, userAgent, contentDisposition, mimeType, contentLength -> val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(url) startActivity(intent) } mCookieManager.acceptCookie() mCookieManager.acceptThirdPartyCookies(mWebView) mWebView.addJavascriptInterface(WebAppInterface(), "Android") mWebView.addJavascriptInterface(SmsRetrieverInterface(this, mWebView), "SmsRetriever") mWebView.loadUrl(Const.PAGE_URL) val window = window window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.statusBarColor = Color.parseColor("#0B192F") // 원하는 색상 설정 window.decorView.systemUiVisibility = 0 // 기본 상태(어두운 배경에 밝은 글자) askNotificationPermission() } // Declare the launcher at the top of your Activity/Fragment: private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission(), ) { isGranted: Boolean -> if (isGranted) { // FCM SDK (and your app) can post notifications. } else { // TODO: Inform user that that your app will not show notifications. } } private fun askNotificationPermission() { // This is only necessary for API level >= 33 (TIRAMISU) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission( this, android.Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED ) { // FCM SDK (and your app) can post notifications. } else if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) { // TODO: display an educational UI explaining to the user the features that will be enabled // by them granting the POST_NOTIFICATION permission. This UI should provide the user // "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission. // If the user selects "No thanks," allow the user to continue without notifications. } else { // Directly ask for the permission requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) } } } @JavascriptInterface fun copyToClipboard(text: String?) { val clipboard: ClipboardManager = mContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Copied Text", text) clipboard.setPrimaryClip(clip) // 사용자에게 피드백 제공 (Toast 메시지) //Toast.makeText(mContext, "클립보드에 복사되었습니다!", Toast.LENGTH_SHORT).show() } override fun onStop() { super.onStop() onBackground() } override fun onPause() { super.onPause() try { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } catch (e: NullPointerException) { } LocalBroadcastManager.getInstance(this).unregisterReceiver(smsReceiver) onBackground() } private fun setCookieAllow(webView: WebView) { try { val cookieManager = CookieManager.getInstance() cookieManager.setAcceptCookie(true) webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW cookieManager.setAcceptThirdPartyCookies(webView, true) } catch (_: Exception) { } } // 페이지 접속 에러 처리 fun showErrorPage(uri: Uri?) { if (uri != null && (uri.toString().endsWith("favicon.ico") || uri.toString() .indexOf("sockjs-node") != -1) ) { return } // 에러 처리 부분 } // 페이지 로드 완료 처리 fun pageFinishedAction(uri: Uri?) { // 로드 완료 처리 부분 } // 링크 주소 인젝션 처리 fun checkUrl(webview: WebView?, uri: Uri?): Boolean { // 링크 주소 중간에 가로채기 관련 별도 처리 추가 if (uri != null) { if (!uri.toString().startsWith(Const.PAGE_URL)) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri.toString())) startActivity(intent) return true } } return false } override fun onBackPressed() { val currentUrl = mWebView.url if (currentUrl != null && (currentUrl.contains("dashboard") || currentUrl.contains("chatlist") || currentUrl.contains("login") || currentUrl.contains("feed") )) { exitAppAction() }else { mWebView.loadUrl("javascript:appBackAction()") } //onBackAction() // if (mWebView.canGoBack()) { // // 웹뷰가 뒤로 갈 수 있는 경우 // val currentUrl = mWebView.url // if (currentUrl != null && (currentUrl.contains("dashboard") || currentUrl.contains("feed") || currentUrl.contains("chatlist"))) { // // }else { // mWebView.loadUrl("javascript:appBackAction()"); // } // } else { // // } //mWebView.loadUrl("javascript:appBackAction()"); } private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { //onBackAction() this.isEnabled = false onBackPressedDispatcher.onBackPressed() } } // 뒤로가기 버튼 처리 var back = 0L // private fun onBackAction() { // val layIntro = findViewById(R.id.lay_intro) // if (layIntro.visibility == View.VISIBLE) { // if (System.currentTimeMillis() > back + 2000) { // back = System.currentTimeMillis() // showCenterToast(R.string.backkey_finish) // } else { // hideCenterToast() // finish() // } // return // } else { // mWebView.loadUrl("javascript:appBackAction()") // return // } // } private fun onBackground() { mWebView.loadUrl("javascript:appBackground()") } private fun onForeground() { mWebView.loadUrl("javascript:appForeground()") } inner class WebAppInterface internal constructor() { @JavascriptInterface fun nativeBridge(cmd: String?, data: String?, callback: String?) { if (cmd != null) { for (bridgeItem in Const.APP_BRIDGE_LIST) { if (bridgeItem.cmd.equals(cmd, true)) { bridgeItem.startAction(mActivity, mWebView, data, callback) } } } } } private fun showCenterToast(msgRes: Int, duration: Int = Toast.LENGTH_LONG) { hideCenterToast() runOnUiThread { mToast = Toast.makeText(mContext, msgRes, duration) mToast!!.setGravity(Gravity.CENTER, 0, 0) mToast!!.show() } } private fun hideCenterToast() { runOnUiThread { if (mToast != null) { mToast!!.cancel() mToast = null } } } override fun hideIntroAction() { val layIntro = findViewById(R.id.lay_intro) if (layIntro.visibility == View.VISIBLE) { val ani = AnimationUtil.getAlpha(layIntro, 350, 1f, 0f) if (ani != null) { ani.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) { } override fun onAnimationEnd(animation: Animator) { layIntro.visibility = View.GONE } override fun onAnimationCancel(animation: Animator) { layIntro.visibility = View.GONE } override fun onAnimationRepeat(animation: Animator) { } }) ani.start() } else { layIntro.visibility = View.GONE } } } override fun exitAppAction() { if (System.currentTimeMillis() > back + 2000) { back = System.currentTimeMillis() showCenterToast(R.string.backkey_finish) } else { hideCenterToast() finish() } } companion object { private const val TAG = "MainActivity" } override fun onResume() { super.onResume() LocalBroadcastManager.getInstance(this) .registerReceiver(smsReceiver, IntentFilter("SMS_CODE_RECEIVED")) onForeground() } private val smsReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val smsCode = intent?.getStringExtra("smsCode") smsCode?.let { val smsRetrieverInterface = SmsRetrieverInterface(this@MainActivity, mWebView) smsRetrieverInterface.onSmsReceived(it) } } } }