// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.sms;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.ui.base.WindowAndroid;
/**
* Simple proxy that provides C++ code with a pathway to the GMS OTP code
* retrieving APIs.
* It can operate in three different backend modes which is determined on
* construction:
* 1. User Consent: use the user consent API (older method with sub-optimal UX)
* 2. Verification: use the browser code API (newer method)
* 3. Auto: prefers to use the verification method but if it is not available
* automatically falls back to using the User Consent method.
*
*/
@JNINamespace("content")
public class SmsProviderGms {
private static final String TAG = "SmsProviderGms";
private static final int MIN_GMS_VERSION_NUMBER_WITH_CODE_BROWSER_BACKEND = 202990000;
private final long mSmsProviderGmsAndroid;
private final @GmsBackend int mBackend;
private SmsUserConsentReceiver mUserConsentReceiver;
private SmsVerificationReceiver mVerificationReceiver;
private Wrappers.WebOTPServiceContext mContext;
private WindowAndroid mWindow;
private Wrappers.SmsRetrieverClientWrapper mClient;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public SmsProviderGms(
long smsProviderGmsAndroid,
@GmsBackend int backend,
boolean isVerificationBackendAvailable) {
mSmsProviderGmsAndroid = smsProviderGmsAndroid;
mBackend = backend;
mContext = new Wrappers.WebOTPServiceContext(ContextUtils.getApplicationContext(), this);
// Creates an mVerificationReceiver regardless of the backend to support requests from
// remote devices.
if (isVerificationBackendAvailable) {
mVerificationReceiver = new SmsVerificationReceiver(this, mContext);
}
if (mBackend == GmsBackend.AUTO || mBackend == GmsBackend.USER_CONSENT) {
mUserConsentReceiver = new SmsUserConsentReceiver(this, mContext);
}
Log.i(TAG, "construction successfull %s, %s", mVerificationReceiver, mUserConsentReceiver);
}
public void setUserConsentReceiverForTesting(SmsUserConsentReceiver userConsentReceiver) {
var oldValue = mUserConsentReceiver;
mUserConsentReceiver = userConsentReceiver;
ResettersForTesting.register(() -> mUserConsentReceiver = oldValue);
}
public void setVerificationReceiverForTesting(SmsVerificationReceiver verificationReceiver) {
var oldValue = mVerificationReceiver;
mVerificationReceiver = verificationReceiver;
ResettersForTesting.register(() -> mVerificationReceiver = oldValue);
}
public SmsUserConsentReceiver getUserConsentReceiverForTesting() {
return mUserConsentReceiver;
}
public SmsVerificationReceiver getVerificationReceiverForTesting() {
return mVerificationReceiver;
}
// Methods that are called by native implementation
@CalledByNative
private static SmsProviderGms create(long smsProviderGmsAndroid, @GmsBackend int backend) {
Log.d(TAG, "Creating SmsProviderGms");
boolean isVerificationBackendAvailable =
GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(
ContextUtils.getApplicationContext(),
MIN_GMS_VERSION_NUMBER_WITH_CODE_BROWSER_BACKEND)
== ConnectionResult.SUCCESS;
return new SmsProviderGms(smsProviderGmsAndroid, backend, isVerificationBackendAvailable);
}
@CalledByNative
private void destroy() {
if (mVerificationReceiver != null) mVerificationReceiver.destroy();
if (mUserConsentReceiver != null) mUserConsentReceiver.destroy();
}
@CalledByNative
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public void listen(WindowAndroid window, boolean isLocalRequest) {
mWindow = window;
// Using the verification receiver is preferable but also start user consent receiver in
// case the verification receiver fails. i.e. if the start of the verification retriever has
// not been successful and the SMS arrives, we fall back to the user consent receiver to
// handle it. Note that starting both receiver means that we may end up using the user
// consent receiver even when the preferred verification backend is available but slow. But
// this is acceptable given that handling SMS should be done in timely manner.
// If the SMS retrieval request is made from a remote device, e.g. desktop, we only proceed
// with the verification receiver because the user consent receiver introduces too much user
// friction. In addition, we do not apply the fallback logic in such case.
boolean shouldUseVerificationReceiver =
mVerificationReceiver != null
&& (!isLocalRequest || mBackend != GmsBackend.USER_CONSENT);
boolean shouldUseUserConsentReceiver =
mUserConsentReceiver != null
&& isLocalRequest
&& mBackend != GmsBackend.VERIFICATION
&& window != null;
if (shouldUseVerificationReceiver) mVerificationReceiver.listen(isLocalRequest);
if (shouldUseUserConsentReceiver) mUserConsentReceiver.listen(window);
}
/**
* Destroys the user consent receiver if the verification receiver succeeded with a local
* request.
*
* @param isLocalRequest Represents whether this request is from local device or not
*/
public void verificationReceiverSucceeded(boolean isLocalRequest) {
if (!isLocalRequest) return;
Log.d(TAG, "DestroyUserConsentReceiver");
if (mUserConsentReceiver != null) mUserConsentReceiver.destroy();
}
/**
* Destroys the verification receiver if it failed with a local request.
*
* @param isLocalRequest Represents whether this request is from local device or not
*/
public void verificationReceiverFailed(boolean isLocalRequest) {
if (!isLocalRequest) return;
Log.d(TAG, "DestroyVerificationReceiver");
if (mVerificationReceiver != null) mVerificationReceiver.destroy();
}
void onMethodNotAvailable(boolean isLocalRequest) {
assert (mBackend != GmsBackend.USER_CONSENT || !isLocalRequest);
// Note on caching method availability status: It is possible to cache the fact that calling
// into verification backend has failed and avoid trying it again on subsequent calls. But
// since that can change at runtime (e.g., if Chrome becomes default browser) then we may
// need to invalidate that cached status. To simplify things we simply attempt the
// verification method first on *each request* and fallback to the user consent method if it
// fails. The initial call to verification is expected to be cheap so this should not have
// any noticeable impact.
// Note that the fallback logic is only applicable if the SMS retrieval request is made from
// a local device.
if (mBackend == GmsBackend.VERIFICATION || !isLocalRequest) onNotAvailable();
}
// --------- Callbacks for receivers
void onReceive(String sms, @GmsBackend int backend) {
SmsProviderGmsJni.get().onReceive(mSmsProviderGmsAndroid, sms, backend);
}
void onTimeout() {
SmsProviderGmsJni.get().onTimeout(mSmsProviderGmsAndroid);
}
void onCancel() {
SmsProviderGmsJni.get().onCancel(mSmsProviderGmsAndroid);
}
void onNotAvailable() {
SmsProviderGmsJni.get().onNotAvailable(mSmsProviderGmsAndroid);
}
public WindowAndroid getWindow() {
return mWindow;
}
public Wrappers.SmsRetrieverClientWrapper getClient() {
if (mClient != null) {
return mClient;
}
mClient =
new Wrappers.SmsRetrieverClientWrapper(
mUserConsentReceiver != null ? mUserConsentReceiver.createClient() : null,
mVerificationReceiver != null
? mVerificationReceiver.createClient()
: null);
return mClient;
}
@CalledByNative
private void setClientAndWindow(
Wrappers.SmsRetrieverClientWrapper client, WindowAndroid window) {
assert mClient == null;
assert mWindow == null;
mClient = client;
mWindow = window;
mClient.setContext(mContext);
}
@NativeMethods
interface Natives {
void onReceive(long nativeSmsProviderGms, String sms, @GmsBackend int backend);
void onTimeout(long nativeSmsProviderGms);
void onCancel(long nativeSmsProviderGms);
void onNotAvailable(long nativeSmsProviderGms);
}
}