Introduction
This two part series of articles will walk you through adding login capability to an Android* application; in this case our existing restaurant app. We want the customers, manager, and everyone in between to be able to log in and access features, content, and capabilities specific to their access level. Our restaurant application is designed to be a shared application installed on a tablet at a table and hence used by multiple people throughout the day. The default Android account manager is not designed to handle such shared public access, so we will implement our own account system. This series will walk you through setting up a login system in Android starting with the UI design and moving onto sending HTTP calls to our account manager server. The primary focus of this article will be on adding login capabilities and features specific to the restaurant owner, so that they can manage the application and users locally through the app. Part two of the series will cover sending and receiving HTTP calls.
Setup
The logins will be managed by a server that the Android application sends HTTP calls to communicate with. The calls going to the server will be handled asynchronously using AsyncTasks so as not to block the main UI thread. No information about users besides the currently logged in user will be stored on the device this way as the server will store all of the data. To get to the login screen, there is a user profile icon added in the action bar in the top right.
Figure 1: Restaurant Application
Figure 2: Screenshot of the restaurant application’s manager portal
Login
Implementing a login screen itself is fairly easy; you need the login button, the login status, and then possibly a pop-up message that the user is now logged in. It is the edge cases and little details surrounding these that you have to watch out for and figure out how to handle.
The first thing to consider is how to layout the landscape view and portrait view. Consider Figure 3 below with the “login” and the “register” button. Putting them side-by-side in the landscape and then on their own separate line in portrait mode uses the screen real estate and space much better. There are still a lot of tweaks that can be done on the below to make them look even better, but it shows that just re-ordering can make a big difference in how your application is presented visually to the viewer. So create different layouts for your “res/layout-land” and “res/layout-port” to improve the user experience.
Figure 3: Different sign-in view layouts
On the login form, we tell the user what to enter into the field using the “hint” attribute, then we make the input type a “textEmailAddress” to pop up the proper keyboard, and finally we add “<requestFocus/>” at the end, so that the user email field is automatically selected and the keyboard pops up upon creation.
<EditText android:id="@+id/email" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="@string/username" android:inputType="textEmailAddress"><requestFocus /></EditText>
Code Example 1: EditText in our layout xml for the email/username field
Additionally we want the keyboard’s “Done” key to trigger login as well as the “Login” button itself. So since the password field is the last EditText in our layout, the “Done” option will automatically appear. We set up a listener for it to trigger the sign in process. However we also don’t want a user to be able to trigger the login process multiple times. We can easily do this with the login button by disabling it, (mSignInButton.setEnabled(false);). We could do the same with the password field as well but the field will be grayed out and that is not as visually appealing. So instead we will keep track of that login state with a mSignInProgress variable and make sure that login is not already in process (this will come in handy for another piece of logic later in this article).
private static final int STATE_DEFAULT = 0; private static final int STATE_IN_PROGRESS = 1; private int mSignInProgress;
Code Example 2: Sign in progress and states
mPassword = (EditText) findViewById(R.id.userPassword); mPassword.setOnEditorActionListener(new OnEditorActionListener(){ @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { //Have the done button on keyboard trigger sign in if (actionId == EditorInfo.IME_ACTION_DONE && mSignInProgress!=STATE_IN_PROGRESS) { onClick(mSignInButton); //hide the keyboard return false; } return false; } });
Code Example 3: Password button listening for keyboard “Done” click
So now that we have built our activity layout and added some logic when the user initiates the sign in process, what will happen when the user rotates the screen mid-login and the whole view is rebuilt? Everything is going to become enabled again and the instance to the login status’s TextView but our Buttons will be gone. However our AsyncTask to log in will continue to run in the background, so the login process will proceed unhindered.
To fix the UI state on rotate, the first step is to make the layout fields static. Hence their instance will be retained when the device is rotated and we will be able to update them after rotating.
private static Button mSignInButton; private static Button mRegisterButton; public static TextView mStatus;
Code Example 4: Static layout fields will retain their instance after onDes
Next we are going to want to save our login progress state in the saved state instance for when the view is destroyed.
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(SAVED_PROGRESS, mSignInProgress); }
Code Example 5: onSaveInstanceState method to save the login progress
And retrieve it in onCreate and gray out the Buttons and set the login status accordingly.
//Retrieve saved state for sign in progress if (savedInstanceState != null) { mSignInProgress = savedInstanceState.getInt(SAVED_PROGRESS, STATE_DEFAULT); } //If in middle of sign-in, gray out the buttons if(mSignInProgress==STATE_IN_PROGRESS){ mStatus.setText(R.string.status_signing_in); mSignInButton.setEnabled(false); mRegisterButton.setEnabled(false); }
Code Example 6: Get the saved state and update the UI
The above works fine when you are rotating an activity, but the register screen is a DialogFragment (see Figure 4). We can still do the same saved state instance and retrieve it in the onCreateView method instead, but to save the instance of our status and Buttons, the process is different.
In the onCreate method call the following
setRetainInstance(true);
Code Example 7: Prevent a Fragment from being re-created
However, there is a bug in android that will dismiss the dialog on rotate when setRetainInstance is true, so override the onDestroyView method to prevent the dialog from being dismissed.
@Override public void onDestroyView(){ Dialog dialog = getDialog(); if ((dialog != null) && getRetainInstance()) dialog.setDismissMessage(null); super.onDestroyView(); }
Code Example 8: Don’t dismiss a fragment onDestroyView
Since we have a status TextView on the fragment as well, we need to make sure it exists before we try and update the field when the AsyncTask completes its job. You can do this by checking if the fragment isAdded(). As the user may also tap away from the fragment or even exit the sign-in intent entirely, you can show a dialogue themed activity over whatever view the user has navigated away to (see Figure 5). To enable this you have to save the context of the DialogueFragment as it will be detached and hence the context normally null.
Create a static reference to the context in your class.
static Context context;
Code Example 9: Static reference to context for when fragment is detached
Initialize it in the onCreate method:
context= getActivity().getApplicationContext();
Code Example 10: Save the Fragment’s context
Then put it all together in the onPostExecute of the register AsyncTask to inform the user that they have either been registered or there was an error. Part two will go more in-depth on the AsyncTask logic.
@Override protected void onPostExecute(String result) { mRegisterProgress= STATE_DEFAULT; //if no errors if(result.equals("")){ //fragment is visible and attached if(isAdded()){ dismiss(); //call came from manager access screen or from a regular log in? if(from.equals(MANAGER_LEVEL)){ LoginManagerViewActivity.userManageStatus.setText(R.string.status_registered); }else{ LoginViewActivity.mStatus.setText(R.string.status_registered); } //fragment not visible, display dialogue intent }else{ Intent intent = new Intent(context, OrderViewDialogue.class); intent.putExtra(LoginViewActivity.DIALOGUE_MESSAGE, context.getResources().getString(R.string.status_registered)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } //else there is an error }else{ //fragment is visible and attached if(isAdded()){ registerStatus.setText(String.format(getString(R.string.status_register_error,result))); //fragment not visible, display dialogue intent }else{ Intent intent = new Intent(context, OrderViewDialogue.class); intent.putExtra(LoginViewActivity.DIALOGUE_MESSAGE, String.format(context.getResources().getString(R.string.status_register_error,result))); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } register.setEnabled(true); } }
Code Example 11: Handle result from the register HTTP server call
Figure 4: Register DialogFragment
Figure 5: Intent message that shows registration complete
Dynamic Navigation Drawer
When adding login capabilities to your application, we want the different users to be able to access various features specialized for their role. The manager especially can have a whole range of tools to help organize and run the restaurant, while the customer can have an account to earn rewards and coupons.
We will start with editing the navigation drawer on login to hide or show items by creating different string arrays in the “res/values/strings.xml” folder with the different titles for each type of user.
<string-array name="drawer_titles_guest"><item>Main</item><item>Locations and Geofences</item><item>My Little Chef Club</item></string-array><string-array name = "drawer_titles_customer"><item>Main</item><item>My Coupons</item><item>Ordering history</item><item>Locations and Geofences</item><item>My Little Chef Club</item></string-array>
Code Example 12: Navigation drawer titles for a guest vs. a registered customer user
You can then reference the different drawer titles in the setUpNavigationDrawer() method using a switch statement based on user access level.
if(currentUser==null){ mDrawerTitles = getResources().getStringArray(R.array.drawer_titles_guest); }else{ switch (currentUser.accessLevel){ case UserFactory.MANAGER_ACCESS: mDrawerTitles = getResources().getStringArray(R.array.drawer_titles_manager); break; case UserFactory.SERVER_ACCESS: mDrawerTitles = getResources().getStringArray(R.array.drawer_titles_server); break; case UserFactory.CUSTOMER_ACCESS: mDrawerTitles = getResources().getStringArray(R.array.drawer_titles_customer); break; default: mDrawerTitles = getResources().getStringArray(R.array.drawer_titles_guest); break; } }
Code Example 13: Initialize the naviation drawer depending on the user
Our navigation drawer’s onItemClick() method was also using hard coded position integer values to tell which title was clicked. Switching that to comparing the strings of the titles instead of the position will enable us to have a dynamic navigation drawer.
/** * This method is called when the user selects an item from navigation drawer */ public void onItemClick(AdapterView<?> parent, View view, int position, long id) { mDrawerList.setItemChecked(position, true); mSelectedDrawerIndex = position; mNavigationDrawer.closeDrawer(mDrawerList); String selectedTitle = mDrawerTitles[position]; if (selectedTitle.equals("Locations and Geofences")) { Intent intent = new Intent(this, GeolocationActivity.class); startActivity(intent); } else if (selectedTitle.equals("Charting")) { Intent intent = new Intent(this, ChartingActivity.class); startActivity(intent); }else if (selectedTitle.equals("My Little Chef Club")) { Intent intent = new Intent(this, LoyaltyMembershipActivity.class); startActivity(intent); } }
Code Example 14: Navigation drawer compares titles instead of position to tell what is clicked
After that write a simple handler interface for the main activity to implement with a method that will call setUpNavigationDrawer() again. Then from inside the class that handles the login logic, initialize the handler, and trigger the callback when the user logs in. The only things to watch out for are that if the login call is made on a AsyncTask like ours is, then the handler needs to be initialized to use the main thread’s looper to get access to the navigation drawer (new Handler(Looper.getMainLooper())). And the next thing to remember is to call it again once the user logs out.
In-App Menu Editing
On login we want the manager to be able to quickly edit the menu in-app without having to go to an external source. So the menu fields need to appear as TextViews to the customer, but as EditText to the manager. There are a number of different ways to handle this and this article will discuss how to hide them in plain sight.
When the view is created, we will change each EditText view to look like a TextView by making the background null and disabling them. And then when the manager logs in, re-enable them and set the background back to make them easily identify as editable.
public void editTextPermission(boolean allow) { mTitle.setEnabled(allow); mDescription.setEnabled(allow); mNutrition.setEnabled(allow); mPrice.setEnabled(allow); if(allow){ mTitle.setBackgroundResource(android.R.drawable.editbox_background_normal); mDescription.setBackgroundResource(android.R.drawable.editbox_background_normal); mNutrition.setBackgroundResource(android.R.drawable.editbox_background_normal); mPrice.setBackgroundResource(android.R.drawable.editbox_background_normal); }else{ mTitle.setBackground(null); mDescription.setBackground(null); mNutrition.setBackground(null); mPrice.setBackground(null); } }
Code Example 15: Call to change EditText to have TextView or normal EditText behavoir
The EditText view will also need some more parameters set when it is first being initialized. The view is going to use the “Done” action on the keyboard as the trigger of when the manager is finished updating the field and ready to commit it to the menu. However when the field is setup with the IME_ACTION_DONE, it makes the field only able to be one line as we have replaced the next line option with the done option. We do not want the text field to scroll horizontally, so we will have to counter this by setting horizontal scrolling to false and setting the number of lines to the maximum possible. This way when the manager enters in a string longer than the screen, it will automatically wrap around as a new line underneath. Below is a generic edit text initialization method with all the logic so the class wouldn’t have the same code repeated over and over again for all four EditText fields. Its inputs are the EditText variable and the string that represents the menu item’s attribute with which it is being filled. For example, the item’s title would be initialized by calling setupEditText(mTitle, RestaurantDatabase.MenuColumns.NAME);.
//set up the EditText field //takes in the EditText and the corresponding database column name as the update field public void setupEditText(final EditText text, final String updateField){ text.setImeOptions(EditorInfo.IME_ACTION_DONE); text.setHorizontallyScrolling(false); text.setMaxLines(Integer.MAX_VALUE); text.setOnEditorActionListener(new OnEditorActionListener(){ @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { String newString= text.getText().toString(); //we stored original string as the tag when we first set it, hence we can get the old value easily String oldString= text.getTag().toString(); //unique key identifier of the item String titleText= mTitle.getTag().toString(); //update the database for the server RestaurantDatabase dB= new RestaurantDatabase(getActivity()); dB.updateMenuDatabase(titleText,newString,oldString,updateField); //update our local menu list MenuFactory mMenuCurrent= MenuFactory.getInstance(); mMenuCurrent.updateMenuItem(mCurrentItem.category,titleText,oldString,newString,updateField); //reset the tag as the new string text.setTag(newString); dismissKeyboard(); return true; } return false; } }); } public void dismissKeyboard(){ InputMethodManager inputManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); View view = getActivity().getCurrentFocus(); if (view != null) { inputManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } }
Code Example 16: EditText logic to update the menu
For the final touch, we do not want the keyboard to pop up by default, and instead only pop up when the manager actively taps on the EditText to change it. The EditText is going to steal the focus by default when the activity or fragment starts, so the main layout needs to be set to command the focus.
android:focusable="true" android:focusableInTouchMode="true"
Code Example 17: Make a UI element the focus
Figure 6: Editable menu for the manager
Summary
This article has gone over how to add login UI logic to your application and add some tweaks for different user access levels for the navigation drawer and in-app menu editing for the manager. Part two of this series will go into depth about how to handling HTTP calls to and from a server to handle the user login logic.
References
Building Dynamic UI for Android* Devices
About the Author
Whitney Foster is a software engineer at Intel in the Software Solutions Group working on scale enabling projects for Android applications.
*Other names and brands may be claimed as the property of others.
**This sample source code is released under the Intel Sample Source Code License Agreement