Not read part 3? Read it first!

Writing a Serverless Android app (ft. Huawei's AppGallery Connect) - Part 3 - More CloudDB
Zachary Powell ・ Aug 4 '21
Over the next couple of months I will be releasing a complete end to end guide to creating an Android app using serverless functionality to completely remove the need for any backend server/hosting etc.
At the bottom of this post you will find my twitch channel where we live stream every Thursday at 2pm BST and the GitHub repo for this project!.
But for those that would rather a written guide, lets get into it!
Today we are going to look at getting the Login and Register process fully complete. This will include some refactoring to the code we have worked on before.
Authentication Manager
With a CloudDBManager
now in place that is able to handle the User object we created its time we make changes to the AuthenticationManager
so that this CloudDBManager is correctly used to retrieve user data at login/register.
Firstly we have a number of variables that we might be passing into the AuthenticationManager
. Up until this point we where only passing in a phone number or an email address and this was handled by the contactString
variable. However now that we will be accepting registration information, more data needs to be accept.
When logging in the use may be using their mobile phone number or their email address. When registering they might provide either the phone number or email address or both, and in addition a username and display name.
With these elements in mind lets create a simple data object to store this and pass it into the AuthenticationManager
as needed. This will look like below, with standard Getters/Setters and constructor.
package site.zpweb.barker.model; | |
public class LoginRegisterData { | |
String phoneNumber,email,username,displayName; | |
public LoginRegisterData(String phoneNumber, String email, String username, String displayName) { | |
this.phoneNumber = phoneNumber; | |
this.email = email; | |
this.username = username; | |
this.displayName = displayName; | |
} | |
public String getPhoneNumber() { | |
return phoneNumber; | |
} | |
public void setPhoneNumber(String phoneNumber) { | |
this.phoneNumber = phoneNumber; | |
} | |
public String getEmail() { | |
return email; | |
} | |
public void setEmail(String email) { | |
this.email = email; | |
} | |
public String getUsername() { | |
return username; | |
} | |
public void setUsername(String username) { | |
this.username = username; | |
} | |
public String getDisplayName() { | |
return displayName; | |
} | |
public void setDisplayName(String displayName) { | |
this.displayName = displayName; | |
} | |
} |
We will pass this data into the AuthenticationManager
constructor like so:
public class AuthenticationManager implements CloudDBManager.UserCallBack{ | |
Toaster toaster = new Toaster(); | |
Context context; | |
int authType; | |
LoginRegisterData loginRegisterData; | |
boolean isLogin; | |
private final CloudDBManager dbManager; | |
private String loginUserUID = "0"; | |
public AuthenticationManager(Context context, int authType, LoginRegisterData loginRegisterData, boolean isLogin){ | |
this.context = context; | |
this.authType = authType; | |
this.loginRegisterData = loginRegisterData; | |
this.isLogin = isLogin; | |
dbManager = new CloudDBManager(context, this); | |
dbManager.createObjectType(); | |
dbManager.openCloudDBZoneV2(); | |
} | |
... | |
} |
You will also notice that we have removed the contactString
from the construct. As this variable has been removed we should also make sure to remove its usage and replace with the correct data from the LoginRegisterData
object.
In places where we where expecting this string to contain the email address we should now use loginRegisterData.getEmail()
and in places where we where expecting the phone number we should use loginRegisterData.getPhoneNumber()
.
Next lets take a look at the getUser()
method. Up until now we have simply gotten the AGConnectUser
for the currently authenticated user, however we haven't actually then done anything with that. Now we should use that authenticated user to get the stored User object from the database.
private void getUser(){ | |
AGConnectUser user = AGConnectAuth.getInstance().getCurrentUser(); | |
loginUserUID = user.getUid(); | |
CloudDBZoneQuery<User> snapshotQuery = CloudDBZoneQuery.where(User.class).equalTo("uid", loginUserUID); | |
dbManager.queryUsers(snapshotQuery); | |
} | |
... | |
@Override | |
public void onQuery(List<User> userList) { | |
if (userList.size() == 1) { | |
User user = userList.get(0); | |
if (user.getUid().equals(loginUserUID)){ | |
saveLoginDetail(user); | |
proceedToFeed(); | |
} | |
} | |
} |
Here we are getting the UID of the authenticated user and then querying the database for the user with that UID.
In the onQuery
callback we can check that only one user was returned, and then triple check that the returned user does match the UID. From here we call two new methods saveLoginDetail()
and proceedToFeed()
.
saveLoginDetail()
is used to save a local copy of the logged in users ID and set a flag to say that we are now logged in. This way the next time the user opens the application we can check this flag and the user will not have to login every time they open the app.
private void saveLoginDetail(User user) { | |
SharedPreferences preferences = context.getSharedPreferences("loginDetail", 0); | |
SharedPreferences.Editor editor = preferences.edit(); | |
editor.putBoolean("isLoginedIn", true); | |
editor.putInt("userId", user.getId()); | |
} |
The proceedToFeed()
method will simply start the FeedActivity
now that we are logged in.
private void proceedToFeed(){ | |
context.startActivity(new Intent(context, FeedActivity.class)); | |
} |
From the Registration side of the process the only thing to change is the addition of being able to set the username and display name as below.
private void saveRegisteredUser(SignInResult signInResult){ | |
User user = new User(); | |
user.setId(dbManager.getMaxUserID() + 1); | |
user.setUid(signInResult.getUser().getUid()); | |
user.setUsername(loginRegisterData.getUsername()); | |
user.setDisplayname(loginRegisterData.getDisplayName()); | |
dbManager.upsertUser(user); | |
} |
In the onUpsert
call back we use the same two methods saveLoginDetail()
and proceedtoFeed()
as the login process.
@Override | |
public void onUpsert(User user){ | |
saveLoginDetail(user); | |
proceedToFeed(); | |
} |
And that's it! Your AuthenticationManager
should now look like this:
public class AuthenticationManager implements CloudDBManager.UserCallBack{ | |
Toaster toaster = new Toaster(); | |
Context context; | |
int authType; | |
LoginRegisterData loginRegisterData; | |
boolean isLogin; | |
private final CloudDBManager dbManager; | |
private String loginUserUID = "0"; | |
public AuthenticationManager(Context context, int authType, LoginRegisterData loginRegisterData, boolean isLogin){ | |
this.context = context; | |
this.authType = authType; | |
this.loginRegisterData = loginRegisterData; | |
this.isLogin = isLogin; | |
dbManager = new CloudDBManager(context, this); | |
dbManager.createObjectType(); | |
dbManager.openCloudDBZoneV2(); | |
} | |
public void sendVerifyCode() { | |
VerifyCodeSettings settings = VerifyCodeSettings.newBuilder() | |
.action(VerifyCodeSettings.ACTION_REGISTER_LOGIN) | |
.sendInterval(30) | |
.locale(Locale.ENGLISH) | |
.build(); | |
if (authType == AuthType.EMAIL) { | |
sendEmailCode(loginRegisterData.getEmail(), settings); | |
} else if (authType == AuthType.PHONE) { | |
sendPhoneCode(loginRegisterData.getPhoneNumber(), settings); | |
} else { | |
toaster.sendErrorToast(context, "please enter either email or phone number"); | |
} | |
} | |
private void sendEmailCode(String emailString, VerifyCodeSettings settings) { | |
Task<VerifyCodeResult> task = EmailAuthProvider.requestVerifyCode(emailString, settings); | |
executeTask(task); | |
} | |
private void sendPhoneCode(String phoneString, VerifyCodeSettings settings){ | |
Task<VerifyCodeResult> task = PhoneAuthProvider.requestVerifyCode("44", phoneString, settings); | |
executeTask(task); | |
} | |
private void executeTask(Task<VerifyCodeResult> task) { | |
task.addOnSuccessListener(TaskExecutors.uiThread(), | |
verifyCodeResult -> authCodeDialog()).addOnFailureListener(TaskExecutors.uiThread(), | |
e -> toaster.sendErrorToast(context, e.getLocalizedMessage())); | |
} | |
private void authCodeDialog() { | |
AlertDialog.Builder alert = new AlertDialog.Builder(context); | |
final EditText authCodeField = new EditText(context); | |
alert.setMessage("Enter your auth code below"); | |
alert.setTitle("Authentication Code"); | |
alert.setView(authCodeField); | |
alert.setPositiveButton("Login", (dialog, which) -> { | |
String authCode = authCodeField.getText().toString(); | |
if (isLogin) { | |
AGConnectAuthCredential credential = null; | |
if (authType == AuthType.EMAIL) { | |
credential = EmailAuthProvider.credentialWithVerifyCode( | |
loginRegisterData.getEmail(), | |
null, | |
authCode); | |
} else if (authType == AuthType.PHONE) { | |
credential = PhoneAuthProvider.credentialWithVerifyCode( | |
"44", | |
loginRegisterData.getPhoneNumber(), | |
null, | |
authCode); | |
} | |
signIn(credential); | |
} else { | |
register(authCode); | |
} | |
}); | |
alert.setNegativeButton("Cancel", | |
(dialog, which) -> toaster.sendErrorToast(context, "Registration Cancelled")); | |
alert.show(); | |
} | |
private void signIn(AGConnectAuthCredential credential) { | |
AGConnectAuth.getInstance().signIn(credential) | |
.addOnSuccessListener(signInResult -> getUser()) | |
.addOnFailureListener(e -> toaster.sendErrorToast(context, e.getLocalizedMessage())); | |
} | |
private void register(String authCode) { | |
if (authType == AuthType.EMAIL) { | |
EmailUser emailUser = new EmailUser.Builder() | |
.setEmail(loginRegisterData.getEmail()) | |
.setVerifyCode(authCode) | |
.build(); | |
AGConnectAuth.getInstance().createUser(emailUser).addOnSuccessListener(this::saveRegisteredUser) | |
.addOnFailureListener(e -> toaster.sendErrorToast(context, e.getLocalizedMessage())); | |
} else if (authType == AuthType.PHONE) { | |
PhoneUser phoneUser = new PhoneUser.Builder() | |
.setPhoneNumber(loginRegisterData.getPhoneNumber()) | |
.setCountryCode("44") | |
.setVerifyCode(authCode) | |
.build(); | |
AGConnectAuth.getInstance().createUser(phoneUser).addOnSuccessListener(this::saveRegisteredUser) | |
.addOnFailureListener(e -> toaster.sendErrorToast(context, e.getLocalizedMessage())); | |
} | |
} | |
private void saveRegisteredUser(SignInResult signInResult){ | |
User user = new User(); | |
user.setId(dbManager.getMaxUserID() + 1); | |
user.setUid(signInResult.getUser().getUid()); | |
user.setUsername(loginRegisterData.getUsername()); | |
user.setDisplayname(loginRegisterData.getDisplayName()); | |
dbManager.upsertUser(user); | |
} | |
private void getUser(){ | |
AGConnectUser user = AGConnectAuth.getInstance().getCurrentUser(); | |
loginUserUID = user.getUid(); | |
CloudDBZoneQuery<User> snapshotQuery = CloudDBZoneQuery.where(User.class).equalTo("uid", loginUserUID); | |
dbManager.queryUsers(snapshotQuery); | |
} | |
private void saveLoginDetail(User user) { | |
SharedPreferences preferences = context.getSharedPreferences("loginDetail", 0); | |
SharedPreferences.Editor editor = preferences.edit(); | |
editor.putBoolean("isLoginedIn", true); | |
editor.putInt("userId", user.getId()); | |
} | |
private void proceedToFeed(){ | |
context.startActivity(new Intent(context, FeedActivity.class)); | |
} | |
@Override | |
public void onUpsert(User user){ | |
saveLoginDetail(user); | |
proceedToFeed(); | |
} | |
@Override | |
public void onQuery(List<User> userList) { | |
if (userList.size() == 1) { | |
User user = userList.get(0); | |
if (user.getUid().equals(loginUserUID)){ | |
saveLoginDetail(user); | |
proceedToFeed(); | |
} | |
} | |
} | |
@Override | |
public void onDelete(List<User> userList) { | |
} | |
@Override | |
public void onError(String errorMessage) { | |
toaster.sendErrorToast(context, errorMessage); | |
} | |
} |
Of course as we have now changed the constructor there are some changes that need to be made in both the login and register activities.
Login Activity
Within the MainActivity
which is the login activity for us, lets start by creating a simple method to generate the LoginRegisterData
object
private LoginRegisterData getLoginRegisterData() { | |
String emailString = email.getText().toString().trim(); | |
String phoneString = phone.getText().toString().trim(); | |
return new LoginRegisterData(phoneString, emailString, "", ""); | |
} |
As you can see we take the email and phone number input and build the object. At this point if we used this method in the phoneLogin and emailLogin onClick listeners we can see there is code duplication. So instead lets extract a method to trigger the login process.
private void login(int authType) { | |
authManager = new AuthenticationManager(MainActivity.this, | |
authType, | |
getLoginRegisterData(), | |
true); | |
authManager.sendVerifyCode(); | |
} |
As you can see we generate the AuthenticateManager
passing in the LoginRegisterData
and the authType
. The OnClick Listeners for each button are now just one line calling this method and passing in the AuthType
as needed. The MainActivity
should now look like this:
public class MainActivity extends AppCompatActivity { | |
Button register, emailLogin, phoneLogin; | |
EditText phone, email; | |
AuthenticationManager authManager; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
register = findViewById(R.id.registerBtn); | |
emailLogin = findViewById(R.id.emailLogin ); | |
phoneLogin = findViewById(R.id.phoneLogin); | |
phone = findViewById(R.id.editTextPhone2); | |
email = findViewById(R.id.editTextTextEmailAddress2); | |
register.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, RegisterActivity.class))); | |
phoneLogin.setOnClickListener(v -> { | |
login(AuthType.PHONE); | |
}); | |
emailLogin.setOnClickListener(v -> { | |
login(AuthType.EMAIL); | |
}); | |
} | |
private void login(int authType) { | |
authManager = new AuthenticationManager(MainActivity.this, | |
authType, | |
getLoginRegisterData(), | |
true); | |
authManager.sendVerifyCode(); | |
} | |
private LoginRegisterData getLoginRegisterData() { | |
String emailString = email.getText().toString().trim(); | |
String phoneString = phone.getText().toString().trim(); | |
return new LoginRegisterData(phoneString, emailString, "", ""); | |
} | |
} |
Register Activity
For the register activity we do the same process, however we will also add two new EditText
fields so that we can accept the user input for username and displayname. Otherwise the process is the same. Create the LoginRegisterData
and pass that into the AuthenticationManager
. This this in mind the Register activity will look something like:
public class RegisterActivity extends AppCompatActivity { | |
EditText email, phone, username, displayName; | |
Button register; | |
AuthenticationManager authManager; | |
Toaster toaster = new Toaster(); | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_register); | |
email = findViewById(R.id.editTextTextEmailAddress); | |
phone = findViewById(R.id.editTextPhone); | |
username = findViewById(R.id.editTextTextUsername); | |
displayName = findViewById(R.id.editTextTextDisplayName); | |
register = findViewById(R.id.registerBtn2); | |
register.setOnClickListener(v -> { | |
String emailString = email.getText().toString().trim(); | |
String phoneString = phone.getText().toString().trim(); | |
String usernameString = username.getText().toString().trim(); | |
String displayNameString = displayName.getText().toString().trim(); | |
LoginRegisterData registerData = new LoginRegisterData(phoneString, emailString, usernameString, displayNameString); | |
if (!emailString.isEmpty()) { | |
authManager = new AuthenticationManager(RegisterActivity.this, | |
AuthType.EMAIL, | |
registerData, | |
false); | |
authManager.sendVerifyCode(); | |
} else if (!phoneString.isEmpty()) { | |
authManager = new AuthenticationManager(RegisterActivity.this, | |
AuthType.PHONE, | |
registerData, | |
false); | |
authManager.sendVerifyCode(); | |
} else { | |
toaster.sendErrorToast(RegisterActivity.this, "please enter either email or phone number"); | |
} | |
}); | |
} | |
} |
And that's it! we are now in a good state with the login and register flow which will result in a user being authenticated, logged in and use saving the ID of that user along with setting a flag to confirm the user is logged in.
Twitch Account
Github Repo
devwithzachary
/
Barker
Project init
Barker - A Serverless Twitter Clone
Barker is a simple twitter style social network written to demostrate the serverless services provided by Huawei's AppGallery Connect platform.
This app is written during live streams over at https://www.twitch.tv/devwithzachary The streams are then edited down into videos which you can watch https://www.youtube.com/channel/UC63PqG8ZnWC4JWYrNJKocMA
Finally there is also a written copy of the guide found on dev.to https://dev.to/devwithzachary
Each episodes code is commited as a single commit to make it easy to step along with the code, simply checkout the specific commit and follow along!
We will be back with the next part next week!
Top comments (0)