Saturday, November 5, 2016

Java Android Camera Example: Taking Photo & Video & Audio + Using Flashlight & Stroboscope

Downloads (Source + APK)

- Quick Way (Intent; Using Default Camera App; Recommended For Beginners & Advanced):

- Deep Dive (android.hardware.camera; Most Flexible Way; Recommended For Advanced):

- Useful Util Classes For Integrating Camera Into Your App (Recommended For Anybody Who Need It):

Contents What We Want. Goal

Our main goal is to learn Android Camera API. But learning can be faster & more interest if we will do it in examples.

Let's do these practical exercises:

  • Write simplest code to capture a bitmap photo from camera (next, this photo can be saved to file, displayed on screen using ImageView, sent by network, etc., and used in different ways, i.e. avatar, picture story, etc.).
  • Improve it for avatars.
  • Improve it to handle possible errors causing some software & hardware incompatibilities.
  • Draw conclusions about our "quick" way vs "deep" way.
  • What We Need. Libraries. Emulators

    I hope you noticed what almost every Android device has at least one camera at its back. You can buy very chip phone for $35-40, but it also have the back camera. Yes, photos will have very poor quality. But they quite suitable for avatars and etc. And, accordingly, such camera is suitable for our development & testing purposes.

    But notice what some Android devices also have a front camera. And it should be primary for selfies, including our goal - photos for avatars. Ideally, it should be default, but user should be able to use back camera, too, because it provides better quality, or user just want use it.

    In fine, it is strongly recommended to have device with both, front & back, cameras to learn & use Camera API. And also, ideally we should test in devices without any camera (we do not see them, but this isn't indicates that they does not exist on the Earth).

    All these configurations can be easy simulated using emulators, i.e. AVD and Genymotion.

    Following image is screenshot of such window in free version of Genymotion (it allows use hardware camera, i.e. web-camera of your laptop, i.e. "Webcam0" or emulate it via stubing with random video, i.e. "Dummy webcam"; we will explore it bit later):

    And this is same in AVD:

    But what about software aspect?

    Probably you know that Android original build includes default camera application. It indicates what we doesn't need any third-party libraries, because camera library included in OS. And more, the default camera application provides some Intents (it is traditional architecture pratice in Android), and if you want simply capture a photo & video, we doesn't need write code using a library, we can just use these Intents. Let's do it.

    What We Do. How To Create

    Tip: See also here.

    Simplest Code To Capture A Photo And Preview In ImageView

    0. At first, let's notice what this way requires no permissions. Yes - at all.

    To use camera directly, Android requiring android.permission.CAMERA. But don't forget: we will not use it directly, we will use default camera application, and this application already has this permission.

    1. To open capturing activity of camera app, we will use a standart pair of Intent with special action MediaStore.ACTION_IMAGE_CAPTURE, and startActivityForResult method. This code can be called from onClick and any other method of Activity class:

    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); this.startActivityForResult(intent, 1234);

    2. To handle result, we should:

  • Check a request code (said 1234 constant).
  • Check a result code (if user maked a photo and approved it, not canceled capturing activity).
  • Get the bitmap from Intent's extras, as Bitmap type.
  • Use this bitmap for some purpose, i.e. show in ImageView using setImageBitmap.
  • Let's do these:

    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1234) { if (resultCode == RESULT_OK) { Bitmap bmp = (Bitmap)data.getExtras().get("data"); imageView1.setImageBitmap(bmp); } else { Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show(); } } } Avatars. Forcibly Using Front Camera (If Exists)

    By default, our code uses back camera even if device has front one. It is not suitable for avatar purpose.

    Yes, default camera application should allow user to change it to front.

    But it isn't convenient. And we can improve it by using some trick. Just call this before startActivityForResult:

    intent.putExtra("android.intent.extras.CAMERA_FACING", 1);

    Or, if you targeting Lollilop or better:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { intent.putExtra("android.intent.extras.LENS_FACING_FRONT", 1); } else { intent.putExtra("android.intent.extras.CAMERA_FACING", 1); }

    It should change camera to front if device has it, and just still use if back if there only this one.

    Unfortunately, it is trick rather than official way, and not supported by some devices and with customized camera applications.

    If we need more stable result, we should access camera directly. This is more flexible, but more complex and hard way. I named it "Deep Dive" and described in separate chapter of this article.

    What If No Camera Or No Camera App

    What if no camera? Let's place this code before all intent works:

    if (!this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && !this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) { Toast.makeText(this, "Device haven't any camera", Toast.LENGTH_SHORT).show(); return; }

    What if no camera app?

    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { this.startActivityForResult(intent, 1234); } else { Toast.makeText(this, "Can't take a photo, please install valid camera application", Toast.LENGTH_SHORT).show(); } Obtaining Full-Sized Photo

    In my opinion, major usage of quick capturing are avatars.

    However, Android tasks are very different, and we can need a full-sized photo.

    Unfortunately, we can't just retrieve it as Bitmap, we need provide a path to file, it will be saved to this file, and next we can open it to retrieve a Bitmap.

    1. At first, we need a permission:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    2. Next, we need an Uri of file. We can got it using such way:

    File f = new File(Environment.getExternalStorageDirectory(), "test-fullsized6.jpg"); Uri photoURI = Uri.fromFile(f);

    2. Next, let's put in as extra for Intent:

    Intent intent = new Intent(... intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);

    3. Finally, now we, unfortunately, can't request a thumbnail by this:

    Quote:

    data.getExtras().get("data")

    Remove it.

    Simplest Code To Capture A Video And Preview In VideoView

    1. Replace ACTION_IMAGE_CAPTURE to ACTION_VIDEO_CAPTURE.

    2. Change the handler:

    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 1234) { if (resultCode == RESULT_OK) { Uri videoUri = intent.getData(); videoView1.setVideoURI(videoUri); } else { Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show(); } } } What We Got. Result

    See here.

    Previewing Video In Activity

    1. At first, we need add a permission to manifest:

    <uses-permission android:name="android.permission.CAMERA"/>

    Tip: If you have any problems of just forgot how to do add permissions properly - see this quick guide for beginners.

    2. Next, we need a Button which will start preview on click. It doesn't required to be exactly Button, you also can use an another way to start a preview, but Button is simplest for experiments).

    3. More important aspect is where we will show the video.

    For first try, Android has quite simple way - use the SurfaceView widget. In Eclipse it lies in Advanced tab.

    Put this widget into Activity and set to fill/match parent.

    4. In Button's onClick write (don't forgot at first import btnStartPreview and surfaceView1 using findViewById in onCreate, this is standard way for all Android widgets):

    Camera cam = Camera.open(); try { cam.setPreviewDisplay(surfaceView1.getHolder()); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "Some error, see logcat.", Toast.LENGTH_SHORT).show(); return; } cam.startPreview();

    5. Finally, build and run your project. It will preview the video from the back camera of your device:

    6. But, if we close our app, camera will still in use, we will can't open our app again, or default camera app, we will should restart device, or manually kill app's process with Settings > Applications > Our App's Name > Force Close.

    To prevent it, we should use onDestroy method:

    Camera cam = null; @Override public void onClick(View v) { ... cam = Camera.open... ... } @Override protected void onDestroy() { super.onDestroy(); if (cam != null) { cam.stopPreview(); cam.release(); cam = null; } }

    7. Another major problem, which so imperceptible at first sight, is camera's orientation. 

    Yes, Android Camera API defaultly returns the image in landscape orientation and does not changes it. It's right if you holding your phone/tablet horizontally. But if you holding it vertically, you will have serious problem with your app, and only close look can you help to detect it:

    This problem can be fixed, if we will programmatically check orientation of Activity (i.e. orientation of display), and set properly orientation of camera. We should do it before startPreview, and don't forget that landscape orientation can be right as well as left:

    if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { cam.setDisplayOrientation(90); } else { switch (this.getWindowManager() .getDefaultDisplay().getRotation()) { case Surface.ROTATION_90: cam.setDisplayOrientation(0); break; case Surface.ROTATION_270: cam.setDisplayOrientation(180); break; } }

    Now it works fine if you start app in "vertical" phone, in "left-/right-horizontal" phone, and if rotating phone with started preview.

    8. Finally, I'll describe another one camera problem, which can stop your work for half-hour or even few hours.

    It's problem with onCreate. If we will just call setPreviewDisplay and startPreview in Activity's onCreate, it will bring our camera down: your willn't crash with exception, but SurfaceView will still be empty.

    Same problem occures with onStart (and also onStart will cause another problem: on shutting display down by short power button press, Android will not call onDestroy, but when we will again press power button to turn display on, it will call onStart which will cause conflict and exception).

    At first, let's make our activity implementing SurfaceHolder.Callback. It will add 3 methods - surfaceCreated, surfaceChanged, and surfaceDestroyed.

    In surfaceCreated, we should set SurfaceHolder and start the Preview, i.e.:

    @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { try { cam.setPreviewDisplay(holder); } catch (IOException e) { e.printStackTrace(); } cam.startPreview(); }

    And in onCreate we should have this code:

    cam = Camera.open(); if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { cam.setDisplayOrientation(90); } else { cam.setDisplayOrientation(0); } SurfaceHolder holder = surfaceView1.getHolder(); holder.addCallback(this);

    Finally, onDestroy should stop preview and free the camera, same as in above examples:

    @Override protected void onDestroy() { super.onDestroy(); cam.stopPreview(); cam.release(); cam = null; }

    Now, basic preview feature working properly, and we can go next - to customize our camera settings.

    Force Camera To Be Front/Back

    One of major features of android.hardware.Camera API is standard possibility to set camera to front facing one.

    Simplest way is to change this line:

    Camera cam = Camera.open();

    To this:

    Camera cam = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);

    And, it will use front camera. But if the device doesn't have front camera, your app will crash with an exception:

    E/AndroidRuntime(6727): java.lang.RuntimeException: Fail to connect to camera service

    Also, it isn't documented. Officially, Camera's open method requiring ID of camera, and not facing id. They are similar on much of devices, but they aren't obliged to be similar...

    Some authors providing a simple solution for 1st problem:

    this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)

    But there are many devices, where no front camera, but it returns true.

    We can create more transparent & safe solution, if we will iterate an array of all cameras in device and check if it contains front camera:

    boolean hasFrontCamera = false; for (int i = 0; i < Camera.getNumberOfCameras(); i++) { Camera.CameraInfo ci = new Camera.CameraInfo(); Camera.getCameraInfo(i, ci); if (ci.facing == CameraInfo.CAMERA_FACING_FRONT) { hasFrontCamera = true; break; } } if (hasFrontCamera) ...

    Also, in this loop we can get the actual ID of camera (it will be equal to i counter), and call open(ID) for front camera, and open() for back.

    - Finished code:

    Camera cam = null; boolean hasFrontCamera = false; int idFront = 0; for (int i = 0; i < Camera.getNumberOfCameras(); i++) { Camera.CameraInfo ci = new Camera.CameraInfo(); Camera.getCameraInfo(i, ci); if (ci.facing == CameraInfo.CAMERA_FACING_FRONT) { hasFrontCamera = true; idFront = i; break; } } if (hasFrontCamera) { cam = Camera.open(idFront); } else { cam = Camera.open(); } ...

    - Or, if you want, you can create an util function, i.e.

    static int getFrontCameraId() { int idFront = -1; for (int i = 0; i < Camera.getNumberOfCameras(); i++) { Camera.CameraInfo ci = new Camera.CameraInfo(); Camera.getCameraInfo(i, ci); if (ci.facing == CameraInfo.CAMERA_FACING_FRONT) { idFront = i; break; } } return idFront; }

    And use this function, i.e. on this screenshot:

    BONUS: Flashlight Torch & Stroboscope

    Many of Android phones have the bright LEDs on back cover which are used by back camera as a flashlight.

    This is excepted what Android have single API for camera and for flashlight.

    It allows developers write an app with 2 features: quite bright flashlight, and a stroboscope (which can be used in extreme situation as a beacon or dazzling light, and etc.)

    But, unfortunately, many phones didn't has such prebuilt app. Let's write it by ourself!

    Tip: Below I will detaily describe process of creation of this app. If you want just view & download code, you can doesn't need scroll whole page, you can just click here.

    1. At fist, I will create my app which minimally targeting for (de facto) oldest Android version, 2.1 (Eclair, API Level 7). It will be need for compatibility with maximum of devices.

    2. Next, I should design application's interface. I want to do it well, and I will do it well! I will do it very simply (I willn't use custom theme) and quite quickly, but still it will be quite well.

    2.1. Firstly, I will not create separated buttons for ON flashlight, OFF flashlight, and same for stroboscope. It isn't convenient, when we can't use same button for ON and for OFF, and should move your finger.

    My buttons will have two states: ON and OFF, which will be switching on their clicking.

    2.2. Secondly, I will not set all buttons' dimensions to fill whole parent. Giant large buttons are wrong in mobile-first / adaptive / response design. Wrong, not right. Extremely large buttons are difficult to perceive by eyes & brain, and can cause unexpected touches.

    2.3. And finally: what about changing screen orientation on device rotation?

    When we will change orientation, it will change buttons sizes.

    When we will change orientation, it will cause re-creating of activity and we can have some additional problems with Camera's, Timer's, Button's and other states in Activity.

    What if just disable changing the orientation, make it permanently portrait?

    android:screenOrientation="portrait" android:configChanges="keyboardHidden|orientation|screenSize"

    Yes, this is very debatable solution, but I think it is quite good for this app, and I will try it.

    2.4. And really finally. The result.

    3. Next, after my design is done and tested, I going to write a code.

    To use the flashlight, I need permission same as for camera:

    <uses-permission android:name="android.permission.CAMERA"/>

    Essentially, to enable flashlight as torch we using this code:

    cam = Camera.open(); Parameters p = cam.getParameters(); p.setFlashMode(Parameters.FLASH_MODE_TORCH); cam.setParameters(p); cam.startPreview();

    And to disable:

    cam.stopPreview(); cam.release(); cam = null;

    My code for torchlight:

    boolean isTorchlightOn = false; Camera cam; @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnTorchlight: if (!isTorchlightOn) { btnTorchlight.setText("OFF Torchlight"); cam = Camera.open(); Parameters p = cam.getParameters(); p.setFlashMode(Parameters.FLASH_MODE_TORCH); cam.setParameters(p); cam.startPreview(); } else { btnTorchlight.setText("ON Torchlight"); if (cam != null) { cam.stopPreview(); cam.release(); cam = null; } } isTorchlightOn = !isTorchlightOn; break; case R.id.btnStroboscope: break; } } @Override public void onDestroy() { super.onDestroy(); if (cam != null) { cam.stopPreview(); cam.release(); cam = null; } }

    4. But we also have a stroboscope and it is more complicated.

    There are no standard API for strobo, we need just start & stop camera with small time intervals.

    And we can't do it in UI thread, therefore we need an AsyncTask, but firstly I worried about performance - can the camera switch on-off so fast?

    If I will just use our code from flashlight in infinity loop, it will work too slow for stroboscope (~1 sec intervals).

    Simply performance profiling shows that main problem is in Camera.open() method. I should find way to avoid it, and it will work fast.

    And I found it - it is Parameters.FLASH_MODE_OFF flag. This is simply, unfinished, dangerous code of stroboscope (danger - it can't simply be off, use Home button and next kill process in Settings > Applications, or just power off your device!):

    cam = Camera.open(); while (true) { Parameters p = cam.getParameters(); p.setFlashMode(Parameters.FLASH_MODE_TORCH); cam.setParameters(p); cam.startPreview(); Parameters p2 = cam.getParameters(); p2.setFlashMode(Parameters.FLASH_MODE_OFF); cam.setParameters(p2); cam.startPreview(); }

    5. Next, it's remaining only to create the AsyncTask, and also handle turning stroboscope off, and it'll work:

    case R.id.btnStroboscope: if (!isStroboscopeOn) { btnStroboscope.setText("OFF Stroboscope"); cam = Camera.open(); stroboTask = new StroboscopeTask(); stroboTask.execute(); } else { btnStroboscope.setText("ON Stroboscope"); stroboTask.cancel(true); if (cam != null) { cam.stopPreview(); cam.release(); cam = null; } } isStroboscopeOn = !isStroboscopeOn; } } @Override public void onDestroy() { super.onDestroy(); if (stroboTask != null) stroboTask.cancel(true); if (cam != null) { cam.stopPreview(); cam.release(); cam = null; } } StroboscopeTask stroboTask; private class StroboscopeTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { while (true) { Parameters p = cam.getParameters(); p.setFlashMode(Parameters.FLASH_MODE_TORCH); cam.setParameters(p); cam.startPreview(); Parameters p2 = cam.getParameters(); p2.setFlashMode(Parameters.FLASH_MODE_OFF); cam.setParameters(p2); cam.startPreview(); } } }

    6. And, to finish app, I'll add in onCreate the checking if torchlight available:

    if (!this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) { Toast.makeText(this, "Unfortunately, your device haven't the torchlight. Please close the app.", 500).show(); }

    7. Done! You can download its source with APK:

    Capturing Image To File

    To capture a photo, after starting preview, we should call takePicture method. It is asynchronous:

    cam.takePicture(null, null, new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { } }

    byte[] data contains the JPEG image. This image can be directly saved to file, sent by network, or readen to Bitmap instance using BitmapFactory, and next previewed in ImageView. For example, this code opens a dialog to see the captured photo:

    cam.takePicture(null, null, new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length); AlertDialog.Builder builder = new AlertDialog.Builder( MainActivity.this); builder.setPositiveButton("Close", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); final AlertDialog dialog = builder.create(); ImageView imageView = new ImageView(MainActivity.this); imageView.setImageBitmap(bmp); dialog.setView(imageView); dialog.show(); } });

    Naturally, taking of photos doesn't requiring any additional permissions except for android.permission.CAMERA.

    But there is a problem. It is problem about rotation. If you don't forget, here we used setDisplayOrientation() to properly preview the photo. But it doesn't take effect to orientation of capturing photos. We should set it separately, after camera is opened, before preview is started:

    Camera.Parameters params = cam.getParameters(); if (this.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) { cam.setDisplayOrientation(90); params.setRotation(90); } else { switch (this.getWindowManager() .getDefaultDisplay().getRotation()) { case Surface.ROTATION_90: cam.setDisplayOrientation(0); params.setRotation(0); break; case Surface.ROTATION_270: cam.setDisplayOrientation(180); params.setRotation(180); break; } } cam.setParameters(params); Capturing Audio To File Capturing Video To File package com.example.androiddeepcapturevideo40; import java.io.IOException; import android.app.Activity; import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Bundle; import android.os.Environment; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends Activity implements OnClickListener { Button btnStartCapturingVideo; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btnStartCapturingVideo = (Button) findViewById( R.id.btnStartCapturingVideo); btnStartCapturingVideo.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btnStartCapturingVideo: MediaRecorder recorder = new MediaRecorder(); recorder.setOutputFile(Environment.getExternalStorageDirectory() + "/record2.mp4"); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); try { recorder.prepare(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } recorder.start(); break; } } }

    Now, we learned basic features of simply & directly working with photo and video from camera, with audio from microphone, and some additional features. Now, we have basic knowledges and skills to use it in different applications.

    However, we have some small, but unpleasant problems, which can't be fully solved by just improving skills. We doesn't obliged to solve them. We can live whole life and don't solve them.

    But, if we do it, it will be a quite significant advantage in integrating these APIs into our application.

    Hm, what are these problems?

    Quick Photo Capturer

    Let's closer look at simple code of "quick way" of photo capturing (using Intent to default camera app):

    private static final int REQUEST_IMAGE_CAPTURE = 1; private void takePhoto() { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { startActivityForResult(intent, REQUEST_IMAGE_CAPTURE); } else { Toast.makeText(this, "Can't take a photo, please install valid camera application", Toast.LENGTH_SHORT).show(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE) { if (resultCode == RESULT_OK) { Bundle extras = data.getExtras(); Bitmap bmp = (Bitmap) extras.get("data"); imageView1.setImageBitmap(bmp); } else { Toast.makeText(this, "Canceled.", Toast.LENGTH_SHORT).show(); } } }

    Let's ask a question to ourself: is this code really simple & enough short?

    And answer to it: yes, it is, if we comparing it with "deep-way", i.e. directly hardware camera accessing.

    However, if we will detail look, we will gradually see: it can be more, more shorter and simpler.

    And it isn't simply perfectionism! This judgment is supported by practical experience in work.

    Main problem of this code is using startActivityForResult-onActivityResult mechanism. And code length (including request code constant which should be declared at least 1 time in every application) isn't major problem. Major problem is limitation of using purposes of this code: startActivityForResult isn't Context's method, it is only Activity's method!

    For example, it willn't work in Service, because in this context we can't startActivityForResult, and can't override onActivityResult.

    This isn't a fictional, but real problem, let's look there:

    Link #1

    Link #2

    Link #3

    Yes, it can be solved if we just create a "dummy" activity, will open it using startActivity() method (this method is available in any Context, unline startActivityForResult() method), and, next, this activity will start the Intent.

    Also, probably, it can be "solved" using reflection with Android source code, but this is undocumented way, and it will be quite complex and long code, I recommend to avoid such ways.

    Therefore, we have to create a "dummy" activity.

    And, I asking you: what is better - do it every time when we creating new application, or do it only single time in our util library?

    IMHO, second way is better. I will choose it.

    Let's create a PhotoCapturer class which is fully designed for this task, and doesn't depends on some excess. This will hide all Intent, Activity and other excess logic inside it.

    Instead activity-result mechanism, we will use the listeners.

    Pseudocode of its usage will be look like it:

    PhotoCapturer capturer = new PhotoCapturer capturer.onCaptured(Bitmap bmp) = { imageView1.setImageBitmap(bmp) } capturer.onCanceled() = { Toast.show("Canceled.") } capturer.start()

    1. At first, this class will have a constructor, which gets a Context instance at once class is instantiated:

    package com.example.androidutilcapturephoto.util; import ...; public class PhotoCapturer { Context context; public PhotoCapturer(Context context) { this.context = context; }

    In Android development, the Context instance can't be useless. It can be useful in any method of our class. This practice is used in all standard widgets classes i.e. Button, in classes i.e. SQLiteOpenHelper, and etc. And we using it, too.

    2. Next, we need two listeners - on photo captured, and (optionally) on capture canceled. Listener is just interface with method:

    public class PhotoCapturer { .... public static interface OnCapturedListener { public void onCaptured(Bitmap bmp); } public static interface OnCanceledListener { public void onCanceled(); }

    Defining interfaces inside class which bound with them - is also standard practice in Android, and we also using it.

    To make 2 listeners usable, to each listener we need 1 field, and at least 1 method - set...Listener():

    public class PhotoCapturer { ... OnCapturedListener onCapturedListener; OnCanceledListener onCanceledListener; ... public void setOnCapturedListener(OnCapturedListener listener) { this.onCapturedListener = listener; } public void setOnCanceledListener(OnCanceledListener listener) { this.onCanceledListener = listener; }

    That's done with listeners, all another is a trick of Java.

    3. Next, we going to culmination of our util development - to creation of a "dummy" activity.

    I will create it inside the PhotoCapturer class, because it's internal class which shouldn't be so visible:

    public class PhotoCapturer { ... public static class InternalCaptureActivity extends Activity { ... private static final int REQUEST_IMAGE_CAPTURE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (intent.resolveActivity(getPackageManager()) != null) { this.startActivityForResult(intent, REQUEST_IMAGE_CAPTURE); } else { Toast.makeText(this, "Can't take a photo, please install valid camera application", Toast.LENGTH_SHORT).show(); this.finish(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE) { if (resultCode == RESULT_OK) { Bitmap bmp = (Bitmap)data.getExtras().get("data"); } else { } this.finish(); } } }

    And, as usually, don't forget to declare in manifest;

    <activity android:name="com.example.androidutilcapturephoto.util.PhotoCapturer$InternalCaptureActivity"> </activity>

    (It isn't mistake, we using the dollar "$" character to say Android that this is enclosed class.)

    Yes, declaration of activity is very unpleasant moment, but at future we will place our class to custom library, and this will be solved by declaration of activity in library's manifest.

    4. Finally, let's write the start() method.

    It's obvious that this method should start our "dummy" activity... And here our "context" field (see here) becomes useful...

    public void start() { Intent target = new Intent(context, InternalCaptureActivity.class); context.startActivity(target); }

    But, how to connect our listeners with Activity?

    Intent.putExtra() takes not any objects! It takes only basic types, and Serializable. Our listeners are interface, we can't make them serializable.

    And we can't access an instance of InternalCaptureActivity, because startActivity() creates it internally using reflection.

    Howevere, we can access static members in activity. And simplest (but, not very right) way to workaround this issue is using static field to send listeners to activity instance.

    4.1. Let's define in InternalCaptureActivity two static fields for two listeners. These fields can be private, because we will use them only from PhotoCapturer which enclosing the Activity:

    public static class InternalCaptureActivity extends Activity { private static OnCapturedListener okListener; private static OnCanceledListener cancelListener;

    Next, in PhotoCapturer.start() method, just set them to their values - the listeners, which set above using PhotoCapturer.set..Listener():

    public void start() { InternalCaptureActivity.okListener = this.onCapturedListener; InternalCaptureActivity.cancelListener = this.onCanceledListener; Intent target = new Intent(context, InternalCaptureActivity.class); context.startActivity(target); }

    And finally, in InternalCaptureActivity just use them to fire listeners where we need:

    public static class InternalCaptureActivity extends Activity { ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE) { if (resultCode == RESULT_OK) { Bitmap bmp = (Bitmap)data.getExtras().get("data"); if (okListener != null) okListener.onCaptured(bmp); } else { if (cancelListener != null) cancelListener.onCanceled(); } this.finish(); } } }

    Yes, that's done, and it will work. But...

    But it isn't good practice. Don't forget that we should not just write an util for simply capturing of photos, let's think beyound: we should simplify our Android development, generally.

    Theoretically, there can be a case where we need to start multiple activity instances at once. For example - from different app processes. And, obviously, the "static" solution will fail if we have multiple instance at one time.

    4.2. What can we do? We can simply improve our static solution, if replace listener fields with listener-array fields, with per-instance indexes.

    More precisely, not array/arraylist, but hashtable, because hastable helps we prevent conflicts, unlike the array:

    public static class InternalCaptureActivity extends Activity { private static Hashtable<Integer,OnCapturedListener> okListeners = new Hashtable<Integer,OnCapturedListener>(); private static Hashtable<Integer,OnCanceledListener> cancelListeners = new Hashtable<Integer,OnCanceledListener>(); private int okListenerIndex = -1; private int cancelListenerIndex = -1;

    Next, initialize per-instance indexes in onCreate:

    public static class InternalCaptureActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.okListenerIndex = getIntent() .getIntExtra("okListenerIndex", -1); this.cancelListenerIndex = getIntent() .getIntExtra("cancelListenerIndex", -1);

    Next, fire listeners by index:

    public static class InternalCaptureActivity extends Activity { ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE) { if (resultCode == RESULT_OK) { Bitmap bmp = (Bitmap)data.getExtras().get("data"); if (okListenerIndex != -1) { this.okListeners.get(okListenerIndex) .onCaptured(bmp); } } else { if (cancelListenerIndex != -1) { this.cancelListeners.get(cancelListenerIndex) .onCanceled(); } } this.finish(); } }

    Don't forgot to remove unused listeners when activity finishes, to prevent memory leakage:

    @Override protected void onDestroy() { super.onDestroy(); if (okListeners.containsKey(this.okListenerIndex)) { okListeners.remove(this.okListenerIndex); } if (cancelListeners.containsKey(this.cancelListenerIndex)) { cancelListeners.remove(this.cancelListenerIndex); } }

    And next in PhotoCapturer, we will just get a key of last listener, and add new listener with key+1. Don't forget about empty hashtable, to prevent access of null pointer.

    public class PhotoCapturer { ... public void start() { Integer[] iOkListeners = InternalCaptureActivity .okListeners.keySet().toArray(new Integer[0]); int iLastOkListener = iOkListeners.length > 0 ? iOkListeners[iOkListeners.length - 1] : -1; InternalCaptureActivity.okListeners.put( iLastOkListener+1, this.onCapturedListener); Integer[] iCancelListeners = InternalCaptureActivity .cancelListeners.keySet().toArray(new Integer[0]); int iLastCancelListener = iCancelListeners.length > 0 ? iCancelListeners[iCancelListeners.length - 1] : -1; InternalCaptureActivity.cancelListeners.put( iLastCancelListener+1, this.onCanceledListener); Intent target = new Intent(context, InternalCaptureActivity.class); target.putExtra("okListenerIndex", iLastOkListener+1); target.putExtra("cancelListenerIndex", iLastCancelListener+1); context.startActivity(target); }

    That's done. It works, and it's better than pure "static" solution.

    But, it is quite complex, isn't it?.. And, isn't it a re-inventing of the wheel?.. Isn't Android provides built-in class for this?.. 

    4.3. At first, let's formulate what we want. 

    Probably, we want a "global" class, which provides a messaging pool, which can be shared between Activity and any other place.

    Probably, it should be in android.os (there are Serializable, Parcelable), or android.content (there is Intent) package.

    And I found such class - it's android.os.Messenger.

    How to use it?

    This is the essentially, simplified example with android.os.Messenger. Notice that it defaulty doesn't creates a thread, therefore we can simply it with Toast.makeText() and do etc. UI operations:

    Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 1234) { Log.i("Test-Msgr", "1234"); Toast.makeText(MainActivity.this, "Received 1234!", Toast.LENGTH_SHORT).show(); } if (msg.what == 5678) { Log.i("Test-Msgr", "5678 " + msg.obj.getClass().getName()); Toast.makeText(MainActivity.this, "Received 5678: " + msg.obj.getClass().getName() + "!", Toast.LENGTH_SHORT).show(); } super.handleMessage(msg); } }; Messenger msgr = new Messenger(mHandler); try { msgr.send(Message.obtain(null, 1234)); } catch (RemoteException e) { e.printStackTrace(); } try { msgr.send(Message.obtain(null, 5678, btnCapturePhoto)); } catch (RemoteException e) { e.printStackTrace(); }

    This is useless, test-purposed code, which sends and handles messages in single method. We seeing what android.os.Messenger class is not "global", it depends on Handler, we requiring a way to send Handler's instance from PhotoCapturer to InternalCaptureActivity, or to send whole Messenger's instance.

    We looking in Android source, and... bingo!

    Quote:

    public final class Messenger implements Parcelable {

    Intent has a putExtra(String, Parcelable) and getParcelableExtra(String), therefore all should work. Let's go code.

    At first, let's go to PhotoCapturer, and define Handler, Messenger, and constants for 2 messages which will cause firing of 2 listeners. Handler and Messenger will be defined and initialized as per-instance, not static fields.

    public class PhotoCapturer { ... private static final int MSG_ONCAPTURED = 1; private static final int MSG_ONCANCELED = 2; private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == MSG_ONCAPTURED) { onCapturedListener.onCaptured((Bitmap) msg.obj); } if (msg.what == MSG_ONCANCELED) { onCanceledListener.onCanceled(); } super.handleMessage(msg); } }; private Messenger msgr = new Messenger(mHandler);

    Next, let's remove all unneccessary code from start(), and just send the Messenger's instance as extra:

    public void start() { Intent target = new Intent(context, InternalCaptureActivity.class); target.putExtra("msgr", this.msgr); context.startActivity(target); }

    Next, go to InternalCaptureActivity. Declare a per-instance Messenger instance, and get it from extra in onCreate:

    public static class InternalCaptureActivity extends Activity { ... private Messenger msgr; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().hasExtra("msgr")) this.msgr = getIntent().getParcelableExtra("msgr"); ...

    Next, send messages in onActivityResult, instead of firing listeners:

    public static class InternalCaptureActivity extends Activity { ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE) { if (resultCode == RESULT_OK) { Bitmap bmp = (Bitmap)data.getExtras().get("data"); if (msgr != null) { try { msgr.send(Message.obtain(null, PhotoCapturer.MSG_ONCAPTURED, bmp)); } catch (RemoteException e) { e.printStackTrace(); } } } else { if (msgr != null) { try { msgr.send(Message.obtain(null, PhotoCapturer.MSG_ONCANCELED)); } catch (RemoteException e) { e.printStackTrace(); } } } this.finish(); } }

    And finally, just remove onDestroy, now we haven't any static fields in memory which requiring freeing.

    That's all. Now, we have a quite good architecture.

    But... Yes, you may think I am a perfectionist, but it still have a problem.

    A small problem is action on screen rotation: if we rotate display after ACTION_IMAGE_CAPTURE opened, it will cause rotation of InternalCaptureActivity, which will cause repeated onCreate call, and opening new ACTION_IMAGE_CAPTURE activity, because we opening it exactly in onCreate.

    To fix it, we can just disable the rotation of activity:

    <activity android:name="com.example.androidutilcapturephoto.util.PhotoCapturer$InternalCaptureActivity" android:screenOrientation="portrait" android:configChanges="keyboardHidden|orientation|screenSize"> </activity>
    Source: Java Android Camera Example: Taking Photo & Video & Audio + Using Flashlight & Stroboscope

    No comments:

    Post a Comment