Objectives

Build a set of views to support signup/login and navigation. Incorporate suitable models. Introduce the Facade pattern to encapsulate Model access.

Setup

This is the archive of the las lab:

https://github.com/wit-design-patterns-2016/pacemaker-android/releases/tag/V2

The starting point for this lab is a small evolution on the last lab, can can be downloaded here:

https://github.com/wit-design-patterns-2016/pacemaker-android/releases/tag/V3

This version has a refactored package structure:

Also, the adapter is reconfigured to load a layout for each row:

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="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="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="org.pacemaker.controllers.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="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="org.pacemaker.controllers.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">

    <application
        android:name=".main.PacemakerApp"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name="org.pacemaker.controllers.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">
        </activity>
        <activity
            android:name="org.pacemaker.controllers.Login">
        </activity>
        <activity
            android:name="org.pacemaker.controllers.CreateActivity">
        </activity>
        <activity
            android:name="org.pacemaker.controllers.ActivitiesList">
        </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 introduce a new User class :

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<MyActivity> activities = new ArrayList<MyActivity>();
  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<MyActivity> activities = new ArrayList<MyActivity>();
  public  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 (MyActivity activity)
  {
    activities.add(activity);
  }

  public List<MyActivity> 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();
    MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    app.createActivity(activity);
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }

ActivitiesList

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

    app = (PacemakerApp) getApplication();

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

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

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

This should work now as expected.