Quickstart(web view)
Android(JavaScript)
Using JavaScript SDK within the Android WebView requires some extra steps.
JavaScript SDK does not provide a push notification.
Use Android SDK instead of the JavaScript SDK to support a push notification. See the instructions of Android(SDK) section to follow the best practices.
Allow new tab
To allow new tab in an Android WebView, some additional configurations are required.
webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView?,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message?
): Boolean {
// intercept new window
val transport = resultMsg?.obj as? WebView.WebViewTransport
val newWebView = WebView(this@WebViewActivity)
view?.addView(newWebView)
transport?.webView = newWebView
resultMsg?.sendToTarget()
newWebView.webViewClient = object : WebViewClient() {
// handle url navigating and create new activity
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
return when {
url.startsWith("tel:") -> {
val intent = Intent(Intent.ACTION_DIAL, Uri.parse(url))
startActivity(intent)
true
}
url.startsWith("mailto:") -> {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse(url))
startActivity(intent)
true
}
else -> {
Intent(view.context, WebViewActivity::class.java)
.apply { putExtra("url", url) }
.also { intent -> startActivity(intent) }
true
}
}
}
}
return true
}
}
Uploading files
If you are using Android WebView, users will not be able to launch a file chooser when they click the attachment button to send files. The problem is that WebView does not implement default file choosing behavior, so we must manually define how we handle the users' actions. With our simple solution and snippets, you will only take a minimal effort on integrating JavaScript SDK with your mobile application.
We highly recommend using theย Android SDKย for those who are looking for effortless and straightforward experiences on mobile devices.
- Allow WebView to access files and setย
WebChromeClient
ย to get notified when the user wants to open the file chooser.
yourWebViewInstance.apply {
settings.apply {
// -- your other WebView options --
// add this line to make WebView to access file system
allowFileAccess = true
}
webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams
): Boolean {
// -- snip --
}
}
- Copy our code ofย
WebViewAttachmentModule
ย class that is responsible for handling file chooser callbacks. Note that you may change the snippet as it is provided to help you write code faster.
Make sure to return true from onShowFileChooser
The
WebChromeClient
does not allowfilePathCallback
to be invoked if onShowFileChooser returns false. As the module always calls the callback when an intent for a file chooser is created, make sure to callgetIntentChooser
only ifonShowFileChooser
returns true.
class WebViewAttachmentModule {
private var filePathCallback: ValueCallback<Array<Uri>>? = null
fun getIntentChooser(filePathCallback: ValueCallback<Array<Uri>>, acceptTypes: Array<String>): Intent {
// init filePathCallback
this.filePathCallback?.onReceiveValue(null)
this.filePathCallback = filePathCallback
return Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, getMimeTypesFromAcceptTypes(acceptTypes))
}
}
// handle onActivityResult process in activity
fun handleResult(requestCode: Int, resultCode: Int, data: Intent?) {
val uri = data?.data
if (requestCode == WEB_VIEW_FILE_UPLOAD_REQUEST_CODE && resultCode == AppCompatActivity.RESULT_OK && uri != null) {
filePathCallback?.onReceiveValue(arrayOf(uri))
} else {
filePathCallback?.onReceiveValue(null)
}
filePathCallback = null
}
private fun getMimeTypesFromAcceptTypes(acceptTypes: Array<String>): Array<String> {
return (acceptTypes
.mapNotNull { acceptType ->
when {
// Seperate case of extensions and MIME Type
acceptType.startsWith(".") -> MimeTypeMap.getSingleton().getMimeTypeFromExtension(acceptType.substring(1))
else -> acceptType
}
}
// setting "*/*" when acceptType is not exist
.filter { it.isNotBlank() }
.takeIf { it.isNotEmpty() }
?: listOf("*/*"))
.toTypedArray()
}
}
File Download
Please be aware that Android WebView does not natively support downloading files such as photos or videos. The following code demonstrates how to intercept URL requests and download files when they are identified as images or videos, specifically from Channel Talk.
- Refer to the following to write a listener responsible for handling image / video downloads.
class WebViewDownloadListener(
private val context: Context,
) : DownloadListener {
override fun onDownloadStart(url: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long) {
val uri = Uri.parse(url)
val fileName = URLUtil.guessFileName(url, contentDisposition, mimetype)
val request = DownloadManager.Request(uri)
val cookie = CookieManager.getInstance().getCookie(url)
request.addRequestHeader("Cookie", cookie)
request.addRequestHeader("User-Agent", userAgent)
request.setTitle(fileName)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager
downloadManager?.enqueue(request)
}
}
- Apply the written listener to the activity. An example is shown below.
yourWebViewInstance.setDownloadListener(WebViewDownloadListener(context))
Usage sample
Please change the following sample code to fit your requirements.
class WebViewActivity : AppCompatActivity() {
private val attachmentModule = WebViewAttachmentModule()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webview.webViewClient = WebViewClient()
val url = intent.getStringExtra("url") ?: SOME_DEFAULT_URL
webview.apply {
// handle file downloads
setDownloadListener(WebViewDownloadListener(context))
settings.apply {
// WebView settings
javaScriptEnabled = true
domStorageEnabled = true
useWideViewPort = true
allowFileAccess = true
setSupportMultipleWindows(true)
javaScriptCanOpenWindowsAutomatically = true
}
webChromeClient = object : WebChromeClient() {
// handle file uploads
override fun onShowFileChooser(
webView: WebView,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams
): Boolean {
startActivityForResult(
attachmentModule.getIntentChooser(
filePathCallback,
fileChooserParams.acceptTypes,
),
Const.WEB_VIEW_FILE_UPLOAD_REQUEST_CODE,
) // Call getIntentChooser() only if the function returns true.
return true
}
override fun onCreateWindow(
view: WebView?,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message?
): Boolean {
// intercept new window
val transport = resultMsg?.obj as? WebView.WebViewTransport
val newWebView = WebView(this@WebViewActivity)
view?.addView(newWebView)
transport?.webView = newWebView
resultMsg?.sendToTarget()
newWebView.webViewClient = object : WebViewClient() {
// handle URL navigation
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
return when {
url.startsWith("tel:") -> {
val intent = Intent(Intent.ACTION_DIAL, Uri.parse(url))
startActivity(intent)
true
}
url.startsWith("mailto:") -> {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse(url))
startActivity(intent)
true
}
else -> {
Intent(view.context, WebViewActivity::class.java)
.apply { putExtra("url", url) }
.also { intent -> startActivity(intent) }
true
}
}
}
}
return true
}
}
}
webview.loadUrl(url)
}
// handle file upload
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
attachmentModule.handleResult(requestCode, resultCode, data)
super.onActivityResult(requestCode, resultCode, data)
}
}
Android(SDK)
Instead of embedding JavaScript SDK to the WebView, you might decide to use Android SDK along with the WebView. A major reason for the decision is when you need to send a push notification. See how to get started with the Android SDK and enabling the push notification.
Hide redundant Channel SDK UI.
When using both JavaScript SDK and Android SDK, set
hideChannelButtonOnBoot
andhidePopup
=true inBoot Option
of JavaScript SDK to hide the Channel button and popups. Android SDK and JavaScript SDK might duplicate Channel SDK UI.
To allow Android SDK to know the current URL of the WebView, call setPage
with appropriate data. Event tracking and workflow targeting will work smoothly with the information.
class WebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
webview.apply {
settings.apply {
// settings
}
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// set current page to SDK
ChannelIO.setPage(url)
}
}
}
webview.loadUrl(url)
}
}
iOS
This section describes how to use the JavaScript SDK installed on the mobile web in an iOS WebView environment.
Ensure filled out your info.plist
Before you get started, refer to this After Installation to make sure you've filled out your Info.plist with a description of the permissions you use.
Push Notification doesnโt work if you are using only JavaScript.
To use the push notification, Refers ChannelIO iOS SDK
The Following Example loads a page with a URL in your YOUR_WEB_URL
on any WebView ViewController.
import UIKit
import WebKit
import Photos
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
enum FileFormat: String {
case image
case video
case file
init(rawValue: String) {
switch rawValue {
case "image": self = .image
case "video": self = .video
default: self = .file
}
}
}
var webView: WKWebView!
var url: String = "YOUR_WEB_URL"
private let indicator: UIActivityIndicatorView = {
$0.style = .medium
$0.startAnimating()
$0.isHidden = true
$0.translatesAutoresizingMaskIntoConstraints = false
return $0
}(UIActivityIndicatorView(style: .large))
override func loadView() {
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true // # 1. modify to true inlineMediaPlay option
webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let myURL = URL(string:self.url)
let myRequest = URLRequest(url: self.myURL!)
self.webView.load(myRequest)
self.setupActivityIndicator()
}
// #2. handle target_blank type route
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}
return nil
}
// # 3. Downloadin File, Image or Video
guard
let response = navigationResponse.response as? HTTPURLResponse,
let url = response.url,
url.pathComponents.contains(where: { $0 == "pri-file"}),
response.statusCode == 200,
let mimeType = response.mimeType?.split(separator: "/").first,
let contentDisposition = response.allHeaderFields["Content-Disposition"] as? String,
contentDisposition.contains("attachment;")
else {
decisionHandler(.allow)
return
}
let fileExtension = contentDisposition.components(separatedBy: ".").last ?? ""
let fileFormat = FileFormat(rawValue: String(mimeType))
self.downloadFile(from: url, fileExtension: String(fileExtension)) { [weak self] result in
switch result {
case .success(let url):
switch fileFormat {
case .image, .video:
self?.saveToPhotoLibrary(fileURL: url, format: fileFormat) { [weak self] _ in
self?.hideActivityIndicator()
}
case .file:
DispatchQueue.main.async { [weak self] in
let controller = UIActivityViewController(
activityItems: [url],
applicationActivities: nil
)
self?.present(controller, animated: true)
}
}
case .failure(let error):
print("file download was ended with failure. \(error.localizedDescription)")
}
if #available(iOS 14.5, *) {
decisionHandler(.download)
} else {
decisionHandler(.allow)
}
}
}
}
extension ViewController {
private func setupActivityIndicator() {
self.view.addSubview(self.indicator)
NSLayoutConstraint.activate([
self.indicator.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.indicator.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
])
}
private func showActivityIndicator() {
DispatchQueue.main.async {
self.indicator.isHidden = false
self.indicator.startAnimating()
}
}
private func hideActivityIndicator() {
DispatchQueue.main.async {
self.indicator.isHidden = true
self.indicator.stopAnimating()
}
}
private func downloadFile(
from url: URL,
fileExtension: String,
completion: @escaping (Result<URL, Error>) -> ()
) {
URLSession.shared.downloadTask(with: url) { location, response, error in
guard let location, error == nil else {
completion(.failure(error ?? NSError(domain: "failed to download.", code: -2) ))
return
}
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsDirectory.appendingPathComponent(url.lastPathComponent + "." + fileExtension)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.moveItem(at: location, to: destinationURL)
completion(.success(destinationURL))
} catch let error {
completion(.failure(error))
}
}
.resume()
}
private func saveToPhotoLibrary(
fileURL: URL,
format: FileFormat,
completion: @escaping (Result<Void, Error>) -> ()
) {
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else {
completion(.failure(NSError(domain: "Failed to require photoLibrary permission", code: -1)))
return
}
PHPhotoLibrary.shared().performChanges({
switch format {
case .image: PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: fileURL)
case .video: PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: fileURL)
case .file:
completion(.failure(NSError(domain: "cannot handle file type assets.", code: -3)))
}
}) { success, error in
if success {
completion(.success(()))
} else if let error {
completion(.failure(error))
}
}
}
}
}
Additional Requirements
These requirements are already described above. each are marked as #1, #2 and #3.
# 1. allowsInlineMediaPlayback
allowsInlineMediaPlayback is a configuration option for whether HTML5 video plays as inline or as full screen. The default value is false. Therefore, If there is a video in the SDK, It will play as full screen automatically.
To fix this, you should modify the allowsInlineMediaPlayback
to true.
# 2. handle target_blank type route
All of the link property of the JavaScript SDK is target="_blank"
. Thus, It will open with a new tap.
However, WKWebView can't handle target="_blank"
automatically, you should add this code example to deal with.
This example is If the link action is from target="_blank"
, retrieve the link from action and load the URL on the WebView.
# 3. Downloading File, Image or Video
Please be aware that WKWebView does not natively support downloading files such as photos or videos. The above code demonstrates how to intercept URL requests and download files when they are identified as file, images or videos, specifically from Channel Talk.
Updated about 1 month ago