This is it. The home straight. The final part of my series on writing a camera app using Oxygene for Android. In this concluding part I shall look at making my application a well behaved Android citizen. As well as pointing out (and fixing) some mistakes I have made along the way, we will add a new capability.
Models of Re-Use
A lot of the code written so far could easily be put into a library to be called much more conveniently in the future, and that might be a good idea if we intended writing lots of camera apps.
But why would we want to do that ?
I suppose we might have another app in the future where we also needed to be able to capture pictures from the camera. But what if the user of our camera app really, really likes using our app to capture their photos ? We wouldn’t want to duplicate the entire app every time.
Android has a feature designed specifically to allow an application to provide services directly to other applications that wish to take advantage. I am sure you can think of any number of applications that include the ability to take a photo from the device camera. The Google+ app is one.
NOTE: The FaceBook Android app does not appear to use intents. When you post a photo from the FaceBook app it uses an internal camera function, ignoring any camera apps on your device.
In the Google+ app a user can directly share a new photo by selecting “Photo” from the main app screen:
This then presents the Google+ image gallery, and in the top right is an action bar button to capture a new image from the camera:
Selecting this takes us to the camera app on the device:
But I really, really like my app. It’s soooo much better than the default camera app. *cough*. I’d like to be able to use my app to take the picture, even when using the Google+ app (which of course knows nothing about my app).
Fortunately Android supports this through a system called Intents.
State Your Intentions
An application can invoke a service by stating that it requires a particular intent to be satisfied. Other applications can indicate the intents that they are capable of satisfying in their manifest.
When an application makes an intent request, the system checks to see what applications are available. If there is only one then this will be called into action. If there is more than one, then the user is asked which they would prefer to use.
If there are none, then the application making the request has to have some fall-back position and either tell the user they need to acquire a suitable app or provide at least a basic facility to meet the need itself.
I won’t be asking for intents in my camera app, but I will be satisfying an intent.
Intents have names, and in this case the intent is android.media.action.IMAGE_CAPTURE.
The first step is to add an entry to the application manifest so that the system knows our application can satisfy this intent. This is called an Intent Filter. We add the intent-filter entry to the Activity that we intend to be invoked to satisfy the intent upon request. In our simple app, this is the MainActivity:
<activity android:label="@string/app_name" android:name="nz.co.deltics.demo.camera.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.media.action.IMAGE_CAPTURE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
Suddenly the existing intent-filter entry that we perhaps took for granted makes a bit more sense. android.intent.action.MAIN identifies MainActivity as the activity in our application that is our application’s MAIN, or Home screen, to be used when launching the app.
Now we have identified that this same Activity is to be used when we respond to a request to capture an image.
If our application was more complex, we might have a different Activity for image capture, in addition to and separate from our MainActivity. The system will know that when it launches our application to take a picture it should launch directly into the image capture activity instead of our app “Home” screen.
With this declaration in our manifest (and my application installed on the device, obviously) when I select the “Take a Photo” action in Google+, my app is now presented as an option:
At this point if I select my app it will launch as normal and I can use it to take a photo, but Google+ won’t know anything about it when I do.
I need to make some changes to my app to make it co-operate with the intents system.
Just What Is Your Intention ?
The first step is identifying the intent with which our app was launched. If you have no specific filters other than the ability to be launched, you can probably just assume this is the intent. But my app now has two possible reasons to be launched. Two intents.
Most of the functionality of my app is the same, regardless of the intent. But there we will need to adapt some of the behaviour if launched with the IMAGE_CAPTURE intent, so we need to be able to identify that.
To test for a specific intent action we can simply write:
if Intent.Action = MediaStore.ACTION_IMAGE_CAPTURE then
This is another example of Oxygene presenting a “getter” method as a property, in this case “Intent” which actually corresponds to the getIntent() method that our MainActivity inherits from the Activity class.
NOTE: The MediaStore class is in the android.provider namespace so this was added to the uses list of the unit. I think I may have forgotten to mention some of the required namespaces that I have introduced along the way. Apologies for that if you’ve been trying to follow along.
The MediaStore.ACTION_IMAGE_CAPTURE constant took a bit of tracking down. Many action identifiers are constants of the Intent class, but since Intent actions can be introduced and defined as needed, these are not an exhaustive list.
This does mean however that if we devised some entirely new application service that other applications might find useful that is not already defined by the Android system, we can filter for our own action and simply document the action name that other applications developers should use if they wish to use that intent.
Satisfying An Intent
When responding to a particular intent it isn’t very useful if you are not doing what the intent, um, intends. Sometimes this is implicit in the action itself, but in other cases an action may be accompanied by additional data that an application is expected to take into account when responding. When provided, this additional data is supplied in an Extras object, as part of the Intent.
This should be described as part of the documentation for the intent action itself. In the case of the MediaStore.ACTION_IMAGE_CAPTURE action there is an optional EXTRA_OUTPUT item that may or may not be supplied. This means we have to allow for two possibilities:
- No EXTRA_OUTPUT – return a thumbnail
- EXTRA_OUTPUT supplied – place the capture image in the specified location
I’m going to cheat a little here.
I know, from inspecting the Intent.Extras in the debugger, that the Google+ app is one that passes an EXTRA_OUTPUT item, so I shall implement only this response and leave the alternate response as a later exercise (perhaps for a future article).
So when responding to this intent, the two changes I need to make are:
- Store the image in the file specified by the Uri in EXTRA_OUTPUT
- Notify the calling activity that the intent response is available and return to that calling activity
These are both in the captureClick() method implementation and both are relatively simple, though the first is a bit messy (but the sort of mess that is easily packaged up in a re-usable library function for future, easier use). For reasons that will become apparent very soon, we’ll use a new local boolean variable to track the success or failure of our image capture which we will initialise (I haven’t yet looked at Oxygene initialisation rules for local variables, so this may not be necessary – but, just in case…).
First, creating the file in the required location:
successful := FALSE; if Intent.Action = MediaStore.ACTION_IMAGE_CAPTURE then begin if assigned(Intent.Extras) and Intent.Extras.containsKey(MediaStore.EXTRA_OUTPUT) then begin outputUri := Uri(Intent.Extras.get(MediaStore.EXTRA_OUTPUT)); if assigned(outputUri) then begin if outputUri.Scheme = 'file' then jpegFile := new File(outputUri.Path) else Log.e('Camera Demo', "Only file: Uri's are supported, sorry"); end else Log.e('Camera Demo', 'EXTRA_OUTPUT was unexpectedly null'); end else Log.i('Camera Demo', 'Thumbnail results not supported, sorry'); end else begin // Create default file in Pictures folder timestamp := new SimpleDateFormat('yyyyMMdd_HHmmss').format(new Date()); dir := Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); jpegFile := new File(dir.Path + File.separator + 'IMG_' + timestamp + '.jpg'); end;
The Intent.Extras object is a Bundle containing any named data that was supplied with the Intent by the caller. We are looking for one called MediaStore.EXTRA_OUTPUT. If found, the named item is a Uri object from which we obtain the full path.
Again a little bit of cheating here, since I am only supporting file: scheme Uri’s (which I determined is what Google+ provides). As a result almost half the added code is just emitting excuses to the Android Log (which will appear in our debugger output window, for example). Log.i() emits information. Log.e() emits an error.
The second and final change is notifying the calling action when we have completed the request and the image is ready. First we set the successful variable indicating when we have successfully saved the file. When responding to an intent, we then use this indicator to ensure we set the correct return code.
All this is achieved with a little bit of additional behaviour after we have saved the JPEG to the output stream:
if assigned(jpegFile) then begin stream := new FileOutputStream(jpegFile); stream.write(aPicture); stream.close; successful := FALSE; end; if Intent.Action = MediaStore.ACTION_IMAGE_CAPTURE then begin // If satisfying an intent, indicate a the appropriate result and // return to the calling app (our activity is finished) setResult(if successful then RESULT_OK else RESULT_CANCELED); finish; end; end;
I could have used the ability to call “setResult” as a “property” setter, but would have had to quality this with self to ensure that the compiler did not mistakenly think I was trying to set the method result value:
self.Result := if successful then RESULT_OK else RESULT_CANCELED;
At this point we see another Oxygene language feature demonstrated that deserves special mention: if expressions.
The value of the parameter passed to setResult is dependent upon the state of the successful Boolean. A common pattern in Delphi code on such occasions involves using a const array declaration, indexed by the selector value:
const RETURN_CODE : array[FALSE..TRUE] of Integer = (RESULT_OK, RESULT_CANCELED); // and then ... setResult(RETURN_CODE[successful]);
I for one much prefer Oxygene‘s if expression solution. 🙂
But, back to the new intent related code.
This extra behaviour only applies when responding to the ACTION_IMAGE_CAPTURE intent. We don’t have to actually make any calls to our caller though. The system knows that it launched our activity with an intent from another app, and simply by indicating that the result is OK and we have finished, the system can then return to that other app.
If we were returning information more directly (such as providing that thumbnail response when no EXTRA_OUTPUT is supplied by the caller) I believe the mechanism that allows us to do that involve passing a new Intent object back to the caller with an overloaded version of setResult().
But since I am not supporting that mechanism at this time, only the result code is being set.
And there we have it. Not only a camera app, but a camera app that will happily respond to requests from another application – should the user decide – to take a photo on their behalf.
Just one final thing to mention.
Debugging Intents
On a couple of occasions I referred to having figured out how the Google+ app was implementing it’s intent request by inspecting aspects of the supplied Intent in the debugger. This might have raised the question as to just how you go about debugging intents when an application is launched not by the debugger but by some other Android app that the debugger doesn’t even (necessarily) know about.
It is actually very straightforward. I didn’t even need to research this, I just tried what I thought would work and it did. 🙂
When I want to debug some aspect of my app when responding to an intent I simply launch the app on my device for debugging as normal with any breakpoint(s) set as appropriate (typically in a branch of the code reachable only when responding to the intent of interest).
Once the app has deployed and launched I simply navigate to the home screen of my device in order to launch the app that I am going to use to issue the intent (or re-activate it via the app list, if already running). In this case Google+. My app is still running, just in the background and not visible at this stage.
I perform whatever operations in the app that are required to trigger the intent request, at which point of course my app comes to the foreground once again and when the breakpoint is reached in my app, the Visual Studio/Oxygene debugger interrupts execution just as you would expect when a breakpoint is reached.
Simple.
Use The Source
As promised I am providing the full source for the app for download, so people can fully mock my efforts. 🙂
Demo Camera App for Android (48.7 KiB, 433 hits)
NOTE: This is the first time I’ve ever tried distributing a VisualStudio project source, let along Oxygene for Android. If I have missed anything please let me know a.a.s.p.
In this source you will notice some differences in a few areas, compared to the code previously posted over the course of this series. This is the result of a general tidy up and my having fixed a couple of mistakes that I realised I had made since I started this series:
- Camera should be releas()ed when app activity is paused (override MainActivity.onPause() method) and (re-)open()ed when resumed (override MainActivity.onResume() method – also called during initial startup)
- Do not explicitly stop preview before calling takePicture(). Camera will stop preview which will stop the preview itself. Also, do not re-start preview in onPictureTaken(), but immediately after calling takePicture()
- Added the code to correctly stop/restart preview when changing camera orientation on older Android versions
- Enable default camera shutter sound on capable Android versions
- Pretty icons 🙂
If there are any Android experts that read my code and spot anything else that I may have inadvertently cocked-up, please let me know (how and why).
Apart from trivial “hello world” exercises in the past, this is the first time I have ever even tried sitting down to write an actual Android app that actually did something. I have been surprised and delighted at both how straightforward it has proven using Oxygene not to mention how much fun it has been.
Caveat Developor
Things to be aware of if you are intending to try this app on your device:
- The use of ActionBar (as implemented) requires a Honeycomb Android device or later (3.0). Older versions of Android can be used but would require a bit of extra work (documented in the Android SDK if you are interested).
- The app assumes the use of camera #0. On my device this is the rear-facing camera. I don’t think that there is any guarantee that this will be the most appropriate camera on your device, and there is no facility for changing cameras.
- The intent support is incomplete. It works with the Google+ app but may not work with other apps.
Thanks, These post is very usefull for me.
But one to share: i have tested with a Nexus 7 (2012). The Rotation of the camera is alway 180 degrees wrong…..
Interesting. Related to this question on StackOverflow perhaps (also involving a Nexus 7). Unfortunately it doesn’t look like a definitive answer to that question was ever forthcoming and the orientation code the poster is using looks to be from the same source as the code I adapted.
As a result, I’m not sure whether it’s a bug in that code or something else which is oddly specific to the Nexus 7. Camera behaviours for some reason seem to be something that are highly susceptible to variations on different devices (even within the same product family).
if mCameraInfo.facing = camera.CameraInfo.CAMERA_FACING_FRONT then begin mCamera.DisplayOrientation := (mCameraInfo.orientation - angle + (360+180)) mod 360; end else mCamera.DisplayOrientation := (mCameraInfo.orientation - angle + (360)) mod 360;
seems to fix the Problem
That’s a bit odd. I did warn that the app assumes the use of camera #0 which may or may not be the rear facing camera. On your device is camera #0 the front camera ? Or was the rear camera on the Nexus reporting itself as a front camera ?
The nexus7 has only one, but mCamera is the actual so where is the problem ?
I’m also a beginner on oxygen and also Android so i’m not sure but for me it looks ok…..
Not a “problem” as such, just a curiosity. The fix you identified only appears to make a difference if the camera in use identifies itself as a front facing camera, but the camera on the Nexus 7 is rear facing, isn’t it ?
No it is on the front 😉
Aha – that would explain it then. 🙂