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.
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