Writing a Real Android App from Scratch: Part 4/9 – Camera, Gallery, and Custom Action Bar

February 4, 2013

Welcome to part 4 of the Writing a Real Android App from Scratch series.

At this point, our app is capable of getting user’s current location and displaying the human-readable address for that location. Now, we need a way to add details to that location. The details, in our case, will contain a description, a category, and a picture. We already have the address so the user doesn’t have to enter any. Adding a description and a category is easy – we just need a text view, and a spinner view. Selecting a picture from gallery or taking a picture with the camera is more difficult, and more engaging. We have to lot to cover in this part – Gallery, camera, custom Action Bar, and other small details. I could have covered all of these one by one breaking into different tutorial parts but these are related, and we want to get one feature done in one part. So, this part the longest part of this tutorial series. Let’s get started.

Pre-requisite:

Plan of Action:

Hooking up the tag button:

At the end of previous part, we left hooking up the tag button that takes you to a different activity for adding details, and a picture. As discussed before, we want DefaultActivity to handle whatever it wants to do with the address. This means we will callback the DefaultActivity with the reverse geocoded address after the user selects the button. When I said callback, you might have guessed it already – we are going to use an interface that has just one public method – onAddressAvailable() and takes an Address as a parameter. But wait! We already have that interface – ReverseGeocodingListener, which declares the exact same method we are after. The only problem is its name — doesn’t sound quite right for what we want to do. Let’s change it.

1. Rename ReverseGeocodingListener class name to AddressResultListener. Don’t forget to change the name of the file itself as well as all the references to this interface in CurrentFragment, and ReverseGeocodingService classes.

2. Make DefaultActivity implement AddressResultListener interface:

1 //file: src/main/java/com.ashokgelal.tagsnap/DefaultActivity.java
2 public class DefaultActivity extends SherlockFragmentActivity implements AddressResultListener {
3 ...
4     @Override
5     public void onAddressAvailable(Address address) {
6     }
7 ...
8 }

As we know, the root activity of our app is DefaultActivity and CurrentFragment is a child of it. The DefaultActivity knows about the CurrentFragment but the reverse is not true. How can we then callback the DefaultActivity? By enforcing the parent activity to implement the AddressResultListener interface, which we already did in step 1 and 2 above. Now, inside the CurrentFragment we need to make sure that the parent, whoever it may be, needs to implement the AddressResultListener interface. The best place to do this is inside an overridden onAttach() method.

3. Override onAttach() method in CurrentFragment:

 1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
 2 ...
 3     private AddressResultListener mAddressResultListener;
 4 
 5     @Override
 6     public void onAttach(Activity activity) {
 7         super.onAttach(activity);
 8         try {
 9             mAddressResultListener = (AddressResultListener) activity;
10         } catch (ClassCastException e) {
11             throw new ClassCastException(activity.toString() + " must implement AddressResultListener");
12         }
13     }
14 ...

Finally, we can now hook up the tag button. All we need to set the action listener on the button.

4. Inside onActivityCreated() method, add the following line right before where we check whether savedInstanceState is null or not:

1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 ...
3     public void onActivityCreated(Bundle savedInstanceState) {
4         ...
5         mTagButton.setOnClickListener(this);
6         ...
7     }
8 ...

5. Make CurrentFragment implement View.OnClickListener interface, and then override onClick():

 1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
 2 public class CurrentFragment extends SherlockFragment implements LocationResultListener, AddressResultListener, View.OnClickListener {
 3 ...
 4     @Override
 5     public void onClick(View view) {
 6         if(mAddressResultListener != null && mLastKnownAddress != null)
 7             mAddressResultListener.onAddressAvailable(mLastKnownAddress);
 8     }
 9 ...
10 }

That’s all we need to pass back the address to DefaultActivity. Before we leave CurrentFragment let’s finish off something that we haven’t yet talked about – stopping the Location Managers that we instantiated in the previous part. Imagine a scenario where we have fired up the LastLocationFetcher timer and waiting for the last known location. During this waiting time the user decides to leave the app or switch to a different app. In this case, we need to make sure that we stop the timer and unregister from receiving any updates from either of the two radios.

6. In LocationService class, add a stop() method:

1 //file: src/main/java/com.ashokgelal.tagsnap/services/LocationService.java
2 ...
3    public void stop() {
4       if (mTimer != null)
5           mTimer.cancel();
6       mLocationManager.removeUpdates(mGpsLocationListener);
7       mLocationManager.removeUpdates(mNetworkLocationListener);
8    }
9 ...

If you want, you can also refactor out the two onLocationChanged() methods inside the LocationService() constructor to call the stop() method. Eventually, we need to call this stop() method from CurrentFragment.

7. In CurrentFragment class, override onStop() method:

1 //file: src/main/java/com.ashokgelal.tagsnap/CurrentFragment.java
2 ...
3     @Override
4     public void onStop() {
5         super.onStop();
6         if (mLocationService != null)
7             mLocationService.stop();
8     }
9 ...

That’s all for hooking up the tag button.

Adding details for current address:

We need an activity and a view for letting user add details and a picture. Let’s call this activity DetailsActivity. When the DefaultActivity receives an Address in onAddressAvailable() callback, we want to start the new DetailsActivity passing the address itself. When DetailsActivity is finished, we get back to the DefaultActivity again where we will extract the description, picture url, category information out of intent data. The recommended way of passing the data between different activities is putting each field into intent itself using key-value pairs. But this is ugly, as we need to remember the exact magic key strings when retrieving the values. A better approach is to have a model class and pass an instance of that model class instead. But this comes with another problem – you cannot just put any object to an Intent. All the primitive types are supported and few others. However, you can let your model class implement Parcelable to be able to put it to intent. We will go this route. First we need a model class; let’s call it TagInfo.

8. Create a new class TagInfo, and make it implement Parcelable. You also need to override few methods from Parcelable:

 1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
 2 public class TagInfo implements Parcelable{
 3     @Override
 4     public int describeContents() {
 5         return 0;
 6     }
 7 
 8     @Override
 9     public void writeToParcel(Parcel parcel, int i) {
10     }
11 }

We will now add required fields as well as the setters and getters.

9. Modify TagInfo class to add the following fields, getters, setters:

 1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
 2 public class TagInfo implements Parcelable{
 3 ...
 4 
 5     private String mDescription;
 6     private String mCategory;
 7     private Uri mPictureUri;
 8     private String mAddress1;
 9     private String mAddress2;
10     private double mLatitude;
11     private double mLongitude;
12     private long mId;
13 
14     public long getId() {
15         return mId;
16     }
17 
18     public void setId(long id) {
19         mId = id;
20     }
21 
22     public String getDescription() {
23         return mDescription;
24     }
25 
26     public void setDescription(String description) {
27         this.mDescription = description;
28     }
29 
30     public String getCategory() {
31         return mCategory;
32     }
33 
34     public void setCategory(String category) {
35         this.mCategory = category;
36     }
37 
38     public Uri getPictureUri() {
39         return mPictureUri;
40     }
41 
42     public void setPictureUri(Uri pictureUri) {
43         this.mPictureUri = pictureUri;
44     }
45 
46     public void setPictureUri(String uri) {
47         setPictureUri(Uri.parse(uri));
48     }
49 
50     public String getAddress1() {
51         return mAddress1;
52     }
53 
54     public void setAddress1(String address1) {
55         this.mAddress1 = address1;
56     }
57 
58     public String getAddress2() {
59         return mAddress2;
60     }
61 
62     public void setAddress2(String address2) {
63         this.mAddress2 = address2;
64     }
65 
66     public double getLatitude() {
67         return mLatitude;
68     }
69 
70     public void setLatitude(double latitude) {
71         this.mLatitude = latitude;
72     }
73 
74     public double getLongitude() {
75         return mLongitude;
76     }
77 
78     public void setLongitude(double longitude) {
79         this.mLongitude = longitude;
80     }
81 ...

10. Add 3 constructors; the one that takes a Parcel data is required to be able to parcel this class back and forth.

 1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
 2 ...
 3     public TagInfo(long id) {
 4         mId = id;
 5     }
 6 
 7     public TagInfo() {
 8         this(-1);
 9     }
10 
11     public TagInfo(Parcel data) {
12         setId(data.readLong());
13         setDescription(data.readString());
14         setCategory(data.readString());
15         setPictureUri(data.readString());
16         setAddress1(data.readString());
17         setAddress2(data.readString());
18         setLatitude(data.readDouble());
19         setLongitude(data.readDouble());
20     }
21 ...

Again, notice the last constructor. We are passed in the Parcel data during deserialization of this class. The deserialization order has to match with that of the serialization, which we will do next.

11. Modify describeMethods() method and writeToParcel() methods to:

 1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
 2 ...
 3     @Override
 4     public int describeContents() {
 5         // just returning the hash code should be enough
 6         return hashCode();
 7     }
 8 
 9     @Override
10     // the orders of 'writing' should match that of 'reading'
11     public void writeToParcel(Parcel parcel, int flags) {
12         parcel.writeLong(getId());
13         parcel.writeString(getDescription());
14         parcel.writeString(getCategory());
15         if (getPictureUri() == null)
16             parcel.writeString("");
17         else
18             parcel.writeString(getPictureUri().getPath());
19         parcel.writeString(getAddress1());
20         parcel.writeString(getAddress2());
21         parcel.writeDouble(getLatitude());
22         parcel.writeDouble(getLongitude());
23     }
24 ...

We are almost done. The final piece that is missing is a static Creator for creating a Parcelable data of type TagInfo and another type of an array of TagInfo

12. Add a static final Creator field at the end of the class:

 1 //file: src/main/java/com.ashokgelal.tagsnap/model/TagInfo.java
 2 ...
 3     public static final Creator<TagInfo> CREATOR = new Creator<TagInfo>() {
 4         @Override
 5         public TagInfo createFromParcel(Parcel parcel) {
 6             return new TagInfo(parcel);
 7         }
 8 
 9         @Override
10         public TagInfo[] newArray(int size) {
11             return new TagInfo[size];
12         }
13         
14     };
15 
16 ...

Now an object of this class can be sent back and forward from one activity to another as a Parcel.

Adding Details:

We want to pass an instance of TagInfo to DetailsActivity, which will be responsible for setting the description, category, and a picture uri for the TagInfo object, and then send it back.

13. Add a new layout details.xml, and replace the content with:

  1 <!-- file: res/layout/details.xml -->
  2 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
  3             android:layout_width="wrap_content"
  4             android:layout_height="wrap_content"
  5             android:id="@+id/scrollView">
  6             
  7     <RelativeLayout
  8         android:orientation="vertical"
  9         android:layout_width="fill_parent"
 10         android:layout_height="wrap_content">
 11 
 12         <EditText
 13             android:id="@+id/description"
 14             android:layout_width="match_parent"
 15             android:layout_height="wrap_content"
 16             android:layout_alignParentLeft="true"
 17             android:layout_alignParentTop="true"
 18             android:layout_marginTop="5dip"
 19             android:hint="@string/description"/>
 20 
 21         <TextView
 22             android:id="@+id/categoryHeader"
 23             style="@style/HeaderText"
 24             android:text="@string/category"
 25             android:layout_alignLeft="@+id/description"
 26             android:layout_below="@+id/description"/>
 27 
 28         <View
 29             style="@style/HeaderTextSeparator"
 30             android:layout_below="@+id/categoryHeader"
 31             android:id="@+id/categorySeparator"/>
 32 
 33         <Spinner
 34             android:layout_width="match_parent"
 35             android:layout_height="wrap_content"
 36             android:id="@+id/category"
 37             android:layout_alignParentLeft="true"
 38             android:layout_below="@+id/categoryHeader"
 39             android:entries="@array/categories_array"
 40             android:layout_marginTop="4dip"
 41             android:layout_marginLeft="4dip"
 42             android:spinnerMode="dialog"
 43             android:layout_marginRight="4dip"/>
 44 
 45         <RelativeLayout
 46             android:layout_width="match_parent"
 47             android:layout_height="wrap_content"
 48             android:layout_alignLeft="@+id/description"
 49             android:layout_below="@+id/category"
 50             android:layout_marginTop="10dip"
 51             android:id="@+id/relativeLayout">
 52 
 53             <ImageView
 54                 android:layout_width="170dip"
 55                 android:layout_height="128dip"
 56                 android:id="@+id/previewImage"
 57                 android:src="@drawable/photo_preview"
 58                 android:layout_marginTop="10dip"
 59                 android:layout_marginLeft="10dip"/>
 60 
 61             <ImageButton
 62                 android:layout_width="wrap_content"
 63                 android:layout_height="wrap_content"
 64                 android:id="@+id/cameraButton"
 65                 android:src="@drawable/add_from_camera"
 66                 android:layout_toLeftOf="@+id/galleryButton"
 67                 android:layout_alignParentTop="false"
 68                 android:layout_centerVertical="true"
 69                 android:visibility="gone"/>
 70 
 71             <ImageButton
 72                 android:layout_width="wrap_content"
 73                 android:layout_height="wrap_content"
 74                 android:id="@+id/galleryButton"
 75                 android:src="@drawable/add_from_gallery"
 76                 android:layout_alignParentTop="false"
 77                 android:baselineAlignBottom="false"
 78                 android:layout_centerVertical="true"
 79                 android:layout_alignParentRight="true"
 80                 android:layout_marginRight="10dip"/>
 81         </RelativeLayout>
 82 
 83         <TextView
 84             style="@style/HeaderText"
 85             android:id="@+id/locationHeader"
 86             android:text="@string/location"
 87             android:layout_width="wrap_content"
 88             android:layout_height="wrap_content"
 89             android:layout_alignLeft="@+id/relativeLayout"
 90             android:layout_below="@+id/relativeLayout"/>
 91 
 92         <View
 93             style="@style/HeaderTextSeparator"
 94             android:layout_below="@+id/locationHeader"
 95             android:id="@+id/locationSeparator"/>
 96 
 97         <TextView
 98             android:id="@+id/addressHeader"
 99             style="@style/LocationHeaderText"
100             android:text="@string/address"
101             android:layout_alignLeft="@+id/locationHeader"
102             android:layout_below="@+id/locationHeader"/>
103 
104         <TextView
105             android:id="@+id/detailsAddress1"
106             style="@style/LocationDetailsText"
107             android:layout_below="@+id/addressHeader"
108             android:layout_alignLeft="@+id/addressHeader"/>
109 
110         <TextView
111             android:id="@+id/detailsAddress2"
112             style="@style/LocationDetailsText"
113             android:layout_alignLeft="@+id/categoryHeader"
114             android:layout_below="@+id/detailsAddress1"/>
115 
116         <View
117             style="@style/LocationTextSeparator"
118             android:layout_below="@+id/detailsAddress2"
119             android:id="@+id/addressSeparator"/>
120 
121         <TextView
122             style="@style/LocationHeaderText"
123             android:id="@+id/latitudeHeader"
124             android:text="@string/latitude"
125             android:layout_width="wrap_content"
126             android:layout_height="wrap_content"
127             android:layout_alignLeft="@+id/addressHeader"
128             android:layout_below="@+id/addressSeparator"/>
129 
130         <TextView
131             style="@style/LocationDetailsText"
132             android:id="@+id/detailsLatitude"
133             android:layout_width="wrap_content"
134             android:layout_height="wrap_content"
135             android:layout_alignLeft="@+id/addressHeader"
136             android:layout_below="@+id/latitudeHeader"/>
137 
138         <View
139             style="@style/LocationTextSeparator"
140             android:layout_below="@+id/detailsLatitude"
141             android:id="@+id/latitudeSeparator"/>
142 
143         <TextView
144             style="@style/LocationHeaderText"
145             android:id="@+id/longitudeHeader"
146             android:text="@string/longitude"
147             android:layout_width="wrap_content"
148             android:layout_height="wrap_content"
149             android:layout_alignLeft="@+id/addressHeader"
150             android:layout_below="@+id/latitudeSeparator"/>
151 
152         <TextView
153             style="@style/LocationDetailsText"
154             android:id="@+id/detailsLongitude"
155             android:layout_width="wrap_content"
156             android:layout_height="wrap_content"
157             android:layout_alignLeft="@+id/addressHeader"
158             android:layout_below="@+id/longitudeHeader"/>
159 
160         <View
161             style="@style/LocationTextSeparator"
162             android:layout_below="@+id/detailsLongitude"
163             android:id="@+id/longitudeSeparator"/>
164 
165     </RelativeLayout>
166 </ScrollView>

Here we just have a bunch of widgets for allowing our user to input details as well as text views for displaying address details.

14. Open strings.xml and add following resources:

1 <!-- file: res/values/strings.xml -->
2     <string name="category">Category</string>
3     <string name="address">Address</string>
4     <string name="description">Description</string>
5     <string name="location">Location</string>
6     <string name="latitude">Latitude</string>
7     <string name="longitude">Longitude</string>

15. We also need few pre-made categories. For now, let’s have 4 categories. You can add more if you want. Add an string array resource to strings.xml file:

1 <!-- file: res/values/strings.xml -->
2     <string-array name="categories_array">
3         <item>Musuem</item>
4         <item>Park</item>
5         <item>Theatre</item>
6         <item>Historial Landmark</item>
7     </string-array>

16. Create a new file styles.xml under res/values/ and replace the content with:

 1 <!-- file: res/values/styles.xml -->
 2 <resources>
 3     <style name="HeaderText">
 4         <item name="android:textSize">14sp</item>
 5         <item name="android:layout_width">match_parent</item>
 6         <item name="android:layout_height">wrap_content</item>
 7         <item name="android:layout_marginLeft">12dip</item>
 8         <item name="android:layout_marginTop">15dip</item>
 9         <item name="android:textAllCaps">true</item>
10         <item name="android:textColor">#999999</item>
11         <item name="android:clickable">false</item>
12     </style>
13 
14     <style name="HeaderTextSeparator">
15         <item name="android:layout_width">match_parent</item>
16         <item name="android:layout_height">1dip</item>
17         <item name="android:background">#55CCCCCC</item>
18         <item name="android:layout_alignParentLeft">true</item>
19         <item name="android:layout_marginLeft">4dip</item>
20         <item name="android:layout_marginRight">4dip</item>
21         <item name="android:clickable">false</item>
22     </style>
23 
24     <style name="LocationHeaderText">
25         <item name="android:layout_width">wrap_content</item>
26         <item name="android:layout_height">wrap_content</item>
27         <item name="android:layout_marginTop">5dip</item>
28         <item name="android:textSize">12sp</item>
29         <item name="android:clickable">false</item>
30     </style>
31 
32     <style name="LocationDetailsText">
33         <item name="android:layout_width">wrap_content</item>
34         <item name="android:layout_height">wrap_content</item>
35         <item name="android:textSize">11sp</item>
36         <item name="android:textColor">#777777</item>
37         <item name="android:clickable">false</item>
38     </style>
39 
40     <style name="LocationTextSeparator">
41         <item name="android:layout_width">match_parent</item>
42         <item name="android:layout_height">0.1dip</item>
43         <item name="android:background">#555555</item>
44         <item name="android:layout_alignParentLeft">true</item>
45         <item name="android:layout_margin">4dip</item>
46         <item name="android:clickable">false</item>
47     </style>
48 </resources>

Here, to keep our code DRY, we have added a bunch of styles. Now, we have all the required resources for details activity, which we are going to add next.

17. Create a class DetailsActivity and make it extend SherlockActivity:

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 public class DetailsActivity extends SherlockActivity {
3 
4 }

18. Override onCreateMethod():

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3     @Override
4     public void onCreate(Bundle savedInstanceState) {
5         super.onCreate(savedInstanceState);
6         setContentView(R.layout.details);
7         setupDefaultValuesFromBundle();
8     }
9 ...

19. Add a setupDefaultValuesFromBundle() method:

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2 ...
 3     private void setupDefaultValuesFromBundle() {
 4         Bundle extras = getIntent().getExtras();
 5         mDescriptionTextView = (TextView) findViewById(R.id.description);
 6         mCategorySpinner = (Spinner) findViewById(R.id.category);
 7         mPreviewImage = (ImageView) findViewById(R.id.previewImage);
 8         mCurrentTaginfo = extras.getParcelable("taginfo");
 9 
10         mDescriptionTextView.setText(mCurrentTaginfo.getDescription());
11 
12         String cat = mCurrentTaginfo.getCategory();
13         if (cat != null && !cat.equals("")) {
14             ArrayAdapter<String> adapter = (ArrayAdapter<String>) mCategorySpinner.getAdapter();
15             mCategorySpinner.setSelection(adapter.getPosition(cat));
16         }
17 
18         Uri uri = mCurrentTaginfo.getPictureUri();
19         if (uri != null) {
20             File file = new File(uri.getPath());
21             if (file.exists()) {
22                 mSelectedPictureUri = uri;
23                 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
24             }
25         }
26 
27         ((TextView) findViewById(R.id.detailsAddress1)).setText(mCurrentTaginfo.getAddress1());
28         ((TextView) findViewById(R.id.detailsAddress2)).setText(mCurrentTaginfo.getAddress2());
29         ((TextView) findViewById(R.id.detailsLatitude)).setText(String.valueOf(mCurrentTaginfo.getLatitude()));
30         ((TextView) findViewById(R.id.detailsLongitude)).setText(String.valueOf(mCurrentTaginfo.getLongitude()));
31     }
32 ...

Here, all we are doing is setting the default values for each UI fields.

Although it is very rare nowadays, some of the Android devices still might not have a ‘physical’ camera for taking pictures. Our app needs to be aware of that. And so, if a camera is not available, we will hide the button for firing up camera intent. Let’s setup these two buttons – one for brining up the gallery and another for brining up the camera, if present.

20. Add a new method setupImageButtons():

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     private void setupImageButtons() {
 4         if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
 5             ImageButton cameraButton = (ImageButton) findViewById(R.id.cameraButton);
 6             cameraButton.setVisibility(View.VISIBLE);
 7             cameraButton.setOnClickListener(new View.OnClickListener() {
 8                 @Override
 9                 public void onClick(View view) {
10                     takePictureWithCamera();
11                 }
12             });
13         }
14         ImageButton galleryButton = (ImageButton) findViewById(R.id.galleryButton);
15         galleryButton.setOnClickListener(new View.OnClickListener() {
16             @Override
17             public void onClick(View view) {
18                 pickPictureFromGallery();
19             }
20         });
21     }
22     ...

First we check if we have camera feature — getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)), if so we set the visibility of the cameraButton to VISIBLE, and set the onClickListener. This button is hidden by default (check the res/layout/details.xml file).

21. Call setupImageButtons() from onCreate() after calling setupDefaultValuesFromBundle():

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2     ...
3     @Override
4     public void onCreate(Bundle savedInstanceState) {
5         ...
6         setupDefaultValuesFromBundle();
7         setupImageButtons();
8     }
9     ...

We are not going to write our own gallery app, or a camera app. Instead, we will request the built-in gallery to help us pick one picture for the user, which will be done using an intent of type Intent.ACTION_PICK, and listen for the result. This is not only convenient on our side, but different apps might have registered for handling the same type of intent. In which case, users will be able to use app of their choice to select an image. This painless integration of apps is one of the many features that make Android platform different and better than others.

Taking a picture from a camera is no different – we just need to fire an intent of type MediaStore.ACTION_IMAGE_CAPTURE and wait for the result. Let’s implement both of these features one by one.

22. Add a new method – takePictureWithCamera():

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     private static final int CAMERA_REQUEST = 0;
 4 
 5     private void takePictureWithCamera() {
 6         Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
 7         mSelectedPictureUri = getOutputMediaFile();
 8         intent.putExtra(MediaStore.EXTRA_OUTPUT, mSelectedPictureUri);
 9         startActivityForResult(intent, CAMERA_REQUEST);
10     }
11     ...

Here we started an activity with an intent of capturing an image. Later, we will be firing up another activity with an intent of picking a picture. To differentiate between these two different requests when the result is available, we need to pass an int ID; that’s what the CAMERA_REQUEST is.

We also need a path and a filename to save the picture captured from camera. For this purpose, we need a helper function that creates a uri to a file.

23. Add a new method getOutputMediaFile():

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     private static Uri getOutputMediaFile() {
 4         // get the Tagsnap directory
 5         File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
 6                 Environment.DIRECTORY_PICTURES), "Tagsnap");
 7 
 8         // Create the storage directory if it does not exist
 9         if (!mediaStorageDir.exists()) {
10             if (!mediaStorageDir.mkdirs()) {
11                 return null;
12             }
13         }
14 
15         // Create a media file name
16         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
17         File mediaFile = new File(mediaStorageDir.getPath() + File.separator +
18                 "IMG_" + timeStamp + ".jpg");
19 
20         return Uri.fromFile(mediaFile);
21     }
22     ...

Before starting to listen for result from camera intent, we will first add a method for firing ‘pick’ from gallery intent.

24. Add a new method – pickPictureFromGallery():

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2     ...
3     private static final int GALLERY_REQUEST = 1;
4     private void pickPictureFromGallery() {
5         Intent intent = new Intent(Intent.ACTION_PICK);
6         intent.setType("image/*");
7         startActivityForResult(intent, GALLERY_REQUEST);
8     }
9     ...

It’s same deal here. We fired up an intent of type ACTION_PICK set the type of file to image type, and start the activity with a request code of 1 — GALLERY_REQUEST. Now we are ready to listen for results from one the above intents.

  1. Override onActionResult() method:
 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     @Override
 4     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
 5         super.onActivityResult(requestCode, resultCode, data);
 6         if (resultCode == RESULT_OK) {
 7             if (requestCode == CAMERA_REQUEST) {
 8                 // set the picture
 9                 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
10             } else if (requestCode == GALLERY_REQUEST) {
11                 // parse the picture path with some cursor management
12                 Uri image = data.getData();
13                 String[] filePathColumn = {MediaStore.Images.Media.DATA};
14                 Cursor cursor = getContentResolver().query(image, filePathColumn, null, null, null);
15                 cursor.moveToFirst();
16                 int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
17                 String filePath = cursor.getString(columnIndex);
18                 cursor.close();
19 
20                 mSelectedPictureUri = Uri.fromFile(new File(filePath));
21                 // set the picture
22                 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
23             }
24         }
25     }
26     ...

If the result is coming back from the capture intent, then we just set the bitmap for our preview image widget. If the result is coming back from the pick intent, we have to do some extra work to find the correct path to the image that user selected, and set the image. It looks good but we have minor issue with this onActivityResult() method.

Notice in the line:

mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));

we are decoding the file from mSelectedPictureUri that we had saved before firing up the capture intent. The intent brings up a totally new activity – camera’s capture activity. This means the DetailsActivity gets killed, and when it is recreated, the mSelectedPictureUri will be no more available. We need to make sure to save this uri, in onSaveInstanceState(), and restore it in onCreate(). But be careful! If we are returning from the gallery activity then mSelectedPictureUri will be null so we have to take care of that too.

26. Override onSaveInstanceState() method:

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2     ...
3     @Override
4     protected void onSaveInstanceState(Bundle outState) {
5         super.onSaveInstanceState(outState);
6         outState.putParcelable("camera_output_path", mSelectedPictureUri);
7     }
8     ...

27. At the end of the onCreate() method, append:

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     public void onCreate(Bundle savedInstanceState) {
 4         ...
 5         if (savedInstanceState != null) {
 6             mSelectedPictureUri = savedInstanceState.getParcelable("camera_output_path");
 7             if(mSelectedPictureUri != null) 
 8                 mPreviewImage.setImageBitmap(BitmapFactory.decodeFile(mSelectedPictureUri.getPath()));
 9         }
10     }
11     ...

Except than adding the DONE and DISCARD menu items, we are almost done here. But first we need to set permission for using the camera in AndroidManifest.xml file.

28. In AndroidManifest.xml file, add the following permission right after the <uses-sdk> element:

1 <!-- file: AndroidManifest.xml -->
2     <uses-permission android:name="android.permission.CAMERA"/>

Allowing user to DONE/DISCARD changes with a custom ActionBar

I strong recommend you to read Roman Nurik’s post on Google+ before adding the DONE/DISCARD menu items to a custom ActionBar.

29. Start by adding a new actionbar_custom_view_done.xml layout:

 1 <!-- file: res/layout/actionbar_custom_view_done.xml -->
 2 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     style="?android:actionButtonStyle"
 4     android:id="@+id/actionbar_done"
 5     android:layout_width="0dp"
 6     android:layout_height="match_parent"
 7     android:layout_weight="1">
 8 
 9     <TextView style="?android:actionBarTabTextStyle"
10         android:layout_width="wrap_content"
11         android:layout_height="wrap_content"
12         android:layout_gravity="center"
13         android:paddingRight="20dp"
14         android:drawableLeft="@drawable/ done"
15         android:drawablePadding="8dp"
16         android:gravity="center_vertical"
17         android:text="@string/done" />
18 </FrameLayout>

This layout has a TextView that will be used as a DONE menu button.

30. Add two new string resources to strings.xml:

1 <!-- file: res/values/strings.xml -->
2     ...
3     <string name="done">Done</string>
4     <string name="discard">Discard</string>
5     ...

31. Add a new menu resource file, discard.xml:

1 <!-- file: res/menu/discard.xml -->
2 <menu xmlns:android="http://schemas.android.com/apk/res/android">
3     <item
4         android:id="@+id/discard"
5         android:title="@string/discard"
6         android:icon="@drawable/discard"
7         android:showAsAction="never"/>
8 </menu>

Now, we need to setup this custom action bar, and the discard menuitem in DetailsActivity.

32. Overrde onCreateOptionsMenu():

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2     ...
3     @Override
4     public boolean onCreateOptionsMenu(Menu menu) {
5         super.onCreateOptionsMenu(menu);
6         getSupportMenuInflater().inflate(R.menu.discard, menu);
7         return true;
8     }

33. Override onOptionsItemSelected() method:

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2     ...
 3     @Override
 4     public boolean onOptionsItemSelected(MenuItem item) {
 5         switch (item.getItemId()) {
 6             case R.id.discard:
 7                 setResult(RESULT_CANCELED);
 8                 finish();
 9                 return true;
10         }
11         return super.onOptionsItemSelected(item);
12     }
13     ...

When user selects discard menu item, we need to finish this activity after setting result to RESULT_CANCELED. This way any activity waiting for the result of this activity can act accordingly.

The last task remaining is to setup our custom action bar, and listen to done button’s click event.

34. Add a new method setupActionBar():

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
 2 ...
 3     private void setupActionBar() {
 4         LayoutInflater inflater = (LayoutInflater) getSupportActionBar().getThemedContext().getSystemService(LAYOUT_INFLATER_SERVICE); 
 5         final View customActionBarView = inflater.inflate(R.layout.actionbar_custom_view_done, null);
 6 
 7         customActionBarView.findViewById(R.id.actionbar_done).setOnClickListener(new View.OnClickListener() {
 8             @Override
 9             public void onClick(View view) {
10                 Intent data = new Intent();
11                 mCurrentTaginfo.setDescription(mDescriptionTextView.getText().toString());
12                 mCurrentTaginfo.setCategory(mCategorySpinner.getSelectedItem().toString());
13                 mCurrentTaginfo.setPictureUri(mSelectedPictureUri);
14                 data.putExtra("taginfo", mCurrentTaginfo);
15                 setResult(RESULT_OK, data);
16                 finish();
17             }
18         });
19 
20         // this is what makes the custom actionbar
21         final ActionBar actionBar = getSupportActionBar();
22         actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE);
23         actionBar.setCustomView(customActionBarView);
24     }
25 ...

When the user selects DONE button, we create a new intent, set the description, category, and picture uri for the current taginfo object. We then put this updated taginfo back in the intent, and finish the activity with result set to OK.

35. Call the setupActionBar() from the onCreate() method right after setContentView(R.layout.details):

1 //file: src/main/java/com.ashokgelal.tagsnap/main/DetailsActivity.java
2 ...
3     public void onCreate(Bundle savedInstanceState) {
4       ...
5       setupActionBar();
6       ...
7     }
8 ...

We are done with the DetailsActivity but we haven’t yet called it from the DefaultActivity.

36. Modify onAddressAvailable() method to:

 1 //file: src/main/java/com.ashokgelal.tagsnap/main/DefaultActivity.java
 2 ...
 3     @Override
 4     public void onAddressAvailable(Address address) {
 5         Intent intent = new Intent(this, DetailsActivity.class);
 6         TagInfo tagsnap = new TagInfo();
 7         if (address.getMaxAddressLineIndex() > 0)
 8             tagsnap.setAddress1(address.getAddressLine(0));
 9 
10         tagsnap.setAddress2(String.format("%s, %s, %s", address.getLocality(), address.getAdminArea(), address.getCountryName()));
11         tagsnap.setLatitude(address.getLatitude());
12         tagsnap.setLongitude(address.getLongitude());
13         intent.putExtra("taginfo", tagsnap);
14         startActivityForResult(intent, ADD_DETAILS_REQUEST);
15     }
16 ...

Here we create an explicit intent for firing up the DetailsActivity, which depends on an instance of TagInfo class. So, we create an instance of TagInfo class, put it in the intent, start the DetailsActivity for result. We won’t be handling the result coming back from the DetailsActivity in this part.

If you try to run the app and select the button for adding details, you will get a ActivityNotFoundException. If you read the error message carefully, it tells you what’s wrong – we have only defined our DetailsActivity class, but haven’t yet declared about this activity to Android system. Every activities in Android that intent to run at some point, should be declared in AndroidMainifest.xml file.

37. In AndroidManifest.xml, just before the closing </application> tag, add:

1 <!-- file: AndroidManifest.xml -->
2 ...
3     <activity
4         android:name=".DetailsActivity"
5         android:label="@string/details"/>
6 ...

38. Add a new string resource in strings.xml file:

1 <!-- file: res/values/strings.xml -->
2 ...
3     <string name="details">Details</string>
4 ...

Run the app and try it. Our app is now able to offer a new activity for adding description, a category, and a picture. We won’t do anything with this information for now. We will save these details to a local SQLite Database in the next part of this series.

You can follow me on Twitter, or add me on Google+.

Discussion, links, and tweets

By day, I ship code at MetaGeek, by night, I hack on my personal projects, and finally, when I get some off time in between, I also serve as a CTO for ClockworkEngine, LLC where so far we have launched two products - Spyglass and LightPaper. Call be a serial coder if you want.

comments powered by Disqus