File IO & Menu Up Navigation

Introduce into MyRent a serialization mechanism to save/restore the residence list to a file. The app will load the contents of this file on launch, and update the file if residence data is updates. In addition, continue to evolve the navigation support in the app, enabling a 'up' navigation in the action bar from the ResidenceActivity to the ResidenceListActivity.

Residence

The residence class will need an ability to save and restore itself to some external format. A convenient choice for this format is JSON:

The ADK has support for this format in the libaries:

import org.json.JSONException;
import org.json.JSONObject;

.. we should define in our classes appropriate names for each of the fields we wish to serialize:

  private static final String JSON_ID             = "id"            ;
  private static final String JSON_GEOLOCATION    = "geolocation"   ; 
  private static final String JSON_DATE           = "date"          ; 
  private static final String JSON_RENTED         = "rented"        ; 

This Residence class will need a new constructor to load a Residence object from JSON:

  public Residence(JSONObject json) throws JSONException
  {
    id            = UUID.fromString(json.getString(JSON_ID));
    geolocation   = json.getString(JSON_GEOLOCATION);
    date          = new Date(json.getLong(JSON_DATE));
    rented        = json.getBoolean(JSON_RENTED);
  }

... and a corresponding method to save an object to JSON:

  public JSONObject toJSON() throws JSONException
  {
    JSONObject json = new JSONObject();
    json.put(JSON_ID            , id.toString());  
    json.put(JSON_GEOLOCATION   , geolocation);
    json.put(JSON_DATE          , date.getTime());       
    json.put(JSON_RENTED        , rented);         
    return json;
  }

Complete Revised Class for References purposes:

package org.wit.myrent.models;

import java.text.DateFormat;
import java.util.Date;
import java.util.UUID;

import org.json.JSONException;
import org.json.JSONObject;

public class Residence
{
  public UUID id;

  public String  geolocation;
  public Date    date;
  public boolean rented;

  private static final String JSON_ID             = "id"            ;
  private static final String JSON_GEOLOCATION    = "geolocation"   ; 
  private static final String JSON_DATE           = "date"          ; 
  private static final String JSON_RENTED         = "rented"        ; 

  public Residence()
  {
    this.id          = UUID.randomUUID();
    this.date        = new Date();
    this.geolocation = "";
  }

  public Residence(JSONObject json) throws JSONException
  {
    id            = UUID.fromString(json.getString(JSON_ID));
    geolocation   = json.getString(JSON_GEOLOCATION);
    date          = new Date(json.getLong(JSON_DATE));
    rented        = json.getBoolean(JSON_RENTED);
  }

  public JSONObject toJSON() throws JSONException
  {
    JSONObject json = new JSONObject();
    json.put(JSON_ID            , id.toString());  
    json.put(JSON_GEOLOCATION   , geolocation);
    json.put(JSON_DATE          , date.getTime());       
    json.put(JSON_RENTED        , rented);         
    return json;
  }  

  public String getDateString()
  {
    return "Registered: " + DateFormat.getDateTimeInstance().format(date);
  }
}

PortfolioSerializer

In order to perform the actual serialization - we provide a new class to save / restore a list of Residences:

package org.wit.myrent.models;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONTokener;

import android.content.Context;

public class PortfolioSerializer
{
  private Context mContext;
  private String mFilename;

  public PortfolioSerializer(Context c, String f)
  {
    mContext = c;
    mFilename = f;
  }

  public void saveResidences(ArrayList<Residence> residences) throws JSONException, IOException
  {
    // build an array in JSON
    JSONArray array = new JSONArray();
    for (Residence c : residences)
      array.put(c.toJSON());

    // write the file to disk
    Writer writer = null;
    try
    {
      OutputStream out = mContext.openFileOutput(mFilename, Context.MODE_PRIVATE);
      writer = new OutputStreamWriter(out);
      writer.write(array.toString());
    }
    finally
    {
      if (writer != null)
        writer.close();
    }
  }

  public ArrayList<Residence> loadResidences() throws IOException, JSONException
  {
    ArrayList<Residence> residences = new ArrayList<Residence>();
    BufferedReader reader = null;
    try
    {
      // open and read the file into a StringBuilder
      InputStream in = mContext.openFileInput(mFilename);
      reader = new BufferedReader(new InputStreamReader(in));
      StringBuilder jsonString = new StringBuilder();
      String line = null;
      while ((line = reader.readLine()) != null)
      {
        // line breaks are omitted and irrelevant
        jsonString.append(line);
      }
      // parse the JSON using JSONTokener
      JSONArray array = (JSONArray) new JSONTokener(jsonString.toString()).nextValue();
      // build the array of residences from JSONObjects
      for (int i = 0; i < array.length(); i++)
      {
        residences.add(new Residence(array.getJSONObject(i)));
      }
    }
    catch (FileNotFoundException e)
    {
      // we will ignore this one, since it happens when we start fresh
    }
    finally
    {
      if (reader != null)
        reader.close();
    }
    return residences;
  }
}

Place this complete class in the 'models' package.

Portfolio

The portfolio class will now be equipped with the capability to use the serializer to save / restore the Residences it is managing. It will use the Serializee we just developed to do this.

First, introduce the serializer as a members of the Portfolio class:

  private PortfolioSerializer   serializer;

... then revise the constructor to take a serializer when it is being initialised:

  public Portfolio(PortfolioSerializer serializer)
  {
    this.serializer = serializer;
    try
    {
      residences = serializer.loadResidences();
    }
    catch (Exception e)
    {
      info(this, "Error loading residences: " + e.getMessage());
      residences = new ArrayList<Residence>();
    }
  }

We can now introduce a new method to save all the residence to disk:

  public boolean saveResidences()
  {
    try
    {
      serializer.saveResidences(residences);
      info(this, "Residences saved to file");
      return true;
    }
    catch (Exception e)
    {
      info(this, "Error saving residences: " + e.getMessage());
      return false;
    }
  }  

The above will requite we add import statement for info:

import static org.wit.android.helpers.LogHelpers.info;

Complete Revised Class for References purposes:

package org.wit.myrent.models;

import static org.wit.android.helpers.LogHelpers.info;

import java.util.ArrayList;
import java.util.UUID;

import android.util.Log;

public class Portfolio
{
  public  ArrayList<Residence>  residences;
  private PortfolioSerializer   serializer;

  public Portfolio(PortfolioSerializer serializer)
  {
    this.serializer = serializer;
    try
    {
      residences = serializer.loadResidences();
    }
    catch (Exception e)
    {
      info(this, "Error loading residences: " + e.getMessage());
      residences = new ArrayList<Residence>();
    }
  }

  public boolean saveResidences()
  {
    try
    {
      serializer.saveResidences(residences);
      info(this, "Residences saved to file");
      return true;
    }
    catch (Exception e)
    {
      info(this, "Error saving residences: " + e.getMessage());
      return false;
    }
  }

  public void addResidence(Residence residence)
  {
    residences.add(residence);
  }

  public Residence getResidence(UUID id)
  {
    Log.i(this.getClass().getSimpleName(), "UUID parameter id: "+ id);

    for (Residence res : residences)
    {
      if(id.equals(res.id))
      {
        return res; 
      }
    }
    info(this, "failed to find residence. returning first element array to avoid crash");
    return null;
  }

  public void deleteResidence(Residence c)
  {
    residences.remove(c);
  }
}

MyRentApp

The entire loading process is initially triggered by the application object, which is the entry point for our application.

First introduce a field to hold the file name we will use to store the portfolio:

  private static final String FILENAME = "portfolio.json";

Then we replace the current portfolio creation with the following:

    PortfolioSerializer serializer = new PortfolioSerializer(this, FILENAME);
    portfolio = new Portfolio(serializer);

Add an import statement for PortfolioSerializer:

import org.wit.myrent.models.PortfolioSerializer;

Run the app now. See if you can create some Residences. Can you determine if they are being saved?

To do this, it may not be enough to just exit the app and launch again - as you will be merely restoring the already active app. See if you can actually 'kill' the application.

If you manage to do this, you might find that the data is not actually saved. We will fix this in the next step.

Complete class for Reference Purposes:

package org.wit.myrent.app;

import static org.wit.android.helpers.LogHelpers.info;

import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.PortfolioSerializer;

import android.app.Application;

public class MyRentApp extends Application
{
  private static final String FILENAME = "portfolio.json";

  public Portfolio portfolio;

  @Override
  public void onCreate()
  {
    super.onCreate();
    PortfolioSerializer serializer = new PortfolioSerializer(this, FILENAME);
    portfolio = new Portfolio(serializer);

    info(this, "RentControl app launched");
  }
}

ResidenceActivity

One method of ensuring the data is in fact saved, will be to trigger a save when the user leaves the ResidenceActivity:

  public void onPause() 
  {
    super.onPause();
    portfolio.saveResidences();
  }

You should now be able to save/load the residences. Verify that this works now - you will need to completely kill the app for this to be verified.

Full class for Reference Purposes

package org.wit.myrent.activities;


import java.util.UUID;

import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

import android.app.Activity;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.CompoundButton.OnCheckedChangeListener;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import android.app.DatePickerDialog;
import android.view.View;
import android.view.View.OnClickListener;

public class ResidenceActivity extends Activity implements TextWatcher, OnCheckedChangeListener, OnClickListener, DatePickerDialog.OnDateSetListener
{
  private EditText geolocation;
  private CheckBox rented;
  private Button   dateButton;

  private Residence residence;
  private Portfolio portfolio; 

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_residence);

    geolocation = (EditText) findViewById(R.id.geolocation);
    dateButton  = (Button)   findViewById(R.id.registration_date);
    rented      = (CheckBox) findViewById(R.id.isrented);

    residence = new Residence();

    geolocation.addTextChangedListener(this);
    geolocation.setText(residence.geolocation);
    rented     .setChecked(residence.rented);
    rented     .setOnCheckedChangeListener(this);

    MyRentApp app = (MyRentApp) getApplication();
    portfolio = app.portfolio;

    UUID resId = (UUID) getIntent().getExtras().getSerializable("RESIDENCE_ID");
    residence = portfolio.getResidence(resId);
    if (residence != null)
    {
      updateControls(residence);
    }

    dateButton  .setOnClickListener(this);
  }

  public void updateControls(Residence residence)
  {
    geolocation.setText(residence.geolocation);
    rented.setChecked(residence.rented);
    dateButton.setText(residence.getDateString());
  }

  @Override
  public void onCheckedChanged(CompoundButton arg0, boolean isChecked)
  {
    Log.i(this.getClass().getSimpleName(), "rented Checked");
    residence.rented = isChecked;
  }

  @Override
  public void afterTextChanged(Editable c)
  {
    Log.i(this.getClass().getSimpleName(), "geolocation " + c.toString());
    residence.geolocation = c.toString();
  }

  @Override
  public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3)
  {
  }

  @Override
  public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3)
  {
  }

  @Override
  public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth)
  {
    Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
    residence.date = date;
    dateButton.setText(residence.getDateString());
  }


  @Override
  public void onClick(View v)
  {
    switch (v.getId())
    {
      case R.id.registration_date      : Calendar c = Calendar.getInstance();
                                         DatePickerDialog dpd = new DatePickerDialog (this, this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
                                         dpd.show();
                                         break; 
    }
  }

  public void onPause() 
  {
    super.onPause();
    portfolio.saveResidences();
  }

}

IntentHelper

In order to support enhanced navigation, we define an additional Helper method in the IntentHeloer class:

  public static void navigateUp(Activity parent)
  {
    Intent upIntent = NavUtils.getParentActivityIntent(parent);
    NavUtils.navigateUpTo(parent, upIntent);
  }

Add an import statement for NavUtils:

import android.support.v4.app.NavUtils;

Full Class for Reference Purposes

package org.wit.android.helpers;
import java.io.Serializable;

import android.app.Activity;
import android.content.Intent;
import android.support.v4.app.NavUtils;

public class IntentHelper
{
  public static void startActivity (Activity parent, Class classname)
  {
    Intent intent = new Intent(parent, classname);
    parent.startActivity(intent);
  }   

  public static void startActivityWithData (Activity parent, Class classname, String extraID, Serializable extraData)
  {
    Intent intent = new Intent(parent, classname);
    intent.putExtra(extraID, extraData);
    parent.startActivity(intent);
  }  

  public static void startActivityWithDataForResult (Activity parent, Class classname, String extraID, Serializable extraData, int idForResult)
  {
    Intent intent = new Intent(parent, classname);
    intent.putExtra(extraID, extraData);
    parent.startActivityForResult(intent, idForResult);
  }

  public static void navigateUp(Activity parent)
  {
    Intent upIntent = NavUtils.getParentActivityIntent(parent);
    NavUtils.navigateUpTo(parent, upIntent);
  }
}

ResidenceActivity

New we can make use of the helper we just introduced. We wish to support the 'up' style navigation:

Figure 1: New Residence menu item & Up button

First introduce the helper method into ResidenceActivity:

import static org.wit.android.helpers.IntentHelper.navigateUp;

Add the statement:

getActionBar().setDisplayHomeAsUpEnabled(true);

to onCreate following *setContentView(...) as indicated in the following code extract:

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_residence);
    getActionBar().setDisplayHomeAsUpEnabled(true);
    ...
  }

Now we override the onOptionsItemSelected method:

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case android.R.id.home:  navigateUp(this);
                               return true;
    }
    return super.onOptionsItemSelected(item);
  }

Try this now - does it work as in Figure 1 above. Not Yet! - we need one more step...

Full class for Reference Purposes

package org.wit.myrent.activities;


import static org.wit.android.helpers.LogHelpers.info;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.UUID;

import org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;
import static org.wit.android.helpers.IntentHelper.navigateUp;

import android.app.Activity;
import android.app.DatePickerDialog;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.CompoundButton.OnCheckedChangeListener;

public class ResidenceActivity extends Activity implements TextWatcher, OnCheckedChangeListener, OnClickListener, DatePickerDialog.OnDateSetListener
{
  private EditText geolocation;
  private CheckBox rented;
  private Button   dateButton;

  private Residence residence;
  private Portfolio portfolio; 

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_residence);
    getActionBar().setDisplayHomeAsUpEnabled(true);

    geolocation = (EditText) findViewById(R.id.geolocation);
    dateButton  = (Button)   findViewById(R.id.registration_date);
    rented      = (CheckBox) findViewById(R.id.isrented);

    geolocation.addTextChangedListener(this);
    dateButton  .setOnClickListener(this);
    rented     .setOnCheckedChangeListener(this);

    MyRentApp app = (MyRentApp) getApplication();
    portfolio = app.portfolio;    

    UUID resId = (UUID) getIntent().getExtras().getSerializable("RESIDENCE_ID");
    residence = portfolio.getResidence(resId);
    if (residence != null)
    {
      updateControls(residence);
    }   
  }

  public void updateControls(Residence residence)
  {
    geolocation.setText(residence.geolocation);
    rented.setChecked(residence.rented);
    dateButton.setText(residence.getDateString());
  }

  public void onPause() 
  {
    super.onPause();
    portfolio.saveResidences();
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
      case android.R.id.home:  navigateUp(this);
                               return true;
    }
    return super.onOptionsItemSelected(item);
  }

  @Override
  public void onCheckedChanged(CompoundButton arg0, boolean isChecked)
  {
    info(this, "rented Checked");
    residence.rented = isChecked;
  }

  @Override
  public void afterTextChanged(Editable c)
  {
    Log.i(this.getClass().getSimpleName(), "geolocation " + c.toString());
    residence.geolocation = c.toString();
  }

  @Override
  public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3)
  {
  }

  @Override
  public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3)
  {
  }

  @Override
  public void onClick(View v)
  {
    switch (v.getId())
    {
      case R.id.registration_date      : Calendar c = Calendar.getInstance();
                                         DatePickerDialog dpd = new DatePickerDialog (this, this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
                                         dpd.show();
                                         break; 
    }
  }

  @Override
  public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth)
  {
    Date date = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
    residence.date = date;
    dateButton.setText(residence.getDateString());
  }
}

AndroidManifest

The final piece of the puzzle is to layout in the manifest the parent/child relationship between the ResidenceListActivity and ResidenceActivity classes:

    <activity
      android:name=".activities.ResidenceActivity"
      android:label="@string/app_name" >  
      <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".activities.ResidenceListActivity"/>  
    </activity> 

In the above we are establishing this relationship. The navigation mechanism should work as expected now.

Full Manifest for Reference Purposes:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="org.wit.myrent"
  android:versionCode="1"
  android:versionName="1.0" >

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

  <application
    android:name=".app.MyRentApp"
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >

    <activity
      android:name=".activities.ResidenceListActivity"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>    

    <activity
      android:name=".activities.ResidenceActivity"
      android:label="@string/app_name" >  
      <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".activities.ResidenceListActivity"/>  
    </activity> 

  </application>

</manifest>

Archives

This is a version of MyRent complete to the end if this lab: