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.
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;
}
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);
}
}
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.
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;
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);
}
}
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.
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");
}
}
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.
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();
}
}
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;
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);
}
}
New we can make use of the helper we just introduced. We wish to support the 'up' style navigation:
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...
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());
}
}
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.
<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>