File IO

Introduce into MyRent a serialization mechanism to save | restore the residence list to and from a file. The app will load the contents of this file on launch and update the file if residence data is updated.

Residence

Continue building the MyRent app that you developed in the previous lab.

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

The Android has support for this format in its 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            = json.getLong(JSON_ID);
    geolocation   = json.getString(JSON_GEOLOCATION);
    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            , Long.toString(id));
    json.put(JSON_GEOLOCATION   , geolocation);
    json.put(JSON_DATE          , date);
    json.put(JSON_RENTED        , rented);
    return json;
  }

Complete refactored model Residence class for reference purposes:

package org.wit.myrent.models;

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

import java.util.Date;
import java.util.Random;


public class Residence
{
  public Long id;
  public Long date;

  //a latitude longitude pair
  //example "52.4566,-6.5444"
  public String geolocation;
  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()
  {
    id = unsignedLong();
    date = new Date().getTime();
    geolocation = "52.253456,-7.187162";
  }

  /**
   * Generate a long greater than zero
   * @return Unsigned Long value greater than zero
   */
  private Long unsignedLong() {
    long rndVal = 0;
    do {
      rndVal = new Random().nextLong();
    } while (rndVal <= 0);
    return rndVal;
  }

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

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

  public void setGeolocation(String geolocation)
  {
    this.geolocation = geolocation;
  }

  public String getGeolocation()
  {
    return geolocation;
  }

  public String getDateString() {
    return "Registered:" + dateString();
  }

  private String dateString() {
    String dateFormat = "EEE d MMM yyyy H:mm";
    return android.text.format.DateFormat.format(dateFormat, date).toString();
  }

}

PortfolioSerializer

In order to perform the actual serialization - we provide a new model 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 PortfolioSerializer class 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 require an import statement for info:

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

Add a method to add a residence to the list:

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

Here is the complete Portfolio class for reference purposes:

package org.wit.myrent.models;

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

import java.util.ArrayList;

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(Long id)
  {
    Log.i(this.getClass().getSimpleName(), "Long 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 residence)
  {
    residences.remove(residence);
    saveResidences();
  }
}

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.

Here is the 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, "MyRent 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:

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

Before concluding this lab we shall fix an issue that prevents the rented residence field from being serialized. Replace this statement:

    rented.setChecked(residence.rented);

with this:

    rented.setOnCheckedChangeListener(this);

It should now be possible to save and load the residences. Verify that this works - you will need to completely kill the app for this to be verified.

Figure 1: Kill app - remove from list

Here is the complete class for reference purposes:

package org.wit.myrent.activities;

import android.support.v7.app.AppCompatActivity;
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 org.wit.myrent.R;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Portfolio;
import org.wit.myrent.models.Residence;

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 AppCompatActivity implements TextWatcher,
    CompoundButton.OnCheckedChangeListener,
    View.OnClickListener,
    DatePickerDialog.OnDateSetListener

{
  private EditText geolocation;
  private Residence residence;

  private CheckBox rented;
  private Button dateButton;

  private Portfolio portfolio;

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

    geolocation = (EditText) findViewById(R.id.geolocation);
    residence = new Residence();

    // Register a TextWatcher in the EditText geolocation object
    geolocation.addTextChangedListener(this);

    dateButton = (Button) findViewById(R.id.registration_date);
    dateButton  .setOnClickListener(this);

    rented = (CheckBox) findViewById(R.id.isrented);

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

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

  @Override
  public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

  }

  @Override
  public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

  }

  @Override
  public void afterTextChanged(Editable editable) {
    residence.setGeolocation(editable.toString());
  }

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

  @Override
  public void onClick(View view) {
    switch (view.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.getTime();
    dateButton.setText(residence.getDateString());
  }

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

}

The application at the end of this lab is available for reference here: myrent-04