Inject mã Javascript vào ứng dụng Android

0

Mình đã từng tham gia một số dự án xây dựng ứng dụng Android mà sử dụng duy nhất một Webview. Tư tưởng của cách làm này giống với các ứng dụng cross platform, chúng ta chỉ cần xây dựng web app trên server, còn phía client như trên Android, iOS… chỉ cần dùng duy nhất Webview để hiển thị là đủ.

Android Webview cho phép hiển thị trang web ngay bên trong ứng dụng. Đặc biệt, webview còn cho phép tương tác với nội dung trang web, chạy được mã javascript, gần tương tự như một trình duyệt.

Tất nhiên, mỗi cách tiếp cận sẽ có ưu và nhược điểm riêng, tùy thuộc vào nhu cầu thực tế của dự án mà chúng ta thực hiện thôi

Khi sử dụng webview, đôi khi chúng ta sẽ cần phải can thiệp, thêm một đoạn mã Javascript trực tiếp vào webview (gọi là inject) mà không muốn sửa mã nguồn ở phía server. Có thể đó là mã đó để theo dõi người dùng, tracking hành động click vào một ảnh, liên kết… Bằng cách inject mã Javascript, bạn dễ dàng liên kết mã JS với mã native Android.

Bài viết này, mình sẽ hướng dẫn cách inject mã Javascript vào mã Android một cách đơn giản nhất. Để minh họa cho bài viết, chúng ta sẽ inject mã Javascript để triển khai cơ chế nhận thông báo khi người dùng nhấp vào các phần tử trên trang web trong webview. Cụ thể: Giả sử, bạn có một trình đọc tài liệu, bạn muốn theo dõi xem tần suất người dùng phải nhấn vào nút “Trợ giúp”.

Demo inject mã Javascript android
Demo ứng dụng minh họa inject mã Javascript

Cách sử dụng webview

Cách sử dụng Android Webview cũng tương tự như các UI component khác.

  • Một là bạn tạo webview trong layout xml, sau đó reference trong mã Kotlin/Java.

Ví dụ:

<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
  • Cách khác, bạn tạo instance webview trong mã Kotlin/Java mà không cần phải khai báo trong layout xml.

Để cho nhanh chóng, chúng ta sẽ sử dụng cách thứ 2 cho bài viết này.

class MainActivity : AppCompatActivity() {  

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState) 
   
    val webViewClient = WebViewClient()
    val webView = WebView(this)    
    
    webView.webViewClient = webViewClient
    webView.settings.javaScriptEnabled = true

    setContentView(webView)
    webView.loadUrl("https://vuetifyjs.com/en/getting-started/quick-start")
  }
}

Ngoài ra, do Webview phải truy cập vào internet để tải trang web nên bạn cần phải thêm quyền sau vào AndroidManifest.xml

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

Inject mã Javascript vào Android

Có hai tình huống mà ứng dụng web có thể phản hồi đối với sự kiện click của người dùng.

  • Tình huống 1: chuyển hướng ứng dụng. Tức là bạn click vào một liên kết, webview sẽ chuyển hướng hiển thị nội dung của liên kết đó. Trong Webview, bạn dễ dàng bắt được sự kiện chuyển hướng này bằng cách override hàm: shouldOverrideUrlLoading()
val webViewClient = object : WebViewClient() {
  override fun shouldOverrideUrlLoading(
    view: WebView, 
    request: WebResourceRequest
  ): Boolean {
    // log click event
    return super.shouldOverrideUrlLoading(view, request)
  }
}
  • Tình huống 2: đây chính là tình huống trong ví dụ của bài viết này. Người dùng click vào nút “Trợ giúp”, ứng dụng không hề chuyển hướng đi đâu cả. Trong trường hợp này, mỗi lần nhấp vào nút “Trợ giúp” sẽ được xử lý bởi mã JS mà chúng ta inject vào.

Export JS function để gọi trong native android

Có một cách để kết nối các JS function trong webview với mã native (Kotlin/Java) của bạn. Giả sử bạn có đoạn mã HTML trong trang web:

<button onclicked="Android.onClicked()">

Hàm Javascript onClicked() được gọi trên đối tượng global Android. Từ mã Kotlin, hàm này có thể được gọi thông qua addJavaScriptInterface().

object AndroidJSInterface {
    
  @JavascriptInterface
  fun onClicked() {
    Log.d(TAG, "Help button clicked")
  }
}

fun bind(webView: WebView) {
  webView.addJavascriptInterface(AndroidJSInterface, "Android")
}

Tiếp theo, chúng ta cần cấu hình thêm để có thể gọi được hàm JS

Truy xuất và chỉnh sửa DOM

Khi bạn làm việc với trang web, bạn sẽ quen thuộc với khái niệm DOM (Document Object Model). Nó là đối tượng đại diện cho toàn bộ trang HTML dưới dạng tree. Thông qua mã JS, bạn có thể truy xuất và chỉnh sửa DOM.

function f() {
  var btns = document.getElementsByTagName('button');
  for (var i = 0, n = btns.length; i < n; i++) {
    if (btns[i].getAttribute('aria-label') === 'Support') {
      btns[i].setAttribute('onclick', 'Android.onClicked()');
    }
  }
}

Giải thích thêm: Hàm trên có tác dụng tìm tất cả các phần tử <button> có thuộc tính aria-label='Support' để thêm vào một thuộc tính mới onclick='Android.onClick()'.

DOM khi inject mã Javascript android

Thực thi mã JS trong webview

Cuối cùng là chúng ta thực thi mã JS để chỉnh sửa DOM trong webview. Đơn giản là chúng ta gọi hàm JS thông qua hàm loadUrl() của webview.

Đoạn mã gọi và thực thi mã JS như sau:

val webViewClient = object : WebViewClient() {
  override fun onPageFinished(view: WebView, url: String) {
    view.loadUrl(
      """javascript:(function f() {
        var btns = document.getElementsByTagName('button');
        for (var i = 0, n = allElements.length; i < n; i++) {
          if (btns[i].getAttribute('aria-label') === 'Support') {
            btns[i].setAttribute('onclick', 'Android.onClicked()');
          }
        }
      })()"""
    )
  }
}

Việc gọi hàm loadUrl() nhiều lần không làm cho nội dung trang bị tải lại nhiều lần, webview sẽ tự động biết điều đó.

Ngoài ra, nếu muốn thực thi mã JS sau khi toàn bộ nội dung trang web được tải xong, đó là lý do chúng ta gọi hàm JS trong onPageFinished() callback.

Như vậy là đã hoàn thành rồi đấy. Đoạn mã đầy đủ việc inject mã JS vào trong mã native Android như sau:

class MainActivity : AppCompatActivity() {

  object AndroidJSInterface {
    @JavascriptInterface
    fun onClicked() {
      Log.d("HelpButton", "Help button clicked")
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val webViewClient = object : WebViewClient() {
      override fun onPageFinished(view: WebView, url: String) {
        loadJs(view)
      }
    }
    
    val webView = WebView(this)
    webView.webViewClient = webViewClient
    webView.settings.javaScriptEnabled = true
    webView.addJavascriptInterface(AndroidJSInterface, "Android")    

    setContentView(webView)
    webView.loadUrl("https://vuetifyjs.com/en/getting-started/quick-start")
  }

  private fun loadJs(webView: WebView) {
    webView.loadUrl(
      """javascript:(function f() {
        var btns = document.getElementsByTagName('button');
        for (var i = 0, n = btns.length; i < n; i++) {
          if (btns[i].getAttribute('aria-label') === 'Support') {
            btns[i].setAttribute('onclick', 'Android.onClicked()');
          }
        }
      })()"""
    )
  }
}

Thay lời kết

Webview mang đến cho nhà lập trình viên nhiều lựa chọn phát triển ứng dụng.

Tuy nhiên, với cách tiếp cận như trong bài viết này, bạn sẽ có nhiều rủi ro về các lỗ hổng bảo mật về XSS.  Hãy chắc chắn rằng, bạn biết những gì bạn đang làm.

Ngoài ra, những thay đổi mã nguồn của trang web trên server cũng có thể khiến việc inject JS hoạt đông sai hoặc không hoạt động hoàn toàn.

Mình hi vọng, bài viết này giúp ích phần nào cho dự án của bạn. Hẹn gặp lại ở bài viết sau nhé.

💦 Đọc thêm bài viết lập trình Android khác:

Tài liệu tham khảo:

  • https://droidmentor.com/bind-javascript-to-android/
  • https://medium.com/@skywall/inject-js-into-androids-webview-8845fb5902b7
Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo