Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
50
firebase-auth/core/build.gradle
Normal file
50
firebase-auth/core/build.gradle
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
dependencies {
|
||||
api project(':firebase-auth')
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
|
||||
implementation project(':play-services-base-core')
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
|
||||
|
||||
implementation "com.android.volley:volley:$volleyVersion"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "org.microg.gms.firebase.auth.core"
|
||||
|
||||
compileSdkVersion androidCompileSdk
|
||||
buildToolsVersion "$androidBuildVersionTools"
|
||||
|
||||
defaultConfig {
|
||||
versionName version
|
||||
minSdkVersion androidMinSdk
|
||||
targetSdkVersion androidTargetSdk
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = 1.8
|
||||
}
|
||||
}
|
||||
28
firebase-auth/core/src/main/AndroidManifest.xml
Normal file
28
firebase-auth/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application>
|
||||
<service android:name="org.microg.gms.firebase.auth.FirebaseAuthService">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.auth.api.gms.service.START" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<activity
|
||||
android:name="org.microg.gms.firebase.auth.ReCaptchaActivity"
|
||||
android:exported="false"
|
||||
android:process=":ui"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog.Alert.NoActionBar" />
|
||||
<service
|
||||
android:name="org.microg.gms.firebase.auth.ReCaptchaOverlayService"
|
||||
android:exported="false"
|
||||
android:process=":ui" />
|
||||
</application>
|
||||
</manifest>
|
||||
54
firebase-auth/core/src/main/assets/recaptcha.html
Normal file
54
firebase-auth/core/src/main/assets/recaptcha.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html style="margin: 5px">
|
||||
<body style="margin: 5px">
|
||||
|
||||
<div id='recaptcha-container'></div>
|
||||
<div style="padding-top: 150px">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
style="margin: auto; background: none; display: block; shape-rendering: auto;" width="64px" height="64px"
|
||||
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<rect x="17.5" y="20.2285" width="15" height="59.543" fill="#000000">
|
||||
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="10;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.2s"></animate>
|
||||
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="80;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.2s"></animate>
|
||||
</rect>
|
||||
<rect x="42.5" y="30" width="15" height="40" fill="#000000">
|
||||
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="15;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.1s"></animate>
|
||||
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="70;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.1s"></animate>
|
||||
</rect>
|
||||
<rect x="67.5" y="30" width="15" height="40" fill="#000000">
|
||||
<animate attributeName="y" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="15;30;30" keySplines="0 0.5 0.5 1;0 0.5 0.5 1"></animate>
|
||||
<animate attributeName="height" repeatCount="indefinite" dur="1s" calcMode="spline" keyTimes="0;0.5;1"
|
||||
values="70;40;40" keySplines="0 0.5 0.5 1;0 0.5 0.5 1"></animate>
|
||||
</rect>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
|
||||
<script src="https://www.gstatic.com/firebasejs/7.22.0/firebase-app.js"></script>
|
||||
<script src="https://www.gstatic.com/firebasejs/7.22.0/firebase-auth.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var firebaseConfig = {
|
||||
apiKey: "%apikey%"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
window.onload = () => {
|
||||
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container', {
|
||||
'size': 'invisible',
|
||||
'callback': function (response) {
|
||||
MyCallback.onReCaptchaToken(response);
|
||||
}
|
||||
});
|
||||
recaptchaVerifier.verify();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,663 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.firebase.auth
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Handler
|
||||
import android.os.Parcel
|
||||
import android.provider.Telephony
|
||||
import android.telephony.SmsMessage
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.common.internal.GetServiceRequest
|
||||
import com.google.android.gms.common.internal.IGmsCallbacks
|
||||
import com.google.firebase.auth.ActionCodeSettings
|
||||
import com.google.firebase.auth.EmailAuthCredential
|
||||
import com.google.firebase.auth.PhoneAuthCredential
|
||||
import com.google.firebase.auth.UserProfileChangeRequest
|
||||
import com.google.firebase.auth.api.internal.*
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.microg.gms.BaseService
|
||||
import org.microg.gms.common.GmsService
|
||||
import org.microg.gms.common.PackageUtils
|
||||
import org.microg.gms.utils.digest
|
||||
import org.microg.gms.utils.getCertificates
|
||||
|
||||
private const val TAG = "GmsFirebaseAuth"
|
||||
|
||||
fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null
|
||||
fun JSONObject.getJSONArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null
|
||||
fun JSONArray?.orEmpty() = this ?: JSONArray()
|
||||
fun JSONObject.getJSONArrayLength(key: String) = getJSONArrayOrNull(key).orEmpty().length()
|
||||
|
||||
private val ActionCodeSettings.requestTypeAsString: String
|
||||
get() = when (requestType) {
|
||||
1 -> "PASSWORD_RESET"
|
||||
2 -> "OLD_EMAIL_AGREE"
|
||||
3 -> "NEW_EMAIL_ACCEPT"
|
||||
4 -> "VERIFY_EMAIL"
|
||||
5 -> "RECOVER_EMAIL"
|
||||
6 -> "EMAIL_SIGNIN"
|
||||
7 -> "VERIFY_AND_CHANGE_EMAIL"
|
||||
8 -> "REVERT_SECOND_FACTOR_ADDITION"
|
||||
else -> "OOB_REQ_TYPE_UNSPECIFIED"
|
||||
}
|
||||
|
||||
private val UserProfileChangeRequest.deleteAttributeList: List<String>
|
||||
get() {
|
||||
val list = arrayListOf<String>()
|
||||
if (shouldRemoveDisplayName) list.add("DISPLAY_NAME")
|
||||
if (shouldRemovePhotoUri) list.add("PHOTO_URL")
|
||||
return list
|
||||
}
|
||||
|
||||
private fun Intent.getSmsMessages(): Array<SmsMessage> {
|
||||
return if (SDK_INT >= 19) {
|
||||
Telephony.Sms.Intents.getMessagesFromIntent(this)
|
||||
} else {
|
||||
(getSerializableExtra("pdus") as? Array<ByteArray>)?.map { SmsMessage.createFromPdu(it) }.orEmpty().toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
class FirebaseAuthService : BaseService(TAG, GmsService.FIREBASE_AUTH) {
|
||||
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService?) {
|
||||
PackageUtils.getAndCheckCallingPackage(this, request.packageName)
|
||||
val apiKey = request.extras?.getString(Constants.EXTRA_API_KEY)
|
||||
val libraryVersion = request.extras?.getString(Constants.EXTRA_LIBRARY_VERSION)
|
||||
if (apiKey == null) {
|
||||
callback.onPostInitComplete(CommonStatusCodes.DEVELOPER_ERROR, null, null)
|
||||
} else {
|
||||
callback.onPostInitComplete(0, FirebaseAuthServiceImpl(this, lifecycle, request.packageName, libraryVersion, apiKey).asBinder(), null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FirebaseAuthServiceImpl(private val context: Context, override val lifecycle: Lifecycle, private val packageName: String, private val libraryVersion: String?, private val apiKey: String) : IFirebaseAuthService.Stub(), LifecycleOwner {
|
||||
private val client by lazy { IdentityToolkitClient(context, apiKey, packageName, context.packageManager.getCertificates(packageName).firstOrNull()?.digest("SHA1")) }
|
||||
private var authorizedDomain: String? = null
|
||||
|
||||
private suspend fun getAuthorizedDomain(): String {
|
||||
authorizedDomain?.let { return it }
|
||||
val authorizedDomain = try {
|
||||
client.getProjectConfig().getJSONArray("authorizedDomains").getString(0)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
"localhost"
|
||||
}
|
||||
this.authorizedDomain = authorizedDomain
|
||||
return authorizedDomain
|
||||
}
|
||||
|
||||
private suspend fun refreshTokenResponse(cachedState: String): GetTokenResponse {
|
||||
var tokenResponse = GetTokenResponse.parseJson(cachedState)
|
||||
if (System.currentTimeMillis() + 300000L < tokenResponse.issuedAt + tokenResponse.expiresIn * 1000) {
|
||||
return tokenResponse
|
||||
}
|
||||
return client.getTokenByRefreshToken(tokenResponse.refreshToken).toGetTokenResponse()
|
||||
}
|
||||
|
||||
private fun JSONObject.toGetTokenResponse() = GetTokenResponse().apply {
|
||||
refreshToken = getStringOrNull("refresh_token")
|
||||
accessToken = getStringOrNull("access_token")
|
||||
expiresIn = getStringOrNull("expires_in")?.toLong()
|
||||
tokenType = getStringOrNull("token_type")
|
||||
}
|
||||
|
||||
private fun JSONObject.toGetAccountInfoUser(): GetAccountInfoUser = GetAccountInfoUser().apply {
|
||||
localId = getStringOrNull("localId")
|
||||
email = getStringOrNull("email")
|
||||
isEmailVerified = optBoolean("emailVerified")
|
||||
displayName = getStringOrNull("displayName")
|
||||
photoUrl = getStringOrNull("photoUrl")
|
||||
for (i in 0 until getJSONArrayLength("providerUserInfo")) {
|
||||
getJSONArray("providerUserInfo").getJSONObject(i).run {
|
||||
providerInfoList.providerUserInfos.add(ProviderUserInfo().apply {
|
||||
federatedId = getStringOrNull("federatedId")
|
||||
displayName = getStringOrNull("displayName")
|
||||
photoUrl = getStringOrNull("photoUrl")
|
||||
providerId = getStringOrNull("providerId")
|
||||
phoneNumber = getStringOrNull("phoneNumber")
|
||||
email = getStringOrNull("email")
|
||||
rawUserInfo = this@run.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
password = getStringOrNull("rawPassword")
|
||||
phoneNumber = getStringOrNull("phoneNumber")
|
||||
creationTimestamp = getStringOrNull("createdAt")?.toLong() ?: 0L
|
||||
lastSignInTimestamp = getStringOrNull("lastLoginAt")?.toLong() ?: 0L
|
||||
}
|
||||
|
||||
private fun JSONObject.toCreateAuthUriResponse(): CreateAuthUriResponse = CreateAuthUriResponse().apply {
|
||||
authUri = getStringOrNull("authUri")
|
||||
isRegistered = optBoolean("registered")
|
||||
providerId = getStringOrNull("providerId")
|
||||
isForExistingProvider = optBoolean("forExistingProvider")
|
||||
for (i in 0 until getJSONArrayLength("allProviders")) {
|
||||
stringList.values.add(getJSONArray("allProviders").getString(i))
|
||||
}
|
||||
for (i in 0 until getJSONArrayLength("signinMethods")) {
|
||||
signInMethods.add(getJSONArray("signinMethods").getString(i))
|
||||
}
|
||||
}
|
||||
|
||||
override fun applyActionCode(request: ApplyActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: applyActionCode")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun applyActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: applyActionCodeCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun changeEmail(request: ChangeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: changeEmail")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun changeEmailCompat(cachedState: String?, email: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: changeEmailCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun changePassword(request: ChangePasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: changePassword")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun changePasswordCompat(cachedState: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: changePasswordCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun checkActionCode(request: CheckActionCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: checkActionCode")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun checkActionCodeCompat(code: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: checkActionCodeCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun confirmPasswordReset(request: ConfirmPasswordResetAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: confirmPasswordReset")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun confirmPasswordResetCompat(code: String?, newPassword: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: confirmPasswordResetCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun createUserWithEmailAndPassword(request: CreateUserWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "createUserWithEmailAndPassword")
|
||||
try {
|
||||
val tokenResult = client.signupNewUser(email = request.email, password = request.password, tenantId = request.tenantId)
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true }
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createUserWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
createUserWithEmailAndPassword(CreateUserWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks)
|
||||
}
|
||||
|
||||
override fun delete(request: DeleteAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: delete")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun deleteCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: deleteCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun finalizeMfaEnrollment(request: FinalizeMfaEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: finalizeMfaEnrollment")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun finalizeMfaSignIn(request: FinalizeMfaSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: finalizeMfaSignIn")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun getAccessToken(request: GetAccessTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "getAccessToken")
|
||||
try {
|
||||
callbacks.onGetTokenResponse(client.getTokenByRefreshToken(request.refreshToken).toGetTokenResponse())
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAccessTokenCompat(refreshToken: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
getAccessToken(GetAccessTokenAidlRequest().apply { this.refreshToken = refreshToken }, callbacks)
|
||||
}
|
||||
|
||||
override fun getProvidersForEmail(request: GetProvidersForEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "getProvidersForEmail")
|
||||
try {
|
||||
callbacks.onCreateAuthUriResponse(client.createAuthUri(identifier = request.email, tenantId = request.tenantId).toCreateAuthUriResponse())
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProvidersForEmailCompat(email: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
getProvidersForEmail(GetProvidersForEmailAidlRequest().apply { this.email = email }, callbacks)
|
||||
}
|
||||
|
||||
override fun linkEmailAuthCredential(request: LinkEmailAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "linkEmailAuthCredential")
|
||||
try {
|
||||
val getTokenResponse = refreshTokenResponse(request.cachedState)
|
||||
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
|
||||
val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, email = request.email, password = request.password).toGetAccountInfoUser()
|
||||
accountInfoResult.email = setAccountInfo.email
|
||||
accountInfoResult.isEmailVerified = setAccountInfo.isEmailVerified
|
||||
accountInfoResult.providerInfoList = setAccountInfo.providerInfoList
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun linkEmailAuthCredentialCompat(email: String?, password: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
linkEmailAuthCredential(LinkEmailAuthCredentialAidlRequest().apply { this.email = email; this.password = password; this.cachedState = cachedState }, callbacks)
|
||||
}
|
||||
|
||||
override fun linkFederatedCredential(request: LinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: linkFederatedCredential")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun linkFederatedCredentialCompat(cachedState: String?, verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: linkFederatedCredentialCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun linkPhoneAuthCredential(request: LinkPhoneAuthCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: linkPhoneAuthCredential")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun linkPhoneAuthCredentialCompat(cachedState: String?, credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: linkPhoneAuthCredentialCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun reload(request: ReloadAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
try {
|
||||
Log.d(TAG, "reload")
|
||||
val getTokenResponse = refreshTokenResponse(request.cachedState)
|
||||
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reloadCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
reload(ReloadAidlRequest().apply { this.cachedState = cachedState }, callbacks)
|
||||
}
|
||||
|
||||
override fun sendEmailVerification(request: SendEmailVerificationWithSettingsAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
try {
|
||||
Log.d(TAG, "sendEmailVerification")
|
||||
client.getOobConfirmationCode(
|
||||
requestType = "VERIFY_EMAIL",
|
||||
idToken = request.token,
|
||||
iOSBundleId = request.settings?.iOSBundle,
|
||||
iOSAppStoreId = request.settings?.iOSAppStoreId,
|
||||
continueUrl = request.settings?.url,
|
||||
androidInstallApp = request.settings?.androidInstallApp,
|
||||
androidMinimumVersion = request.settings?.androidMinimumVersion,
|
||||
androidPackageName = request.settings?.androidPackageName,
|
||||
canHandleCodeInApp = request.settings?.handleCodeInApp
|
||||
)
|
||||
callbacks.onEmailVerificationResponse()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendEmailVerificationCompat(token: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) {
|
||||
sendEmailVerification(SendEmailVerificationWithSettingsAidlRequest().apply { this.token = token; this.settings = actionCodeSettings }, callbacks)
|
||||
}
|
||||
|
||||
override fun sendVerificationCode(request: SendVerificationCodeAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
try {
|
||||
Log.d(TAG, "sendVerificationCode")
|
||||
val reCaptchaToken = when {
|
||||
request.request.recaptchaToken != null -> request.request.recaptchaToken
|
||||
ReCaptchaOverlayService.isSupported(context) -> ReCaptchaOverlayService.awaitToken(context, apiKey, getAuthorizedDomain())
|
||||
ReCaptchaActivity.isSupported(context) -> ReCaptchaActivity.awaitToken(context, apiKey, getAuthorizedDomain())
|
||||
else -> throw RuntimeException("No recaptcha token available")
|
||||
}
|
||||
var sessionInfo: String? = null
|
||||
var registered = true
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
var smsCode: String? = null
|
||||
for (message in intent.getSmsMessages()) {
|
||||
smsCode = Regex("\\b([0-9]{6})\\b").find(message.messageBody)?.groups?.get(1)?.value
|
||||
?: continue
|
||||
Log.d(TAG, "Received SMS verification code: $smsCode")
|
||||
break
|
||||
}
|
||||
if (smsCode == null) return
|
||||
registered = false
|
||||
context.unregisterReceiver(this)
|
||||
try {
|
||||
callbacks.onVerificationCompletedResponse(PhoneAuthCredential().apply {
|
||||
this.phoneNumber = request.request.phoneNumber
|
||||
this.sessionInfo = sessionInfo
|
||||
this.smsCode = smsCode
|
||||
})
|
||||
Log.d(TAG, "callback: onVerificationCompletedResponse")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
context.registerReceiver(receiver, IntentFilter("android.provider.Telephony.SMS_RECEIVED"))
|
||||
var timeout = request.request.timeoutInSeconds * 1000L
|
||||
if (timeout <= 0L) timeout = 120000L
|
||||
Handler().postDelayed({
|
||||
if (registered) {
|
||||
Log.d(TAG, "Waited ${timeout}ms for verification code SMS, timeout.")
|
||||
context.unregisterReceiver(receiver)
|
||||
callbacks.onVerificationAutoTimeOut(sessionInfo)
|
||||
Log.d(TAG, "callback: onVerificationAutoTimeOut")
|
||||
}
|
||||
}, timeout)
|
||||
sessionInfo = client.sendVerificationCode(phoneNumber = request.request.phoneNumber, reCaptchaToken = reCaptchaToken).getString("sessionInfo")
|
||||
callbacks.onSendVerificationCodeResponse(sessionInfo)
|
||||
Log.d(TAG, "callback: onSendVerificationCodeResponse")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendVerificationCodeCompat(request: SendVerificationCodeRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
sendVerificationCode(SendVerificationCodeAidlRequest().apply { this.request = request }, callbacks)
|
||||
}
|
||||
|
||||
override fun sendGetOobConfirmationCodeEmail(request: SendGetOobConfirmationCodeEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
try {
|
||||
Log.d(TAG, "sendGetOobConfirmationCodeEmail")
|
||||
client.getOobConfirmationCode(
|
||||
requestType = request.settings?.requestTypeAsString ?: "OOB_REQ_TYPE_UNSPECIFIED",
|
||||
email = request.email,
|
||||
iOSBundleId = request.settings?.iOSBundle,
|
||||
iOSAppStoreId = request.settings?.iOSAppStoreId,
|
||||
continueUrl = request.settings?.url,
|
||||
androidInstallApp = request.settings?.androidInstallApp,
|
||||
androidMinimumVersion = request.settings?.androidMinimumVersion,
|
||||
androidPackageName = request.settings?.androidPackageName,
|
||||
canHandleCodeInApp = request.settings?.handleCodeInApp
|
||||
)
|
||||
Log.d(TAG, "callback: onResetPasswordResponse")
|
||||
callbacks.onResetPasswordResponse(null)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendGetOobConfirmationCodeEmailCompat(email: String?, actionCodeSettings: ActionCodeSettings?, callbacks: IFirebaseAuthCallbacks) {
|
||||
sendGetOobConfirmationCodeEmail(SendGetOobConfirmationCodeEmailAidlRequest().apply { this.email = email; this.settings = actionCodeSettings }, callbacks)
|
||||
}
|
||||
|
||||
override fun setFirebaseUiVersion(request: SetFirebaseUiVersionAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: setFirebaseUiVersion")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun setFirebaseUIVersionCompat(firebaseUiVersion: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: setFirebaseUIVersionCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun signInAnonymously(request: SignInAnonymouslyAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "signInAnonymously")
|
||||
try {
|
||||
val tokenResult = client.signupNewUser(tenantId = request.tenantId)
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = true }
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun signInAnonymouslyCompat(callbacks: IFirebaseAuthCallbacks) {
|
||||
signInAnonymously(SignInAnonymouslyAidlRequest(), callbacks)
|
||||
}
|
||||
|
||||
override fun signInWithCredential(request: SignInWithCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "signInWithCredential request: ${request.request}")
|
||||
try {
|
||||
val tokenResult = client.verifyAssertion(request.request.requestUri, request.request.postBody, request.request.returnSecureToken, request.request.returnIdpCredential)
|
||||
Log.d(TAG, "signInWithCredential callback: $tokenResult ")
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
|
||||
Log.d(TAG, "signInWithCredential callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "signInWithCredential callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun signInWithCredentialCompat(verifyAssertionRequest: VerifyAssertionRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "signInWithCredentialCompat verifyAssertionRequest: $verifyAssertionRequest")
|
||||
signInWithCredential(SignInWithCredentialAidlRequest(verifyAssertionRequest), callbacks)
|
||||
}
|
||||
|
||||
override fun signInWithCustomToken(request: SignInWithCustomTokenAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "signInWithCustomToken")
|
||||
try {
|
||||
val tokenResult = client.verifyCustomToken(token = request.token)
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val isNewUser = tokenResult.optBoolean("isNewUser")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser }
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun signInWithCustomTokenCompat(token: String, callbacks: IFirebaseAuthCallbacks) {
|
||||
signInWithCustomToken(SignInWithCustomTokenAidlRequest().apply { this.token = token }, callbacks)
|
||||
}
|
||||
|
||||
override fun signInWithEmailAndPassword(request: SignInWithEmailAndPasswordAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "signInWithEmailAndPassword")
|
||||
try {
|
||||
val tokenResult = client.verifyPassword(email = request.email, password = request.password, tenantId = request.tenantId)
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken = idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun signInWithEmailAndPasswordCompat(email: String?, password: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
signInWithEmailAndPassword(SignInWithEmailAndPasswordAidlRequest().apply { this.email = email; this.password = password }, callbacks)
|
||||
}
|
||||
|
||||
override fun signInWithEmailLink(request: SignInWithEmailLinkAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: signInWithEmailLink")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun signInWithEmailLinkCompat(credential: EmailAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: signInWithEmailLinkCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun signInWithPhoneNumber(request: SignInWithPhoneNumberAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "signInWithPhoneNumber")
|
||||
try {
|
||||
val tokenResult = client.verifyPhoneNumber(
|
||||
phoneNumber = request.credential.phoneNumber,
|
||||
temporaryProof = request.credential.temporaryProof,
|
||||
sessionInfo = request.credential.sessionInfo,
|
||||
code = request.credential.smsCode
|
||||
)
|
||||
val idToken = tokenResult.getString("idToken")
|
||||
val refreshToken = tokenResult.getString("refreshToken")
|
||||
val isNewUser = tokenResult.optBoolean("isNewUser")
|
||||
val getTokenResponse = client.getTokenByRefreshToken(refreshToken).toGetTokenResponse()
|
||||
val accountInfoResult = client.getAccountInfo(idToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser().apply { this.isNewUser = isNewUser }
|
||||
Log.d(TAG, "callback: onGetTokenResponseAndUser")
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun signInWithPhoneNumberCompat(credential: PhoneAuthCredential?, callbacks: IFirebaseAuthCallbacks) {
|
||||
signInWithPhoneNumber(SignInWithPhoneNumberAidlRequest().apply { this.credential = credential }, callbacks)
|
||||
}
|
||||
|
||||
override fun startMfaEnrollmentWithPhoneNumber(request: StartMfaPhoneNumberEnrollmentAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: startMfaEnrollmentWithPhoneNumber")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun startMfaSignInWithPhoneNumber(request: StartMfaPhoneNumberSignInAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: startMfaSignInWithPhoneNumber")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun unenrollMfa(request: UnenrollMfaAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: unenrollMfa")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun unlinkEmailCredential(request: UnlinkEmailCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: unlinkEmailCredential")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun unlinkEmailCredentialCompat(cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: unlinkEmailCredentialCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun unlinkFederatedCredential(request: UnlinkFederatedCredentialAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: unlinkFederatedCredential")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun unlinkFederatedCredentialCompat(provider: String?, cachedState: String?, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: unlinkFederatedCredentialCompat")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
override fun updateProfile(request: UpdateProfileAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
Log.d(TAG, "updateProfile")
|
||||
try {
|
||||
val getTokenResponse = refreshTokenResponse(request.cachedState)
|
||||
val accountInfoResult = client.getAccountInfo(idToken = getTokenResponse.accessToken).getJSONArray("users").getJSONObject(0).toGetAccountInfoUser()
|
||||
val setAccountInfo = client.setAccountInfo(idToken = getTokenResponse.accessToken, localId = accountInfoResult.localId, displayName = request.request.displayName, photoUrl = request.request.photoUrl, deleteAttribute = request.request.deleteAttributeList).toGetAccountInfoUser()
|
||||
accountInfoResult.photoUrl = setAccountInfo.photoUrl
|
||||
accountInfoResult.displayName = setAccountInfo.displayName
|
||||
callbacks.onGetTokenResponseAndUser(getTokenResponse, accountInfoResult)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "callback: onFailure", e)
|
||||
callbacks.onFailure(Status(CommonStatusCodes.INTERNAL_ERROR, e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateProfileCompat(cachedState: String?, userProfileChangeRequest: UserProfileChangeRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
updateProfile(UpdateProfileAidlRequest().apply { this.cachedState = cachedState; this.request = userProfileChangeRequest}, callbacks)
|
||||
}
|
||||
|
||||
override fun verifyBeforeUpdateEmail(request: VerifyBeforeUpdateEmailAidlRequest, callbacks: IFirebaseAuthCallbacks) {
|
||||
Log.d(TAG, "Not yet implemented: verifyBeforeUpdateEmail")
|
||||
callbacks.onFailure(Status(CommonStatusCodes.CANCELED, "Not supported"))
|
||||
}
|
||||
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
if (super.onTransact(code, data, reply, flags)) return true
|
||||
Log.d(TAG, "onTransact: $code, $data, $flags")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.firebase.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.android.volley.NetworkResponse
|
||||
import com.android.volley.ParseError
|
||||
import com.android.volley.Request.Method.GET
|
||||
import com.android.volley.Request.Method.POST
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.VolleyError
|
||||
import com.android.volley.toolbox.HttpHeaderParser
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.JsonRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.microg.gms.utils.singleInstanceOf
|
||||
import org.microg.gms.utils.toHexString
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.lang.RuntimeException
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private const val TAG = "GmsFirebaseAuthClient"
|
||||
|
||||
class IdentityToolkitClient(context: Context, private val apiKey: String, private val packageName: String? = null, private val certSha1Hash: ByteArray? = null) {
|
||||
private val queue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) }
|
||||
|
||||
private fun buildRelyingPartyUrl(method: String) = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/$method?key=$apiKey"
|
||||
private fun buildStsUrl(method: String) = "https://securetoken.googleapis.com/v1/$method?key=$apiKey"
|
||||
|
||||
private fun getRequestHeaders(): Map<String, String> = hashMapOf<String, String>().apply {
|
||||
if (packageName != null) put("X-Android-Package", packageName)
|
||||
if (certSha1Hash != null) put("X-Android-Cert", certSha1Hash.toHexString().uppercase())
|
||||
}
|
||||
|
||||
private suspend fun request(method: String, data: JSONObject): JSONObject = suspendCoroutine { continuation ->
|
||||
queue.add(object : JsonObjectRequest(POST, buildRelyingPartyUrl(method), data, {
|
||||
continuation.resume(it)
|
||||
}, {
|
||||
Log.d(TAG, "Error: ${it.networkResponse?.data?.decodeToString() ?: it.message}")
|
||||
continuation.resumeWithException(RuntimeException(it))
|
||||
}) {
|
||||
override fun getHeaders(): Map<String, String> = getRequestHeaders()
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun createAuthUri(identifier: String? = null, tenantId: String? = null, continueUri: String? = "http://localhost"): JSONObject =
|
||||
request("createAuthUri", JSONObject()
|
||||
.put("identifier", identifier)
|
||||
.put("tenantId", tenantId)
|
||||
.put("continueUri", continueUri))
|
||||
|
||||
suspend fun getAccountInfo(idToken: String? = null): JSONObject =
|
||||
request("getAccountInfo", JSONObject()
|
||||
.put("idToken", idToken))
|
||||
|
||||
suspend fun getProjectConfig(): JSONObject = suspendCoroutine { continuation ->
|
||||
queue.add(JsonObjectRequest(GET, buildRelyingPartyUrl("getProjectConfig"), null, { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }))
|
||||
}
|
||||
|
||||
suspend fun getOobConfirmationCode(requestType: String, email: String? = null, newEmail: String? = null, continueUrl: String? = null, idToken: String? = null, iOSBundleId: String? = null, iOSAppStoreId: String? = null, androidMinimumVersion: String? = null, androidInstallApp: Boolean? = null, androidPackageName: String? = null, canHandleCodeInApp: Boolean? = null): JSONObject =
|
||||
request("getOobConfirmationCode", JSONObject()
|
||||
.put("kind", "identitytoolkit#relyingparty")
|
||||
.put("requestType", requestType)
|
||||
.put("email", email)
|
||||
.put("newEmail", newEmail)
|
||||
.put("continueUrl", continueUrl)
|
||||
.put("idToken", idToken)
|
||||
.put("iOSBundleId", iOSBundleId)
|
||||
.put("iOSAppStoreId", iOSAppStoreId)
|
||||
.put("androidMinimumVersion", androidMinimumVersion)
|
||||
.put("androidInstallApp", androidInstallApp)
|
||||
.put("androidPackageName", androidPackageName)
|
||||
.put("canHandleCodeInApp", canHandleCodeInApp))
|
||||
|
||||
|
||||
suspend fun sendVerificationCode(phoneNumber: String? = null, reCaptchaToken: String? = null): JSONObject =
|
||||
request("sendVerificationCode", JSONObject()
|
||||
.put("phoneNumber", phoneNumber)
|
||||
.put("recaptchaToken", reCaptchaToken))
|
||||
|
||||
suspend fun setAccountInfo(idToken: String? = null, localId: String? = null, email: String? = null, password: String? = null, displayName: String? = null, photoUrl: String? = null, deleteAttribute: List<String> = emptyList()): JSONObject =
|
||||
request("setAccountInfo", JSONObject()
|
||||
.put("idToken", idToken)
|
||||
.put("localId", localId)
|
||||
.put("email", email)
|
||||
.put("password", password)
|
||||
.put("displayName", displayName)
|
||||
.put("photoUrl", photoUrl)
|
||||
.put("deleteAttribute", JSONArray().apply { deleteAttribute.map { put(it) } }))
|
||||
|
||||
suspend fun signupNewUser(email: String? = null, password: String? = null, tenantId: String? = null): JSONObject =
|
||||
request("signupNewUser", JSONObject()
|
||||
.put("email", email)
|
||||
.put("password", password)
|
||||
.put("tenantId", tenantId))
|
||||
|
||||
suspend fun verifyCustomToken(token: String? = null, returnSecureToken: Boolean = true): JSONObject =
|
||||
request("verifyCustomToken", JSONObject()
|
||||
.put("token", token)
|
||||
.put("returnSecureToken", returnSecureToken))
|
||||
|
||||
suspend fun verifyAssertion(requestUri: String? = null, postBody: String? = null, returnSecureToken: Boolean = true, returnIdpCredential: Boolean = true): JSONObject =
|
||||
request("verifyAssertion", JSONObject()
|
||||
.put("requestUri", requestUri)
|
||||
.put("postBody", postBody)
|
||||
.put("returnSecureToken", returnSecureToken)
|
||||
.put("returnIdpCredential", returnIdpCredential))
|
||||
|
||||
suspend fun verifyPassword(email: String? = null, password: String? = null, tenantId: String? = null, returnSecureToken: Boolean = true): JSONObject =
|
||||
request("verifyPassword", JSONObject()
|
||||
.put("email", email)
|
||||
.put("password", password)
|
||||
.put("tenantId", tenantId)
|
||||
.put("returnSecureToken", returnSecureToken))
|
||||
|
||||
suspend fun verifyPhoneNumber(phoneNumber: String? = null, sessionInfo: String? = null, code: String? = null, idToken: String? = null, verificationProof: String? = null, temporaryProof: String? = null): JSONObject =
|
||||
request("verifyPhoneNumber", JSONObject()
|
||||
.put("verificationProof", verificationProof)
|
||||
.put("code", code)
|
||||
.put("idToken", idToken)
|
||||
.put("temporaryProof", temporaryProof)
|
||||
.put("phoneNumber", phoneNumber)
|
||||
.put("sessionInfo", sessionInfo))
|
||||
|
||||
suspend fun getTokenByRefreshToken(refreshToken: String): JSONObject = suspendCoroutine { continuation ->
|
||||
queue.add(object : JsonRequest<JSONObject>(POST, buildStsUrl("token"), "grant_type=refresh_token&refresh_token=$refreshToken", { continuation.resume(it) }, { continuation.resumeWithException(RuntimeException(it)) }) {
|
||||
override fun parseNetworkResponse(response: NetworkResponse): Response<JSONObject> {
|
||||
return try {
|
||||
val jsonString = String(response.data, Charset.forName(HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)))
|
||||
Response.success(JSONObject(jsonString), null)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Response.error(ParseError(e))
|
||||
} catch (je: JSONException) {
|
||||
Response.error(ParseError(je))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getBodyContentType(): String {
|
||||
return "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
override fun getHeaders(): Map<String, String> = getRequestHeaders()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.firebase.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.*
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import android.util.Log
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.microg.gms.firebase.auth.core.R
|
||||
import org.microg.gms.profile.Build
|
||||
import org.microg.gms.profile.ProfileManager
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private const val TAG = "GmsFirebaseAuthCaptcha"
|
||||
|
||||
class ReCaptchaActivity : AppCompatActivity() {
|
||||
private val receiver: ResultReceiver?
|
||||
get() = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
|
||||
private val hostname: String
|
||||
get() = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000"
|
||||
private var finished = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
openWebsite()
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||
private fun openWebsite() {
|
||||
val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return finishResult(Activity.RESULT_CANCELED)
|
||||
setContentView(R.layout.activity_recaptcha)
|
||||
val view = findViewById<WebView>(R.id.web)
|
||||
val settings = view.settings
|
||||
settings.javaScriptEnabled = true
|
||||
settings.useWideViewPort = false
|
||||
settings.setSupportZoom(false)
|
||||
settings.displayZoomControls = false
|
||||
settings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
ProfileManager.ensureInitialized(this)
|
||||
settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString)
|
||||
view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback")
|
||||
val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey)
|
||||
view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/")
|
||||
}
|
||||
|
||||
fun finishResult(resultCode: Int, token: String? = null) {
|
||||
finished = true
|
||||
setResult(resultCode, token?.let { Intent().apply { putExtra(EXTRA_TOKEN, it) } })
|
||||
receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } })
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!finished) receiver?.send(Activity.RESULT_CANCELED, null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
class ReCaptchaCallback(val activity: ReCaptchaActivity) {
|
||||
@JavascriptInterface
|
||||
fun onReCaptchaToken(token: String) {
|
||||
Log.d(TAG, "onReCaptchaToken: $token")
|
||||
activity.finishResult(Activity.RESULT_OK, token)
|
||||
}
|
||||
}
|
||||
|
||||
fun isSupported(context: Context): Boolean = true
|
||||
|
||||
suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine<String> { continuation ->
|
||||
val intent = Intent(context, ReCaptchaActivity::class.java)
|
||||
val resultReceiver = object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||
try {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
continuation.resume(resultData?.getString(EXTRA_TOKEN)!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
intent.putExtra(EXTRA_API_KEY, apiKey)
|
||||
intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
|
||||
intent.putExtra(EXTRA_HOSTNAME, hostname)
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.firebase.auth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Service
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.ResultReceiver
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.FrameLayout
|
||||
import org.microg.gms.firebase.auth.core.R
|
||||
import org.microg.gms.profile.Build
|
||||
import org.microg.gms.profile.ProfileManager
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private const val TAG = "GmsFirebaseAuthCaptcha"
|
||||
|
||||
class ReCaptchaOverlayService : Service() {
|
||||
|
||||
private var receiver: ResultReceiver? = null
|
||||
private var hostname: String? = null
|
||||
private var apiKey: String? = null
|
||||
|
||||
private var finished = false
|
||||
private var container: View? = null
|
||||
private var windowManager: WindowManager? = null
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
init(intent)
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
finishResult(Activity.RESULT_CANCELED)
|
||||
return super.onUnbind(intent)
|
||||
}
|
||||
|
||||
private fun init(intent: Intent) {
|
||||
apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return finishResult(Activity.RESULT_CANCELED)
|
||||
receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)
|
||||
hostname = intent.getStringExtra(EXTRA_HOSTNAME) ?: "localhost:5000"
|
||||
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
show()
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||
private fun show() {
|
||||
val layoutParamsType = if (android.os.Build.VERSION.SDK_INT >= 26) {
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
} else {
|
||||
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
|
||||
}
|
||||
|
||||
val params = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
layoutParamsType,
|
||||
0,
|
||||
PixelFormat.TRANSLUCENT)
|
||||
|
||||
params.gravity = Gravity.CENTER or Gravity.START
|
||||
params.x = 0
|
||||
params.y = 0
|
||||
|
||||
val interceptorLayout: FrameLayout = object : FrameLayout(this) {
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BACK || event.keyCode == KeyEvent.KEYCODE_HOME) {
|
||||
finishResult(Activity.RESULT_CANCELED)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as? LayoutInflater?
|
||||
if (inflater != null) {
|
||||
val container = inflater.inflate(R.layout.activity_recaptcha, interceptorLayout)
|
||||
this.container = container
|
||||
container.setBackgroundResource(androidx.appcompat.R.drawable.abc_dialog_material_background)
|
||||
val pad = (5.0 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||
container.setOnTouchListener { v, _ ->
|
||||
v.performClick()
|
||||
finishResult(Activity.RESULT_CANCELED)
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
val view = container.findViewById<WebView>(R.id.web)
|
||||
view.setPadding(pad, pad, pad, pad)
|
||||
val settings = view.settings
|
||||
settings.javaScriptEnabled = true
|
||||
settings.useWideViewPort = false
|
||||
settings.setSupportZoom(false)
|
||||
settings.displayZoomControls = false
|
||||
settings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
ProfileManager.ensureInitialized(this)
|
||||
settings.userAgentString = Build.generateWebViewUserAgentString(settings.userAgentString)
|
||||
view.addJavascriptInterface(ReCaptchaCallback(this), "MyCallback")
|
||||
val captcha = assets.open("recaptcha.html").bufferedReader().readText().replace("%apikey%", apiKey!!)
|
||||
view.loadDataWithBaseURL("https://$hostname/", captcha, null, null, "https://$hostname/")
|
||||
windowManager?.addView(container, params)
|
||||
}
|
||||
}
|
||||
|
||||
fun finishResult(resultCode: Int, token: String? = null) {
|
||||
if (!finished) {
|
||||
finished = true
|
||||
receiver?.send(resultCode, token?.let { Bundle().apply { putString(EXTRA_TOKEN, it) } })
|
||||
}
|
||||
container?.let { windowManager?.removeView(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val recaptchaServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
Log.d(TAG, "onReCaptchaToken: onServiceConnected: $name")
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.d(TAG, "onReCaptchaToken: onServiceDisconnected: $name")
|
||||
}
|
||||
}
|
||||
|
||||
class ReCaptchaCallback(private val overlay: ReCaptchaOverlayService) {
|
||||
@JavascriptInterface
|
||||
fun onReCaptchaToken(token: String) {
|
||||
Log.d(TAG, "onReCaptchaToken: $token")
|
||||
overlay.finishResult(Activity.RESULT_OK, token)
|
||||
}
|
||||
}
|
||||
|
||||
fun isSupported(context: Context): Boolean = android.os.Build.VERSION.SDK_INT < 23 || Settings.canDrawOverlays(context)
|
||||
|
||||
suspend fun awaitToken(context: Context, apiKey: String, hostname: String? = null) = suspendCoroutine { continuation ->
|
||||
val intent = Intent(context, ReCaptchaOverlayService::class.java)
|
||||
val resultReceiver = object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||
context.unbindService(recaptchaServiceConnection)
|
||||
try {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
continuation.resume(resultData?.getString(EXTRA_TOKEN)!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
intent.putExtra(EXTRA_API_KEY, apiKey)
|
||||
intent.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver)
|
||||
intent.putExtra(EXTRA_HOSTNAME, hostname)
|
||||
context.bindService(intent, recaptchaServiceConnection, BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.firebase.auth
|
||||
|
||||
const val EXTRA_TOKEN = "token"
|
||||
const val EXTRA_API_KEY = "api_key"
|
||||
const val EXTRA_HOSTNAME = "hostname"
|
||||
const val EXTRA_RESULT_RECEIVER = "receiver"
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="450dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/web"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:minHeight="450dp" />
|
||||
</FrameLayout>
|
||||
Loading…
Add table
Add a link
Reference in a new issue