diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 05e0205..1474fb1 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -12,6 +12,11 @@ android:launchMode="singleTask" android:theme="@style/AuthgearTheme" android:configChanges="orientation|screenSize" /> + diff --git a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt index ca3c8d3..5b5ff7b 100644 --- a/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt +++ b/android/src/main/kotlin/com/authgear/flutter/AuthgearPlugin.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageInfo +import android.graphics.Color import android.net.Uri import android.os.Build import android.os.Handler @@ -32,7 +33,7 @@ import io.flutter.plugin.common.PluginRegistry import org.json.JSONObject import java.security.* import java.security.interfaces.RSAPublicKey -import java.util.UUID +import java.util.* class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener { @@ -125,9 +126,12 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - this.storeWechat(call) when (call.method) { - "authenticate" -> { + "registerWechatRedirectURI" -> { + this.storeWechat(call) + result.success(null) + } + "openAuthorizeURL" -> { val url = Uri.parse(call.argument("url")) val redirectURI = Uri.parse(call.argument("redirectURI")) // Custom tabs do not support incognito mode for now. @@ -142,6 +146,24 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg val intent = OAuthCoordinatorActivity.createAuthorizationIntent(activity, url) activity.startActivityForResult(intent, requestCode) } + "openAuthorizeURLWithWebView" -> { + val url = Uri.parse(call.argument("url")) + val redirectURI = Uri.parse(call.argument("redirectURI")) + val actionBarBackgroundColor = this.readColorInt(call, "actionBarBackgroundColor") + val actionBarButtonTintColor = this.readColorInt(call, "actionBarButtonTintColor") + val options = WebKitWebViewActivity.Options(url, redirectURI) + options.actionBarBackgroundColor = actionBarBackgroundColor + options.actionBarButtonTintColor = actionBarButtonTintColor + + val requestCode = startActivityHandles.push(StartActivityHandle(TAG_AUTHENTICATION, result)) + val activity = activityBinding?.activity + if (activity == null) { + result.noActivity() + return + } + val intent = WebKitWebViewActivity.createIntent(activity, options) + activity.startActivityForResult(intent, requestCode) + } "openURL" -> { val url = Uri.parse(call.argument("url")) val requestCode = startActivityHandles.push(StartActivityHandle(TAG_OPEN_URL, result)) @@ -781,6 +803,19 @@ class AuthgearPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginReg val sig = signature.sign() return "$strToSign.${sig.base64URLEncode()}" } + + private fun readColorInt(call: MethodCall, key: String): Int? { + val s: String? = call.argument(key) + if (s != null) { + val l = s.toLong(16) + val a = (l shr 24 and 0xff).toInt() + val r = (l shr 16 and 0xff).toInt() + val g = (l shr 8 and 0xff).toInt() + val b = (l and 0xff).toInt() + return Color.argb(a, r, g, b) + } + return null + } } internal fun Result.noActivity() { diff --git a/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt new file mode 100644 index 0000000..9dddb03 --- /dev/null +++ b/android/src/main/kotlin/com/authgear/flutter/WebKitWebViewActivity.kt @@ -0,0 +1,269 @@ +package com.authgear.flutter + +import android.annotation.TargetApi +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.webkit.* +import androidx.annotation.* +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.DrawableCompat + +class WebKitWebViewActivity: AppCompatActivity() { + companion object { + private const val MENU_ID_CANCEL = 1 + private const val KEY_OPTIONS = "KEY_OPTIONS" + private const val KEY_WEB_VIEW_STATE = "KEY_WEB_VIEW_STATE" + private const val TAG_FILE_CHOOSER = 1 + + fun createIntent(ctx: Context, options: Options): Intent { + val intent = Intent(ctx, WebKitWebViewActivity::class.java) + intent.putExtra(KEY_OPTIONS, options.toBundle()) + return intent + } + } + + private lateinit var mWebView: WebView + private var result: Uri? = null + private val handles = StartActivityHandles>>() + + class Options { + var url: Uri + var redirectURI: Uri + var actionBarBackgroundColor: Int? = null + var actionBarButtonTintColor: Int? = null + + constructor(url: Uri, redirectURI: Uri) { + this.url = url + this.redirectURI = redirectURI + } + + internal constructor(bundle: Bundle) { + this.url = bundle.getParcelable("url")!! + this.redirectURI = bundle.getParcelable("redirectURI")!! + if (bundle.containsKey("actionBarBackgroundColor")) { + this.actionBarBackgroundColor = bundle.getInt("actionBarBackgroundColor") + } + if (bundle.containsKey("actionBarButtonTintColor")) { + this.actionBarButtonTintColor = bundle.getInt("actionBarButtonTintColor") + } + } + + fun toBundle(): Bundle { + val bundle = Bundle() + bundle.putParcelable("url", this.url) + bundle.putParcelable("redirectURI", this.redirectURI) + this.actionBarBackgroundColor?.let { + bundle.putInt("actionBarBackgroundColor", it) + } + this.actionBarButtonTintColor?.let { + bundle.putInt("actionBarButtonTintColor", it) + } + return bundle + } + } + + private class MyWebViewClient constructor(private val activity: WebKitWebViewActivity) : + WebViewClient() { + + @TargetApi(Build.VERSION_CODES.N) + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + val uri = request?.url!! + if (this.shouldOverrideUrlLoading(uri)) { + return true + } + return super.shouldOverrideUrlLoading(view, request) + } + + @SuppressWarnings("deprecation") + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + val uri = Uri.parse(url!!)!! + if (this.shouldOverrideUrlLoading(uri)) { + return true + } + return super.shouldOverrideUrlLoading(view, url) + } + + private fun shouldOverrideUrlLoading(uri: Uri): Boolean { + if (this.checkRedirectURI(uri)) { + return true; + } + return false; + } + + private fun checkRedirectURI(uri: Uri): Boolean { + val redirectURI = this.activity.getOptions().redirectURI + val withoutQuery = this.removeQueryAndFragment(uri) + if (withoutQuery.toString() == redirectURI.toString()) { + this.activity.result = uri + this.activity.callSetResult() + this.activity.finish() + return true + } + return false; + } + + private fun removeQueryAndFragment(uri: Uri): Uri { + return uri.buildUpon().query(null).fragment(null).build() + } + } + + private class MyWebChromeClient constructor(private val activity: WebKitWebViewActivity) : + WebChromeClient() { + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + val handle = StartActivityHandle(TAG_FILE_CHOOSER, filePathCallback!!) + val requestCode = this.activity.handles.push(handle) + val intent = fileChooserParams!!.createIntent() + this.activity.startActivityForResult(intent, requestCode) + return true + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val options = this.getOptions() + + // Do not show title. + supportActionBar?.setDisplayShowTitleEnabled(false) + + // Configure navigation bar background color. + options.actionBarBackgroundColor?.let { + supportActionBar?.setBackgroundDrawable(ColorDrawable(it)) + } + + // Show back button. + supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_HOME or ActionBar.DISPLAY_HOME_AS_UP + + // Configure the back button. + var backButtonDrawable = getDrawableCompat(R.drawable.ic_arrow_back) + if (options.actionBarButtonTintColor != null) { + backButtonDrawable = + tintDrawable(backButtonDrawable, options.actionBarButtonTintColor!!) + } + supportActionBar?.setHomeAsUpIndicator(backButtonDrawable) + + // Configure web view. + this.mWebView = WebView(this) + this.setContentView(this.mWebView) + this.mWebView.setWebViewClient(MyWebViewClient(this)) + this.mWebView.setWebChromeClient(MyWebChromeClient(this)) + val webSettings: WebSettings = this.mWebView.getSettings() + webSettings.javaScriptEnabled = true + + if (savedInstanceState == null) { + this.mWebView.loadUrl(options.url.toString()) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + val webViewBundle = Bundle() + this.mWebView.saveState(webViewBundle) + outState.putBundle(KEY_WEB_VIEW_STATE, webViewBundle) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + val bundle = savedInstanceState.getBundle(KEY_WEB_VIEW_STATE) + if (bundle != null) { + this.mWebView.restoreState(bundle) + } + } + + override fun onBackPressed() { + if (mWebView.canGoBack()) { + mWebView.goBack() + } else { + callSetResult() + super.onBackPressed() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val options = getOptions() + + // Configure the close button. + var drawable = getDrawableCompat(R.drawable.ic_close) + if (options.actionBarButtonTintColor != null) { + drawable = tintDrawable(drawable, options.actionBarButtonTintColor!!) + } + menu.add(Menu.NONE, MENU_ID_CANCEL, Menu.NONE, android.R.string.cancel) + .setIcon(drawable) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(@NonNull item: MenuItem): Boolean { + if (item.getItemId() === android.R.id.home) { + onBackPressed() + return true + } + if (item.getItemId() === MENU_ID_CANCEL) { + callSetResult() + finish() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + val handle = handles.pop(requestCode) + ?: return + when (handle.tag) { + TAG_FILE_CHOOSER -> when (resultCode) { + Activity.RESULT_CANCELED -> handle.value.onReceiveValue(null) + Activity.RESULT_OK -> if (data != null && data.data != null) { + handle.value.onReceiveValue(arrayOf(data.data!!)) + } else { + handle.value.onReceiveValue(null) + } + } + } + } + + private fun getOptions(): Options { + val bundle: Bundle = this.intent.getParcelableExtra(KEY_OPTIONS)!! + return Options(bundle) + } + + private fun callSetResult() { + if (this.result == null) { + this.setResult(Activity.RESULT_CANCELED) + } else { + val intent = Intent() + intent.data = this.result + this.setResult(Activity.RESULT_OK, intent) + } + } + + private fun getDrawableCompat(@DrawableRes id: Int): Drawable { + return ResourcesCompat.getDrawable(resources, id, null)!! + } + + private fun tintDrawable(drawable: Drawable, @ColorInt color: Int): Drawable { + val newDrawable: Drawable = + DrawableCompat.wrap(drawable).constantState!!.newDrawable().mutate() + DrawableCompat.setTint(newDrawable, color) + return newDrawable + } +} \ No newline at end of file diff --git a/android/src/main/res/drawable/ic_arrow_back.xml b/android/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..0e2e863 --- /dev/null +++ b/android/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/src/main/res/drawable/ic_close.xml b/android/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..7a0ff35 --- /dev/null +++ b/android/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a1a6af5..971f1d4 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" - android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">