Objectives

Build a set of views to support signup/login and navigation, Incorporate suitable models. Explore how flow-synchronisation works in this context.

Setup

This is v2 of the pacemaker-android project from the last android lab:

You can import this for reference purposes, or keep working with your own version.

Welcome View

Design a new activity to look like this:

You may end up with new entries in the strings.xml file:

res/values/strings.xml

    <string name="title_activity_welcome">Pacemaker</string>
    <string name="welcomeLogin">Login</string>
    <string name="welcomeSignup">Sign up</string>

And this layout:

res/layout/activity_welcome.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/welcomeLogin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="102dp"
        android:onClick="loginPressed"
        android:text="@string/welcomeLogin" />

    <Button
        android:id="@+id/welcomeSignup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:onClick="signupPressed"
        android:text="@string/welcomeSignup" />

</RelativeLayout>

Signup View

Design a new activity to look like this:

You may end up with new entries in the strings.xml file:

res/values/strings.xml

    <string name="title_activity_signup">Signup</string>
    <string name="signupTitle">Sign up for the Pacemaker</string>
    <string name="signupSubtitle">Enter details below</string>
    <string name="signupFirstname">First name</string>
    <string name="signupLastName">Last Name</string>
    <string name="signupEmail">Email</string>
    <string name="signupPassword">Password</string>
    <string name="signupRegister">Register</string>

And this layout:

res/layout/activity_signup.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".Signup" >

    <TextView
        android:id="@+id/signupTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="32dp"
        android:layout_marginTop="28dp"
        android:text="@string/signupTitle"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/signupSubtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/signupTitle"
        android:layout_below="@+id/signupTitle"
        android:layout_marginLeft="55dp"
        android:layout_marginTop="30dp"
        android:text="@string/signupSubtitle"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <EditText
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/signupSubtitle"
        android:layout_marginTop="40dp"
        android:ems="10"
        android:hint="@string/signupFirstname"
        android:inputType="textPersonName"/>

        <requestFocus />

    <EditText
        android:id="@+id/lastName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/firstName"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/firstName"
        android:ems="10"
        android:hint="@string/signupLastName"
        android:inputType="textPersonName" >

    </EditText>

    <EditText
        android:id="@+id/Email"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/lastName"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/lastName"
        android:ems="10"
        android:hint="@string/signupEmail"
        android:inputType="textEmailAddress" />

    <EditText
        android:id="@+id/Password"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/Email"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/Email"
        android:ems="10"
        android:hint="@string/signupPassword"
        android:inputType="textPassword" />

    <Button
        android:id="@+id/register"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="28dp"
        android:onClick="registerPressed"
        android:text="@string/signupRegister" />

</RelativeLayout>

Login View

Design a new activity to look like this:

You may end up with new entries in the strings.xml file:

res/values/strings.xml

    <string name="title_activity_login">Login</string>
    <string name="loginTitle">Login to Donation</string>
    <string name="loginSubtitle">You must be reigstered</string>
    <string name="loginSignin">Sign in</string>
    <string name="loginEmail">Email</string>
    <string name="loginPassword">Password</string>

And this layout:

res/layout/activity_login.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".Login" >

    <TextView
        android:id="@+id/loginTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:layout_marginTop="18dp"
        android:text="@string/loginTitle"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <TextView
        android:id="@+id/loginSubtitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/loginTitle"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/loginTitle"
        android:text="@string/loginSubtitle"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <EditText
        android:id="@+id/loginEmail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/loginSubtitle"
        android:layout_alignRight="@+id/loginSubtitle"
        android:layout_below="@+id/loginSubtitle"
        android:layout_marginTop="17dp"
        android:ems="10"
        android:hint="@string/loginEmail"
        android:inputType="textEmailAddress" >

        <requestFocus />
    </EditText>

    <EditText
        android:id="@+id/loginPassword"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/loginEmail"
        android:layout_alignRight="@+id/loginEmail"
        android:layout_below="@+id/loginEmail"
        android:ems="10"
        android:hint="@string/loginPassword"
        android:inputType="textPassword" />

    <Button
        android:id="@+id/login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:onClick="signinPressed"        
        android:text="@string/loginSignin" />

</RelativeLayout>

Activity Classes

These three classes will load the views we have just created. Place them in the org.pacemaker.conrollers package:

Welcome.java

package org.pacemaker.controllers;

import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class Welcome extends Activity
{
  PacemakerApp app;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_welcome);
    app = (PacemakerApp) getApplication();
  }

  public void loginPressed (View view) 
  {
    startActivity (new Intent(this, Login.class));
  }
  public void signupPressed (View view) 
  {
    startActivity (new Intent(this, Signup.class));
  }
}

Signup.java

package org.pacemaker.controllers;

import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;

public class Signup extends Activity 
{
  private PacemakerApp app;

  @Override
  protected void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_signup);
    app = (PacemakerApp) getApplication();
  }

  public void registerPressed (View view) 
  {
    TextView firstName = (TextView)  findViewById(R.id.firstName);
    TextView lastName  = (TextView)  findViewById(R.id.lastName);
    TextView email     = (TextView)  findViewById(R.id.Email);
    TextView password  = (TextView)  findViewById(R.id.Password);

    startActivity (new Intent(this, Login.class));
  }
}

Login.java

package org.pacemaker.controllers;

import org.pacemaker.R;
import org.pacemaker.main.PacemakerApp;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;

public class Login extends Activity
{
  PacemakerApp app;

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

  public void signinPressed (View view) 
  {
    app = (PacemakerApp) getApplication();

    TextView email     = (TextView)  findViewById(R.id.loginEmail);
    TextView password  = (TextView)  findViewById(R.id.loginPassword);

    startActivity (new Intent(this, CreateActivity.class));
  }
}

Manifest

These new activities must be registered in the AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.pacemaker"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="18"
        android:targetSdkVersion="18" />

    <application
        android:name="org.pacemaker.main.PacemakerApp"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <activity
            android:name="org.pacemaker.controllers.Welcome"
            android:label="@string/title_activity_welcome" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name="org.pacemaker.controllers.Signup"
            android:label="@string/title_activity_signup" >
        </activity>
        <activity
            android:name="org.pacemaker.controllers.Login"
            android:label="@string/title_activity_login">
            android:windowSoftInputMode="adjustResize|stateVisible" >
        </activity>        
        <activity
            android:name="org.pacemaker.controllers.CreateActivity"
            android:label="@string/app_name" >
        </activity>
        <activity
            android:name="org.pacemaker.controllers.ActivitiesList"
            android:label="@string/title_activity_activities_list" >
        </activity>
    </application>
</manifest>

Note that we have marked the Welcome activity as the on with the intent-filter MAIN

Run the application now. You should be able to navigate from view to view.

Models

We can simplify the Activity Model class to just this version:

{
  public Long id;
  public String type;
  public String location;
  public double distance;

  public Activity()
  {}

  public Activity(String type, String location, double distance)
  {
    this.type      = type;
    this.location  = location;
    this.distance  = distance;
  }
}

... and introduce a new User class, also kept very simple:

package org.pacemaker.models;

public class User 
{
  public Long   id;
  public String firstname;
  public String lastname;
  public String email;
  public String password;

  public User()
  {}

  public User(String firstname, String lastname, String email, String password)
  {
    this.firstname = firstname;
    this.lastname = lastname;
    this.email = email;
    this.password = password;
  } 
}

We already have a list of activities in the PacemakerApp class - we can introduce a map of users alongside it:

  public List<Activity>    actvities = new ArrayList<Activity>();
  public Map<String, User> users     = new HashMap<String, User>();

In Signup, we can create a new entry in this map :

  public void registerPressed (View view) 
  {
    TextView firstName = (TextView)  findViewById(R.id.firstName);
    TextView lastName  = (TextView)  findViewById(R.id.lastName);
    TextView email     = (TextView)  findViewById(R.id.Email);
    TextView password  = (TextView)  findViewById(R.id.Password);

    User user = new User (firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
    app.users.put(user.email, user);

    startActivity (new Intent(this, Login.class));
  }

... and in Login we can validate the user against these entries:

  public void signinPressed (View view) 
  {
    app = (PacemakerApp) getApplication();

    TextView email     = (TextView)  findViewById(R.id.loginEmail);
    TextView password  = (TextView)  findViewById(R.id.loginPassword);

    String emailStr    = email.getText().toString();
    String passwordStr = password.getText().toString();

    User user = app.users.get(emailStr);
    if (user != null && user.password.equals(passwordStr))
    {
      startActivity (new Intent(this, CreateActivity.class));
    }
    else
    {
      Toast toast = Toast.makeText(this, "Invalid Credentials", Toast.LENGTH_SHORT);
      toast.show();
    }
  }

We should be able to register and log in now. And if we provide invalid credentials, we should be kept out.

Facade

There are, of course, some serious problems with our app. The list of activities is not associated with any specific user, the user and activities themselves are not persisted.

These issues could be tackled piecemeal - in each of the respective views for instance, but we might be more advised strengthen some of the abstractions around user/activity management first.

This pattern here may be appropriate:

Instead of a very exposed set of data structures in PacemakerApp:

public class PacemakerApp extends Application
{
  public List<Activity>    actvities = new ArrayList<Activity>();
  public Map<String, User> users     = new HashMap<String, User>();

  @Override
  public void onCreate()
  {
    super.onCreate();
    Log.v("Pacemaker", "Pacemaker App Started");
  }
}

We could remodel this class to provide a encapsulated perspective - a Facade - to the these models:

public class PacemakerApp extends Application
{
  private List<Activity>    activities = new ArrayList<Activity>();
  private Map<String, User> users      = new HashMap<String, User>();
  private User              loggedInUser;

  public void registerUser(User user)
  {
    users.put(user.email, user);
  }

  public boolean loginUser(String email, String password)
  {
    loggedInUser = users.get(email);
    if (loggedInUser != null && !loggedInUser.password.equals(password))
    {
      loggedInUser = null;
    }
    return loggedInUser != null;
  }

  public void createActivity (Activity activity)
  {
    activities.add(activity);
  }

  public List<Activity> getActivities()
  {
    return activities;
  }

  @Override
  public void onCreate()
  {
    super.onCreate();
    Log.v("Pacemaker", "Pacemaker App Started");
  }
}

This will require a refactoring of most of the controllers:

Signup

  public void registerPressed (View view) 
  {
    TextView firstName = (TextView)  findViewById(R.id.firstName);
    TextView lastName  = (TextView)  findViewById(R.id.lastName);
    TextView email     = (TextView)  findViewById(R.id.Email);
    TextView password  = (TextView)  findViewById(R.id.Password);

    User user = new User (firstName.getText().toString(), lastName.getText().toString(), email.getText().toString(), password.getText().toString());
    app.registerUser(user);

    startActivity (new Intent(this, Login.class));
  }

Login

  public void signinPressed (View view) 
  {
    app = (PacemakerApp) getApplication();

    TextView email     = (TextView)  findViewById(R.id.loginEmail);
    TextView password  = (TextView)  findViewById(R.id.loginPassword);

    boolean loggedIn = app.loginUser(email.getText().toString(), password.getText().toString());
    if (loggedIn)
    {
      startActivity (new Intent(this, CreateActivity.class));
    }
    else
    {
      Toast toast = Toast.makeText(this, "Invalid Credentials", Toast.LENGTH_SHORT);
      toast.show();
    }
  }

CreateActivity

  public void createActivityButtonPressed (View view) 
  {  
    double distance = distancePicker.getValue();
    Activity activity = new Activity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    app.createActivity(activity);
  }

ActivitiesList

~~java protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activities_list);

app = (PacemakerApp) getApplication();

activitiesListView = (ListView) findViewById(R.id.activitiesListView);

List<Activity> activities  = app.getActivities();

ActivityAdapter activitiesAdapter = new ActivityAdapter(this,  activities);
activitiesListView.setAdapter(activitiesAdapter);
activitiesAdapter.notifyDataSetChanged();

} ~~~

This should work now as expected.

Archive

..of the app so far:

Exercises

Exercise 1:

Currenty, once logged in, there is no way for a user to log out again. Introduce this facility.

Exercise 2:

There is only a single activities list - and all activities are inserted into this list. Refactor such that we are seeing the activities associated with the logged in user.

As we are using a Facade, this should not require extensive changes on any of the controllers.