Android 루팅(Rooting) 여부 체크

RootBeer의 간단한 버전으로 루팅여부를 체크해보자.

class RootChecker(private val context: Context) { private val rootFiles = arrayOf( "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/system/usr/we-need-root/", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su", "/su/bin", "/system/xbin/daemonsu" ) private val rootPackages = arrayOf( "com.devadvance.rootcloak", "com.devadvance.rootcloakplus", "com.koushikdutta.superuser", "com.thirdparty.superuser", "eu.chainfire.supersu", "de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot", "com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "com.noshufou.android.su", "com.noshufou.android.su.elite", "com.yellowes.su", "com.topjohnwu.magisk", "com.kingroot.kinguser", "com.kingo.root", "com.smedialink.oneclickroot", "com.zhiqupk.root.global", "com.alephzain.framaroot" ) private val runtime by lazy { Runtime.getRuntime() } fun isDeviceRooted(): Boolean { return checkRootFiles() || checkSUExist() || checkRootPackages() } private fun checkRootFiles(): Boolean { for (path in rootFiles) { try { if (File(path).exists()) { return true } } catch (e: RuntimeException) { } } return false } private fun checkSUExist(): Boolean { var process: Process? = null val su = arrayOf("/system/xbin/which", "su") try { process = runtime.exec(su) BufferedReader( InputStreamReader( process.inputStream, Charset.forName("UTF-8") ) ).use { reader -> return reader.readLine() != null } } catch (e: IOException) { } catch (e: Exception) { } finally { process?.destroy() } return false } private fun checkRootPackages(): Boolean { val pm = context.packageManager if (pm != null) { for (pkg in rootPackages) { try { pm.getPackageInfo(pkg, 0) return true } catch (ignored: PackageManager.NameNotFoundException) { // fine, package doesn't exist. } } } return false } }


Android Webview ERR_CACHE_MISS 오류

Android 웹뷰(WebView)를 사용할때 net::ERR_CACHE_MISS 가 나타나는 경우가 있다.

해당 에러는 캐시를 정상적으로 사용할 수 없는 경우에 나타나는 에러이다.

보통은 다음과 같은 코드 추가로 해결이 가능하다.

menifest.xml 에 네트워크 접속권한 추가

<!-- 네트워크 접속 권한 -->

<uses-permission android:name="android.permission.INTERNET"/>


위와 같은 방법으로도 해결이 안되는 경우들도 있는 것 같아 다음의 내용을 기술해 본다.

HTML, 웹서버에서 캐쉬 관련 부분으로 해결이 가능하기도 하다.

1. HTML 인 경우
<META http-equiv=”Expires” content=”-1″> <META http-equiv=”Pragma” content=”no-cache”> <META http-equiv=”Cache-Control” content=”No-Cache”>
2. ASP인 경우
<%     Response.Expires = 0     Response.AddHeader “Pragma”,”no-cache”     Response.AddHeader “Cache-Control”,”no-cache,must-revalidate” %>
3. JSP인 경우
<%     response.setHeader(“Cache-Control”,”no-store”);     response.setHeader(“Pragma”,”no-cache”);     response.setDateHeader(“Expires”,0);     if (request.getProtocol().equals(“HTTP/1.1”))         response.setHeader(“Cache-Control”, “no-cache”); %>
3. PHP인 경우
<?     header(“Pragma: no-cache”);     header(“Cache-Control: no-cache,must-revalidate”);     header("Cache-Control: no-cache"); ?>


Jetpack Compose WebView 사용 이슈정리

Android webview 를 구현함에 있어서 기존 방식(as-is)과 compose 에서 webview 사용방식(to-be)의 차이점 위주로 내용을 정리해봤습니다.

(2024–01–12)

  • Accompanist Webview 같은 경우 Deprecated 되었습니다.
  • Deprecated 된 이유는 별도의 커스텀이 필요하지 않다면, Accompanist Webview를 그대로 사용해도 좋지만, 그게 아니라면 fork하거나 생성해서 사용해야합니다.

https://medium.com/androiddevelopers/an-update-on-jetpack-compose-accompanist-libraries-august-2023-ac4cbbf059f1



의존성 추가

Compose 용 WebView를 구현하기 위해선 아래 라이브러리 추가가 필요합니다.

(app/build.gradle) 작성 당시 0.24.13-rc가 최신이었으나 계속 업데이트 되고 있기 때문에 최신버전을 권장드립니다.

dependencies {

     implementation "com.google.accompanist:accompanist-webview:0.24.13-rc" 

}

WebView 비교해보기

- 기존 loadUrl 대체

//as-is
binding.webview.loadUrl("www.naver.com")
//to-be
val webViewState =
rememberWebViewState(
url = "www.naver.com",
additionalHttpHeaders = emptyMap()
)
WebView(state = webViewState)

webViewState안에서 url과 additionalHttpHeaders를 추가해 줄 수 있는데,
로드하고자 하는 Url과 HttpHeaders(AccessToken 등) 또한 설정할 수 있습니다.


- 기존 WebClient, ChromeClient 대체

//as-is
binding.webview.webViewClient = WebViewClient()
binding.webview.webChromeClient = WebChromeClient()
//to-be
private val webViewClient = AccompanistWebViewClient()
private val webChromeClient = AccompanistWebChromeClient()
WebView(
state = webViewState,
client = webViewClient,
chromeClient = webChromeClient
)

기존에 사용하던 WebViewClient, WebChromeClient을 사용하는 것이 아닌 각각 AccompanistWebViewClient, AccompanistWebChromeClient을 사용해야 합니다. 단 Accompanist 용 Client는 Composable이 아니기 때문에 전역변수로 선언해놓고 사용하는것이 좋을 것 같습니다.


- 기존 WebView Setting 대체

//as-is
webview.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = false
}
//to-be
WebView(
state = webViewState,
client = webViewClient,
chromeClient = webChromeClient,
onCreated = { webView ->
with(webView) {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = false
}
}
}
)

WebView의 onCreated는 WebView가 처음 생성될 때 호출됩니다. 위와 같은 웹뷰 추가 설정을 하려면 Composable WebView의 onCreated 인자값으로 람다를 넘겨서 설정해야합니다. 또한 공식문서에선 WebViewClient와 WebChromeClient는 onCreated 람다가 호출된 후에 client가 설정되므로 내부에서 설정하지 말라고 명시되어 있습니다.


- 기존 WebView 뒤로가기 제어

//as-is
override fun onBackPressed() { // Activity 기준
if(binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
super.onBackPressed()
}
}
//to-be
val webViewNavigator = rememberWebViewNavigator()
WebView(
state = webViewState,
navigator = webViewNavigator,
client = webViewClient,
chromeClient = webChromeClient,
onCreated = onCreated
)
BackHandler(enabled = true) {
if (webViewNavigator.canGoBack) {
webViewNavigator.navigateBack()
} else {
findNavController().popBackStack() // 프로젝트에 맞게 사용
}
}

기존 Activity나 Fragment를 사용하는 경우 onBackPressed 또는 OnBackPressedCallback을 구현해 내부에서 WebView에 대한 동작을 처리했었습니다.

Composable WebView에서는 webViewNavigator를 생성해 BackHandler Composable 안에서 WebView에 대한 동작을 처리해야 합니다.

BackHandler Composable은 시스템 뒤로가기 버튼을 눌렀을때 동작을 정의 할 수 있는 Composable입니다. 컴포저블에서 BackHandler를 사용하면LocalOnBackPressedDispatcherOwner 의 OnBackPressedDispatcher 에 추가됩니다.

// WebView Composable 내부 코드BackHandler(captureBackPresses && navigator.canGoBack) {
webView?.goBack()
}

WebView Composable 내부에는 BackHandler가 추가되어있어, 필요에 따라 별도로 구현해주시면 됩니다.


- WebView 브릿지

onCreated = { webView ->
with(webView) {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = false
}
addJavascriptInterface(Bridge(), "Bridge")

}
}

브릿지 같은 경우에는 Composable 내 onCreated에서 설정이 가능합니다.

- State 중요 참고사항 

추가로 state같은 경우 remember를 사용할 경우 Configuration Change가 발생 했을 경우 초기화 될 수 있기 때문에 rememberSavable을 사용해 Bundle에 저장하여 상태를 저장할 수 있습니다.

다만 Activity에 상태를 저장하는 것은 이상적이지 않으므로 ViewModel을 사용하는 것이 UI와 상태를 분리할 수 있는 장점이 있고, remember를 사용하지 않고도 상태를 유지할 수 있기 때문에 ViewModel 내에서 State를 구현하는 것을 추천드립니다.

class WebViewViewModel:ViewModel() {
val webViewState = WebViewState(
WebContent.Url(
url = "www.naver.com",
additionalHttpHeaders = emptyMap()
)
)
val webViewNavigator = WebViewNavigator(viewModelScope)

}
...// MainActivity.ktsetContent {
val webViewState = viewModel.webViewState
val webViewNavigator = viewModel.webViewNavigator


Android 13 Notification Permission 알림권한허용

Android 12 까지는 앱에서 별도의 권한을 받지 않고 Notification 을 띄울 수 있었다. (활성화 상태가 디폴트값)

앱 설정에서 Notification 알림 활성을 통해 Notification 권한을 조정할수 있었다.

하지만, Android 13 부터는 Notificatioin 권한이 기본적으로 비활성 상태이며 유저가 권한을 허용해주어야만 활성화 상태가 된다.

AndroidManifest 에서 Notificatioin 권한을 요청할 수 있다. 

Target SDK API 33 이상에서만 권한 수가가 가능하며, 그 이후는 기존 안드로이드 권한 요청과 동일하다.

<manifest ...> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <application ...> ... </application> </manifest>


Target SDK 33인 앱이 Android 12 이하의 디바이스에 설치된다면?

기존 안드로이드 OS에서는 따로 알림 관련된 권한 요청이 없었다.

POST_NOTIFICATIONS 해당 권한 요청을 아무리 보내도 권한 알림 팝업은 노출되지 않는다.

알림 권한은 기존과 동일하게 설정에 들어가서 활성화/비활성화를 진행하며 기본 앱 진입 시 무조건 활성화 상태로 진입되게 된다.


Target SDK 32인 앱이 Android 13 이상의 디바이스에 설치된다면?

Android 13 부터는 Notification 권한이 존재한다. 그러나 Target SDK 33 이상부터 알림 관련 퍼미션을 정의할 수 있다. (android.permission.POST_NOTIFICATIONS)

notificationChannel을 등록할 때 자동으로 알림 권한 팝업이 노출된다

POST_NOTIFICATIONS (Notification Permission) 은 Target SDK API 33 이상부터 추가 가능

Target SDK API 32 이하의 앱이 Android 13 디바이스에 설치되면 Notification Channel을 등록할 때 자동으로 Notification 권한 요청 팝업이 나옴

Target SDK API 33 이상의 앱이 Android 13 디바이스에 설치되면 Notification 권한요청을 개발자가 원하는 타이밍에 노출 가능

Target SDK API 33 이상의 앱이 Android 12 이하 디바이스에 설치되면 기존과 동일하게 Notification 권한 요청 없이 사용 가능

Target SDK API 32 앱을 33으로 업데이트 시 기존 알림 권한 동의 상태라면 업데이트 이후 기본으로 허용이지만 예외 있음

(기기에따라 자동으로 허용되지 않고 다시 한 번 권한을 얻어야하는 경우가 있음)

//알림권한허용 팝업 (AOS 13부터) private fun postNotificationCheck() { Log.d("postNotificationCheck()") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { TedPermission.with(this) .setPermissionListener(postNotificationListener) .setPermissions(Manifest.permission.POST_NOTIFICATIONS) .check() SharedPref.getInstance().setShowPostNotificationPopup(true) } } private val postNotificationListener: PermissionListener = object : PermissionListener { override fun onPermissionGranted() { Log.d("postNotificationListener onPermissionGranted()") } override fun onPermissionDenied(deniedPermissions: List) { Log.d("postNotificationListener onPermissionDenied()") CommonDialog(this, getString(R.string.common_noti_label), getString(R.string.perm_post_notification_denied_message), false, object : DialogClickListener { override fun onPositiveClick() { //알림설정화면 이동 openAppNotificationSettings(this) } override fun onNegativeClick() { } }) } }


Android 13에서는 Notification 권한 요청을 기존 권한들과 동일한 방식으로 요청하며

Android 12에서는 Notification 권한이 기본적으로 무조건 활성화 상태였다면

Android 13에서는 Notification 권한이 기본적으로 비활성화 상태이며 

사용자가 권한을 허용해줘야 활성화 상태가 됩니다.


그외에 알림권한과 관련된 유용한 정보들은 공식문서를 통해서 확인하실 수 있습니다.

https://developer.android.com/about/versions/13/changes/notification-permission


Android WebView WebViewClient (웹뷰에서 일어나는 요청, 상태, 에러 등 다양한 상황) 재정의 사용

 WebViewClient를 통해 웹뷰에서 일어나는 요청, 상태, 에러 등 다양한 상황에서의 콜백을 조작할 수 있습니다. 

다양한 메소드를 제공하고 있습니다만 대표적으로 사용되는 몇 가지 메소드만 살펴보도록 하겠습니다. 

전체 메소드에 대해서는 developer 사이트에서 확인하실 수 있습니다.


1. shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?)

- 현재 웹뷰에 로드될 URL에 대한 컨트롤을 할 수 있는 메소드


2. onPageStarted(view: WebView?, url: String?, favicon: Bitmap?)

- page loading을 시작했을 때 호출되는 콜백 메소드


3. onPageFinished(view: WebView?, url: String?)

- page loading을 끝냈을 때 호출되는 콜백 메소드


4. onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?)

- request 에 대해 에러가 발생했을 때 호출되는 콜백 메소드. error 변수에 에러에 대한 정보가 담겨져있음


5. onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?)

- 웹서버의 http 에러 발생시 호출되는 콜백 메서드


6. onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?)

- 웹뷰 SSL 오류관련 핸들러 콜백 메소드



override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { super.onReceivedError(view, request, error) Log.e("onReceivedError errorCode : ${error.errorCode}, description : ${error.description}, failingUrl : ${request.url}") val errorCode = error.errorCode //CONNECTION, TIMED_OUT, PROXY 경우 네트워크 에러페이지 노출 //errorCode : -5, description : net::ERROR_PROXY_AUTHENTICATION //errorCode : -6, description : net::ERR_CONNECTION_CLOSED //errorCode : -6, description : net::ERR_CONNECTION_ABORTED, ERR_CONNECTION_FAILED //errorCode : -6, description : net::ERR_CONNECTION_RESET, ERR_CONNECTION_REFUSED //errorCode : -6, description : net::ERR_SOCKET_NOT_CONNECTED //errorCode : -8, description : net::ERR_CONNECTION_TIMED_OUT, net::ERR_TIMED_OUT

    when (errorCode) {

ERROR_AUTHENTICATION -> {} ERROR_BAD_URL -> {} ERROR_CONNECT -> { //errorCode : -6, description : net::ERR_CONNECTION_CLOSED //errorCode : -6, description : net::ERR_CONNECTION_ABORTED //errorCode : -6, description : net::ERR_CONNECTION_FAILED //errorCode : -6, description : net::ERR_CONNECTION_RESET //errorCode : -6, description : net::ERR_CONNECTION_REFUSED //errorCode : -6, description : net::ERR_SOCKET_NOT_CONNECTED } ERROR_FAILED_SSL_HANDSHAKE -> {} ERROR_FILE -> {} ERROR_FILE_NOT_FOUND -> {} ERROR_HOST_LOOKUP -> {} ERROR_IO -> {} ERROR_IO -> {} ERROR_PROXY_AUTHENTICATION -> { //errorCode : -5, description : net::ERROR_PROXY_AUTHENTICATION } ERROR_REDIRECT_LOOP -> {} ERROR_TIMEOUT -> { //errorCode : -8, description : net::ERR_CONNECTION_TIMED_OUT //errorCode : -8, description : net::ERR_TIMED_OUT } ERROR_TOO_MANY_REQUESTS -> {} ERROR_UNKNOWN -> {} ERROR_UNSUPPORTED_AUTH_SCHEME -> {} ERROR_UNSUPPORTED_SCHEME -> {} }

}


override fun onReceivedHttpError( view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) { super.onReceivedHttpError(view, request, errorResponse) Log.d("onReceivedHttpError() url : ${view?.url}, statusCode : ${errorResponse?.statusCode}") // HTTP STATUS CODE 503 (WAS 죽음) // HTTP STATUS CODE 502 (Bad Gateway) try { var statusCode = errorResponse?.statusCode if (statusCode == 503 || statusCode == 502) { //네트워크 에러페이지 노출 } } catch (e: Exception) {} }

Android WebView 타임아웃(TimeOut) 기능

Android WebView는 페이지 관련해서 타이아웃 설정이 없다

상황에 따라 bad network 상황시 WebViewClient에 타이아웃 관련 기능을 통해 사용자에게 네트워크 장애표시를 해야할 필요가 있었다.

다음의 동작으로 간략한 webview에 타임아웃 기능을 구현해 보았다.



mWebView.setWebViewClient(new WebViewClient() {
     private val loadingTimeOut = 1000 * 10
     private var timeout = false


    //페이지 타임아웃 타이머
    private val pageTimeoutTimer: CountDownTimer = object : CountDownTimer(loadingTimeOut.toLong(), 1000) {

        override fun onTick(millisUntilFinished: Long) {
        }

        override fun onFinish() {
            Log.d("pageTimeoutTimer onFinish")


            if (timeout) {
                //네트워크 장애관련 페이지 노출
                Log.d("onPageStarted networkFailActivityForError")

            }

            pageFinishDone()
        }
    }


    //페이지 완료 처리
    private fun pageFinishDone() {
        pageTimeoutTimer.cancel()
        timeout = false

        dismissLoading()
    }


    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        Log.d("onPageStarted WebView URL : $url")

        pageTimeoutTimer.cancel()
        pageTimeoutTimer.start()
        timeout = true

        showLoading()
    }

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        Log.d("onPageFinished WebView URL : $url")

        pageFinishDone()
    }

});

Android Compose Animation

나타남 /  사라짐 애니메이션 AnimatedVisibility 의 enter 및 exit 매개변수를 사용하면 컴포저블이 표시되고 사라질 때의 동작을 구성할 수 있습니다. 자세한 내용은  전체 문서 를 참고하세요. 컴포저블의 가시성을 애니메이션화하는 ...