[Android] Creating Custom Login Screen for AWS Mobile Hub

November 20, 2017
aws android howto login mobilehub

Introduction

If you’d like to skip this introduction please click here.

For this post, I’m going to be talking about another AWS service called Mobile Hub. This is a manager for a set of other AWS services like Cognito, DynamoDB, APIGateway & Lambda, S3, Lex and many others (see AWS Mobile Hub). AWS Mobile Hub is designed to easily and quickly configure AWS services and integrate them into your mobile app project.

In my opinion, AWS is a company with great products for cloud solutions, but their documentation needs a few improvements. I struggled so many times trying to make a proper and functional login screen on my Android app using AWS Mobile Hub, and it’s quite confusing how hard it is to find a solution for these Android/AWS problems.

So, I decided to make this post a tutorial showing how to make a custom login for a Mobile Hub project, in Android.

Oh, and I’m also assuming you already have an AWS account, and you know how to see if the things I’ll show here are eligible for the free tier trial.

In the next chapters, I’ll explain how to create these services and make your own login screen for AWS Mobile Hub. So let’s go to the solution!

Contents

1. AWS Mobile Hub

Go to the AWS Mobile Hub main page. This is where it all begins, click on Create for a new project.

aws mobile hub main screen

Give it a name for your project, mine is AWS Login Tutorial.

create project page 1

Select the platform you are developing first, this tutorial will be be an Android app.

create project page 2

We don’t need to download the configuration file right now because we’ll set up AWS Cognito right after creating this project. So, just click Next.

create project page 3

Click done and lets configure AWS Cognito for user pool.

On your project’s main page, add the User Sign-in feature.

create project page 4

I’m making this example very simple, so users will be required just for a valid e-mail, passwords must be just 6 characters long. For a real project you should know how important this is to your app and custom accordingly.

create project page 5

Now that we have set up Cognito (user login) we can download the configuration file. Click on Integrate and then Download Cloud Config. I’ll explain later how to add this file to our Android project, and a few cautions you should take with this file.

2. Android Studio

From now on, all the work we have to do in this tutorial will be basically inside our app. I will show how we successfully created an user in the AWS Cognito page, but it’s just to verify the operation went okay.

So, start a new Android Studio project, with an empty Activity, and give it a name for the first activity of your app, which in my case I named SplashActivity, because I want it to start with a splash screen (logo).

2.1. Adding AWS Dependencies

Now we need to add AWS libraries to our project. Inside build.gradle (Module: app) add the dependencies below. You will need to sync your project after adding these dependencies.

dependencies {
    ...

    // Mobile Client for initializing the SDK
    compile('com.amazonaws:aws-android-sdk-mobile-client:2.6.+@aar') { transitive = true; }

    // Cognito UserPools for SignIn
    compile 'com.android.support:support-v4:26.+'
    compile('com.amazonaws:aws-android-sdk-auth-userpools:2.6.+@aar') { transitive = true; }
}

If you cannot find the file in your project, just tap the left shift key twice and a pop up window will open to search for any file inside the project.

2.2. Creating LoginActivity and MainActivity

The SplashActivity created at the start of the project will serve to initiate the instance with AWS and to check if we have signed in previously (in this device), so we don’t need to go to the login screen every time we open the app.

We also need to create two new activities:

So, go to File -> New -> Activity -> Empty Activity and create the LoginActivity, then do it again to create the MainActivity.

new activity

By doing so, Android Studio will create two files for every activity, a java file for the class and a xml file for the layout.

new activity

2.3. Screen Titles and Permissions

We need to check and set up some configurations in the AndroidManifest.xml file.

Add the permissions below inside the manifest tag.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

Certify the SplashActivity has the intent-filter tag that sets the launcher activity. Also, add the label attribute in the activity tag for the screen title.

<activity
    android:name=".SplashActivity"
    android:label="Logo Screen (Splash)">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Then, add title for the LoginActivity and for the MainActivity.

<activity
    android:name=".MainActivity"
    android:label="Main Screen" />
<activity
    android:name=".LoginActivity"
    android:label="Login Screen" />

2.4. Editing the Layouts

For the sake of this tutorial, let’s try to make it very simple. The layouts here are really ugly, but easily understandable.

2.4.1. Layout for SplashActivity

SplashActivity’s layout will be as simple as possible, let’s just add a TextView to identify the screen.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wtmimura.awsandroid.SplashActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="This is the Splash Screen" />
</RelativeLayout>
layout view for splashactivity

2.4.2. Layout for LoginActivity

LoginActivity’s layout is somewhat large, but it’s also very simple.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.wtmimura.awsandroid.LoginActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="25dp"
        android:text="This is the Login Screen" />

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="25dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_span="3"
            android:text="Register Section" />

        <TableRow>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Username:" />

            <EditText
                android:id="@+id/registerUsername"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="text"
                android:lines="1" />

        </TableRow>

        <TableRow>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="E-mail:" />

            <EditText
                android:id="@+id/registerEmail"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:inputType="textWebEmailAddress"
                android:lines="1" />

        </TableRow>

        <TableRow>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Password:" />

            <EditText
                android:id="@+id/registerPassword"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:inputType="textPassword"
                android:lines="1" />

        </TableRow>

        <Button
            android:id="@+id/registerButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Register" />

        <TableRow>

            <Button
                android:id="@+id/confirmButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="confirm" />

            <EditText
                android:id="@+id/confirmationCode"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />

        </TableRow>

    </TableLayout>

    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="25dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_span="3"
            android:text="Login Section" />


        <TableRow>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Username/E-mail:" />

            <EditText
                android:id="@+id/loginUserOrEmail"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="text"
                android:lines="1" />

        </TableRow>

        <TableRow>

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Password:" />

            <EditText
                android:id="@+id/loginPassword"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:inputType="textPassword"
                android:lines="1" />

        </TableRow>

        <Button
            android:id="@+id/loginButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Login" />

    </TableLayout>

</LinearLayout>
layout view for loginactivity

2.4.3. Layout for MainActivity

MainActivity’s layout is as simple as SplashActivity’s. ConstraintLayout is used for this case, but it’s just to illustrate you can use different layout type to build the same appearance.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.wtmimura.awsandroid.MainActivity">

    <TextView
        android:id="@+id/hello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
layout view for mainactivity

2.5. Login Model

It’s time to create the model. This is the core of the project, and it will contain the main logic and actions to manage the login process.

Create a new package named aws, then create a class named AWSLoginModel and an interface named AWSLoginHandler.

new package and class and interface

2.5.1. Interface AWSLoginHandler

In this interface, we’ll control the callbacks coming from AWS operations. We’ll create four of them.

/**
 * Callback used for model {@link AWSLoginModel}. This needs to be implemented when the constructor
 * of {@link AWSLoginModel} is called.
 */
public interface AWSLoginHandler {

    /**
     * Successful completion of the first step of the registration process.
     * This will output mustConfirmToComplete in case there's the need to confirm registration to complete this process.
     *
     * @param mustConfirmToComplete     will be {@code true} if there's the need to confirm registration,
     *                                  otherwise {@code false}.
     */
    void onRegisterSuccess(boolean mustConfirmToComplete);

    /**
     * Successful completion of the registration process.
     */
    void onRegisterConfirmed();

    /**
     * Successful completion of the sign in process.
     */
    void onSignInSuccess();

    /**
     * Failure of the process called.
     *
     * @param process       what process was called.
     * @param exception     failure details.
     */
    void onFailure(int process,Exception exception);

}

2.5.2. Class AWSLoginModel

In this class, we’ll create all necessary methods related to login with AWS Mobile Hub.

The constructor will take care of instantiating CognitoUserPool from the configurations of the AWS Mobile Hub.

The other methods will contain logic necessary to perform their objectives and to respond to a unique interface created previously.

/**
 * This represents a model for login operations on AWS Mobile Hub. It manages login operations
 * such as:
 * - Sign In
 * - Sign Up
 * - Confirm Sign Up
 * - Get User Name (current signed in)
 * - Get User E-mail (current signed in)
 *
 */
@SuppressWarnings("unused")
public class AWSLoginModel {
    // constants
    private final String ATTR_EMAIL = "email";
    private static final String SHARED_PREFERENCE = "SavedValues";
    private static final String PREFERENCE_USER_NAME = "awsUserName";
    private static final String PREFERENCE_USER_EMAIL = "awsUserEmail";
    public static final int PROCESS_SIGN_IN = 1;
    public static final int PROCESS_REGISTER = 2;
    public static final int PROCESS_CONFIRM_REGISTRATION = 3;

    // interface handler
    private AWSLoginHandler mCallback;

    // control variables
    private String userName, userPassword;
    private Context mContext;
    private CognitoUserPool mCognitoUserPool;
    private CognitoUser mCognitoUser;

    private final AuthenticationHandler authenticationHandler = new AuthenticationHandler() {
        @Override
        public void onSuccess(CognitoUserSession userSession, CognitoDevice newDevice) {
            // Get details of the logged user (in this case, only the e-mail)
            mCognitoUser.getDetailsInBackground(new GetDetailsHandler() {
                @Override
                public void onSuccess(CognitoUserDetails cognitoUserDetails) {
                    // Save in SharedPreferences
                    SharedPreferences.Editor editor = mContext.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE).edit();
                    String email = cognitoUserDetails.getAttributes().getAttributes().get(ATTR_EMAIL);
                    editor.putString(PREFERENCE_USER_EMAIL, email);
                    editor.apply();
                }

                @Override
                public void onFailure(Exception exception) {
                    exception.printStackTrace();
                }
            });

            // Save in SharedPreferences
            SharedPreferences.Editor editor = mContext.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE).edit();
            editor.putString(PREFERENCE_USER_NAME, userName);
            editor.apply();
            mCallback.onSignInSuccess();
        }

        @Override
        public void getAuthenticationDetails(AuthenticationContinuation authenticationContinuation, String userId) {
            final AuthenticationDetails authenticationDetails = new AuthenticationDetails(userName, userPassword, null);
            authenticationContinuation.setAuthenticationDetails(authenticationDetails);
            authenticationContinuation.continueTask();
            userPassword = "";
        }

        @Override
        public void getMFACode(MultiFactorAuthenticationContinuation continuation) {
            // Not implemented for this Model
        }

        @Override
        public void authenticationChallenge(ChallengeContinuation continuation) {
            // Not implemented for this Model
        }

        @Override
        public void onFailure(Exception exception) {
            mCallback.onFailure(PROCESS_SIGN_IN, exception);
        }
    };


    /**
     * Constructs the model for login functions in AWS Mobile Hub.
     *
     * @param context         REQUIRED: Android application context.
     * @param callback        REQUIRED: Callback handler for login operations.
     *
     */
    public AWSLoginModel(Context context, AWSLoginHandler callback) {
        mContext = context;
        IdentityManager identityManager = IdentityManager.getDefaultIdentityManager();
        try{
            JSONObject myJSON = identityManager.getConfiguration().optJsonObject("CognitoUserPool");
            final String COGNITO_POOL_ID = myJSON.getString("PoolId");
            final String COGNITO_CLIENT_ID = myJSON.getString("AppClientId");
            final String COGNITO_CLIENT_SECRET = myJSON.getString("AppClientSecret");
            final String REGION = myJSON.getString("Region");
            mCognitoUserPool = new CognitoUserPool(context, COGNITO_POOL_ID, COGNITO_CLIENT_ID, COGNITO_CLIENT_SECRET, Regions.fromName(REGION));
        } catch (JSONException e) {
            e.printStackTrace();
        }

        mCallback = callback;
    }

    /**
     * Registers new user to the AWS Cognito User Pool.
     *
     * This will trigger {@link AWSLoginHandler} interface defined when the constructor was called.
     *
     * @param userName         REQUIRED: Username to be registered. Must be unique in the User Pool.
     * @param userEmail        REQUIRED: E-mail to be registered. Must be unique in the User Pool.
     * @param userPassword     REQUIRED: Password of this new account.
     *
     */
    public void registerUser(String userName, String userEmail, String userPassword) {
        CognitoUserAttributes userAttributes = new CognitoUserAttributes();
        userAttributes.addAttribute(ATTR_EMAIL, userEmail);

        final SignUpHandler signUpHandler = new SignUpHandler() {
            @Override
            public void onSuccess(CognitoUser user, boolean signUpConfirmationState, CognitoUserCodeDeliveryDetails cognitoUserCodeDeliveryDetails) {
                mCognitoUser = user;
                mCallback.onRegisterSuccess(!signUpConfirmationState);
            }

            @Override
            public void onFailure(Exception exception) {
                mCallback.onFailure(PROCESS_REGISTER, exception);
            }
        };

        mCognitoUserPool.signUpInBackground(userName, userPassword, userAttributes, null, signUpHandler);
    }

    /**
     * Confirms registration of the new user in AWS Cognito User Pool.
     *
     * This will trigger {@link AWSLoginHandler} interface defined when the constructor was called.
     *
     * @param confirmationCode      REQUIRED: Code sent from AWS to the user.
     */
    public void confirmRegistration(String confirmationCode) {
        final GenericHandler confirmationHandler = new GenericHandler() {
            @Override
            public void onSuccess() {
                mCallback.onRegisterConfirmed();
            }

            @Override
            public void onFailure(Exception exception) {
                mCallback.onFailure(PROCESS_CONFIRM_REGISTRATION, exception);
            }
        };

        mCognitoUser.confirmSignUpInBackground(confirmationCode, false, confirmationHandler);
    }

    /**
     * Sign in process. If succeeded, this will save the user name and e-mail in SharedPreference of
     * this context.
     *
     * This will trigger {@link AWSLoginHandler} interface defined when the constructor was called.
     *
     * @param userNameOrEmail        REQUIRED: Username or e-mail.
     * @param userPassword           REQUIRED: Password.
     */
    public void signInUser(String userNameOrEmail, String userPassword) {
        this.userName = userNameOrEmail;
        this.userPassword = userPassword;

        mCognitoUser = mCognitoUserPool.getUser(userName);
        mCognitoUser.getSessionInBackground(authenticationHandler);
    }

    /**
     * Gets the user name saved in SharedPreferences.
     *
     * @param context               REQUIRED: Android application context.
     * @return                      user name saved in SharedPreferences.
     */
    public static String getSavedUserName(Context context) {
        SharedPreferences savedValues = context.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE);
        return savedValues.getString(PREFERENCE_USER_NAME, "");
    }

    /**
     * Gets the user e-mail saved in SharedPreferences.
     *
     * @param context               REQUIRED: Android application context.
     * @return                      user e-mail saved in SharedPreferences.
     */
    public static String getSavedUserEmail(Context context) {
        SharedPreferences savedValues = context.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE);
        return savedValues.getString(PREFERENCE_USER_EMAIL, "");
    }
}

2.6. SplashActivity Class

This class needs to initialize AWS SDK Client, and also needs to check if the user logged in previously.

public class SplashActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);

        AWSMobileClient.getInstance().initialize(SplashActivity.this, new AWSStartupHandler() {
            @Override
            public void onComplete(AWSStartupResult awsStartupResult) {
                IdentityManager identityManager = IdentityManager.getDefaultIdentityManager();
                identityManager.resumeSession(SplashActivity.this, new StartupAuthResultHandler() {
                    @Override
                    public void onComplete(StartupAuthResult authResults) {
                        if (authResults.isUserSignedIn()) {
                            startActivity(new Intent(SplashActivity.this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
                        } else {
                            startActivity(new Intent(SplashActivity.this, LoginActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
                        }
                    }
                }, 3000);
            }
        }).execute();

    }
}

2.7. LoginActivity Class

This class will control the event listeners of the login and registration operations. So, this is the class which will make use of our core model AWSLoginModel.

public class LoginActivity extends AppCompatActivity implements View.OnClickListener, AWSLoginHandler {

    AWSLoginModel awsLoginModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // instantiating AWSLoginModel(context, callback)
        awsLoginModel = new AWSLoginModel(this, this);

        findViewById(R.id.registerButton).setOnClickListener(this);
        findViewById(R.id.loginButton).setOnClickListener(this);
        findViewById(R.id.confirmButton).setOnClickListener(this);

    }

    @Override
    public void onRegisterSuccess(boolean mustConfirmToComplete) {
        if (mustConfirmToComplete) {
            Toast.makeText(LoginActivity.this, "Almost done! Confirm code to complete registration", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(LoginActivity.this, "Registered! Login Now!", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    public void onRegisterConfirmed() {
        Toast.makeText(LoginActivity.this, "Registered! Login Now!", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onSignInSuccess() {
        LoginActivity.this.startActivity(new Intent(LoginActivity.this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
    }

    @Override
    public void onFailure(int process, Exception exception) {
        exception.printStackTrace();
        String whatProcess = "";
        switch (process) {
            case AWSLoginModel.PROCESS_SIGN_IN:
                whatProcess = "Sign In:";
                break;
            case AWSLoginModel.PROCESS_REGISTER:
                whatProcess = "Registration:";
                break;
            case AWSLoginModel.PROCESS_CONFIRM_REGISTRATION:
                whatProcess = "Registration Confirmation:";
                break;
        }
        Toast.makeText(LoginActivity.this, whatProcess + exception.getMessage(), Toast.LENGTH_LONG).show();
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.registerButton:
                registerAction();
                break;
            case R.id.confirmButton:
                confirmAction();
                break;
            case R.id.loginButton:
                loginAction();
                break;
        }
    }

    private void registerAction() {
        EditText userName = findViewById(R.id.registerUsername);
        EditText email = findViewById(R.id.registerEmail);
        EditText password = findViewById(R.id.registerPassword);

        // do register and handles on interface
        awsLoginModel.registerUser(userName.getText().toString(), email.getText().toString(), password.getText().toString());
    }

    private void confirmAction() {
        EditText confirmationCode = findViewById(R.id.confirmationCode);

        // do confirmation and handles on interface
        awsLoginModel.confirmRegistration(confirmationCode.getText().toString());
    }

    private void loginAction() {
        EditText userOrEmail = findViewById(R.id.loginUserOrEmail);
        EditText password = findViewById(R.id.loginPassword);

        // do sign in and handles on interface
        awsLoginModel.signInUser(userOrEmail.getText().toString(), password.getText().toString());
    }
}

2.8. MainActivity Class

This class is just showing our username gotten from AWS when we log in.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();

        String who = AWSLoginModel.getSavedUserName(MainActivity.this);

        TextView hello = findViewById(R.id.hello);
        hello.setText("Hello " + who + "!");
    }
}

2.9. Adding the Configuration File

On Chapter 1 we downloaded the configuration file for the AWS Mobile Hub project. We just need to add this file to the Android application, so on the res directory (inside app) create a new resource directory with the raw type and named raw. Then, paste the awsconfiguration.json file in there.

2.10. Testing the App

We are ready to test our app. Choose a device to emulate and run it.

testing screen 7

3. Conclusion

AWS is a company with great services and products for cloud solution. It might be a little difficult to understand some of their SDKs, but once you learn them it becomes a powerful tool for applications you might want to build.

I do recommend researching about AWS, but be prepared because this company is constantly changing (updating and upgrading) their products, so their documentation is not always the best way to learn about their platform.

4. Resources

Creating and Hosting a Wordpress Site (Using AWS EC2 & RDS)

November 2, 2017
aws ec2 howto rds wordpress

Serving Dynamic Web Pages Using HapiJS

October 11, 2017
dynamic html hapijs howto nodejs

Calling an API on Android Studio

September 19, 2017
android api howto java