diff --git a/app/build.gradle b/app/build.gradle index f3daf3229..2b22a1a84 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,6 +38,9 @@ dependencies { compile "com.google.firebase:firebase-database:$firebaseVersion" compile "com.google.firebase:firebase-storage:$firebaseVersion" + compile('com.facebook.android:facebook-android-sdk:4.22.1') + compile("com.twitter.sdk.android:twitter-core:3.0.0@aar") { transitive = true } + // The following dependencies are not required to use the Firebase UI library. // They are used to make some aspects of the demo app implementation simpler for // demonstrative purposes, and you may find them useful in your own apps; YMMV. diff --git a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java index b431a376b..0c396a0e2 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/AuthUiActivity.java @@ -71,6 +71,9 @@ public class AuthUiActivity extends AppCompatActivity { @BindView(R.id.email_provider) CheckBox mUseEmailProvider; + @BindView(R.id.phone_provider) + CheckBox mUsePhoneProvider; + @BindView(R.id.google_provider) CheckBox mUseGoogleProvider; @@ -107,8 +110,11 @@ public class AuthUiActivity extends AppCompatActivity { @BindView(R.id.no_logo) RadioButton mNoLogo; - @BindView(R.id.smartlock_enabled) - CheckBox mEnableSmartLock; + @BindView(R.id.credential_selector_enabled) + CheckBox mEnableCredentialSelector; + + @BindView(R.id.hint_selector_enabled) + CheckBox mEnableHintSelector; @BindView(R.id.allow_new_email_accounts) CheckBox mAllowNewEmailAccounts; @@ -194,7 +200,8 @@ public void signIn(View view) { .setAvailableProviders(getSelectedProviders()) .setTosUrl(getSelectedTosUrl()) .setPrivacyPolicyUrl(getSelectedPrivacyPolicyUrl()) - .setIsSmartLockEnabled(mEnableSmartLock.isChecked()) + .setIsSmartLockEnabled(mEnableCredentialSelector.isChecked(), + mEnableHintSelector.isChecked()) .setAllowNewEmailAccounts(mAllowNewEmailAccounts.isChecked()) .build(), RC_SIGN_IN); @@ -252,7 +259,8 @@ private void startSignedInActivity(IdpResponse response) { getSelectedTheme(), getSelectedProviders(), getSelectedTosUrl(), - mEnableSmartLock.isChecked()))); + mEnableCredentialSelector.isChecked(), + mEnableHintSelector.isChecked()))); } @MainThread @@ -324,6 +332,11 @@ private List getSelectedProviders() { selectedProviders.add(new IdpConfig.Builder(AuthUI.EMAIL_PROVIDER).build()); } + if (mUsePhoneProvider.isChecked()) { + selectedProviders.add( + new IdpConfig.Builder(AuthUI.PHONE_VERIFICATION_PROVIDER).build()); + } + return selectedProviders; } diff --git a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java index 087d9ee41..fedb61a13 100644 --- a/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java +++ b/app/src/main/java/com/firebase/uidemo/auth/SignedInActivity.java @@ -68,6 +68,10 @@ public class SignedInActivity extends AppCompatActivity { @BindView(R.id.user_display_name) TextView mUserDisplayName; + + @BindView(R.id.user_phone_number) + TextView mUserPhoneNumber; + @BindView(R.id.user_enabled_providers) TextView mEnabledProviders; @@ -157,6 +161,8 @@ private void populateProfile() { mUserEmail.setText( TextUtils.isEmpty(user.getEmail()) ? "No email" : user.getEmail()); + mUserPhoneNumber.setText( + TextUtils.isEmpty(user.getPhoneNumber()) ? "No phone number" : user.getPhoneNumber()); mUserDisplayName.setText( TextUtils.isEmpty(user.getDisplayName()) ? "No display name" : user.getDisplayName()); @@ -223,18 +229,21 @@ static final class SignedInConfig implements Parcelable { int theme; List providerInfo; String tosUrl; - boolean isSmartLockEnabled; + boolean isCredentialSelectorEnabled; + boolean isHintSelectorEnabled; SignedInConfig(int logo, int theme, List providerInfo, String tosUrl, - boolean isSmartLockEnabled) { + boolean isCredentialSelectorEnabled, + boolean isHintSelectorEnabled) { this.logo = logo; this.theme = theme; this.providerInfo = providerInfo; this.tosUrl = tosUrl; - this.isSmartLockEnabled = isSmartLockEnabled; + this.isCredentialSelectorEnabled = isCredentialSelectorEnabled; + this.isHintSelectorEnabled = isHintSelectorEnabled; } SignedInConfig(Parcel in) { @@ -243,7 +252,8 @@ static final class SignedInConfig implements Parcelable { providerInfo = new ArrayList<>(); in.readList(providerInfo, IdpConfig.class.getClassLoader()); tosUrl = in.readString(); - isSmartLockEnabled = in.readInt() != 0; + isCredentialSelectorEnabled = in.readInt() != 0; + isHintSelectorEnabled = in.readInt() != 0; } public static final Creator CREATOR = new Creator() { @@ -269,7 +279,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(theme); dest.writeList(providerInfo); dest.writeString(tosUrl); - dest.writeInt(isSmartLockEnabled ? 1 : 0); + dest.writeInt(isCredentialSelectorEnabled ? 1 : 0); + dest.writeInt(isHintSelectorEnabled ? 1 : 0); } } diff --git a/app/src/main/res/layout/auth_ui_layout.xml b/app/src/main/res/layout/auth_ui_layout.xml index 36f4bc3ee..e9e96abff 100644 --- a/app/src/main/res/layout/auth_ui_layout.xml +++ b/app/src/main/res/layout/auth_ui_layout.xml @@ -112,6 +112,13 @@ android:checked="true" android:text="@string/email_label"/> + + + + + android:text="@string/enable_hint_selector"/> + + None Use auth providers: Email + Phone Facebook Facebook - configuration missing Twitter @@ -49,7 +50,6 @@ Default theme Configuration is required - see README.md Other Options: - Enable SmartLock for Passwords This sample will read an image from local storage to upload to Cloud Storage. Anonymous authentication failed, various components of the demo will not work. Make sure your device is online and that Anonymous Auth is configured in your Firebase project(https://console.firebase.google.com/project/_/authentication/providers) Example extra Google scopes @@ -71,4 +71,6 @@ No messages. Start chatting at the bottom! Signed In + Enable Smart Lock\'s credential selector + Enable Smart Lock\'s hint selector diff --git a/auth/README.md b/auth/README.md index 4c7da19a8..a47bf60d1 100644 --- a/auth/README.md +++ b/auth/README.md @@ -47,6 +47,12 @@ Gradle, add the dependency: dependencies { // ... compile 'com.firebaseui:firebase-ui-auth:1.2.0' + + // Required only if Facebook login support is required + compile('com.facebook.android:facebook-android-sdk:4.22.1') + + // Required only if Twitter login support is required + compile("com.twitter.sdk.android:twitter-core:3.0.0@aar") { transitive = true } } ``` diff --git a/auth/auth-proguard.pro b/auth/auth-proguard.pro new file mode 100644 index 000000000..02e0a6970 --- /dev/null +++ b/auth/auth-proguard.pro @@ -0,0 +1,2 @@ +-dontwarn com.twitter.** +-dontwarn com.facebook.** \ No newline at end of file diff --git a/auth/build.gradle b/auth/build.gradle index 1055dfdf8..73ae08779 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'com.android.library' -apply plugin: 'io.fabric' apply from: '../library/quality/quality.gradle' android { @@ -18,7 +17,7 @@ android { manifestPlaceholders = [enableFbLogging: true] minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt') + consumerProguardFiles getDefaultProguardFile('proguard-android.txt'), 'auth-proguard.pro' } debug { @@ -44,8 +43,8 @@ dependencies { compile "com.google.firebase:firebase-auth:$firebaseVersion" compile "com.google.android.gms:play-services-auth:$firebaseVersion" - compile 'com.facebook.android:facebook-android-sdk:4.23.0' - compile("com.twitter.sdk.android:twitter:2.3.2@aar") { transitive = true } + provided 'com.facebook.android:facebook-android-sdk:4.23.0' + provided("com.twitter.sdk.android:twitter-core:3.0.0@aar") { transitive = true } // The following libraries are needed to prevent incompatibilities with the facebook // library when updating com.android.support libraries: @@ -57,6 +56,7 @@ dependencies { testCompile 'org.robolectric:robolectric:3.2.2' // See https://github.com/robolectric/robolectric/issues/1932#issuecomment-219796474 testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1' + testCompile 'com.facebook.android:facebook-android-sdk:4.23.0' } javadoc.exclude([ diff --git a/auth/src/main/AndroidManifest.xml b/auth/src/main/AndroidManifest.xml index 59e72f1a1..d26d526c6 100644 --- a/auth/src/main/AndroidManifest.xml +++ b/auth/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -39,6 +40,12 @@ android:label="@string/sign_in_default" android:exported="false"/> + + + android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" + tools:ignore="MissingRegistered" /> + android:exported="true" + tools:ignore="MissingRegistered"> diff --git a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java index a81ab3296..a0f4c83a9 100644 --- a/auth/src/main/java/com/firebase/ui/auth/AuthUI.java +++ b/auth/src/main/java/com/firebase/ui/auth/AuthUI.java @@ -44,8 +44,8 @@ import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.TwitterAuthProvider; -import com.twitter.sdk.android.Twitter; import java.util.ArrayList; import java.util.Arrays; @@ -55,8 +55,6 @@ import java.util.List; import java.util.Set; -import io.fabric.sdk.android.Fabric; - /** * The entry point to the AuthUI authentication flow, and related utility methods. If your * application uses the default {@link FirebaseApp} instance, an AuthUI instance can be retrieved @@ -70,9 +68,10 @@ public class AuthUI { @StringDef({ EmailAuthProvider.PROVIDER_ID, EMAIL_PROVIDER, + PhoneAuthProvider.PROVIDER_ID, PHONE_VERIFICATION_PROVIDER, GoogleAuthProvider.PROVIDER_ID, GOOGLE_PROVIDER, FacebookAuthProvider.PROVIDER_ID, FACEBOOK_PROVIDER, - TwitterAuthProvider.PROVIDER_ID, TWITTER_PROVIDER + TwitterAuthProvider.PROVIDER_ID, TWITTER_PROVIDER, }) public @interface SupportedProvider {} @@ -97,6 +96,11 @@ public class AuthUI { */ public static final String TWITTER_PROVIDER = TwitterAuthProvider.PROVIDER_ID; + /** + * Provider identifier for Phone, for use with {@link SignInIntentBuilder#setProviders}. + */ + public static final String PHONE_VERIFICATION_PROVIDER = PhoneAuthProvider.PROVIDER_ID; + /** * Default value for logo resource, omits the logo from the {@link AuthMethodPickerActivity}. */ @@ -110,7 +114,8 @@ public class AuthUI { EMAIL_PROVIDER, GOOGLE_PROVIDER, FACEBOOK_PROVIDER, - TWITTER_PROVIDER + TWITTER_PROVIDER, + PHONE_VERIFICATION_PROVIDER ))); private static final IdentityHashMap INSTANCES = new IdentityHashMap<>(); @@ -179,12 +184,18 @@ public Task signOut(@NonNull FragmentActivity activity) { Task signOutTask = signInHelper.signOut(); // Facebook sign out - LoginManager.getInstance().logOut(); + try { + LoginManager.getInstance().logOut(); + } catch (NoClassDefFoundError e) { + // do nothing + } // Twitter sign out - if (!Fabric.isInitialized()) TwitterProvider.initialize(activity); - Twitter.logOut(); - + try { + TwitterProvider.signout(activity); + } catch (NoClassDefFoundError e) { + // do nothing + } // Wait for all tasks to complete return Tasks.whenAll(disableCredentialsTask, signOutTask); } @@ -374,7 +385,8 @@ private abstract class AuthIntentBuilder { List mProviders = new ArrayList<>(); String mTosUrl; String mPrivacyPolicyUrl; - boolean mIsSmartLockEnabled = true; + boolean mEnableCredentials = true; + boolean mEnableHints = true; private AuthIntentBuilder() {} @@ -438,6 +450,26 @@ public T setAvailableProviders(@NonNull List idpConfigs) { } else { mProviders.add(config); } + + if (config.getProviderId().equals(FACEBOOK_PROVIDER)) { + try { + Class c = com.facebook.FacebookSdk.class; + } catch (NoClassDefFoundError e) { + throw new RuntimeException("Facebook provider cannot be configured " + + "without dependency. Did you forget to add " + + "'com.facebook.android:facebook-android-sdk:VERSION' dependency?"); + } + } + + if (config.getProviderId().equals(TWITTER_PROVIDER)) { + try { + Class c = com.twitter.sdk.android.core.TwitterCore.class; + } catch (NoClassDefFoundError e) { + throw new RuntimeException("Twitter provider cannot be configured " + + "without dependency. Did you forget to add " + + "'com.twitter.sdk.android:twitter-core:VERSION' dependency?"); + } + } } return (T) this; @@ -473,11 +505,31 @@ public T setProviders(@NonNull List idpConfigs) { /** * Enables or disables the use of Smart Lock for Passwords in the sign in flow. + * To (en)disable hint selector and credential selector independently + * use {@link #setIsSmartLockEnabled(boolean, boolean)} *

*

SmartLock is enabled by default. + * + * @param enabled enables smartlock's credential selector and hint selector */ public T setIsSmartLockEnabled(boolean enabled) { - mIsSmartLockEnabled = enabled; + setIsSmartLockEnabled(enabled, enabled); + return (T) this; + } + + /** + * Enables or disables the use of Smart Lock for Passwords credential selector and hint + * selector. + *

+ *

Both selectors are enabled by default. + + * @param enableCredentials enables credential selector before signup + * @param enableHints enable hint selector in respective signup screens + * @return + */ + public T setIsSmartLockEnabled(boolean enableCredentials, boolean enableHints) { + mEnableCredentials = enableCredentials; + mEnableHints = enableHints; return (T) this; } @@ -522,7 +574,8 @@ protected FlowParameters getFlowParams() { mLogo, mTosUrl, mPrivacyPolicyUrl, - mIsSmartLockEnabled, + mEnableCredentials, + mEnableHints, mAllowNewEmailAccounts); } } diff --git a/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java b/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java index b83b2fbbc..2c9797a0a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java +++ b/auth/src/main/java/com/firebase/ui/auth/IdpResponse.java @@ -34,22 +34,25 @@ public class IdpResponse implements Parcelable { private final String mProviderId; private final String mEmail; + private final String mPhoneNumber; private final String mToken; private final String mSecret; private final int mErrorCode; private IdpResponse(int errorCode) { - this(null, null, null, null, errorCode); + this(null, null, null, null, null, errorCode); } private IdpResponse( String providerId, String email, + String phoneNumber, String token, String secret, int errorCode) { mProviderId = providerId; mEmail = email; + mPhoneNumber = phoneNumber; mToken = token; mSecret = secret; mErrorCode = errorCode; @@ -97,6 +100,14 @@ public String getEmail() { return mEmail; } + /** + * Get the phone number used to sign in. + */ + @Nullable + public String getPhoneNumber() { + return mPhoneNumber; + } + /** * Get the token received as a result of logging in with the specified IDP */ @@ -129,6 +140,7 @@ public int describeContents() { public void writeToParcel(Parcel dest, int flags) { dest.writeString(mProviderId); dest.writeString(mEmail); + dest.writeString(mPhoneNumber); dest.writeString(mToken); dest.writeString(mSecret); dest.writeInt(mErrorCode); @@ -142,6 +154,7 @@ public IdpResponse createFromParcel(Parcel in) { in.readString(), in.readString(), in.readString(), + in.readString(), in.readInt() ); } @@ -156,6 +169,7 @@ public IdpResponse[] newArray(int size) { public static class Builder { private String mProviderId; private String mEmail; + private String mPhoneNumber; private String mToken; private String mSecret; @@ -164,6 +178,11 @@ public Builder(@AuthUI.SupportedProvider @NonNull String providerId, @Nullable S mEmail = email; } + public Builder setPhoneNumber(String phoneNumber) { + mPhoneNumber = phoneNumber; + return this; + } + public Builder setToken(String token) { mToken = token; return this; @@ -189,7 +208,7 @@ public IdpResponse build() { "Secret cannot be null when using the Twitter provider."); } - return new IdpResponse(mProviderId, mEmail, mToken, mSecret, ResultCodes.OK); + return new IdpResponse(mProviderId, mEmail, mPhoneNumber, mToken, mSecret, ResultCodes.OK); } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/provider/EmailProvider.java b/auth/src/main/java/com/firebase/ui/auth/provider/EmailProvider.java index 0c7cbe14e..fd22120cf 100644 --- a/auth/src/main/java/com/firebase/ui/auth/provider/EmailProvider.java +++ b/auth/src/main/java/com/firebase/ui/auth/provider/EmailProvider.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.support.annotation.LayoutRes; +import com.firebase.ui.auth.AuthUI; import com.firebase.ui.auth.R; import com.firebase.ui.auth.ResultCodes; import com.firebase.ui.auth.ui.BaseHelper; @@ -28,6 +29,7 @@ public String getName(Context context) { } @Override + @AuthUI.SupportedProvider public String getProviderId() { return EmailAuthProvider.PROVIDER_ID; } diff --git a/auth/src/main/java/com/firebase/ui/auth/provider/PhoneProvider.java b/auth/src/main/java/com/firebase/ui/auth/provider/PhoneProvider.java new file mode 100644 index 000000000..741d47cd7 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/provider/PhoneProvider.java @@ -0,0 +1,57 @@ +package com.firebase.ui.auth.provider; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.LayoutRes; + +import com.firebase.ui.auth.AuthUI; +import com.firebase.ui.auth.R; +import com.firebase.ui.auth.ResultCodes; +import com.firebase.ui.auth.ui.BaseHelper; +import com.firebase.ui.auth.ui.phone.PhoneVerificationActivity; +import com.google.firebase.auth.PhoneAuthProvider; + +public class PhoneProvider implements Provider { + + private static final int RC_PHONE_FLOW = 4; + + private Activity mActivity; + private BaseHelper mHelper; + + public PhoneProvider(Activity activity, BaseHelper helper) { + mActivity = activity; + mHelper = helper; + } + + @Override + public String getName(Context context) { + return context.getString(R.string.provider_name_phone); + } + + @Override + @AuthUI.SupportedProvider + public String getProviderId() { + return PhoneAuthProvider.PROVIDER_ID; + } + + @Override + @LayoutRes + public int getButtonLayout() { + return R.layout.provider_button_phone; + } + + @Override + public void startLogin(Activity activity) { + activity.startActivityForResult( + PhoneVerificationActivity.createIntent(activity, mHelper.getFlowParams(), null), + RC_PHONE_FLOW); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == RC_PHONE_FLOW && resultCode == ResultCodes.OK) { + mHelper.finishActivity(mActivity, ResultCodes.OK, data); + } + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java b/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java index f8d70e6bb..dd64d077c 100644 --- a/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java +++ b/auth/src/main/java/com/firebase/ui/auth/provider/TwitterProvider.java @@ -12,17 +12,18 @@ import com.firebase.ui.auth.R; import com.google.firebase.auth.AuthCredential; import com.google.firebase.auth.TwitterAuthProvider; -import com.twitter.sdk.android.Twitter; import com.twitter.sdk.android.core.Callback; import com.twitter.sdk.android.core.Result; +import com.twitter.sdk.android.core.Twitter; import com.twitter.sdk.android.core.TwitterAuthConfig; +import com.twitter.sdk.android.core.TwitterConfig; +import com.twitter.sdk.android.core.TwitterCore; import com.twitter.sdk.android.core.TwitterException; import com.twitter.sdk.android.core.TwitterSession; import com.twitter.sdk.android.core.identity.TwitterAuthClient; import java.lang.ref.WeakReference; -import io.fabric.sdk.android.Fabric; public class TwitterProvider extends Callback implements IdpProvider { private static final String TAG = "TwitterProvider"; @@ -46,7 +47,20 @@ public static void initialize(Context context) { TwitterAuthConfig authConfig = new TwitterAuthConfig( context.getString(R.string.twitter_consumer_key), context.getString(R.string.twitter_consumer_secret)); - Fabric.with(context.getApplicationContext(), new Twitter(authConfig)); + TwitterConfig config = new TwitterConfig.Builder(context) + .twitterAuthConfig(authConfig) + .build(); + Twitter.initialize(config); + } + + public static void signout(Context context) { + try { + Twitter.getInstance(); + } catch (IllegalStateException e) { + initialize(context); + } + + signOut(); } @Override @@ -128,4 +142,8 @@ private IdpResponse createIdpResponse(String email) { .build(); } } + + private static void signOut() throws IllegalStateException { + TwitterCore.getInstance().getSessionManager().clearActiveSession(); + } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/BaseHelper.java b/auth/src/main/java/com/firebase/ui/auth/ui/BaseHelper.java index c79f9809b..27df410fe 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/BaseHelper.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/BaseHelper.java @@ -18,6 +18,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.PhoneAuthProvider; import static com.firebase.ui.auth.util.Preconditions.checkNotNull; @@ -96,6 +97,10 @@ public SaveSmartLock getSaveSmartLockInstance(FragmentActivity activity) { return SaveSmartLock.getInstance(activity, getFlowParams()); } + public PhoneAuthProvider getPhoneAuthProviderInstance() { + return PhoneAuthProvider.getInstance(); + } + public void saveCredentialsOrFinish( @Nullable SaveSmartLock saveSmartLock, Activity activity, diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/ExtraConstants.java b/auth/src/main/java/com/firebase/ui/auth/ui/ExtraConstants.java index 9f9ee92db..5f28841da 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/ExtraConstants.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/ExtraConstants.java @@ -25,5 +25,6 @@ public class ExtraConstants { public static final String EXTRA_IDP_RESPONSE = "extra_idp_response"; public static final String EXTRA_USER = "extra_user"; public static final String EXTRA_EMAIL = "extra_email"; + public static final String EXTRA_PHONE = "extra_phone_number"; public static final String HAS_EXISTING_INSTANCE = "has_existing_instance"; } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/FlowParameters.java b/auth/src/main/java/com/firebase/ui/auth/ui/FlowParameters.java index 9a6b4c112..7f3bd5e08 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/FlowParameters.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/FlowParameters.java @@ -51,10 +51,11 @@ public class FlowParameters implements Parcelable { @Nullable public final String privacyPolicyUrl; - public final boolean smartLockEnabled; - public final boolean allowNewEmailAccounts; + public final boolean enableCredentials; + public final boolean enableHints; + public FlowParameters( @NonNull String appName, @NonNull List providerInfo, @@ -62,7 +63,8 @@ public FlowParameters( @DrawableRes int logoId, @Nullable String termsOfServiceUrl, @Nullable String privacyPolicyUrl, - boolean smartLockEnabled, + boolean enableCredentials, + boolean enableHints, boolean allowNewEmailAccounts) { this.appName = Preconditions.checkNotNull(appName, "appName cannot be null"); this.providerInfo = Collections.unmodifiableList( @@ -71,7 +73,8 @@ public FlowParameters( this.logoId = logoId; this.termsOfServiceUrl = termsOfServiceUrl; this.privacyPolicyUrl = privacyPolicyUrl; - this.smartLockEnabled = smartLockEnabled; + this.enableCredentials = enableCredentials; + this.enableHints = enableHints; this.allowNewEmailAccounts = allowNewEmailAccounts; } @@ -83,7 +86,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(logoId); dest.writeString(termsOfServiceUrl); dest.writeString(privacyPolicyUrl); - dest.writeInt(smartLockEnabled ? 1 : 0); + dest.writeInt(enableCredentials ? 1 : 0); + dest.writeInt(enableHints ? 1 : 0); dest.writeInt(allowNewEmailAccounts ? 1 : 0); } @@ -101,10 +105,9 @@ public FlowParameters createFromParcel(Parcel in) { int logoId = in.readInt(); String termsOfServiceUrl = in.readString(); String privacyPolicyUrl = in.readString(); - boolean smartLockEnabled = in.readInt() != 0; + boolean enableCredentials = in.readInt() != 0; + boolean enableHints = in.readInt() != 0; boolean allowNewEmailAccounts = in.readInt() != 0; - boolean isReauth = in.readInt() != 0; - String reauthReason = in.readString(); return new FlowParameters( appName, @@ -113,7 +116,8 @@ public FlowParameters createFromParcel(Parcel in) { logoId, termsOfServiceUrl, privacyPolicyUrl, - smartLockEnabled, + enableCredentials, + enableHints, allowNewEmailAccounts); } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/TermsTextView.java b/auth/src/main/java/com/firebase/ui/auth/ui/TermsTextView.java new file mode 100644 index 000000000..77058716a --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/TermsTextView.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui; + +import android.content.Context; +import android.support.annotation.StringRes; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; + +import com.firebase.ui.auth.ui.email.PreambleHandler; + +/** + * Text view to display terms of service before completing signup. + * The view helps display TOS linking to the provided custom URI. + * It handles the styling of the link and opens the uri in a CustomTabs on click. + */ +public class TermsTextView extends AppCompatTextView { + public TermsTextView(Context context) { + super(context); + } + + public TermsTextView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TermsTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * @param params FlowParameters containing terms URLs. + * @param buttonText for the button that represents the "action" described in the terms. + */ + public void showTerms(FlowParameters params, @StringRes int buttonText) { + PreambleHandler handler = new PreambleHandler(getContext(), params, buttonText); + handler.setPreamble(this); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackIdpPrompt.java b/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackIdpPrompt.java index b73575228..c965cbb81 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackIdpPrompt.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackIdpPrompt.java @@ -158,7 +158,8 @@ public void onSuccess(AuthResult result) { result.getUser() .linkWithCredential(mPrevCredential) .addOnFailureListener(new TaskFailureLogger( - TAG, "Error signing in with previous credential " + idpResponse.getProviderType())) + TAG, "Error signing in with previous credential " + + idpResponse.getProviderType())) .addOnCompleteListener(new FinishListener(idpResponse)); } else { finish(ResultCodes.OK, idpResponse.toIntent()); @@ -172,12 +173,14 @@ public void onFailure(@NonNull Exception e) { } }) .addOnFailureListener( - new TaskFailureLogger(TAG, "Error signing in with new credential " + idpResponse.getProviderType())); + new TaskFailureLogger(TAG, "Error signing in with new credential " + + idpResponse.getProviderType())); } else { currentUser .linkWithCredential(newCredential) .addOnFailureListener( - new TaskFailureLogger(TAG, "Error linking with credential " + idpResponse.getProviderType())) + new TaskFailureLogger(TAG, "Error linking with credential " + + idpResponse.getProviderType())) .addOnCompleteListener(new FinishListener(idpResponse)); } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackPasswordPrompt.java b/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackPasswordPrompt.java index 9be6fff2b..6ac33f2e3 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackPasswordPrompt.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/accountlink/WelcomeBackPasswordPrompt.java @@ -169,7 +169,8 @@ public void onSuccess(AuthResult authResult) { authResult.getUser() .linkWithCredential(authCredential) .addOnFailureListener(new TaskFailureLogger( - TAG, "Error signing in with credential " + authCredential.getProvider())) + TAG, "Error signing in with credential " + + authCredential.getProvider())) .addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(AuthResult authResult) { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java index 421a1b987..82d1dd12f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/CheckEmailFragment.java @@ -43,7 +43,10 @@ * Host Activities should implement {@link CheckEmailListener}. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public class CheckEmailFragment extends FragmentBase implements View.OnClickListener, ImeHelper.DonePressedListener { +public class CheckEmailFragment extends FragmentBase implements + View.OnClickListener, + ImeHelper.DonePressedListener { + /** * Interface to be implemented by Activities hosting this Fragment. */ @@ -133,7 +136,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { // Use email passed in mEmailEditText.setText(email); validateAndProceed(); - } else if (mHelper.getFlowParams().smartLockEnabled) { + } else if (mHelper.getFlowParams().enableHints) { // Try SmartLock email autocomplete hint showEmailAutoCompleteHint(); } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/PreambleHandler.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/PreambleHandler.java index 1b621709b..24c0ca26e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/PreambleHandler.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/PreambleHandler.java @@ -19,18 +19,21 @@ import com.firebase.ui.auth.ui.FlowParameters; public class PreambleHandler { + private static final String BTN_TARGET = "%BTN%"; private static final String TOS_TARGET = "%TOS%"; private static final String PP_TARGET = "%PP%"; private final Context mContext; private final FlowParameters mFlowParameters; + private final int mButtonText; private final ForegroundColorSpan mLinkSpan; private SpannableStringBuilder mBuilder; - public PreambleHandler(Context context, FlowParameters parameters) { + public PreambleHandler(Context context, FlowParameters parameters, @StringRes int buttonText) { mContext = context; mFlowParameters = parameters; + mButtonText = buttonText; mLinkSpan = new ForegroundColorSpan(ContextCompat.getColor(mContext, R.color.linkColor)); setupCreateAccountPreamble(); @@ -51,11 +54,20 @@ private void setupCreateAccountPreamble() { mContext.getResources().getStringArray(R.array.create_account_preamble); mBuilder = new SpannableStringBuilder(preambles[preambleType]); - replaceTarget(TOS_TARGET, R.string.terms_of_service, mFlowParameters.termsOfServiceUrl); - replaceTarget(PP_TARGET, R.string.privacy_policy, mFlowParameters.privacyPolicyUrl); + replaceTarget(BTN_TARGET, mButtonText); + replaceUrlTarget(TOS_TARGET, R.string.terms_of_service, mFlowParameters.termsOfServiceUrl); + replaceUrlTarget(PP_TARGET, R.string.privacy_policy, mFlowParameters.privacyPolicyUrl); } - private void replaceTarget(String target, @StringRes int replacementRes, String url) { + private void replaceTarget(String target, @StringRes int replacementRes) { + int targetIndex = mBuilder.toString().indexOf(target); + if (targetIndex != -1) { + String replacement = mContext.getString(replacementRes); + mBuilder.replace(targetIndex, targetIndex + target.length(), replacement); + } + } + + private void replaceUrlTarget(String target, @StringRes int replacementRes, String url) { int targetIndex = mBuilder.toString().indexOf(target); if (targetIndex != -1) { String replacement = mContext.getString(replacementRes); diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/email/RegisterEmailFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/email/RegisterEmailFragment.java index dc9c8cce5..29f1e5dbd 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/email/RegisterEmailFragment.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/email/RegisterEmailFragment.java @@ -10,7 +10,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.EditText; -import android.widget.TextView; import android.widget.Toast; import com.firebase.ui.auth.IdpResponse; @@ -21,6 +20,7 @@ import com.firebase.ui.auth.ui.FragmentBase; import com.firebase.ui.auth.ui.ImeHelper; import com.firebase.ui.auth.ui.TaskFailureLogger; +import com.firebase.ui.auth.ui.TermsTextView; import com.firebase.ui.auth.ui.User; import com.firebase.ui.auth.ui.accountlink.WelcomeBackIdpPrompt; import com.firebase.ui.auth.ui.accountlink.WelcomeBackPasswordPrompt; @@ -52,7 +52,7 @@ public class RegisterEmailFragment extends FragmentBase implements private EditText mEmailEditText; private EditText mNameEditText; private EditText mPasswordEditText; - private TextView mAgreementText; + private TermsTextView mAgreementText; private TextInputLayout mEmailInput; private TextInputLayout mPasswordInput; @@ -95,7 +95,7 @@ public View onCreateView(LayoutInflater inflater, mEmailEditText = (EditText) v.findViewById(R.id.email); mNameEditText = (EditText) v.findViewById(R.id.name); mPasswordEditText = (EditText) v.findViewById(R.id.password); - mAgreementText = (TextView) v.findViewById(R.id.create_account_text); + mAgreementText = (TermsTextView) v.findViewById(R.id.create_account_text); mEmailInput = (TextInputLayout) v.findViewById(R.id.email_layout); mPasswordInput = (TextInputLayout) v.findViewById(R.id.password_layout); @@ -157,7 +157,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { getActivity().setTitle(R.string.title_register_email); mSaveSmartLock = mHelper.getSaveSmartLockInstance(getActivity()); - new PreambleHandler(getContext(), mHelper.getFlowParams()).setPreamble(mAgreementText); + mAgreementText.showTerms(mHelper.getFlowParams(), R.string.button_text_save); } @Override @@ -271,8 +271,11 @@ public void onSuccess(String provider) { .show(); if (provider == null) { - throw new IllegalStateException( - "User has no providers even though we got a FirebaseAuthUserCollisionException"); + String msg = + "User has no providers even though " + + "we got a " + + "FirebaseAuthUserCollisionException"; + throw new IllegalStateException(msg); } else if (EmailAuthProvider.PROVIDER_ID.equalsIgnoreCase( provider)) { getActivity().startActivityForResult( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java index baab9072b..de617fd23 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java +++ b/auth/src/main/java/com/firebase/ui/auth/ui/idp/AuthMethodPickerActivity.java @@ -34,6 +34,7 @@ import com.firebase.ui.auth.provider.GoogleProvider; import com.firebase.ui.auth.provider.IdpProvider; import com.firebase.ui.auth.provider.IdpProvider.IdpCallback; +import com.firebase.ui.auth.provider.PhoneProvider; import com.firebase.ui.auth.provider.Provider; import com.firebase.ui.auth.provider.TwitterProvider; import com.firebase.ui.auth.ui.AppCompatBase; @@ -41,6 +42,7 @@ import com.firebase.ui.auth.ui.FlowParameters; import com.firebase.ui.auth.ui.TaskFailureLogger; import com.firebase.ui.auth.ui.email.RegisterEmailActivity; +import com.firebase.ui.auth.ui.phone.PhoneVerificationActivity; import com.firebase.ui.auth.util.signincontainer.SaveSmartLock; import com.google.firebase.auth.AuthCredential; @@ -51,11 +53,13 @@ * Presents the list of authentication options for this app to the user. If an * identity provider option is selected, a {@link CredentialSignInHandler} * is launched to manage the IDP-specific sign-in flow. If email authentication is chosen, - * the {@link RegisterEmailActivity} is started. + * the {@link RegisterEmailActivity} is started. if phone authentication is chosen, the + * {@link com.firebase.ui.auth.ui.phone.PhoneVerificationActivity} is started. */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public class AuthMethodPickerActivity extends AppCompatBase implements IdpCallback { private static final String TAG = "AuthMethodPicker"; + private static final int RC_ACCOUNT_LINK = 3; private List mProviders; @@ -100,6 +104,9 @@ private void populateIdpList(List providers) { case AuthUI.EMAIL_PROVIDER: mProviders.add(new EmailProvider(this, mActivityHelper)); break; + case AuthUI.PHONE_VERIFICATION_PROVIDER: + mProviders.add(new PhoneProvider(this, mActivityHelper)); + break; default: Log.e(TAG, "Encountered unknown provider parcel with type: " + idpConfig.getProviderId()); @@ -145,7 +152,9 @@ public void onSuccess(final IdpResponse response) { mActivityHelper.getFirebaseAuth() .signInWithCredential(credential) .addOnFailureListener( - new TaskFailureLogger(TAG, "Firebase sign in with credential " + credential.getProvider() + " unsuccessful. Visit https://console.firebase.google.com to enable it.")) + new TaskFailureLogger(TAG, "Firebase sign in with credential " + + credential.getProvider() + " unsuccessful. " + + "Visit https://console.firebase.google.com to enable it.")) .addOnCompleteListener(new CredentialSignInHandler( this, mActivityHelper, diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/BucketedTextChangeListener.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/BucketedTextChangeListener.java new file mode 100644 index 000000000..0de8728c4 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/BucketedTextChangeListener.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ + +package com.firebase.ui.auth.ui.phone; + +import android.annotation.SuppressLint; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.widget.EditText; + +import java.util.Collections; + +/** + * Listens for changes to a text field that has hyphens and replaces with the characted being + * typed + * ------ + * 7----- + * 76---- + * 764--- + * 7641-- + * 76417- + * 764176 + */ +final class BucketedTextChangeListener implements TextWatcher { + private final EditText editText; + private final ContentChangeCallback callback; + private final String[] postFixes; + private final String placeHolder; + private final int expectedContentLength; + + public BucketedTextChangeListener(EditText editText, int expectedContentLength, String + placeHolder, ContentChangeCallback callback) { + this.editText = editText; + this.expectedContentLength = expectedContentLength; + this.postFixes = generatePostfixArray(placeHolder, expectedContentLength); + this.callback = callback; + this.placeHolder = placeHolder; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @SuppressLint("SetTextI18n") + @Override + public void onTextChanged(CharSequence s, int ignoredParam1, int ignoredParam2, int + ignoredParam3) { + // The listener is expected to be used in conjunction with the SpacedEditText. + + // Approach + // 1) Strip all spaces and hyphens introduced by the SET for aesthetics + final String numericContents = s.toString().replaceAll(" ", "").replaceAll(placeHolder, ""); + + // 2) Trim the content to acceptable length. + final int enteredContentLength = Math.min(numericContents.length(), expectedContentLength); + final String enteredContent = numericContents.substring(0, enteredContentLength); + + // 3) Reset the text to be the content + required hyphens. The SET automatically inserts + // spaces requires for aesthetics. This requires removing and reseting the listener to + // avoid recursion. + editText.removeTextChangedListener(this); + editText.setText(enteredContent + postFixes[expectedContentLength - enteredContentLength]); + editText.setSelection(enteredContentLength); + editText.addTextChangedListener(this); + + // 4) Callback listeners waiting on content to be of expected length + if (enteredContentLength == expectedContentLength && callback != null) { + callback.whileComplete(); + } else if (callback != null) { + callback.whileIncomplete(); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + + /** + * {@link #generatePostfixArray(CharSequence, int)} with params ("-", 6) returns + * {"", "-", "--", "---", "----", "-----", "------"} + * + * @param repeatableChar + * @param length + * @return + */ + private String[] generatePostfixArray(CharSequence repeatableChar, int length) { + final String[] ret = new String[length + 1]; + + for (int i = 0; i <= length; i++) { + ret[i] = TextUtils.join("", Collections.nCopies(i, repeatableChar)); + } + + return ret; + } + + interface ContentChangeCallback { + /** + * Idempotent function invoked by the listener when the edit text changes and is of + * expected length + */ + void whileComplete(); + + /** + * Idempotent function invoked by the listener when the edit text changes and is not of + * expected length + */ + void whileIncomplete(); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CompletableProgressDialog.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CompletableProgressDialog.java new file mode 100644 index 000000000..66fc89e4c --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CompletableProgressDialog.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.phone; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.firebase.ui.auth.R; + +public final class CompletableProgressDialog extends ProgressDialog { + private ProgressBar mProgress; + private TextView mMessageView; + private CharSequence mMessage; + private ImageView mSuccessImage; + + public CompletableProgressDialog(Context context) { + super(context); + } + + public CompletableProgressDialog(Context context, int theme) { + super(context, theme); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.phone_progress_dialog); + + mProgress = (ProgressBar) findViewById(R.id.progress_bar); + mMessageView = (TextView) findViewById(R.id.progress_msg); + mSuccessImage = (ImageView) findViewById(R.id.progress_success_imaage); + + if (mMessage != null) { + setMessage(mMessage); + } + } + + @Override + public void show() { + super.show(); + } + + public void complete(String msg) { + setMessage(msg); + mProgress.setVisibility(View.GONE); + mSuccessImage.setVisibility(View.VISIBLE); + } + + @Override + public void setMessage(CharSequence message) { + if (mProgress != null) { + mMessageView.setText(message); + } else { + mMessage = message; + } + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryInfo.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryInfo.java new file mode 100644 index 000000000..f514e1057 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + * + */ +package com.firebase.ui.auth.ui.phone; + +import java.text.Collator; +import java.util.Locale; + +final class CountryInfo implements Comparable { + private final Collator collator; + public final Locale locale; + public final int countryCode; + + public CountryInfo(Locale locale, int countryCode) { + collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.PRIMARY); + + this.locale = locale; + this.countryCode = countryCode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final CountryInfo that = (CountryInfo) o; + + if (countryCode != that.countryCode) return false; + return locale != null ? locale.equals(that.locale) : that.locale == null; + } + + @Override + public int hashCode() { + int result = locale != null ? locale.hashCode() : 0; + result = 31 * result + countryCode; + return result; + } + + @Override + public String toString() { + return localeToEmoji(locale) + " " + this.locale.getDisplayCountry() + " +" + countryCode; + } + + public static String localeToEmoji(Locale locale) { + String countryCode = locale.getCountry(); + // 0x41 is Letter A + // 0x1F1E6 is Regional Indicator Symbol Letter A + // Example : + // firstLetter U => 20 + 0x1F1E6 + // secondLetter S => 18 + 0x1F1E6 + // See: https://en.wikipedia.org/wiki/Regional_Indicator_Symbol + int firstLetter = Character.codePointAt(countryCode, 0) - 0x41 + 0x1F1E6; + int secondLetter = Character.codePointAt(countryCode, 1) - 0x41 + 0x1F1E6; + return new String(Character.toChars(firstLetter)) + new String(Character.toChars + (secondLetter)); + } + + @Override + public int compareTo(CountryInfo info) { + return collator.compare(this.locale.getDisplayCountry(), info.locale.getDisplayCountry()); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListAdapter.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListAdapter.java new file mode 100644 index 000000000..505896928 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListAdapter.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ + +package com.firebase.ui.auth.ui.phone; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.SectionIndexer; + +import com.firebase.ui.auth.R; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +/* + * Array adapter used to display a list of countries with section indices. + */ +final class CountryListAdapter extends ArrayAdapter implements SectionIndexer { + private final HashMap alphaIndex = new LinkedHashMap<>(); + private final HashMap countryPosition = new LinkedHashMap<>(); + private String[] sections; + + public CountryListAdapter(Context context) { + super(context, R.layout.dgts__country_row, android.R.id.text1); + } + + // The list of countries should be sorted using locale-sensitive string comparison + public void setData(List countries) { + // Create index and add entries to adapter + int index = 0; + for (CountryInfo countryInfo : countries) { + final String key = countryInfo.locale.getDisplayCountry().substring(0, 1).toUpperCase + (Locale.getDefault()); + + if (!alphaIndex.containsKey(key)) { + alphaIndex.put(key, index); + } + countryPosition.put(countryInfo.locale.getDisplayCountry(), index); + + index++; + add(countryInfo); + } + + sections = new String[alphaIndex.size()]; + alphaIndex.keySet().toArray(sections); + + notifyDataSetChanged(); + } + + @Override + public Object[] getSections() { + return sections; + } + + @Override + public int getPositionForSection(int index) { + if (sections == null) { + return 0; + } + + // Check index bounds + if (index <= 0) { + return 0; + } + if (index >= sections.length) { + index = sections.length - 1; + } + + // Return the position + return alphaIndex.get(sections[index]); + } + + @Override + public int getSectionForPosition(int position) { + return 0; + } + + public int getPositionForCountry(String country) { + final Integer position = countryPosition.get(country); + return position == null ? 0 : position; + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListLoadTask.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListLoadTask.java new file mode 100644 index 000000000..3c3c3f603 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListLoadTask.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ + +package com.firebase.ui.auth.ui.phone; + +import android.os.AsyncTask; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +// We need to move away from ListView and AsyncTask in the future and use (say) +// RecyclerView and Task/ThreadPoolExecutor . +final class CountryListLoadTask extends AsyncTask> { + private static final int MAX_COUNTRIES = 291; + + private final Listener listener; + + public CountryListLoadTask(Listener listener) { + this.listener = listener; + } + + @Override + protected List doInBackground(Void... params) { + final List countryInfoList = new ArrayList<>(MAX_COUNTRIES); + countryInfoList.add(new CountryInfo(new Locale("", "AF"), 93)); + countryInfoList.add(new CountryInfo(new Locale("", "AX"), 358)); + countryInfoList.add(new CountryInfo(new Locale("", "AL"), 355)); + countryInfoList.add(new CountryInfo(new Locale("", "DZ"), 213)); + countryInfoList.add(new CountryInfo(new Locale("", "AS"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "AD"), 376)); + countryInfoList.add(new CountryInfo(new Locale("", "AO"), 244)); + countryInfoList.add(new CountryInfo(new Locale("", "AI"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "AG"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "AR"), 54)); + countryInfoList.add(new CountryInfo(new Locale("", "AM"), 374)); + countryInfoList.add(new CountryInfo(new Locale("", "AW"), 297)); + countryInfoList.add(new CountryInfo(new Locale("", "AC"), 247)); + countryInfoList.add(new CountryInfo(new Locale("", "AU"), 61)); + countryInfoList.add(new CountryInfo(new Locale("", "AT"), 43)); + countryInfoList.add(new CountryInfo(new Locale("", "AZ"), 994)); + countryInfoList.add(new CountryInfo(new Locale("", "BS"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "BH"), 973)); + countryInfoList.add(new CountryInfo(new Locale("", "BD"), 880)); + countryInfoList.add(new CountryInfo(new Locale("", "BB"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "BY"), 375)); + countryInfoList.add(new CountryInfo(new Locale("", "BE"), 32)); + countryInfoList.add(new CountryInfo(new Locale("", "BZ"), 501)); + countryInfoList.add(new CountryInfo(new Locale("", "BJ"), 229)); + countryInfoList.add(new CountryInfo(new Locale("", "BM"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "BT"), 975)); + countryInfoList.add(new CountryInfo(new Locale("", "BO"), 591)); + countryInfoList.add(new CountryInfo(new Locale("", "BA"), 387)); + countryInfoList.add(new CountryInfo(new Locale("", "BW"), 267)); + countryInfoList.add(new CountryInfo(new Locale("", "BR"), 55)); + countryInfoList.add(new CountryInfo(new Locale("", "IO"), 246)); + countryInfoList.add(new CountryInfo(new Locale("", "VG"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "BN"), 673)); + countryInfoList.add(new CountryInfo(new Locale("", "BG"), 359)); + countryInfoList.add(new CountryInfo(new Locale("", "BF"), 226)); + countryInfoList.add(new CountryInfo(new Locale("", "BI"), 257)); + countryInfoList.add(new CountryInfo(new Locale("", "KH"), 855)); + countryInfoList.add(new CountryInfo(new Locale("", "CM"), 237)); + countryInfoList.add(new CountryInfo(new Locale("", "CA"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "CV"), 238)); + countryInfoList.add(new CountryInfo(new Locale("", "BQ"), 599)); + countryInfoList.add(new CountryInfo(new Locale("", "KY"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "CF"), 236)); + countryInfoList.add(new CountryInfo(new Locale("", "TD"), 235)); + countryInfoList.add(new CountryInfo(new Locale("", "CL"), 56)); + countryInfoList.add(new CountryInfo(new Locale("", "CN"), 86)); + countryInfoList.add(new CountryInfo(new Locale("", "CX"), 61)); + countryInfoList.add(new CountryInfo(new Locale("", "CC"), 61)); + countryInfoList.add(new CountryInfo(new Locale("", "CO"), 57)); + countryInfoList.add(new CountryInfo(new Locale("", "KM"), 269)); + countryInfoList.add(new CountryInfo(new Locale("", "CD"), 243)); + countryInfoList.add(new CountryInfo(new Locale("", "CG"), 242)); + countryInfoList.add(new CountryInfo(new Locale("", "CK"), 682)); + countryInfoList.add(new CountryInfo(new Locale("", "CR"), 506)); + countryInfoList.add(new CountryInfo(new Locale("", "CI"), 225)); + countryInfoList.add(new CountryInfo(new Locale("", "HR"), 385)); + countryInfoList.add(new CountryInfo(new Locale("", "CU"), 53)); + countryInfoList.add(new CountryInfo(new Locale("", "CW"), 599)); + countryInfoList.add(new CountryInfo(new Locale("", "CY"), 357)); + countryInfoList.add(new CountryInfo(new Locale("", "CZ"), 420)); + countryInfoList.add(new CountryInfo(new Locale("", "DK"), 45)); + countryInfoList.add(new CountryInfo(new Locale("", "DJ"), 253)); + countryInfoList.add(new CountryInfo(new Locale("", "DM"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "DO"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "TL"), 670)); + countryInfoList.add(new CountryInfo(new Locale("", "EC"), 593)); + countryInfoList.add(new CountryInfo(new Locale("", "EG"), 20)); + countryInfoList.add(new CountryInfo(new Locale("", "SV"), 503)); + countryInfoList.add(new CountryInfo(new Locale("", "GQ"), 240)); + countryInfoList.add(new CountryInfo(new Locale("", "ER"), 291)); + countryInfoList.add(new CountryInfo(new Locale("", "EE"), 372)); + countryInfoList.add(new CountryInfo(new Locale("", "ET"), 251)); + countryInfoList.add(new CountryInfo(new Locale("", "FK"), 500)); + countryInfoList.add(new CountryInfo(new Locale("", "FO"), 298)); + countryInfoList.add(new CountryInfo(new Locale("", "FJ"), 679)); + countryInfoList.add(new CountryInfo(new Locale("", "FI"), 358)); + countryInfoList.add(new CountryInfo(new Locale("", "FR"), 33)); + countryInfoList.add(new CountryInfo(new Locale("", "GF"), 594)); + countryInfoList.add(new CountryInfo(new Locale("", "PF"), 689)); + countryInfoList.add(new CountryInfo(new Locale("", "GA"), 241)); + countryInfoList.add(new CountryInfo(new Locale("", "GM"), 220)); + countryInfoList.add(new CountryInfo(new Locale("", "GE"), 995)); + countryInfoList.add(new CountryInfo(new Locale("", "DE"), 49)); + countryInfoList.add(new CountryInfo(new Locale("", "GH"), 233)); + countryInfoList.add(new CountryInfo(new Locale("", "GI"), 350)); + countryInfoList.add(new CountryInfo(new Locale("", "GR"), 30)); + countryInfoList.add(new CountryInfo(new Locale("", "GL"), 299)); + countryInfoList.add(new CountryInfo(new Locale("", "GD"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "GP"), 590)); + countryInfoList.add(new CountryInfo(new Locale("", "GU"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "GT"), 502)); + countryInfoList.add(new CountryInfo(new Locale("", "GG"), 44)); + countryInfoList.add(new CountryInfo(new Locale("", "GN"), 224)); + countryInfoList.add(new CountryInfo(new Locale("", "GW"), 245)); + countryInfoList.add(new CountryInfo(new Locale("", "GY"), 592)); + countryInfoList.add(new CountryInfo(new Locale("", "HT"), 509)); + countryInfoList.add(new CountryInfo(new Locale("", "HM"), 672)); + countryInfoList.add(new CountryInfo(new Locale("", "HN"), 504)); + countryInfoList.add(new CountryInfo(new Locale("", "HK"), 852)); + countryInfoList.add(new CountryInfo(new Locale("", "HU"), 36)); + countryInfoList.add(new CountryInfo(new Locale("", "IS"), 354)); + countryInfoList.add(new CountryInfo(new Locale("", "IN"), 91)); + countryInfoList.add(new CountryInfo(new Locale("", "ID"), 62)); + countryInfoList.add(new CountryInfo(new Locale("", "IR"), 98)); + countryInfoList.add(new CountryInfo(new Locale("", "IQ"), 964)); + countryInfoList.add(new CountryInfo(new Locale("", "IE"), 353)); + countryInfoList.add(new CountryInfo(new Locale("", "IM"), 44)); + countryInfoList.add(new CountryInfo(new Locale("", "IL"), 972)); + countryInfoList.add(new CountryInfo(new Locale("", "IT"), 39)); + countryInfoList.add(new CountryInfo(new Locale("", "JM"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "JP"), 81)); + countryInfoList.add(new CountryInfo(new Locale("", "JE"), 44)); + countryInfoList.add(new CountryInfo(new Locale("", "JO"), 962)); + countryInfoList.add(new CountryInfo(new Locale("", "KZ"), 7)); + countryInfoList.add(new CountryInfo(new Locale("", "KE"), 254)); + countryInfoList.add(new CountryInfo(new Locale("", "KI"), 686)); + countryInfoList.add(new CountryInfo(new Locale("", "XK"), 381)); + countryInfoList.add(new CountryInfo(new Locale("", "KW"), 965)); + countryInfoList.add(new CountryInfo(new Locale("", "KG"), 996)); + countryInfoList.add(new CountryInfo(new Locale("", "LA"), 856)); + countryInfoList.add(new CountryInfo(new Locale("", "LV"), 371)); + countryInfoList.add(new CountryInfo(new Locale("", "LB"), 961)); + countryInfoList.add(new CountryInfo(new Locale("", "LS"), 266)); + countryInfoList.add(new CountryInfo(new Locale("", "LR"), 231)); + countryInfoList.add(new CountryInfo(new Locale("", "LY"), 218)); + countryInfoList.add(new CountryInfo(new Locale("", "LI"), 423)); + countryInfoList.add(new CountryInfo(new Locale("", "LT"), 370)); + countryInfoList.add(new CountryInfo(new Locale("", "LU"), 352)); + countryInfoList.add(new CountryInfo(new Locale("", "MO"), 853)); + countryInfoList.add(new CountryInfo(new Locale("", "MK"), 389)); + countryInfoList.add(new CountryInfo(new Locale("", "MG"), 261)); + countryInfoList.add(new CountryInfo(new Locale("", "MW"), 265)); + countryInfoList.add(new CountryInfo(new Locale("", "MY"), 60)); + countryInfoList.add(new CountryInfo(new Locale("", "MV"), 960)); + countryInfoList.add(new CountryInfo(new Locale("", "ML"), 223)); + countryInfoList.add(new CountryInfo(new Locale("", "MT"), 356)); + countryInfoList.add(new CountryInfo(new Locale("", "MH"), 692)); + countryInfoList.add(new CountryInfo(new Locale("", "MQ"), 596)); + countryInfoList.add(new CountryInfo(new Locale("", "MR"), 222)); + countryInfoList.add(new CountryInfo(new Locale("", "MU"), 230)); + countryInfoList.add(new CountryInfo(new Locale("", "YT"), 262)); + countryInfoList.add(new CountryInfo(new Locale("", "MX"), 52)); + countryInfoList.add(new CountryInfo(new Locale("", "FM"), 691)); + countryInfoList.add(new CountryInfo(new Locale("", "MD"), 373)); + countryInfoList.add(new CountryInfo(new Locale("", "MC"), 377)); + countryInfoList.add(new CountryInfo(new Locale("", "MN"), 976)); + countryInfoList.add(new CountryInfo(new Locale("", "ME"), 382)); + countryInfoList.add(new CountryInfo(new Locale("", "MS"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "MA"), 212)); + countryInfoList.add(new CountryInfo(new Locale("", "MZ"), 258)); + countryInfoList.add(new CountryInfo(new Locale("", "MM"), 95)); + countryInfoList.add(new CountryInfo(new Locale("", "NA"), 264)); + countryInfoList.add(new CountryInfo(new Locale("", "NR"), 674)); + countryInfoList.add(new CountryInfo(new Locale("", "NP"), 977)); + countryInfoList.add(new CountryInfo(new Locale("", "NL"), 31)); + countryInfoList.add(new CountryInfo(new Locale("", "NC"), 687)); + countryInfoList.add(new CountryInfo(new Locale("", "NZ"), 64)); + countryInfoList.add(new CountryInfo(new Locale("", "NI"), 505)); + countryInfoList.add(new CountryInfo(new Locale("", "NE"), 227)); + countryInfoList.add(new CountryInfo(new Locale("", "NG"), 234)); + countryInfoList.add(new CountryInfo(new Locale("", "NU"), 683)); + countryInfoList.add(new CountryInfo(new Locale("", "NF"), 672)); + countryInfoList.add(new CountryInfo(new Locale("", "KP"), 850)); + countryInfoList.add(new CountryInfo(new Locale("", "MP"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "NO"), 47)); + countryInfoList.add(new CountryInfo(new Locale("", "OM"), 968)); + countryInfoList.add(new CountryInfo(new Locale("", "PK"), 92)); + countryInfoList.add(new CountryInfo(new Locale("", "PW"), 680)); + countryInfoList.add(new CountryInfo(new Locale("", "PS"), 970)); + countryInfoList.add(new CountryInfo(new Locale("", "PA"), 507)); + countryInfoList.add(new CountryInfo(new Locale("", "PG"), 675)); + countryInfoList.add(new CountryInfo(new Locale("", "PY"), 595)); + countryInfoList.add(new CountryInfo(new Locale("", "PE"), 51)); + countryInfoList.add(new CountryInfo(new Locale("", "PH"), 63)); + countryInfoList.add(new CountryInfo(new Locale("", "PL"), 48)); + countryInfoList.add(new CountryInfo(new Locale("", "PT"), 351)); + countryInfoList.add(new CountryInfo(new Locale("", "PR"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "QA"), 974)); + countryInfoList.add(new CountryInfo(new Locale("", "RE"), 262)); + countryInfoList.add(new CountryInfo(new Locale("", "RO"), 40)); + countryInfoList.add(new CountryInfo(new Locale("", "RU"), 7)); + countryInfoList.add(new CountryInfo(new Locale("", "RW"), 250)); + countryInfoList.add(new CountryInfo(new Locale("", "BL"), 590)); + countryInfoList.add(new CountryInfo(new Locale("", "SH"), 290)); + countryInfoList.add(new CountryInfo(new Locale("", "KN"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "LC"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "MF"), 590)); + countryInfoList.add(new CountryInfo(new Locale("", "PM"), 508)); + countryInfoList.add(new CountryInfo(new Locale("", "VC"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "WS"), 685)); + countryInfoList.add(new CountryInfo(new Locale("", "SM"), 378)); + countryInfoList.add(new CountryInfo(new Locale("", "ST"), 239)); + countryInfoList.add(new CountryInfo(new Locale("", "SA"), 966)); + countryInfoList.add(new CountryInfo(new Locale("", "SN"), 221)); + countryInfoList.add(new CountryInfo(new Locale("", "RS"), 381)); + countryInfoList.add(new CountryInfo(new Locale("", "SC"), 248)); + countryInfoList.add(new CountryInfo(new Locale("", "SL"), 232)); + countryInfoList.add(new CountryInfo(new Locale("", "SG"), 65)); + countryInfoList.add(new CountryInfo(new Locale("", "SX"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "SK"), 421)); + countryInfoList.add(new CountryInfo(new Locale("", "SI"), 386)); + countryInfoList.add(new CountryInfo(new Locale("", "SB"), 677)); + countryInfoList.add(new CountryInfo(new Locale("", "SO"), 252)); + countryInfoList.add(new CountryInfo(new Locale("", "ZA"), 27)); + countryInfoList.add(new CountryInfo(new Locale("", "GS"), 500)); + countryInfoList.add(new CountryInfo(new Locale("", "KR"), 82)); + countryInfoList.add(new CountryInfo(new Locale("", "SS"), 211)); + countryInfoList.add(new CountryInfo(new Locale("", "ES"), 34)); + countryInfoList.add(new CountryInfo(new Locale("", "LK"), 94)); + countryInfoList.add(new CountryInfo(new Locale("", "SD"), 249)); + countryInfoList.add(new CountryInfo(new Locale("", "SR"), 597)); + countryInfoList.add(new CountryInfo(new Locale("", "SJ"), 47)); + countryInfoList.add(new CountryInfo(new Locale("", "SZ"), 268)); + countryInfoList.add(new CountryInfo(new Locale("", "SE"), 46)); + countryInfoList.add(new CountryInfo(new Locale("", "CH"), 41)); + countryInfoList.add(new CountryInfo(new Locale("", "SY"), 963)); + countryInfoList.add(new CountryInfo(new Locale("", "TW"), 886)); + countryInfoList.add(new CountryInfo(new Locale("", "TJ"), 992)); + countryInfoList.add(new CountryInfo(new Locale("", "TZ"), 255)); + countryInfoList.add(new CountryInfo(new Locale("", "TH"), 66)); + countryInfoList.add(new CountryInfo(new Locale("", "TG"), 228)); + countryInfoList.add(new CountryInfo(new Locale("", "TK"), 690)); + countryInfoList.add(new CountryInfo(new Locale("", "TO"), 676)); + countryInfoList.add(new CountryInfo(new Locale("", "TT"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "TN"), 216)); + countryInfoList.add(new CountryInfo(new Locale("", "TR"), 90)); + countryInfoList.add(new CountryInfo(new Locale("", "TM"), 993)); + countryInfoList.add(new CountryInfo(new Locale("", "TC"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "TV"), 688)); + countryInfoList.add(new CountryInfo(new Locale("", "VI"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "UG"), 256)); + countryInfoList.add(new CountryInfo(new Locale("", "UA"), 380)); + countryInfoList.add(new CountryInfo(new Locale("", "AE"), 971)); + countryInfoList.add(new CountryInfo(new Locale("", "GB"), 44)); + countryInfoList.add(new CountryInfo(new Locale("", "US"), 1)); + countryInfoList.add(new CountryInfo(new Locale("", "UY"), 598)); + countryInfoList.add(new CountryInfo(new Locale("", "UZ"), 998)); + countryInfoList.add(new CountryInfo(new Locale("", "VU"), 678)); + countryInfoList.add(new CountryInfo(new Locale("", "VA"), 379)); + countryInfoList.add(new CountryInfo(new Locale("", "VE"), 58)); + countryInfoList.add(new CountryInfo(new Locale("", "VN"), 84)); + countryInfoList.add(new CountryInfo(new Locale("", "WF"), 681)); + countryInfoList.add(new CountryInfo(new Locale("", "EH"), 212)); + countryInfoList.add(new CountryInfo(new Locale("", "YE"), 967)); + countryInfoList.add(new CountryInfo(new Locale("", "ZM"), 260)); + countryInfoList.add(new CountryInfo(new Locale("", "ZW"), 263)); + Collections.sort(countryInfoList); + return countryInfoList; + } + + @Override + protected void onPostExecute(List result) { + if (listener != null) { + listener.onLoadComplete(result); + } + } + + public interface Listener { + void onLoadComplete(List result); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListSpinner.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListSpinner.java new file mode 100644 index 000000000..dda0e7c16 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CountryListSpinner.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ +package com.firebase.ui.auth.ui.phone; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.AppCompatEditText; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ListView; + +import java.util.List; +import java.util.Locale; + +public final class CountryListSpinner extends AppCompatEditText implements + View.OnClickListener, CountryListLoadTask.Listener { + private String textFormat; + private DialogPopup dialogPopup; + private CountryListAdapter countryListAdapter; + private OnClickListener listener; + private String selectedCountryName; + + public CountryListSpinner(Context context) { + this(context, null, android.R.attr.spinnerStyle); + } + + public CountryListSpinner(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.spinnerStyle); + } + + public CountryListSpinner(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + @VisibleForTesting + void setDialogPopup(DialogPopup dialog) { + this.dialogPopup = dialog; + } + + private void init() { + super.setOnClickListener(this); + + countryListAdapter = new CountryListAdapter(getContext()); + dialogPopup = new DialogPopup(countryListAdapter); + textFormat = "%1$s +%2$d"; + selectedCountryName = ""; + final CountryInfo countryInfo = PhoneNumberUtils.getCurrentCountryInfo(getContext()); + setSpinnerText(countryInfo.countryCode, countryInfo.locale); + } + + private void setSpinnerText(int countryCode, Locale locale) { + setText(String.format(textFormat, CountryInfo.localeToEmoji(locale), countryCode)); + setTag(new CountryInfo(locale, countryCode)); + } + + public void setSelectedForCountry(final Locale locale, String countryCode) { + final String countryName = locale.getDisplayName(); + if (!TextUtils.isEmpty(countryName) && !TextUtils.isEmpty(countryCode)) { + selectedCountryName = countryName; + setSpinnerText(Integer.parseInt(countryCode), locale); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (dialogPopup.isShowing()) { + dialogPopup.dismiss(); + } + } + + @Override + public void setOnClickListener(OnClickListener l) { + listener = l; + } + + @Override + public void onClick(View view) { + if (countryListAdapter.getCount() == 0) { + loadCountryList(); + } else { + dialogPopup.show(countryListAdapter.getPositionForCountry(selectedCountryName)); + } + hideKeyboard(getContext(), CountryListSpinner.this); + executeUserClickListener(view); + } + + private void loadCountryList() { + new CountryListLoadTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void executeUserClickListener(View view) { + if (listener != null) { + listener.onClick(view); + } + } + + @Override + public void onLoadComplete(List result) { + countryListAdapter.setData(result); + dialogPopup.show(countryListAdapter.getPositionForCountry(selectedCountryName)); + } + + public class DialogPopup implements DialogInterface.OnClickListener { + //Delay for postDelayed to set selection without showing the scroll animation + private static final long DELAY_MILLIS = 10L; + private final CountryListAdapter listAdapter; + private AlertDialog dialog; + + DialogPopup(CountryListAdapter adapter) { + listAdapter = adapter; + } + + public void dismiss() { + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + } + + public boolean isShowing() { + return dialog != null && dialog.isShowing(); + } + + public void show(final int selected) { + if (listAdapter == null) { + return; + } + + final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + dialog = builder.setSingleChoiceItems(listAdapter, 0, this).create(); + dialog.setCanceledOnTouchOutside(true); + final ListView listView = dialog.getListView(); + listView.setFastScrollEnabled(true); + listView.setScrollbarFadingEnabled(false); + listView.postDelayed(new Runnable() { + @Override + public void run() { + listView.setSelection(selected); + } + }, DELAY_MILLIS); + dialog.show(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final CountryInfo countryInfo = listAdapter.getItem(which); + selectedCountryName = countryInfo.locale.getDisplayCountry(); + setSpinnerText(countryInfo.countryCode, countryInfo.locale); + dismiss(); + } + } + + public static void hideKeyboard(Context context, View view) { + final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context + .INPUT_METHOD_SERVICE); + if (imm != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/CustomCountDownTimer.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CustomCountDownTimer.java new file mode 100644 index 000000000..3bed8ac85 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/CustomCountDownTimer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.phone; + +import android.os.CountDownTimer; + +abstract class CustomCountDownTimer { + private final long mMillisInFuture; + private final long mCountDownInterval; + private CountDownTimer mCountDownTimer; + + CustomCountDownTimer(long millisInFuture, long countDownInterval) { + mMillisInFuture = millisInFuture; + mCountDownInterval = countDownInterval; + mCountDownTimer = create(millisInFuture, countDownInterval); + } + + public void update(long millisInFuture) { + mCountDownTimer.cancel(); + mCountDownTimer = create(millisInFuture, mCountDownInterval); + mCountDownTimer.start(); + } + + public void renew() { + update(mMillisInFuture); + } + + CountDownTimer create(long millisInFuture, long counDownInterval) { + return new CountDownTimer(millisInFuture, counDownInterval) { + @Override + public void onTick(long millisUntilFinished) { + CustomCountDownTimer.this.onTick(millisUntilFinished); + } + + @Override + public void onFinish() { + CustomCountDownTimer.this.onFinish(); + } + }; + } + + public void cancel() { + mCountDownTimer.cancel(); + } + + public void start() { + mCountDownTimer.start(); + } + + protected abstract void onFinish(); + + protected abstract void onTick(long millisUntilFinished); +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumber.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumber.java new file mode 100644 index 000000000..90eb9eb88 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumber.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ +package com.firebase.ui.auth.ui.phone; + +import android.text.TextUtils; + +final class PhoneNumber { + private static final PhoneNumber EMPTY_PHONE_NUMBER = new PhoneNumber("", "", ""); + + private final String phoneNumber; + private final String countryIso; + private final String countryCode; + + public PhoneNumber(String phoneNumber, String countryIso, String countryCode) { + this.phoneNumber = phoneNumber; + this.countryIso = countryIso; + this.countryCode = countryCode; + } + + /** + * Returns an empty instance of this class + */ + public static PhoneNumber emptyPhone() { + return EMPTY_PHONE_NUMBER; + } + + /** + * Returns country code + */ + public String getCountryCode() { + return countryCode; + } + + /** + * Returns phone number without country code + */ + public String getPhoneNumber() { + return phoneNumber; + } + + /** + * Returns 2 char country ISO + */ + public String getCountryIso() { + return countryIso; + } + + public static boolean isValid(PhoneNumber phoneNumber) { + return phoneNumber != null && !EMPTY_PHONE_NUMBER.equals(phoneNumber) && !TextUtils + .isEmpty(phoneNumber.getPhoneNumber()) && !TextUtils.isEmpty(phoneNumber + .getCountryCode()) && !TextUtils.isEmpty(phoneNumber.getCountryIso()); + } + + public static boolean isCountryValid(PhoneNumber phoneNumber) { + return phoneNumber != null && !EMPTY_PHONE_NUMBER.equals(phoneNumber) && !TextUtils + .isEmpty(phoneNumber.getCountryCode()) && !TextUtils.isEmpty(phoneNumber + .getCountryIso()); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumberUtils.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumberUtils.java new file mode 100644 index 000000000..bd43ff9c8 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneNumberUtils.java @@ -0,0 +1,1350 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ +package com.firebase.ui.auth.ui.phone; + +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +final class PhoneNumberUtils { + private static final int DEFAULT_COUNTRY_CODE_INT = 1; + private static final String DEFAULT_COUNTRY_CODE = String.valueOf(DEFAULT_COUNTRY_CODE_INT); + private static final Locale DEFAULT_LOCALE = Locale.US; + private static final CountryInfo DEFAULT_COUNTRY = + new CountryInfo(DEFAULT_LOCALE, DEFAULT_COUNTRY_CODE_INT); + + private static final int MAX_COUNTRIES = 291; + private static final int MAX_COUNTRY_CODES = 286; + private static final int MAX_LENGTH_COUNTRY_CODE = 3; + + private static final Map> CountryCodeToRegionCodeMap; + private static final Map CountryCodeByIsoMap; + + static { + CountryCodeToRegionCodeMap = Collections.unmodifiableMap(createCountryCodeToRegionCodeMap + ()); + CountryCodeByIsoMap = Collections.unmodifiableMap(createCountryCodeByIsoMap()); + } + + /** + * This method may be used to force initialize the static members in the class. + * It is recommended to do this in the background since the HashMaps created above + * may be time consuming operations on some devices. + */ + static void load() { + } + + /** + * This method works as follow: + *

  1. When the android version is LOLLIPOP or greater, the reliable + * {{@link android.telephony.PhoneNumberUtils#formatNumberToE164}} is used to format.
  2. + *
  3. For lower versions, we construct a value with the input phone number stripped of + * non numeric characters and prefix it with a "+" and country code
  4. + *
+ + * @param phoneNumber that may or may not itself have country code + * @param countryInfo must have locale with ISO 3166 2-letter code for country + * @return + */ + @Nullable + static String formatPhoneNumber(@NonNull String phoneNumber, @NonNull CountryInfo countryInfo) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return android.telephony.PhoneNumberUtils + .formatNumberToE164(phoneNumber, countryInfo.locale.getCountry()); + } + return phoneNumber.startsWith("+") + ? phoneNumber + : ("+" + String.valueOf(countryInfo.countryCode) + + phoneNumber.replaceAll("[^\\d.]", "")); + } + + + /** + * This method uses the country returned by {@link #getCurrentCountryInfo(Context)} to + * format the phone number. Internall invokes {@link #formatPhoneNumber(String, CountryInfo)} + * @param phoneNumber that may or may not itself have country code + * @return + */ + + @Nullable + static String formatPhoneNumberUsingCurrentCountry( + @NonNull String phoneNumber, Context context) { + final CountryInfo currentCountry = PhoneNumberUtils.getCurrentCountryInfo(context); + return formatPhoneNumber(phoneNumber, currentCountry); + } + + @NonNull + static CountryInfo getCurrentCountryInfo(@NonNull Context context) { + Locale locale = getSimBasedLocale(context); + + if (locale == null) { + locale = getOSLocale(); + } + + if (locale == null) { + return DEFAULT_COUNTRY; + } + + Integer countryCode = PhoneNumberUtils.getCountryCode(locale.getCountry()); + + return countryCode == null ? DEFAULT_COUNTRY : new CountryInfo(locale, countryCode); + } + + /** + * This method should not be called on UI thread. Potentially creates a country code by iso + * map which can take long in some devices + * @param providedPhoneNumber works best when formatted as e164 + * + * @return an instance of the PhoneNumber using the SIM information + */ + + protected static PhoneNumber getPhoneNumber(@NonNull String providedPhoneNumber) { + String countryCode = DEFAULT_COUNTRY_CODE; + String countryIso = DEFAULT_LOCALE.getCountry(); + + String phoneNumber = providedPhoneNumber; + if (providedPhoneNumber.startsWith("+")) { + countryCode = countryCodeForPhoneNumber(providedPhoneNumber); + countryIso = countryIsoForCountryCode(countryCode); + phoneNumber = stripCountryCode(providedPhoneNumber, countryCode); + } + return new PhoneNumber(phoneNumber, countryIso, countryCode); + } + + private static String countryIsoForCountryCode(String countryCode) { + final List countries = CountryCodeToRegionCodeMap.get(Integer.valueOf(countryCode)); + if (countries != null) { + return countries.get(0); + } + return DEFAULT_LOCALE.getCountry(); + } + + /** + * Country code extracted using shortest matching prefix like libPhoneNumber. See: + * https://github.com/googlei18n/libphonenumber/blob/master/java/libphonenumber/src/com + * /google/i18n/phonenumbers/PhoneNumberUtil.java#L2395 + */ + private static String countryCodeForPhoneNumber(String normalizedPhoneNumber) { + final String phoneWithoutPlusPrefix = normalizedPhoneNumber + .replaceFirst("^\\+", ""); + final int numberLength = phoneWithoutPlusPrefix.length(); + + for (int i = 1; i <= MAX_LENGTH_COUNTRY_CODE && i <= numberLength; i++) { + final String potentialCountryCode = phoneWithoutPlusPrefix.substring(0, i); + final Integer countryCodeKey = Integer.valueOf(potentialCountryCode); + + if (CountryCodeToRegionCodeMap.containsKey(countryCodeKey)) { + return potentialCountryCode; + } + } + + return DEFAULT_COUNTRY_CODE; + } + + private static Map> createCountryCodeToRegionCodeMap() { + final Map> countryCodeToRegionCodeMap = new ConcurrentHashMap<> + (MAX_COUNTRY_CODES); + + ArrayList listWithRegionCode; + + listWithRegionCode = new ArrayList<>(25); + listWithRegionCode.add("US"); + listWithRegionCode.add("AG"); + listWithRegionCode.add("AI"); + listWithRegionCode.add("AS"); + listWithRegionCode.add("BB"); + listWithRegionCode.add("BM"); + listWithRegionCode.add("BS"); + listWithRegionCode.add("CA"); + listWithRegionCode.add("DM"); + listWithRegionCode.add("DO"); + listWithRegionCode.add("GD"); + listWithRegionCode.add("GU"); + listWithRegionCode.add("JM"); + listWithRegionCode.add("KN"); + listWithRegionCode.add("KY"); + listWithRegionCode.add("LC"); + listWithRegionCode.add("MP"); + listWithRegionCode.add("MS"); + listWithRegionCode.add("PR"); + listWithRegionCode.add("SX"); + listWithRegionCode.add("TC"); + listWithRegionCode.add("TT"); + listWithRegionCode.add("VC"); + listWithRegionCode.add("VG"); + listWithRegionCode.add("VI"); + countryCodeToRegionCodeMap.put(1, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("RU"); + listWithRegionCode.add("KZ"); + countryCodeToRegionCodeMap.put(7, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("EG"); + countryCodeToRegionCodeMap.put(20, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ZA"); + countryCodeToRegionCodeMap.put(27, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GR"); + countryCodeToRegionCodeMap.put(30, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NL"); + countryCodeToRegionCodeMap.put(31, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BE"); + countryCodeToRegionCodeMap.put(32, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("FR"); + countryCodeToRegionCodeMap.put(33, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ES"); + countryCodeToRegionCodeMap.put(34, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("HU"); + countryCodeToRegionCodeMap.put(36, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IT"); + countryCodeToRegionCodeMap.put(39, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("RO"); + countryCodeToRegionCodeMap.put(40, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CH"); + countryCodeToRegionCodeMap.put(41, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AT"); + countryCodeToRegionCodeMap.put(43, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(4); + listWithRegionCode.add("GB"); + listWithRegionCode.add("GG"); + listWithRegionCode.add("IM"); + listWithRegionCode.add("JE"); + countryCodeToRegionCodeMap.put(44, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("DK"); + countryCodeToRegionCodeMap.put(45, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SE"); + countryCodeToRegionCodeMap.put(46, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("NO"); + listWithRegionCode.add("SJ"); + countryCodeToRegionCodeMap.put(47, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PL"); + countryCodeToRegionCodeMap.put(48, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("DE"); + countryCodeToRegionCodeMap.put(49, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PE"); + countryCodeToRegionCodeMap.put(51, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MX"); + countryCodeToRegionCodeMap.put(52, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CU"); + countryCodeToRegionCodeMap.put(53, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AR"); + countryCodeToRegionCodeMap.put(54, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BR"); + countryCodeToRegionCodeMap.put(55, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CL"); + countryCodeToRegionCodeMap.put(56, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CO"); + countryCodeToRegionCodeMap.put(57, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("VE"); + countryCodeToRegionCodeMap.put(58, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MY"); + countryCodeToRegionCodeMap.put(60, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(3); + listWithRegionCode.add("AU"); + listWithRegionCode.add("CC"); + listWithRegionCode.add("CX"); + countryCodeToRegionCodeMap.put(61, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ID"); + countryCodeToRegionCodeMap.put(62, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PH"); + countryCodeToRegionCodeMap.put(63, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NZ"); + countryCodeToRegionCodeMap.put(64, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SG"); + countryCodeToRegionCodeMap.put(65, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TH"); + countryCodeToRegionCodeMap.put(66, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("JP"); + countryCodeToRegionCodeMap.put(81, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KR"); + countryCodeToRegionCodeMap.put(82, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("VN"); + countryCodeToRegionCodeMap.put(84, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CN"); + countryCodeToRegionCodeMap.put(86, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TR"); + countryCodeToRegionCodeMap.put(90, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IN"); + countryCodeToRegionCodeMap.put(91, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PK"); + countryCodeToRegionCodeMap.put(92, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AF"); + countryCodeToRegionCodeMap.put(93, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LK"); + countryCodeToRegionCodeMap.put(94, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MM"); + countryCodeToRegionCodeMap.put(95, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IR"); + countryCodeToRegionCodeMap.put(98, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SS"); + countryCodeToRegionCodeMap.put(211, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("MA"); + listWithRegionCode.add("EH"); + countryCodeToRegionCodeMap.put(212, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("DZ"); + countryCodeToRegionCodeMap.put(213, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TN"); + countryCodeToRegionCodeMap.put(216, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LY"); + countryCodeToRegionCodeMap.put(218, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GM"); + countryCodeToRegionCodeMap.put(220, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SN"); + countryCodeToRegionCodeMap.put(221, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MR"); + countryCodeToRegionCodeMap.put(222, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ML"); + countryCodeToRegionCodeMap.put(223, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GN"); + countryCodeToRegionCodeMap.put(224, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CI"); + countryCodeToRegionCodeMap.put(225, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BF"); + countryCodeToRegionCodeMap.put(226, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NE"); + countryCodeToRegionCodeMap.put(227, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TG"); + countryCodeToRegionCodeMap.put(228, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BJ"); + countryCodeToRegionCodeMap.put(229, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MU"); + countryCodeToRegionCodeMap.put(230, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LR"); + countryCodeToRegionCodeMap.put(231, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SL"); + countryCodeToRegionCodeMap.put(232, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GH"); + countryCodeToRegionCodeMap.put(233, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NG"); + countryCodeToRegionCodeMap.put(234, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TD"); + countryCodeToRegionCodeMap.put(235, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CF"); + countryCodeToRegionCodeMap.put(236, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CM"); + countryCodeToRegionCodeMap.put(237, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CV"); + countryCodeToRegionCodeMap.put(238, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ST"); + countryCodeToRegionCodeMap.put(239, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GQ"); + countryCodeToRegionCodeMap.put(240, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GA"); + countryCodeToRegionCodeMap.put(241, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CG"); + countryCodeToRegionCodeMap.put(242, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CD"); + countryCodeToRegionCodeMap.put(243, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AO"); + countryCodeToRegionCodeMap.put(244, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GW"); + countryCodeToRegionCodeMap.put(245, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IO"); + countryCodeToRegionCodeMap.put(246, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AC"); + countryCodeToRegionCodeMap.put(247, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SC"); + countryCodeToRegionCodeMap.put(248, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SD"); + countryCodeToRegionCodeMap.put(249, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("RW"); + countryCodeToRegionCodeMap.put(250, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ET"); + countryCodeToRegionCodeMap.put(251, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SO"); + countryCodeToRegionCodeMap.put(252, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("DJ"); + countryCodeToRegionCodeMap.put(253, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KE"); + countryCodeToRegionCodeMap.put(254, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TZ"); + countryCodeToRegionCodeMap.put(255, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("UG"); + countryCodeToRegionCodeMap.put(256, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BI"); + countryCodeToRegionCodeMap.put(257, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MZ"); + countryCodeToRegionCodeMap.put(258, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ZM"); + countryCodeToRegionCodeMap.put(260, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MG"); + countryCodeToRegionCodeMap.put(261, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("RE"); + listWithRegionCode.add("YT"); + countryCodeToRegionCodeMap.put(262, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ZW"); + countryCodeToRegionCodeMap.put(263, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NA"); + countryCodeToRegionCodeMap.put(264, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MW"); + countryCodeToRegionCodeMap.put(265, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LS"); + countryCodeToRegionCodeMap.put(266, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BW"); + countryCodeToRegionCodeMap.put(267, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SZ"); + countryCodeToRegionCodeMap.put(268, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KM"); + countryCodeToRegionCodeMap.put(269, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("SH"); + listWithRegionCode.add("TA"); + countryCodeToRegionCodeMap.put(290, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ER"); + countryCodeToRegionCodeMap.put(291, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AW"); + countryCodeToRegionCodeMap.put(297, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("FO"); + countryCodeToRegionCodeMap.put(298, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GL"); + countryCodeToRegionCodeMap.put(299, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GI"); + countryCodeToRegionCodeMap.put(350, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PT"); + countryCodeToRegionCodeMap.put(351, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LU"); + countryCodeToRegionCodeMap.put(352, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IE"); + countryCodeToRegionCodeMap.put(353, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IS"); + countryCodeToRegionCodeMap.put(354, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AL"); + countryCodeToRegionCodeMap.put(355, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MT"); + countryCodeToRegionCodeMap.put(356, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CY"); + countryCodeToRegionCodeMap.put(357, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("FI"); + listWithRegionCode.add("AX"); + countryCodeToRegionCodeMap.put(358, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BG"); + countryCodeToRegionCodeMap.put(359, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LT"); + countryCodeToRegionCodeMap.put(370, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LV"); + countryCodeToRegionCodeMap.put(371, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("EE"); + countryCodeToRegionCodeMap.put(372, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MD"); + countryCodeToRegionCodeMap.put(373, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AM"); + countryCodeToRegionCodeMap.put(374, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BY"); + countryCodeToRegionCodeMap.put(375, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AD"); + countryCodeToRegionCodeMap.put(376, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MC"); + countryCodeToRegionCodeMap.put(377, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SM"); + countryCodeToRegionCodeMap.put(378, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("VA"); + countryCodeToRegionCodeMap.put(379, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("UA"); + countryCodeToRegionCodeMap.put(380, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("RS"); + countryCodeToRegionCodeMap.put(381, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("ME"); + countryCodeToRegionCodeMap.put(382, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("HR"); + countryCodeToRegionCodeMap.put(385, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SI"); + countryCodeToRegionCodeMap.put(386, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BA"); + countryCodeToRegionCodeMap.put(387, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MK"); + countryCodeToRegionCodeMap.put(389, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CZ"); + countryCodeToRegionCodeMap.put(420, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SK"); + countryCodeToRegionCodeMap.put(421, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LI"); + countryCodeToRegionCodeMap.put(423, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("FK"); + countryCodeToRegionCodeMap.put(500, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BZ"); + countryCodeToRegionCodeMap.put(501, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GT"); + countryCodeToRegionCodeMap.put(502, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SV"); + countryCodeToRegionCodeMap.put(503, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("HN"); + countryCodeToRegionCodeMap.put(504, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NI"); + countryCodeToRegionCodeMap.put(505, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CR"); + countryCodeToRegionCodeMap.put(506, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PA"); + countryCodeToRegionCodeMap.put(507, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PM"); + countryCodeToRegionCodeMap.put(508, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("HT"); + countryCodeToRegionCodeMap.put(509, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(3); + listWithRegionCode.add("GP"); + listWithRegionCode.add("BL"); + listWithRegionCode.add("MF"); + countryCodeToRegionCodeMap.put(590, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BO"); + countryCodeToRegionCodeMap.put(591, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GY"); + countryCodeToRegionCodeMap.put(592, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("EC"); + countryCodeToRegionCodeMap.put(593, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GF"); + countryCodeToRegionCodeMap.put(594, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PY"); + countryCodeToRegionCodeMap.put(595, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MQ"); + countryCodeToRegionCodeMap.put(596, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SR"); + countryCodeToRegionCodeMap.put(597, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("UY"); + countryCodeToRegionCodeMap.put(598, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(2); + listWithRegionCode.add("CW"); + listWithRegionCode.add("BQ"); + countryCodeToRegionCodeMap.put(599, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TL"); + countryCodeToRegionCodeMap.put(670, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NF"); + countryCodeToRegionCodeMap.put(672, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BN"); + countryCodeToRegionCodeMap.put(673, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NR"); + countryCodeToRegionCodeMap.put(674, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PG"); + countryCodeToRegionCodeMap.put(675, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TO"); + countryCodeToRegionCodeMap.put(676, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SB"); + countryCodeToRegionCodeMap.put(677, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("VU"); + countryCodeToRegionCodeMap.put(678, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("FJ"); + countryCodeToRegionCodeMap.put(679, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PW"); + countryCodeToRegionCodeMap.put(680, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("WF"); + countryCodeToRegionCodeMap.put(681, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("CK"); + countryCodeToRegionCodeMap.put(682, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NU"); + countryCodeToRegionCodeMap.put(683, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("WS"); + countryCodeToRegionCodeMap.put(685, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KI"); + countryCodeToRegionCodeMap.put(686, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NC"); + countryCodeToRegionCodeMap.put(687, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TV"); + countryCodeToRegionCodeMap.put(688, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PF"); + countryCodeToRegionCodeMap.put(689, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TK"); + countryCodeToRegionCodeMap.put(690, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("FM"); + countryCodeToRegionCodeMap.put(691, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MH"); + countryCodeToRegionCodeMap.put(692, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(800, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(808, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KP"); + countryCodeToRegionCodeMap.put(850, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("HK"); + countryCodeToRegionCodeMap.put(852, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MO"); + countryCodeToRegionCodeMap.put(853, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KH"); + countryCodeToRegionCodeMap.put(855, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LA"); + countryCodeToRegionCodeMap.put(856, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(870, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(878, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BD"); + countryCodeToRegionCodeMap.put(880, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(881, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(882, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(883, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TW"); + countryCodeToRegionCodeMap.put(886, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(888, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MV"); + countryCodeToRegionCodeMap.put(960, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("LB"); + countryCodeToRegionCodeMap.put(961, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("JO"); + countryCodeToRegionCodeMap.put(962, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SY"); + countryCodeToRegionCodeMap.put(963, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IQ"); + countryCodeToRegionCodeMap.put(964, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KW"); + countryCodeToRegionCodeMap.put(965, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("SA"); + countryCodeToRegionCodeMap.put(966, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("YE"); + countryCodeToRegionCodeMap.put(967, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("OM"); + countryCodeToRegionCodeMap.put(968, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("PS"); + countryCodeToRegionCodeMap.put(970, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AE"); + countryCodeToRegionCodeMap.put(971, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("IL"); + countryCodeToRegionCodeMap.put(972, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BH"); + countryCodeToRegionCodeMap.put(973, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("QA"); + countryCodeToRegionCodeMap.put(974, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("BT"); + countryCodeToRegionCodeMap.put(975, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("MN"); + countryCodeToRegionCodeMap.put(976, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("NP"); + countryCodeToRegionCodeMap.put(977, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("001"); + countryCodeToRegionCodeMap.put(979, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TJ"); + countryCodeToRegionCodeMap.put(992, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("TM"); + countryCodeToRegionCodeMap.put(993, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("AZ"); + countryCodeToRegionCodeMap.put(994, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("GE"); + countryCodeToRegionCodeMap.put(995, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("KG"); + countryCodeToRegionCodeMap.put(996, listWithRegionCode); + + listWithRegionCode = new ArrayList<>(1); + listWithRegionCode.add("UZ"); + countryCodeToRegionCodeMap.put(998, listWithRegionCode); + + return countryCodeToRegionCodeMap; + } + + private static synchronized Map createCountryCodeByIsoMap() { + final Map countryCodeByIso = new HashMap<>(MAX_COUNTRIES); + countryCodeByIso.put("AF", 93); + countryCodeByIso.put("AX", 358); + countryCodeByIso.put("AL", 355); + countryCodeByIso.put("DZ", 213); + countryCodeByIso.put("AS", 1); + countryCodeByIso.put("AD", 376); + countryCodeByIso.put("AO", 244); + countryCodeByIso.put("AI", 1); + countryCodeByIso.put("AG", 1); + countryCodeByIso.put("AR", 54); + countryCodeByIso.put("AM", 374); + countryCodeByIso.put("AW", 297); + countryCodeByIso.put("AC", 247); + countryCodeByIso.put("AU", 61); + countryCodeByIso.put("AT", 43); + countryCodeByIso.put("AZ", 994); + countryCodeByIso.put("BS", 1); + countryCodeByIso.put("BH", 973); + countryCodeByIso.put("BD", 880); + countryCodeByIso.put("BB", 1); + countryCodeByIso.put("BY", 375); + countryCodeByIso.put("BE", 32); + countryCodeByIso.put("BZ", 501); + countryCodeByIso.put("BJ", 229); + countryCodeByIso.put("BM", 1); + countryCodeByIso.put("BT", 975); + countryCodeByIso.put("BO", 591); + countryCodeByIso.put("BA", 387); + countryCodeByIso.put("BW", 267); + countryCodeByIso.put("BR", 55); + countryCodeByIso.put("IO", 246); + countryCodeByIso.put("VG", 1); + countryCodeByIso.put("BN", 673); + countryCodeByIso.put("BG", 359); + countryCodeByIso.put("BF", 226); + countryCodeByIso.put("BI", 257); + countryCodeByIso.put("KH", 855); + countryCodeByIso.put("CM", 237); + countryCodeByIso.put("CA", 1); + countryCodeByIso.put("CV", 238); + countryCodeByIso.put("BQ", 599); + countryCodeByIso.put("KY", 1); + countryCodeByIso.put("CF", 236); + countryCodeByIso.put("TD", 235); + countryCodeByIso.put("CL", 56); + countryCodeByIso.put("CN", 86); + countryCodeByIso.put("CX", 61); + countryCodeByIso.put("CC", 61); + countryCodeByIso.put("CO", 57); + countryCodeByIso.put("KM", 269); + countryCodeByIso.put("CD", 243); + countryCodeByIso.put("CG", 242); + countryCodeByIso.put("CK", 682); + countryCodeByIso.put("CR", 506); + countryCodeByIso.put("CI", 225); + countryCodeByIso.put("HR", 385); + countryCodeByIso.put("CU", 53); + countryCodeByIso.put("CW", 599); + countryCodeByIso.put("CY", 357); + countryCodeByIso.put("CZ", 420); + countryCodeByIso.put("DK", 45); + countryCodeByIso.put("DJ", 253); + countryCodeByIso.put("DM", 1); + countryCodeByIso.put("DO", 1); + countryCodeByIso.put("TL", 670); + countryCodeByIso.put("EC", 593); + countryCodeByIso.put("EG", 20); + countryCodeByIso.put("SV", 503); + countryCodeByIso.put("GQ", 240); + countryCodeByIso.put("ER", 291); + countryCodeByIso.put("EE", 372); + countryCodeByIso.put("ET", 251); + countryCodeByIso.put("FK", 500); + countryCodeByIso.put("FO", 298); + countryCodeByIso.put("FJ", 679); + countryCodeByIso.put("FI", 358); + countryCodeByIso.put("FR", 33); + countryCodeByIso.put("GF", 594); + countryCodeByIso.put("PF", 689); + countryCodeByIso.put("GA", 241); + countryCodeByIso.put("GM", 220); + countryCodeByIso.put("GE", 995); + countryCodeByIso.put("DE", 49); + countryCodeByIso.put("GH", 233); + countryCodeByIso.put("GI", 350); + countryCodeByIso.put("GR", 30); + countryCodeByIso.put("GL", 299); + countryCodeByIso.put("GD", 1); + countryCodeByIso.put("GP", 590); + countryCodeByIso.put("GU", 1); + countryCodeByIso.put("GT", 502); + countryCodeByIso.put("GG", 44); + countryCodeByIso.put("GN", 224); + countryCodeByIso.put("GW", 245); + countryCodeByIso.put("GY", 592); + countryCodeByIso.put("HT", 509); + countryCodeByIso.put("HM", 672); + countryCodeByIso.put("HN", 504); + countryCodeByIso.put("HK", 852); + countryCodeByIso.put("HU", 36); + countryCodeByIso.put("IS", 354); + countryCodeByIso.put("IN", 91); + countryCodeByIso.put("ID", 62); + countryCodeByIso.put("IR", 98); + countryCodeByIso.put("IQ", 964); + countryCodeByIso.put("IE", 353); + countryCodeByIso.put("IM", 44); + countryCodeByIso.put("IL", 972); + countryCodeByIso.put("IT", 39); + countryCodeByIso.put("JM", 1); + countryCodeByIso.put("JP", 81); + countryCodeByIso.put("JE", 44); + countryCodeByIso.put("JO", 962); + countryCodeByIso.put("KZ", 7); + countryCodeByIso.put("KE", 254); + countryCodeByIso.put("KI", 686); + countryCodeByIso.put("XK", 381); + countryCodeByIso.put("KW", 965); + countryCodeByIso.put("KG", 996); + countryCodeByIso.put("LA", 856); + countryCodeByIso.put("LV", 371); + countryCodeByIso.put("LB", 961); + countryCodeByIso.put("LS", 266); + countryCodeByIso.put("LR", 231); + countryCodeByIso.put("LY", 218); + countryCodeByIso.put("LI", 423); + countryCodeByIso.put("LT", 370); + countryCodeByIso.put("LU", 352); + countryCodeByIso.put("MO", 853); + countryCodeByIso.put("MK", 389); + countryCodeByIso.put("MG", 261); + countryCodeByIso.put("MW", 265); + countryCodeByIso.put("MY", 60); + countryCodeByIso.put("MV", 960); + countryCodeByIso.put("ML", 223); + countryCodeByIso.put("MT", 356); + countryCodeByIso.put("MH", 692); + countryCodeByIso.put("MQ", 596); + countryCodeByIso.put("MR", 222); + countryCodeByIso.put("MU", 230); + countryCodeByIso.put("YT", 262); + countryCodeByIso.put("MX", 52); + countryCodeByIso.put("FM", 691); + countryCodeByIso.put("MD", 373); + countryCodeByIso.put("MC", 377); + countryCodeByIso.put("MN", 976); + countryCodeByIso.put("ME", 382); + countryCodeByIso.put("MS", 1); + countryCodeByIso.put("MA", 212); + countryCodeByIso.put("MZ", 258); + countryCodeByIso.put("MM", 95); + countryCodeByIso.put("NA", 264); + countryCodeByIso.put("NR", 674); + countryCodeByIso.put("NP", 977); + countryCodeByIso.put("NL", 31); + countryCodeByIso.put("NC", 687); + countryCodeByIso.put("NZ", 64); + countryCodeByIso.put("NI", 505); + countryCodeByIso.put("NE", 227); + countryCodeByIso.put("NG", 234); + countryCodeByIso.put("NU", 683); + countryCodeByIso.put("NF", 672); + countryCodeByIso.put("KP", 850); + countryCodeByIso.put("MP", 1); + countryCodeByIso.put("NO", 47); + countryCodeByIso.put("OM", 968); + countryCodeByIso.put("PK", 92); + countryCodeByIso.put("PW", 680); + countryCodeByIso.put("PS", 970); + countryCodeByIso.put("PA", 507); + countryCodeByIso.put("PG", 675); + countryCodeByIso.put("PY", 595); + countryCodeByIso.put("PE", 51); + countryCodeByIso.put("PH", 63); + countryCodeByIso.put("PL", 48); + countryCodeByIso.put("PT", 351); + countryCodeByIso.put("PR", 1); + countryCodeByIso.put("QA", 974); + countryCodeByIso.put("RE", 262); + countryCodeByIso.put("RO", 40); + countryCodeByIso.put("RU", 7); + countryCodeByIso.put("RW", 250); + countryCodeByIso.put("BL", 590); + countryCodeByIso.put("SH", 290); + countryCodeByIso.put("KN", 1); + countryCodeByIso.put("LC", 1); + countryCodeByIso.put("MF", 590); + countryCodeByIso.put("PM", 508); + countryCodeByIso.put("VC", 1); + countryCodeByIso.put("WS", 685); + countryCodeByIso.put("SM", 378); + countryCodeByIso.put("ST", 239); + countryCodeByIso.put("SA", 966); + countryCodeByIso.put("SN", 221); + countryCodeByIso.put("RS", 381); + countryCodeByIso.put("SC", 248); + countryCodeByIso.put("SL", 232); + countryCodeByIso.put("SG", 65); + countryCodeByIso.put("SX", 1); + countryCodeByIso.put("SK", 421); + countryCodeByIso.put("SI", 386); + countryCodeByIso.put("SB", 677); + countryCodeByIso.put("SO", 252); + countryCodeByIso.put("ZA", 27); + countryCodeByIso.put("GS", 500); + countryCodeByIso.put("KR", 82); + countryCodeByIso.put("SS", 211); + countryCodeByIso.put("ES", 34); + countryCodeByIso.put("LK", 94); + countryCodeByIso.put("SD", 249); + countryCodeByIso.put("SR", 597); + countryCodeByIso.put("SJ", 47); + countryCodeByIso.put("SZ", 268); + countryCodeByIso.put("SE", 46); + countryCodeByIso.put("CH", 41); + countryCodeByIso.put("SY", 963); + countryCodeByIso.put("TW", 886); + countryCodeByIso.put("TJ", 992); + countryCodeByIso.put("TZ", 255); + countryCodeByIso.put("TH", 66); + countryCodeByIso.put("TG", 228); + countryCodeByIso.put("TK", 690); + countryCodeByIso.put("TO", 676); + countryCodeByIso.put("TT", 1); + countryCodeByIso.put("TN", 216); + countryCodeByIso.put("TR", 90); + countryCodeByIso.put("TM", 993); + countryCodeByIso.put("TC", 1); + countryCodeByIso.put("TV", 688); + countryCodeByIso.put("VI", 1); + countryCodeByIso.put("UG", 256); + countryCodeByIso.put("UA", 380); + countryCodeByIso.put("AE", 971); + countryCodeByIso.put("GB", 44); + countryCodeByIso.put("US", 1); + countryCodeByIso.put("UY", 598); + countryCodeByIso.put("UZ", 998); + countryCodeByIso.put("VU", 678); + countryCodeByIso.put("VA", 379); + countryCodeByIso.put("VE", 58); + countryCodeByIso.put("VN", 84); + countryCodeByIso.put("WF", 681); + countryCodeByIso.put("EH", 212); + countryCodeByIso.put("YE", 967); + countryCodeByIso.put("ZM", 260); + countryCodeByIso.put("ZW", 263); + return countryCodeByIso; + } + + @Nullable + public static Integer getCountryCode(String countryIso) { + return countryIso == null + ? null + : CountryCodeByIsoMap.get(countryIso.toUpperCase(Locale.getDefault())); + } + + private static String stripCountryCode(String phoneNumber, String countryCode) { + return phoneNumber.replaceFirst("^\\+?" + countryCode, ""); + } + + private static Locale getSimBasedLocale(@NonNull Context context) { + final TelephonyManager tm = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final String countryIso = tm != null ? tm.getSimCountryIso() : null; + return TextUtils.isEmpty(countryIso) ? null : new Locale("", countryIso); + } + + private static Locale getOSLocale() { + return Locale.getDefault(); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneVerificationActivity.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneVerificationActivity.java new file mode 100644 index 000000000..a31cebac2 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/PhoneVerificationActivity.java @@ -0,0 +1,395 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.phone; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; +import android.util.Log; + +import com.firebase.ui.auth.IdpResponse; +import com.firebase.ui.auth.R; +import com.firebase.ui.auth.ResultCodes; +import com.firebase.ui.auth.ui.AppCompatBase; +import com.firebase.ui.auth.ui.BaseHelper; +import com.firebase.ui.auth.ui.ExtraConstants; +import com.firebase.ui.auth.ui.FlowParameters; +import com.firebase.ui.auth.util.signincontainer.SaveSmartLock; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; +import com.google.firebase.FirebaseException; +import com.google.firebase.auth.AuthResult; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.auth.PhoneAuthCredential; +import com.google.firebase.auth.PhoneAuthProvider; + +import java.util.concurrent.TimeUnit; + +/** + * Activity to control the entire phone verification flow. Plays host to + * {@link VerifyPhoneNumberFragment} and {@link SubmitConfirmationCodeFragment} + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class PhoneVerificationActivity extends AppCompatBase { + private static final String PHONE_VERIFICATION_LOG_TAG = "PhoneVerification"; + static final long SHORT_DELAY_MILLIS = 750; + static final long AUTO_RETRIEVAL_TIMEOUT_MILLIS = 120000; + static final String ERROR_INVALID_PHONE = "ERROR_INVALID_PHONE_NUMBER"; + static final String ERROR_INVALID_VERIFICATION = "ERROR_INVALID_VERIFICATION_CODE"; + static final String ERROR_TOO_MANY_REQUESTS = "ERROR_TOO_MANY_REQUESTS"; + static final String ERROR_QUOTA_EXCEEDED = "ERROR_QUOTA_EXCEEDED"; + static final String ERROR_SESSION_EXPIRED = "ERROR_SESSION_EXPIRED"; + static final String KEY_VERIFICATION_PHONE = "KEY_VERIFICATION_PHONE"; + static final String KEY_STATE = "KEY_STATE"; + + enum VerificationState { + VERIFICATION_NOT_STARTED, VERIFICATION_STARTED, VERIFIED; + } + + private AlertDialog mAlertDialog; + private SaveSmartLock mSaveSmartLock; + private CompletableProgressDialog mProgressDialog; + private Handler mHandler; + private String mPhoneNumber; + private String mVerificationId; + private Boolean mIsDestroyed = false; + private PhoneAuthProvider.ForceResendingToken mForceResendingToken; + private VerificationState mVerificationState; + + public static Intent createIntent(Context context, FlowParameters flowParams, String phone) { + return BaseHelper.createBaseIntent(context, PhoneVerificationActivity.class, flowParams) + .putExtra(ExtraConstants.EXTRA_PHONE, phone); + } + + @Override + protected void onCreate(final Bundle savedInstance) { + super.onCreate(savedInstance); + setContentView(R.layout.activity_register_phone); + + mSaveSmartLock = mActivityHelper.getSaveSmartLockInstance(); + mHandler = new Handler(); + mVerificationState = VerificationState.VERIFICATION_NOT_STARTED; + if (savedInstance != null && !savedInstance.isEmpty()) { + mPhoneNumber = savedInstance.getString(KEY_VERIFICATION_PHONE); + if (savedInstance.getSerializable(KEY_STATE) != null) { + mVerificationState = (VerificationState) savedInstance.getSerializable(KEY_STATE); + } + return; + } + + String phone = getIntent().getExtras().getString(ExtraConstants.EXTRA_PHONE); + VerifyPhoneNumberFragment fragment = VerifyPhoneNumberFragment.newInstance + (mActivityHelper.getFlowParams(), phone); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_verify_phone, + fragment, VerifyPhoneNumberFragment.TAG).disallowAddToBackStack().commit(); + } + + @Override + protected void onStart() { + super.onStart(); + //Actvitiy can be restarted in any of the following states + // 1) VERIFICATION_STARTED + // 2) SMS_RETRIEVED + // 3) INSTANT_VERIFIED + // 4) VERIFIED + // For the first three cases, we can simply resubscribe to the + // OnVerificationStateChangedCallbacks + // For 4, we simply finish the activity + if (mVerificationState.equals(VerificationState.VERIFICATION_STARTED)) { + sendCode(mPhoneNumber, false); + } else if (mVerificationState == VerificationState.VERIFIED) { + // activity was recreated when verified dialog was displayed + finish(mActivityHelper.getFirebaseAuth().getCurrentUser()); + } + } + + @Override + public void onBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + mVerificationState = VerificationState.VERIFICATION_NOT_STARTED; + getSupportFragmentManager().popBackStack(); + } else { + super.onBackPressed(); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putSerializable(KEY_STATE, mVerificationState); + outState.putString(KEY_VERIFICATION_PHONE, mPhoneNumber); + super.onSaveInstanceState(outState); + } + + @Override + protected void onDestroy() { + mIsDestroyed = true; + mHandler.removeCallbacksAndMessages(null); + dismissLoadingDialog(); + super.onDestroy(); + } + + void verifyPhoneNumber(String phoneNumber, boolean forceResend) { + sendCode(phoneNumber, forceResend); + if (forceResend) { + showLoadingDialog(getString(R.string.resending)); + } else { + showLoadingDialog(getString(R.string.verifying)); + } + } + + public void submitConfirmationCode(String confirmationCode) { + showLoadingDialog(getString(R.string.verifying)); + signingWithCreds(PhoneAuthProvider.getCredential(mVerificationId, confirmationCode)); + } + + void onVerificationSuccess(@NonNull final PhoneAuthCredential phoneAuthCredential) { + if (TextUtils.isEmpty(phoneAuthCredential.getSmsCode())) { + signingWithCreds(phoneAuthCredential); + } else { + //Show Fragment if it is not already visible + showSubmitCodeFragment(); + SubmitConfirmationCodeFragment submitConfirmationCodeFragment = + getSubmitConfirmationCodeFragment(); + + + showLoadingDialog(getString(R.string.retrieving_sms)); + if (submitConfirmationCodeFragment != null) { + submitConfirmationCodeFragment.setConfirmationCode(String.valueOf + (phoneAuthCredential.getSmsCode())); + } + signingWithCreds(phoneAuthCredential); + } + } + + void onCodeSent() { + completeLoadingDialog(getString(R.string.code_sent)); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + dismissLoadingDialog(); + showSubmitCodeFragment(); + } + }, SHORT_DELAY_MILLIS); + } + + void onVerificationFailed(@NonNull FirebaseException ex) { + VerifyPhoneNumberFragment verifyPhoneNumberFragment = (VerifyPhoneNumberFragment) + getSupportFragmentManager().findFragmentByTag(VerifyPhoneNumberFragment.TAG); + + if (verifyPhoneNumberFragment == null) { + return; + } + if (ex instanceof FirebaseAuthException) { + FirebaseAuthException firebaseAuthException = (FirebaseAuthException) ex; + switch (firebaseAuthException.getErrorCode()) { + case ERROR_INVALID_PHONE: + verifyPhoneNumberFragment.showError(getString(R.string.invalid_phone_number)); + dismissLoadingDialog(); + break; + case ERROR_TOO_MANY_REQUESTS: + showAlertDialog(getString(R.string.error_too_many_attempts), null); + dismissLoadingDialog(); + break; + case ERROR_QUOTA_EXCEEDED: + showAlertDialog(getString(R.string.error_quota_exceeded), null); + dismissLoadingDialog(); + break; + default: + Log.w(PHONE_VERIFICATION_LOG_TAG, ex.getLocalizedMessage()); + dismissLoadingDialog(); + showAlertDialog(ex.getLocalizedMessage(), null); + } + } else { + Log.w(PHONE_VERIFICATION_LOG_TAG, ex.getLocalizedMessage()); + dismissLoadingDialog(); + showAlertDialog(ex.getLocalizedMessage(), null); + } + } + + + private void sendCode(String phoneNumber, boolean forceResend) { + mPhoneNumber = phoneNumber; + mVerificationState = VerificationState.VERIFICATION_STARTED; + + mActivityHelper.getPhoneAuthProviderInstance().verifyPhoneNumber(phoneNumber, + AUTO_RETRIEVAL_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS, this, new PhoneAuthProvider + .OnVerificationStateChangedCallbacks() { + @Override + public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) { + if (!mIsDestroyed) { + PhoneVerificationActivity.this.onVerificationSuccess(phoneAuthCredential); + } + } + + @Override + public void onVerificationFailed(FirebaseException ex) { + if (!mIsDestroyed) { + PhoneVerificationActivity.this.onVerificationFailed(ex); + } + } + + @Override + public void onCodeSent(@NonNull String verificationId, @NonNull PhoneAuthProvider + .ForceResendingToken forceResendingToken) { + mVerificationId = verificationId; + mForceResendingToken = forceResendingToken; + if (!mIsDestroyed) { + PhoneVerificationActivity.this.onCodeSent(); + } + } + }, forceResend ? mForceResendingToken : null); + } + + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + protected AlertDialog getAlertDialog() { + // It is hard to test AlertDialogs currently with robo electric. See: + // https://github.com/robolectric/robolectric/issues/1944 + // We just test that the error was not displayed inline + return mAlertDialog; + } + + private void showSubmitCodeFragment() { + // idempotent function + if (getSubmitConfirmationCodeFragment() == null) { + SubmitConfirmationCodeFragment f = SubmitConfirmationCodeFragment.newInstance + (mActivityHelper.getFlowParams(), mPhoneNumber); + FragmentTransaction t = getSupportFragmentManager().beginTransaction().replace(R.id + .fragment_verify_phone, f, SubmitConfirmationCodeFragment.TAG).addToBackStack + (null); + + if (!isFinishing() && !mIsDestroyed) { + t.commitAllowingStateLoss(); + } + } + } + + private void finish(FirebaseUser user) { + IdpResponse response = new IdpResponse.Builder(PhoneAuthProvider.PROVIDER_ID, null) + .setPhoneNumber(user.getPhoneNumber()) + .build(); + setResult(ResultCodes.OK, response.toIntent()); + finish(); + } + + private void showAlertDialog(@NonNull String s, DialogInterface.OnClickListener + onClickListener) { + mAlertDialog = new AlertDialog.Builder(this) + .setMessage(s) + .setPositiveButton(R.string.incorrect_code_dialog_positive_button_text, onClickListener) + .show(); + } + + private void signingWithCreds(@NonNull PhoneAuthCredential phoneAuthCredential) { + mActivityHelper.getFirebaseAuth().signInWithCredential(phoneAuthCredential) + .addOnSuccessListener(this, new OnSuccessListener() { + @Override + public void onSuccess(final AuthResult authResult) { + mVerificationState = VerificationState.VERIFIED; + completeLoadingDialog(getString(R.string.verified)); + + // Activity can be recreated before this message is handled + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!mIsDestroyed) { + dismissLoadingDialog(); + finish(authResult.getUser()); + } + } + }, SHORT_DELAY_MILLIS); + } + }).addOnFailureListener(this, new OnFailureListener() { + @Override + public void onFailure(@NonNull Exception e) { + dismissLoadingDialog(); + //incorrect confirmation code + if (e instanceof FirebaseAuthInvalidCredentialsException) { + FirebaseAuthInvalidCredentialsException firebaseAuthInvalidCredentialsException + = (FirebaseAuthInvalidCredentialsException) e; + switch (firebaseAuthInvalidCredentialsException.getErrorCode()) { + case ERROR_INVALID_VERIFICATION: + showAlertDialog(getString(R.string.incorrect_code_dialog_body), new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SubmitConfirmationCodeFragment f + = getSubmitConfirmationCodeFragment(); + f.setConfirmationCode(""); + } + }); + break; + case ERROR_SESSION_EXPIRED: + showAlertDialog(getString(R.string.error_session_expired), new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SubmitConfirmationCodeFragment f + = getSubmitConfirmationCodeFragment(); + f.setConfirmationCode(""); + } + }); + break; + default: + showAlertDialog(e.getLocalizedMessage(), null); + } + } else { + showAlertDialog(e.getLocalizedMessage(), null); + } + } + }); + } + + private void completeLoadingDialog(String content) { + if (mProgressDialog != null) { + mProgressDialog.complete(content); + } + } + + private void showLoadingDialog(String message) { + dismissLoadingDialog(); + + if (mProgressDialog == null) { + mProgressDialog = new CompletableProgressDialog(this); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setTitle(""); + } + + mProgressDialog.setMessage(message); + mProgressDialog.show(); + } + + private void dismissLoadingDialog() { + if (mProgressDialog != null) { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + + private SubmitConfirmationCodeFragment getSubmitConfirmationCodeFragment() { + return (SubmitConfirmationCodeFragment) getSupportFragmentManager().findFragmentByTag + (SubmitConfirmationCodeFragment.TAG); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/SpacedEditText.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/SpacedEditText.java new file mode 100644 index 000000000..998391141 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/SpacedEditText.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2017 Google Inc + */ + +package com.firebase.ui.auth.ui.phone; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v7.widget.AppCompatEditText; +import android.text.Editable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ScaleXSpan; +import android.util.AttributeSet; + +import com.firebase.ui.auth.R; + +/** + * This element inserts spaces between characters in the edit text and expands the width of the + * spaces using spannables. + * This is required since Android's letter spacing is not available until API 21. + */ +public final class SpacedEditText extends AppCompatEditText { + private float proportion; + private SpannableStringBuilder originalText; + + public SpacedEditText(Context context) { + super(context); + } + + public SpacedEditText(Context context, AttributeSet attrs) { + super(context, attrs); + initAttrs(context, attrs); + } + + void initAttrs(Context context, AttributeSet attrs) { + originalText = new SpannableStringBuilder(""); + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SpacedEditText); + //Controls the ScaleXSpan applied on the injected spaces + proportion = array.getFloat(R.styleable.SpacedEditText_spacingProportion, 1); + array.recycle(); + } + + @Override + public void setText(CharSequence text, BufferType type) { + originalText = new SpannableStringBuilder(text); + final SpannableStringBuilder spacedOutString = getSpacedOutString(text); + super.setText(spacedOutString, BufferType.SPANNABLE); + } + + /** + * Set the selection after recalculating the index intended by the caller. + * + * @param index + */ + @Override + public void setSelection(int index) { + //if the index is the leading edge, there are no spaces before it. + //for all other cases, the index is preceeded by index - 1 spaces. + int spacesUptoIndex; + if (index == 0) { + spacesUptoIndex = 0; + } else { + spacesUptoIndex = index - 1; + } + final int recalculatedIndex = index + spacesUptoIndex; + + super.setSelection(recalculatedIndex); + } + + private SpannableStringBuilder getSpacedOutString(CharSequence text) { + final SpannableStringBuilder builder = new SpannableStringBuilder(); + final int textLength = text.length(); + int lastSpaceIndex = -1; + + //Insert a space in front of all characters upto the last character + //Scale the space without scaling the character to preserve font appearance + for (int i = 0; i < textLength - 1; i++) { + builder.append(text.charAt(i)); + builder.append(" "); + lastSpaceIndex += 2; + builder.setSpan(new ScaleXSpan(proportion), lastSpaceIndex, lastSpaceIndex + 1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + //Append the last character + if (textLength != 0) builder.append(text.charAt(textLength - 1)); + + return builder; + } + + public Editable getUnspacedText() { + return this.originalText; + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/SubmitConfirmationCodeFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/SubmitConfirmationCodeFragment.java new file mode 100644 index 000000000..dead802e1 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/SubmitConfirmationCodeFragment.java @@ -0,0 +1,275 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.phone; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.v4.app.FragmentActivity; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.TextView; + +import com.firebase.ui.auth.R; +import com.firebase.ui.auth.ui.ExtraConstants; +import com.firebase.ui.auth.ui.FlowParameters; +import com.firebase.ui.auth.ui.FragmentBase; +import com.firebase.ui.auth.ui.TermsTextView; + +/** + * Display confirmation code to verify phone numbers input in {{@link VerifyPhoneNumberFragment}} + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class SubmitConfirmationCodeFragment extends FragmentBase { + + public static final String TAG = "SubmitConfirmationCodeFragment"; + + private static final long RESEND_WAIT_MILLIS = 15000; + private static final String EXTRA_MILLIS_UNTIL_FINISHED = "EXTRA_MILLIS_UNTIL_FINISHED"; + + private TextView mEditPhoneTextView; + private TextView mResendCodeTextView; + private TextView mCountDownTextView; + private SpacedEditText mConfirmationCodeEditText; + private Button mSubmitConfirmationButton; + private CustomCountDownTimer mCountdownTimer; + private PhoneVerificationActivity mVerifier; + private TermsTextView mAgreementText; + private long mMillisUntilFinished; + + public static SubmitConfirmationCodeFragment newInstance(FlowParameters flowParameters, + String phoneNumber) { + SubmitConfirmationCodeFragment fragment = new SubmitConfirmationCodeFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ExtraConstants.EXTRA_FLOW_PARAMS, flowParameters); + args.putString(ExtraConstants.EXTRA_PHONE, phoneNumber); + + fragment.setArguments(args); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable + Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.confirmation_code_layout, container, false); + FragmentActivity parentActivity = getActivity(); + + mEditPhoneTextView = (TextView) v.findViewById(R.id.edit_phone_number); + mCountDownTextView = (TextView) v.findViewById(R.id.ticker); + mResendCodeTextView = (TextView) v.findViewById(R.id.resend_code); + mConfirmationCodeEditText = (SpacedEditText) v.findViewById(R.id.confirmation_code); + mSubmitConfirmationButton = (Button) v.findViewById(R.id.submit_confirmation_code); + mAgreementText = (TermsTextView) v.findViewById(R.id.create_account_tos); + + final String phoneNumber = getArguments().getString(ExtraConstants.EXTRA_PHONE); + + parentActivity.setTitle(getString(R.string.verify_your_phone_title)); + setupConfirmationCodeEditText(); + setupEditPhoneNumberTextView(phoneNumber); + setupCountDown(RESEND_WAIT_MILLIS); + setupSubmitConfirmationCodeButton(); + setupResendConfirmationCodeTextView(phoneNumber); + setUpTermsOfService(); + return v; + } + + @Override + public void onStart() { + super.onStart(); + mConfirmationCodeEditText.requestFocus(); + InputMethodManager imgr = (InputMethodManager) getActivity().getSystemService( + Context.INPUT_METHOD_SERVICE); + imgr.showSoftInput(mConfirmationCodeEditText, 0); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) { + Long t = savedInstanceState.getLong(EXTRA_MILLIS_UNTIL_FINISHED); + if (t != null) { + mCountdownTimer.update(t); + } + } + + if (!(getActivity() instanceof PhoneVerificationActivity)) { + throw new IllegalStateException("Activity must implement PhoneVerificationHandler"); + } + mVerifier = (PhoneVerificationActivity) getActivity(); + } + + @Override + public void onDestroy() { + cancelTimer(); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putLong(EXTRA_MILLIS_UNTIL_FINISHED, mMillisUntilFinished); + } + + private void setTimer(long millisUntilFinished) { + mCountDownTextView.setText(String.format(getString(R.string.resend_code_in), + timeRoundedToSeconds(millisUntilFinished))); + } + + private void setupResendConfirmationCodeTextView(final String phoneNumber) { + mResendCodeTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mVerifier.verifyPhoneNumber(phoneNumber, true); + mResendCodeTextView.setVisibility(View.GONE); + mCountDownTextView.setVisibility(View.VISIBLE); + mCountDownTextView.setText(String.format(getString(R.string.resend_code_in), + RESEND_WAIT_MILLIS / 1000)); + mCountdownTimer.renew(); + } + }); + } + + private void setupCountDown(long startTimeMillis) { + //set the timer view + setTimer(startTimeMillis / 1000); + + //create a countdown + mCountdownTimer = createCountDownTimer(mCountDownTextView, mResendCodeTextView, this, + startTimeMillis); + + //start the countdown + startTimer(); + } + + private void setupSubmitConfirmationCodeButton() { + mSubmitConfirmationButton.setEnabled(false); + + mSubmitConfirmationButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final String confirmationCode = mConfirmationCodeEditText.getUnspacedText() + .toString(); + mVerifier.submitConfirmationCode(confirmationCode); + } + }); + } + + private void setupEditPhoneNumberTextView(@Nullable String phoneNumber) { + mEditPhoneTextView.setText(TextUtils.isEmpty(phoneNumber) ? "" : phoneNumber); + mEditPhoneTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getFragmentManager().getBackStackEntryCount() > 0) { + getFragmentManager().popBackStack(); + } + } + }); + } + + private void setupConfirmationCodeEditText() { + mConfirmationCodeEditText.setText("------"); + BucketedTextChangeListener listener = createBucketedTextChangeListener(); + mConfirmationCodeEditText.addTextChangedListener(listener); + } + + @NonNull + private View.OnFocusChangeListener createOnFocusChangeListener() { + return new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + mConfirmationCodeEditText.setSelection(0); + } + } + }; + } + + private BucketedTextChangeListener createBucketedTextChangeListener() { + return new BucketedTextChangeListener(this.mConfirmationCodeEditText, 6, "-", + createBucketOnEditCallback(mSubmitConfirmationButton)); + } + + private void startTimer() { + if (mCountdownTimer != null) { + mCountdownTimer.start(); + } + } + + private void cancelTimer() { + if (mCountdownTimer != null) { + mCountdownTimer.cancel(); + } + } + + private void setUpTermsOfService() { + mAgreementText.showTerms(mHelper.getFlowParams(), R.string.continue_phone_login); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + CustomCountDownTimer getmCountdownTimer() { + return mCountdownTimer; + } + + private int timeRoundedToSeconds(double millis) { + return (int) Math.ceil(millis / 1000); + } + + private CustomCountDownTimer createCountDownTimer(final TextView timerText, final TextView + resendCode, final SubmitConfirmationCodeFragment fragment, final long startTimeMillis) { + return new CustomCountDownTimer(startTimeMillis, 500) { + SubmitConfirmationCodeFragment mSubmitConfirmationCodeFragment = fragment; + + public void onTick(long millisUntilFinished) { + mMillisUntilFinished = millisUntilFinished; + mSubmitConfirmationCodeFragment.setTimer(millisUntilFinished); + } + + public void onFinish() { + timerText.setText(""); + timerText.setVisibility(View.GONE); + resendCode.setVisibility(View.VISIBLE); + } + }; + } + + private BucketedTextChangeListener.ContentChangeCallback createBucketOnEditCallback(final + Button button) { + return new BucketedTextChangeListener.ContentChangeCallback() { + @Override + public void whileComplete() { + button.setEnabled(true); + } + + @Override + public void whileIncomplete() { + button.setEnabled(false); + } + }; + } + + void setConfirmationCode(String code) { + mConfirmationCodeEditText.setText(code); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/phone/VerifyPhoneNumberFragment.java b/auth/src/main/java/com/firebase/ui/auth/ui/phone/VerifyPhoneNumberFragment.java new file mode 100644 index 000000000..e2a3b0253 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/ui/phone/VerifyPhoneNumberFragment.java @@ -0,0 +1,241 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.phone; + +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RestrictTo; +import android.support.v4.app.FragmentActivity; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.firebase.ui.auth.R; +import com.firebase.ui.auth.ui.ExtraConstants; +import com.firebase.ui.auth.ui.FlowParameters; +import com.firebase.ui.auth.ui.FragmentBase; +import com.firebase.ui.auth.util.GoogleApiHelper; +import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.auth.api.credentials.Credential; +import com.google.android.gms.auth.api.credentials.CredentialPickerConfig; +import com.google.android.gms.auth.api.credentials.HintRequest; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; + +import java.util.Locale; + +/** + * Displays country selector and phone number input form for users + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class VerifyPhoneNumberFragment extends FragmentBase implements View.OnClickListener { + public static final String TAG = "VerifyPhoneFragment"; + + private CountryListSpinner countryListSpinner; + private EditText mPhoneEditText; + private TextView errorEditText; + private Button sendCodeButton; + private PhoneVerificationActivity mVerifier; + private TextView mSmsTermsText; + + private static final int RC_PHONE_HINT = 22; + + public static VerifyPhoneNumberFragment newInstance(FlowParameters flowParameters, + String phone) { + VerifyPhoneNumberFragment fragment = new VerifyPhoneNumberFragment(); + + Bundle args = new Bundle(); + args.putParcelable(ExtraConstants.EXTRA_FLOW_PARAMS, flowParameters); + args.putString(ExtraConstants.EXTRA_PHONE, phone); + + fragment.setArguments(args); + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable + Bundle savedInstanceState) { + + View v = inflater.inflate(R.layout.phone_layout, container, false); + + countryListSpinner = (CountryListSpinner) v.findViewById(R.id.country_list); + mPhoneEditText = (EditText) v.findViewById(R.id.phone_number); + errorEditText = (TextView) v.findViewById(R.id.phone_number_error); + sendCodeButton = (Button) v.findViewById(R.id.send_code); + mSmsTermsText = (TextView) v.findViewById(R.id.send_sms_tos); + + FragmentActivity parentActivity = getActivity(); + parentActivity.setTitle(getString(R.string.verify_phone_number_title)); + setUpCountrySpinner(); + setupSendCodeButton(); + setupTerms(); + + return v; + } + + private void setupTerms() { + final String verifyPhoneButtonText = getString(R.string.verify_phone_number); + final String terms = getString(R.string.sms_terms_of_service, verifyPhoneButtonText); + mSmsTermsText.setText(terms); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Set listener + if (!(getActivity() instanceof PhoneVerificationActivity)) { + throw new IllegalStateException("Activity must implement PhoneVerificationHandler"); + } + mVerifier = (PhoneVerificationActivity) getActivity(); + + if (savedInstanceState != null) { + return; + } + + // Check for phone + // It is assumed that the phone number that are being wired in via Credential Selector + // are e164 since we store it. + String phone = getArguments().getString(ExtraConstants.EXTRA_PHONE); + if (!TextUtils.isEmpty(phone)) { + // Use phone passed in + PhoneNumber phoneNumber = PhoneNumberUtils.getPhoneNumber(phone); + setPhoneNumber(phoneNumber); + setCountryCode(phoneNumber); + } else if (mHelper.getFlowParams().enableHints) { + // Try SmartLock phone autocomplete hint + showPhoneAutoCompleteHint(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == RC_PHONE_HINT) { + if (data != null) { + Credential cred = data.getParcelableExtra(Credential.EXTRA_KEY); + if (cred != null) { + // Hint selector does not always return phone numbers in e164 format. + // To accommodate either case, we normalize to e164 with best effort + final String unformattedPhone = cred.getId(); + final String formattedPhone = + PhoneNumberUtils + .formatPhoneNumberUsingCurrentCountry(unformattedPhone, + getContext()); + if (formattedPhone == null) { + Log.e(TAG, "Unable to normalize phone number from hint selector:" + + unformattedPhone); + return; + } + final PhoneNumber phoneNumberObj = + PhoneNumberUtils.getPhoneNumber(formattedPhone); + setPhoneNumber(phoneNumberObj); + setCountryCode(phoneNumberObj); + } + } + } + } + + @Override + public void onClick(View v) { + String phoneNumber = getPseudoValidPhoneNumber(); + if (phoneNumber == null) { + errorEditText.setText(R.string.invalid_phone_number); + } else { + mVerifier.verifyPhoneNumber(phoneNumber, false); + } + } + + @Nullable + String getPseudoValidPhoneNumber() { + final CountryInfo countryInfo = (CountryInfo) countryListSpinner.getTag(); + final String everythingElse = mPhoneEditText.getText().toString(); + + if (TextUtils.isEmpty(everythingElse)) { + return null; + } + + return PhoneNumberUtils.formatPhoneNumber(everythingElse, countryInfo); + } + + private void setUpCountrySpinner() { + //clear error when spinner is clicked on + countryListSpinner.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + errorEditText.setText(""); + } + }); + } + + private void setupSendCodeButton() { + sendCodeButton.setOnClickListener(this); + } + + private void showPhoneAutoCompleteHint() { + try { + mHelper.startIntentSenderForResult(getPhoneHintIntent().getIntentSender(), + RC_PHONE_HINT); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "Unable to start hint intent", e); + } + } + + private PendingIntent getPhoneHintIntent() { + GoogleApiClient client = new GoogleApiClient.Builder(getContext()).addApi(Auth + .CREDENTIALS_API).enableAutoManage(getActivity(), GoogleApiHelper + .getSafeAutoManageId(), new GoogleApiClient.OnConnectionFailedListener() { + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + Log.e(TAG, "Client connection failed: " + connectionResult.getErrorMessage()); + } + }).build(); + + + HintRequest hintRequest = new HintRequest.Builder().setHintPickerConfig(new + CredentialPickerConfig.Builder().setShowCancelButton(true).build()) + .setPhoneNumberIdentifierSupported(true).setEmailAddressIdentifierSupported + (false).build(); + + return Auth.CredentialsApi.getHintPickerIntent(client, hintRequest); + } + + private void setPhoneNumber(PhoneNumber phoneNumber) { + if (PhoneNumber.isValid(phoneNumber)) { + mPhoneEditText.setText(phoneNumber.getPhoneNumber()); + mPhoneEditText.setSelection(phoneNumber.getPhoneNumber().length()); + } + } + + private void setCountryCode(PhoneNumber phoneNumber) { + if (PhoneNumber.isCountryValid(phoneNumber)) { + countryListSpinner.setSelectedForCountry(new Locale("", phoneNumber.getCountryIso()), + phoneNumber.getCountryCode()); + } + } + + void showError(String e) { + errorEditText.setText(e); + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/util/GoogleApiHelper.java b/auth/src/main/java/com/firebase/ui/auth/util/GoogleApiHelper.java index 61882c895..c64da3d25 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/GoogleApiHelper.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/GoogleApiHelper.java @@ -20,7 +20,10 @@ /** * A {@link Task} based wrapper to get a connect {@link GoogleApiClient}. */ -public abstract class GoogleApiHelper implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { +public abstract class GoogleApiHelper implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + private static final AtomicInteger SAFE_ID = new AtomicInteger(10); protected GoogleApiClient mClient; diff --git a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/IdpSignInContainer.java b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/IdpSignInContainer.java index dd38806dd..b16e17451 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/IdpSignInContainer.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/IdpSignInContainer.java @@ -133,7 +133,8 @@ public void onSuccess(final IdpResponse response) { mHelper.getFirebaseAuth() .signInWithCredential(credential) .addOnFailureListener( - new TaskFailureLogger(TAG, "Failure authenticating with credential " + credential.getProvider())) + new TaskFailureLogger(TAG, "Failure authenticating with credential " + + credential.getProvider())) .addOnCompleteListener(new CredentialSignInHandler( getActivity(), mHelper, diff --git a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SaveSmartLock.java b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SaveSmartLock.java index abe4985cc..4a5a09539 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SaveSmartLock.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SaveSmartLock.java @@ -203,7 +203,7 @@ public void saveCredentialsOrFinish(FirebaseUser firebaseUser, @Nullable IdpResponse response) { mResponse = response; - if (!mHelper.getFlowParams().smartLockEnabled) { + if (!mHelper.getFlowParams().enableCredentials) { finish(); return; } diff --git a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SignInDelegate.java b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SignInDelegate.java index 668969559..15ef6f22f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SignInDelegate.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SignInDelegate.java @@ -24,6 +24,7 @@ import com.firebase.ui.auth.ui.User; import com.firebase.ui.auth.ui.email.RegisterEmailActivity; import com.firebase.ui.auth.ui.idp.AuthMethodPickerActivity; +import com.firebase.ui.auth.ui.phone.PhoneVerificationActivity; import com.firebase.ui.auth.util.GoogleApiHelper; import com.firebase.ui.auth.util.GoogleSignInHelper; import com.google.android.gms.auth.api.Auth; @@ -43,6 +44,7 @@ import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseAuthInvalidUserException; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.TwitterAuthProvider; import java.util.ArrayList; import java.util.HashMap; @@ -64,6 +66,7 @@ public class SignInDelegate extends SmartLockBase { private static final int RC_IDP_SIGNIN = 3; private static final int RC_AUTH_METHOD_PICKER = 4; private static final int RC_EMAIL_FLOW = 5; + private static final int RC_PHONE_FLOW = 6; private Credential mCredential; @@ -95,7 +98,7 @@ public void onCreate(Bundle savedInstance) { } FlowParameters flowParams = mHelper.getFlowParams(); - if (flowParams.smartLockEnabled) { + if (flowParams.enableCredentials) { mHelper.showLoadingDialog(R.string.progress_dialog_loading); mGoogleApiClient = new GoogleApiClient.Builder(getContext().getApplicationContext()) @@ -242,16 +245,22 @@ private void startAuthMethodChoice() { List visibleProviders = idpConfigs; - // If the only provider is Email, immediately launch the email flow. Otherwise, launch - // the auth method picker screen. + // If there is only one provider selected, launch the flow directly if (visibleProviders.size() == 1) { - if (visibleProviders.get(0).getProviderId().equals(EmailAuthProvider.PROVIDER_ID)) { + String firstProvider = visibleProviders.get(0).getProviderId(); + if (firstProvider.equals(EmailAuthProvider.PROVIDER_ID)) { + // Go directly to email flow startActivityForResult( RegisterEmailActivity.createIntent(getContext(), flowParams), RC_EMAIL_FLOW); + } else if (firstProvider.equals(PhoneAuthProvider.PROVIDER_ID)) { + // Go directly to phone flow + startActivityForResult( + PhoneVerificationActivity.createIntent(getContext(), flowParams, null), + RC_PHONE_FLOW); } else { - redirectToIdpSignIn(null, providerIdToAccountType( - visibleProviders.get(0).getProviderId())); + // Launch IDP flow + redirectToIdpSignIn(null, providerIdToAccountType(firstProvider)); } } else { startActivityForResult( diff --git a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SmartLockBase.java b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SmartLockBase.java index 3deeee234..6c63a9f7e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SmartLockBase.java +++ b/auth/src/main/java/com/firebase/ui/auth/util/signincontainer/SmartLockBase.java @@ -21,6 +21,7 @@ import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.PhoneAuthProvider; import com.google.firebase.auth.TwitterAuthProvider; import com.google.firebase.auth.UserInfo; @@ -54,6 +55,9 @@ public static String providerIdToAccountType(@AuthUI.SupportedProvider @NonNull case EmailAuthProvider.PROVIDER_ID: // The account type for email/password creds is null return null; + case PhoneAuthProvider.PROVIDER_ID: + // The account type for phone creds is null + return null; default: return null; } diff --git a/auth/src/main/res/drawable/done_check_mark.xml b/auth/src/main/res/drawable/done_check_mark.xml new file mode 100644 index 000000000..9671f43d6 --- /dev/null +++ b/auth/src/main/res/drawable/done_check_mark.xml @@ -0,0 +1,11 @@ + + + + diff --git a/auth/src/main/res/drawable/ic_settings_phone_white_24dp.xml b/auth/src/main/res/drawable/ic_settings_phone_white_24dp.xml new file mode 100644 index 000000000..ef75bd7e7 --- /dev/null +++ b/auth/src/main/res/drawable/ic_settings_phone_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/auth/src/main/res/drawable/idp_button_background_facebook.xml b/auth/src/main/res/drawable/idp_button_background_facebook.xml index fd5ae41c0..0589c181c 100644 --- a/auth/src/main/res/drawable/idp_button_background_facebook.xml +++ b/auth/src/main/res/drawable/idp_button_background_facebook.xml @@ -3,7 +3,7 @@ - + diff --git a/auth/src/main/res/drawable/idp_button_background_phone.xml b/auth/src/main/res/drawable/idp_button_background_phone.xml new file mode 100644 index 000000000..7cfd58030 --- /dev/null +++ b/auth/src/main/res/drawable/idp_button_background_phone.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/auth/src/main/res/drawable/idp_button_background_twitter.xml b/auth/src/main/res/drawable/idp_button_background_twitter.xml index 1bb555374..84314b897 100644 --- a/auth/src/main/res/drawable/idp_button_background_twitter.xml +++ b/auth/src/main/res/drawable/idp_button_background_twitter.xml @@ -3,7 +3,7 @@ - + diff --git a/auth/src/main/res/layout/activity_register_phone.xml b/auth/src/main/res/layout/activity_register_phone.xml new file mode 100644 index 000000000..94f5ce4bd --- /dev/null +++ b/auth/src/main/res/layout/activity_register_phone.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/auth/src/main/res/layout/confirmation_code_layout.xml b/auth/src/main/res/layout/confirmation_code_layout.xml new file mode 100644 index 000000000..865898934 --- /dev/null +++ b/auth/src/main/res/layout/confirmation_code_layout.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + +