Embedded Map & Orientation

Introduce a Google Map including necessary supporting resources such as Google Play Services Library and a personal API key. Additionally, support a landscape layout, validation of geolocation input, immediate mirroring any changes in this input in the view title and support for smaller small factor devices.

Preview

Here we shall embed a Google map in the ResidenceFragment class. This entails using:

The layout shall be refactored as shown here.

Figure 1: Embedding Google Map in ResidenceFragment

Additionally, we shall:

Figure 2: Landscape mode added. Landscape and Portrait scrollable

The instructions in this lab refer to development in debug mode. This means that the application you develop will not be suitable to publish on Play. Further information is available about this topic is available here and here.

Google Play

It is necessary to add Google Play Services to the project.

<uses-sdk android:minSdkVersion="16"/>
<meta-data 
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version"/>  

Figure 6: Addition to manifest file to facilitate use of google play services library

MyRent project has now been set up to reference Google Play Services.

Official documentation is available at the Google Maps Android API v2 page.

Important note: as an aid to portability it would help were you to locate the google-play-services_lib folder in the same containing folder as the MyRent project as shown here:

API Key

We recommend that you become familiar with the official documentation for Google Maps Android API v2.

It is necessary to obtain an API key before using the Google Maps API. This can be achieved as follows:

Figure 2: Create new project Figure 3: New Project

Figure 4: Create new key

Figure 5: Create an Android key

Figure 6: Create an Android key and configure allowed Android applications

Figure 7: Public API access

Figure 8: Turn on Google Maps Android API v2

Here is a summary of the data that should be retained securely:

Resources (Manifest)

In AndroidManifest.xml:

  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <!--  The following two permissions are not required to use
        Google Maps Android API v2, but are recommended. -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <meta-data
      android:name="com.google.android.maps.v2.API_KEY"
      android:value="TODO: Insert your API key here" />

    <meta-data 
      android:name="com.google.android.gms.version"
      android:value="@integer/google_play_services_version"/>  

Replace the placeholder TODO: Inert your API key here with your key, created in an earlier step.

Verify that the Google Play Services library has been included by selecting the project name in Eclipse Package Explorer, right-click to open Properties window and then add google_play_services_lib as shown in Figure 1.

Figure 1: Add google play services library

For reference here is the up-to-date manifest file:

<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" />

  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <!--  The following two permissions are not required to use
        Google Maps Android API v2, but are recommended. -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

  <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.ResidencePagerActivity"
        android:label="@string/app_name">

        <meta-data  
          android:name="android.support.PARENT_ACTIVITY"
          android:value=".activities.ResidenceListActivity"/>
    </activity>  

    <meta-data 
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version"/>  

    <meta-data
      android:name="com.google.android.maps.v2.API_KEY"
      android:value="<TODO: Insert your API key here" />

    <meta-data 
      android:name="com.google.android.gms.version"
      android:value="@integer/google_play_services_version"/>     
  </application>

</manifest>

Resources (Layout)

Refactor the layout of the detail view:

Figure 1: ResidenceFragment layout

Filename: 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" >   
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="60"
      android:baselineAligned="false"
      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:layout_marginLeft="16dp"
          android:layout_marginRight="16dp"
          android:layout_marginTop="16dp"
          android:enabled="false" />

      </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>   
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="40"
    android:baselineAligned="false"
    android:orientation="vertical" >

    <FrameLayout
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="0.88" />
  </LinearLayout>
</LinearLayout>

Figure 2 should help you to understand the changes made here.

Figure 2: Refactored layout

Model

Before we introduce the map code we shall modify the Residence model class allowing it to store the current zoom level of the map.

Here are the code snippets to be added to Residence.java:

    //New fields
    public double  zoom       ;//zoom level of accompanying map
    private static final String JSON_ZOOM           = "zoom"          ; //map zoom level

Constructors:

  public Residence()
  {
    ...
    zoom        = 16.0f;

  }

  public Residence(JSONObject json) throws JSONException
  {
    ...
    zoom          = json.getDouble(JSON_ZOOM);
  }

toJSON method

  public JSONObject toJSON() throws JSONException
  {
    ...
    json.put(JSON_ZOOM          , zoom);

    return json;
  }

Helpers

Add this helper class to the org.wit.android.helpers package:

package org.wit.android.helpers;

import android.content.Context;
import com.google.android.gms.maps.model.LatLng;


public class MapHelper
{
  public static LatLng latLng(Context context, String geolocation)
  {
    String[] g = geolocation.split(",");
    if (g.length == 2)
    {
      return new LatLng(Double.parseDouble(g[0]), Double.parseDouble(g[1]));
    }
    return new LatLng(0, 0);

  }

  public static String latLng(LatLng geo)
  {

    return String.format("%.6f", geo.latitude) + ", " + String.format("%.6f", geo.longitude);
  }

}

The method LatLng latLng(String geolocation) returns a LatLng object containing the latitude and longitude coordinates contained in the String geolocation.

The method String latLng(LatLng geo) returns a single string version of the coordinates contained in a LatLng object formed by concatenating the coordinates but separating them with a comma. For example:

52.253456,-7.187162

LatLng is a Google class representing a pair of latitude and longitude coordinates stored as degrees.

Please note that the use of a Context type as an argument above is not necessary at this time. We are including it here because it will be required later when we introduce validation and wish to generate Toast messages.

ResidenceFragment

Add imports

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.SupportMapFragment;

Add a method to initialize the map fragment:

  private void initializeMapFragment()
  {
    FragmentManager fm = getChildFragmentManager();
    mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
    if (mapFragment == null)
    {
      mapFragment = SupportMapFragment.newInstance();
      fm.beginTransaction().replace(R.id.map, mapFragment).commit();
    }
  }

This fragment may be initialized only when the containing fragment has been created (ResidenceFragment).

  @Override
  public void onActivityCreated(Bundle savedInstanceState) 
  {
    super.onActivityCreated(savedInstanceState);
    initializeMapFragment();
  }

Disable mapButton and associated statements:

private Button mapButton;

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

mapButton .setOnClickListener(this);

Add instance variables;

  SupportMapFragment mapFragment;
  GoogleMap gmap;
  Marker marker;
  LatLng markerPosition;
  boolean markerDragged;

Note the boolean flag above (markerDragged).

We use the flag to avoid conflict that would otherwise arise as follows:

To avoid this potential cyclical behaviour, replace the afterTextChanged(Editable) method with the following:

  @Override
  public void afterTextChanged(Editable c)
  {
    String thisGeolocation = c.toString();
    Log.i(this.getClass().getSimpleName(), "geolocation " + thisGeolocation);
    residence.geolocation = thisGeolocation;
    getActivity().setTitle(thisGeolocation);
    // using a flag, markerDragged, to avoid race condition
    if (markerDragged == true)
    {
      markerDragged = false;
    }
    else
    {
      renderMap(MapHelper.latLng(getActivity(), thisGeolocation));
    }
  }

Change class signature to implement:

GoogleMap.OnMarkerDragListener
GoogleMap.OnCameraChangeListener

New signature is:

public class ResidenceFragment extends SupportMapFragment implements TextWatcher, 
                                                        OnCheckedChangeListener, 
                                                        OnClickListener, 
                                                        DatePickerDialog.OnDateSetListener,
                                                        GoogleMap.OnMarkerDragListener,
                                                        GoogleMap.OnCameraChangeListener

This change necessitates implementation of associated interface methods which you may use QuickFix to resolve:

Here are the methods which we have, where applicable, fully implemented. First are the implementations of the OnMarkerDragListener interface:

  @Override
  public void onMarkerDrag(Marker arg0)
  {
  }

  @Override
  public void onMarkerDragEnd(Marker arg0)
  {
    residence.geolocation = MapHelper.latLng(arg0.getPosition());
    getActivity().setTitle(residence.geolocation);
    gmap.animateCamera(CameraUpdateFactory.newLatLng(arg0.getPosition()));
    markerDragged = true;
  }

  @Override
  public void onMarkerDragStart(Marker arg0)
  {
  }

Here is the implementation of the OnCameraChangeListener:

  @Override
  public void onCameraChange(CameraPosition arg0)
  {
    residence.zoom = arg0.zoom;
    markerPosition = MapHelper.latLng(getActivity(), residence.geolocation);
    if (marker != null)
    {
      marker.remove();
      marker = null;
    }

    marker = gmap.addMarker(new MarkerOptions().position(markerPosition).draggable(true).title("Residence").alpha(0.7f)
        .snippet("GPS : " + markerPosition.toString()));

  }

Next is the method to render the map:

  private void renderMap(LatLng markerPosition)
  {
    if (mapFragment != null)
    {
      gmap = mapFragment.getMap();
      if (gmap != null)
      {
        gmap.animateCamera(CameraUpdateFactory.newLatLngZoom(markerPosition, (float) residence.zoom));
        gmap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
        gmap.setOnMarkerDragListener(this);
        gmap.setOnCameraChangeListener(this);
      }
    }
  }

Next we override onStart() within which we invoke renderMap and register a listener to detect any changes to the map:

  @Override
  public void onStart()
  {
    super.onStart();
    renderMap(MapHelper.latLng(getActivity(), residence.geolocation));
    gmap.setOnCameraChangeListener(this);
  }

Ensure that super is called in onCreateView:

    super.onCreateView(inflater,  parent, savedInstanceState);

Test the app as follows:

If you modify the geolocation such that the input becomes invalid then the app will crash. Here is example of invalid data:

Figure 1: Example of invalid data

We can solve this by introducing a try-catch block to MapHelper.latLng(Context, String) as follows:

package org.wit.android.helpers;

import android.content.Context;
import android.widget.Toast;
import com.google.android.gms.maps.model.LatLng;
import java.lang.NumberFormatException;
import static org.wit.android.helpers.LogHelpers.info;

public class MapHelper
{
  public static LatLng latLng(Context context, String geolocation)
  {
    String[] g = geolocation.split(",");
    try
    {
      if (g.length == 2)
      {
        return new LatLng(Double.parseDouble(g[0]), Double.parseDouble(g[1]));
      }
    }
    catch (NumberFormatException e)
    {
      info(context, "Number format exception: invalid geolocation: " + e.getMessage());
    }
    Toast.makeText(context, "An invalid geolocation has been entered: defaulting to 0,0", Toast.LENGTH_SHORT).show();
    return new LatLng(0, 0);

  }

  public static String latLng(LatLng geo)
  {

    return String.format("%.6f", geo.latitude) + ", " + String.format("%.6f", geo.longitude);
  }

}

Add these import statements:

import java.lang.NumberFormatException;
import android.widget.Toast;

Test this validation as follows:

Figure 2: Incorrectly entered geolocation triggers Toast warning

The final task in this step is to immediately mirror the geolocation in the title as the input is changed (as shown in Figure 3).

Figure 3: Title mirrors geolocation input field

Simply add the following line of code at the end of onMarkerDragEnd.

geolocation.setText(residence.geolocation);

Run the app and test the features added above namely those that ensure that:

Scrollable (portrait)

Make the data-input portion of the details view scrollable.

Official documentation on ScrollView is available here:

Here is the refactoted 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" >   
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="60"
      android:baselineAligned="false"
      android:orientation="vertical" > 
      <ScrollView 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">  
        <LinearLayout 
          android:id="@+id/container"
          android:orientation="vertical" 
          android:layout_width="fill_parent"
          android:layout_height="wrap_content">   
          <!-- 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:layout_marginLeft="16dp"
              android:layout_marginRight="16dp"
              android:layout_marginTop="16dp"
              android:enabled="false" />        
            </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>
      </ScrollView>
    </LinearLayout>   
    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="40"
      android:baselineAligned="false"
      android:orientation="vertical" >

      <FrameLayout
          android:id="@+id/map"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"/>
    </LinearLayout>
</LinearLayout>

Replace the existing code with above and test that the details view is now scrollable:

    <activity 
        android:name=".activities.ResidencePagerActivity"
        android:label="@string/app_name"
        android:windowSoftInputMode="stateHidden|adjustResize" >

Figure 1 should help you to understand the changes made here.

Figure 1: Refactored layout includes ScrollView

Landscape

We shall now add a landscape layout.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal" android:baselineAligned="false">
  <LinearLayout
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="60"
    android:baselineAligned="false"
    android:orientation="vertical" >
    <ScrollView 
      android:layout_width="fill_parent"
      android:layout_height="wrap_content">  
      <LinearLayout 
        android:id="@+id/container"
        android:orientation="vertical" 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">   
       <!-- 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:layout_marginLeft="16dp"
            android:layout_marginRight="16dp"
            android:layout_marginTop="16dp"
            android:enabled="false" />      
        </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>
    </ScrollView>
  </LinearLayout>   
  <LinearLayout
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_weight="40"
    android:baselineAligned="false"
    android:orientation="vertical" >

    <FrameLayout
      android:id="@+id/map"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </LinearLayout>
</LinearLayout>

Test the scrollable feature in both portrait and landscape modes.

Archives

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