Fragments & Maps

Modern Android Activities tend to be composed of one or more Fragments - as opposed to consisting of a single layouts. This introduces a slightly more complicated creation process. However, using fragments opens up other opportunities we will explore in later labs.

Preview

This lab comprises the following:

Figure 2: Using external Maps application to identify residence's geolocation

ResidenceActivity

Much of the content of ResidenceActivity inherited from the previous version is being moved to its associated new fragment class, ResidenceFragment.

We now begin refactoring:

package org.wit.myrent.activities;


import org.wit.myrent.R;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;

public class ResidenceActivity extends FragmentActivity
{
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_container);

    FragmentManager manager = getSupportFragmentManager();
    Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
    if (fragment == null)
    {
      fragment = new ResidenceFragment();
      manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
    }
  }
}

Add a container for the associated fragment:

Filename: res/layout/fragment_container.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragmentContainer"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  />

ResidenceFragment

Here is the new ResidenceFragment class which comprises, mostly, code migrated from ResidencyActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.UUID;
import org.wit.android.helpers.ContactHelper;
import org.wit.android.helpers.IntentHelper;
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.content.Intent;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.app.DatePickerDialog;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.DatePicker;
import android.widget.EditText;
import static org.wit.android.helpers.IntentHelper.sendEmail;
import static org.wit.android.helpers.IntentHelper.navigateUp;


public class ResidenceFragment extends Fragment implements TextWatcher, 
                                                        OnCheckedChangeListener, 
                                                        OnClickListener, 
                                                        DatePickerDialog.OnDateSetListener
{
  public static   final String  EXTRA_RESIDENCE_ID = "myrent.RESIDENCE_ID";

  private static  final int     REQUEST_CONTACT = 1;

  private EditText geolocation;
  private CheckBox rented;
  private Button   dateButton;
  private Button   tenantButton;
  private Button   reportButton;

  private Residence   residence;
  private Portfolio   portfolio;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);

    UUID resId = (UUID)getActivity().getIntent().getSerializableExtra(EXTRA_RESIDENCE_ID);

    MyRentApp app = (MyRentApp) getActivity().getApplication();
    portfolio = app.portfolio; 
    residence = portfolio.getResidence(resId);  

  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
  {
    View v = inflater.inflate(R.layout.fragment_residence, parent, false);

    getActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
    addListeners(v);
    updateControls(residence);

    return v;
  } 

  private void addListeners(View v)
  {
    geolocation = (EditText) v.findViewById(R.id.geolocation);
    dateButton  = (Button)   v.findViewById(R.id.registration_date);
    rented      = (CheckBox) v.findViewById(R.id.isrented);
    tenantButton = (Button)  v.findViewById(R.id.tenant);
    reportButton = (Button)  v.findViewById(R.id.residence_reportButton);


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

  }

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

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

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

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    if (resultCode != Activity.RESULT_OK)
    {
      return;
    }
    else
      if (requestCode == REQUEST_CONTACT)
      {
        String name = ContactHelper.getContact(getActivity(), data);
        residence.tenant = name;
        tenantButton.setText(name);
      }   
  }

  @Override
  public void beforeTextChanged(CharSequence s, int start, int count, int after)
  { }

  @Override
  public void onTextChanged(CharSequence s, int start, int before, int count)
  {}

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

  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
  {
    residence.rented = isChecked;   
  }

  @Override
  public void onClick(View v)
  {
    switch (v.getId())
    {
      case R.id.registration_date      : Calendar c = Calendar.getInstance();
                                         DatePickerDialog dpd = new DatePickerDialog (getActivity(), this, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH));
                                         dpd.show();
                                         break; 
      case R.id.tenant                 : Intent i = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
                                         startActivityForResult(i, REQUEST_CONTACT);
                                         if (residence.tenant != null)
                                         {
                                           tenantButton.setText("Tenant: "+residence.tenant);
                                         }   
                                         break;
      case R.id.residence_reportButton : sendEmail(getActivity(), "", getString(R.string.residence_report_subject), residence.getResidenceReport(getActivity()));                           
                                         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());
  }
}

Rename res/layout/activity_residence.xml to the more appropriate name: fragment_residence.xml.

ResidenceListActivity

As is the case with ResidenceActivity, much of ResidenceListActivity's code is also moved to its new associated fragment class - ResidenceListFragment.

Here is the refactored ResidenceListActivity:

package org.wit.myrent.activities;

import org.wit.myrent.R;

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;


public class ResidenceListActivity extends FragmentActivity
{

  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_container);

    FragmentManager manager = getSupportFragmentManager();
    Fragment fragment = manager.findFragmentById(R.id.fragmentContainer);
    if (fragment == null)
    {
      fragment = new ResidenceListFragment();
      manager.beginTransaction().add(R.id.fragmentContainer, fragment).commit();
    }
  }
}

ResidenceListFragment

Here is the ResidenceListFragment class which comprises, mostly, code migrated from ResidencyListActivity in the previous version of MyRent:

package org.wit.myrent.activities;

import java.util.ArrayList;
import org.wit.android.helpers.IntentHelper;
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.widget.ListView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.TextView;
import android.widget.CheckBox;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.widget.AdapterView.OnItemClickListener;

public class ResidenceListFragment extends ListFragment implements OnItemClickListener
{
  private ArrayList<Residence> residences;
  private Portfolio portfolio;
  private ResidenceAdapter adapter;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setHasOptionsMenu(true);
    getActivity().setTitle(R.string.app_name);

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

    adapter = new ResidenceAdapter(getActivity(), residences);
    setListAdapter(adapter);

  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)
  {
    View v = super.onCreateView(inflater, parent, savedInstanceState);
    return v;
  }

  @Override
  public void onListItemClick(ListView l, View v, int position, long id)
  {
    Residence res = ((ResidenceAdapter) getListAdapter()).getItem(position);
    Intent i = new Intent(getActivity(), ResidenceActivity.class);
    i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, res.id);
    startActivityForResult(i,0);
  }

  @Override
  public void onResume()
  {
    super.onResume();
    ((ResidenceAdapter) getListAdapter()).notifyDataSetChanged();
  }

  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
  {
    super.onCreateOptionsMenu(menu, inflater);
    inflater.inflate(R.menu.residencelist, menu);
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item)
  {
    switch (item.getItemId())
    {
    case R.id.menu_item_new_residence:
      Residence residence = new Residence();
      portfolio.addResidence(residence);

      Intent i = new Intent(getActivity(), ResidenceActivity.class);
      i.putExtra(ResidenceFragment.EXTRA_RESIDENCE_ID, residence.id);
      startActivityForResult(i, 0);
      return true;

    default:
      return super.onOptionsItemSelected(item);
    }
  }

  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id)
  {
    Residence residence = adapter.getItem(position);
    IntentHelper.startActivityWithData(getActivity(), ResidenceActivity.class, "RESIDENCE_ID", residence.id);
  }

  class ResidenceAdapter extends ArrayAdapter<Residence>
  {
    private Context context;

    public ResidenceAdapter(Context context, ArrayList<Residence> residences)
    {
      super(context, 0, residences);
      this.context = context;
    }

    @SuppressLint("InflateParams")
    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
      LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      if (convertView == null)
      {
        convertView = inflater.inflate(R.layout.list_item_residence, null);
      }
      Residence res = getItem(position);

      TextView geolocation = (TextView) convertView.findViewById(R.id.residence_list_item_geolocation);
      geolocation.setText(res.geolocation);

      TextView dateTextView = (TextView) convertView.findViewById(R.id.residence_list_item_dateTextView);
      dateTextView.setText(res.getDateString());

      CheckBox rentedCheckBox = (CheckBox) convertView.findViewById(R.id.residence_list_item_isrented);
      rentedCheckBox.setChecked(res.rented);

      return convertView;
    }
  }
}

Build and install the app on a device and verify that it functions correctly.

Next we shall engage an external maps application.

Resources (Layout & Strings)

  <string name="show_map">Show Map</string>  
  <string name="geolocation">Geolocation</string>

Here is the modified layout:

Filename: res/layout/fragment_residence.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

  <!-- LOCATION -->
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/location"
    style="?android:listSeparatorTextViewStyle"/> 
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal" 
    android:baselineAligned="false">

    <!-- Geolocation (GPS Coords) -->      
    <LinearLayout
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:layout_weight="60"
      android:orientation="vertical" >

      <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:text="@string/geolocation" />

      <EditText   
        android:id="@+id/geolocation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="@string/geolocation_hint" >
       <requestFocus />   
      </EditText> 
    </LinearLayout> 
    <!-- Show Map Button -->          
    <LinearLayout
      android:layout_width="0dp"
      android:layout_height="match_parent"
      android:layout_weight="40"
      android:orientation="vertical" >

    <Button android:id="@+id/show_map"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/show_map"
      android:layout_marginTop="16dp"
      android:layout_marginLeft="16dp"
      android:layout_marginRight="16dp"/>  
  </LinearLayout>
  </LinearLayout>

  <!-- STATUS -->   
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/status"
    style="?android:listSeparatorTextViewStyle"/>

  <Button android:id="@+id/registration_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"/>

  <CheckBox
    android:id="@+id/isrented"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:enabled="true"
    android:focusable="false"
    android:gravity="center"
    android:text="@string/rented_checkbox_text"/>
  <Button
    android:id="@+id/tenant"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:text="@string/landlord" />  

  <Button android:id="@+id/residence_reportButton"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:text="@string/residence_report"/> 
</LinearLayout>

The changes are evident in the following Figure 1.

Figure 1: Layout before and following refactoring

Map

Add the following method to IntentHelper:

  public static void openPreferredLocationInMap(Activity parent, String location)
  {
    Uri geoLocation = Uri.parse("geo:0,0?").buildUpon().appendQueryParameter("q", location).build();

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setData(geoLocation);

    if (intent.resolveActivity(parent.getPackageManager()) != null)
    {
      parent.startActivity(intent);
    }
    else
    {
      LogHelpers.info(parent, "Couldn't call " + location + ", no receiving apps installed!");
    }
  } 

In the above code you will notice that we are using an implicit Intent.

Invoke this helper method from within ResidenceFragment by pressing the mapButton.

Here is the wiring to facilitate this. It should be added to ResidenceFragment:

Declare the map button:

  private Button   mapButton;

Add a map button listener:

   mapButton    = (Button)  v.findViewById(R.id.show_map);

Register the map button listener:

    mapButton   .setOnClickListener(this);

Add a handler in the existing onClick(...) method:

      case R.id.show_map               : IntentHelper.openPreferredLocationInMap(getActivity(), residence.geolocation);
                                         break;   

The following import statement will be necessary:

import org.wit.android.helpers.IntentHelper;

Test

Build and install the app on a phsical device or emulator:

Figure 1: Test map functionality

Archives

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