1 package org.visp.android;
2 
3 import java.util.List;
4 
5 import org.visp.R;
6 import org.visp.core.VpImageRGBa;
7 import org.visp.core.VpImageUChar;
8 
9 import android.app.Activity;
10 import android.app.AlertDialog;
11 import android.content.Context;
12 import android.content.DialogInterface;
13 import android.content.res.TypedArray;
14 import android.graphics.Bitmap;
15 import android.graphics.Canvas;
16 import android.graphics.Rect;
17 import android.util.AttributeSet;
18 import android.util.Log;
19 import android.view.SurfaceHolder;
20 import android.view.SurfaceView;
21 
22 /**
23  * This is a basic class, implementing the interaction with Camera and ViSP library.
24  * The main responsibility of it - is to control when camera can be enabled, process the frame,
25  * call external listener to make any adjustments to the frame and then draw the resulting
26  * frame to the screen.
27  * The clients shall implement VpCameraViewListener.
28  */
29 public abstract class CameraBridgeViewBase extends SurfaceView implements SurfaceHolder.Callback {
30 
31     private static final String TAG = "CameraBridge";
32     private static final int MAX_UNSPECIFIED = -1;
33     private static final int STOPPED = 0;
34     private static final int STARTED = 1;
35 
36     private int mState = STOPPED;
37     private Bitmap mCacheBitmap;
38     private VpCameraViewListener2 mListener;
39     private boolean mSurfaceExist;
40     private final Object mSyncObject = new Object();
41 
42     protected int mFrameWidth;
43     protected int mFrameHeight;
44     protected int mMaxHeight;
45     protected int mMaxWidth;
46     protected float mScale = 0;
47     protected int mPreviewFormat = RGBA;
48     protected int mCameraIndex = CAMERA_ID_ANY;
49     protected boolean mEnabled;
50     protected FpsMeter mFpsMeter = null;
51 
52     public static final int CAMERA_ID_ANY   = -1;
53     public static final int CAMERA_ID_BACK  = 99;
54     public static final int CAMERA_ID_FRONT = 98;
55     public static final int RGBA = 1;
56     public static final int GRAY = 2;
57 
CameraBridgeViewBase(Context context, int cameraId)58     public CameraBridgeViewBase(Context context, int cameraId) {
59         super(context);
60         mCameraIndex = cameraId;
61         getHolder().addCallback(this);
62         mMaxWidth = MAX_UNSPECIFIED;
63         mMaxHeight = MAX_UNSPECIFIED;
64     }
65 
CameraBridgeViewBase(Context context, AttributeSet attrs)66     public CameraBridgeViewBase(Context context, AttributeSet attrs) {
67         super(context, attrs);
68 
69         int count = attrs.getAttributeCount();
70         Log.d(TAG, "Attr count: " + Integer.valueOf(count));
71 
72         TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase);
73         if (styledAttrs.getBoolean(R.styleable.CameraBridgeViewBase_show_fps, false))
74             enableFpsMeter();
75 
76         mCameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1);
77 
78         getHolder().addCallback(this);
79         mMaxWidth = MAX_UNSPECIFIED;
80         mMaxHeight = MAX_UNSPECIFIED;
81         styledAttrs.recycle();
82     }
83 
84     /**
85      * Sets the camera index
86      * @param cameraIndex new camera index
87      */
setCameraIndex(int cameraIndex)88     public void setCameraIndex(int cameraIndex) {
89         this.mCameraIndex = cameraIndex;
90     }
91 
92     public interface VpCameraViewListener {
93         /**
94          * This method is invoked when camera preview has started. After this method is invoked
95          * the frames will start to be delivered to client via the onCameraFrame() callback.
96          * @param width -  the width of the frames that will be delivered
97          * @param height - the height of the frames that will be delivered
98          */
onCameraViewStarted(int width, int height)99         public void onCameraViewStarted(int width, int height);
100 
101         /**
102          * This method is invoked when camera preview has been stopped for some reason.
103          * No frames will be delivered via onCameraFrame() callback after this method is called.
104          */
onCameraViewStopped()105         public void onCameraViewStopped();
106 
107         /**
108          * This method is invoked when delivery of the frame needs to be done.
109          * The returned values - is a modified frame which needs to be displayed on the screen.
110          * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
111          */
onCameraFrame(VpImageUChar image)112         public VpImageUChar onCameraFrame(VpImageUChar image);
onCameraFrame(VpImageRGBa image)113         public VpImageRGBa onCameraFrame(VpImageRGBa image);
114     }
115 
116     public interface VpCameraViewListener2 {
117         /**
118          * This method is invoked when camera preview has started. After this method is invoked
119          * the frames will start to be delivered to client via the onCameraFrame() callback.
120          * @param width -  the width of the frames that will be delivered
121          * @param height - the height of the frames that will be delivered
122          */
onCameraViewStarted(int width, int height)123         public void onCameraViewStarted(int width, int height);
124 
125         /**
126          * This method is invoked when camera preview has been stopped for some reason.
127          * No frames will be delivered via onCameraFrame() callback after this method is called.
128          */
onCameraViewStopped()129         public void onCameraViewStopped();
130 
131         /**
132          * This method is invoked when delivery of the frame needs to be done.
133          * The returned values - is a modified frame which needs to be displayed on the screen.
134          * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc)
135          */
onCameraFrame(VpCameraViewFrame inputFrame)136         public long onCameraFrame(VpCameraViewFrame inputFrame);
137     };
138 
139     protected class VpCameraViewListenerAdapter implements VpCameraViewListener2  {
VpCameraViewListenerAdapter(VpCameraViewListener oldStypeListener)140         public VpCameraViewListenerAdapter(VpCameraViewListener oldStypeListener) {
141             mOldStyleListener = oldStypeListener;
142         }
143 
onCameraViewStarted(int width, int height)144         public void onCameraViewStarted(int width, int height) {
145             mOldStyleListener.onCameraViewStarted(width, height);
146         }
147 
onCameraViewStopped()148         public void onCameraViewStopped() {
149             mOldStyleListener.onCameraViewStopped();
150         }
151 
onCameraFrame(VpCameraViewFrame inputFrame)152         public long onCameraFrame(VpCameraViewFrame inputFrame) {
153              long result = -1;
154              switch (mPreviewFormat) {
155                 case RGBA:
156                     result = mOldStyleListener.onCameraFrame(inputFrame.rgba()).nativeObj;
157                     break;
158                 case GRAY:
159                     result = mOldStyleListener.onCameraFrame(inputFrame.gray()).nativeObj;
160                     break;
161                 default:
162                     Log.e(TAG, "Invalid frame format! Only RGBA and Gray Scale are supported!");
163             };
164 
165             return result;
166         }
167 
setFrameFormat(int format)168         public void setFrameFormat(int format) {
169             mPreviewFormat = format;
170         }
171 
172         private int mPreviewFormat = RGBA;
173         private VpCameraViewListener mOldStyleListener;
174     };
175 
176     /**
177      * This class interface is abstract representation of single frame from camera for onCameraFrame callback
178      * Attention: Do not use objects, that represents this interface out of onCameraFrame callback!
179      */
180     public interface VpCameraViewFrame {
181 
182         /**
183          * This method returns RGBA VpImageRGBa with frame
184          */
rgba()185         public VpImageRGBa rgba();
186 
187         /**
188          * This method returns single channel gray scale VpImageUChar with frame
189          */
gray()190         public VpImageUChar gray();
191     };
192 
surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)193     public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
194         Log.d(TAG, "call surfaceChanged event");
195         synchronized(mSyncObject) {
196             if (!mSurfaceExist) {
197                 mSurfaceExist = true;
198                 checkCurrentState();
199             } else {
200                 /** Surface changed. We need to stop camera and restart with new parameters */
201                 /* Pretend that old surface has been destroyed */
202                 mSurfaceExist = false;
203                 checkCurrentState();
204                 /* Now use new surface. Say we have it now */
205                 mSurfaceExist = true;
206                 checkCurrentState();
207             }
208         }
209     }
210 
surfaceCreated(SurfaceHolder holder)211     public void surfaceCreated(SurfaceHolder holder) {
212         /* Do nothing. Wait until surfaceChanged delivered */
213     }
214 
surfaceDestroyed(SurfaceHolder holder)215     public void surfaceDestroyed(SurfaceHolder holder) {
216         synchronized(mSyncObject) {
217             mSurfaceExist = false;
218             checkCurrentState();
219         }
220     }
221 
222     /**
223      * This method is provided for clients, so they can enable the camera connection.
224      * The actual onCameraViewStarted callback will be delivered only after both this method is called and surface is available
225      */
enableView()226     public void enableView() {
227         synchronized(mSyncObject) {
228             mEnabled = true;
229             checkCurrentState();
230         }
231     }
232 
233     /**
234      * This method is provided for clients, so they can disable camera connection and stop
235      * the delivery of frames even though the surface view itself is not destroyed and still stays on the scren
236      */
disableView()237     public void disableView() {
238         synchronized(mSyncObject) {
239             mEnabled = false;
240             checkCurrentState();
241         }
242     }
243 
244     /**
245      * This method enables label with fps value on the screen
246      */
enableFpsMeter()247     public void enableFpsMeter() {
248         if (mFpsMeter == null) {
249             mFpsMeter = new FpsMeter();
250             mFpsMeter.setResolution(mFrameWidth, mFrameHeight);
251         }
252     }
253 
disableFpsMeter()254     public void disableFpsMeter() {
255             mFpsMeter = null;
256     }
257 
258     /**
259      *
260      * @param listener
261      */
262 
setVpCameraViewListener(VpCameraViewListener2 listener)263     public void setVpCameraViewListener(VpCameraViewListener2 listener) {
264         mListener = listener;
265     }
266 
setVpCameraViewListener(VpCameraViewListener listener)267     public void setVpCameraViewListener(VpCameraViewListener listener) {
268         VpCameraViewListenerAdapter adapter = new VpCameraViewListenerAdapter(listener);
269         adapter.setFrameFormat(mPreviewFormat);
270         mListener = adapter;
271     }
272 
273     /**
274      * This method sets the maximum size that camera frame is allowed to be. When selecting
275      * size - the biggest size which less or equal the size set will be selected.
276      * As an example - we set setMaxFrameSize(200,200) and we have 176x152 and 320x240 sizes. The
277      * preview frame will be selected with 176x152 size.
278      * This method is useful when need to restrict the size of preview frame for some reason (for example for video recording)
279      * @param maxWidth - the maximum width allowed for camera frame.
280      * @param maxHeight - the maximum height allowed for camera frame
281      */
setMaxFrameSize(int maxWidth, int maxHeight)282     public void setMaxFrameSize(int maxWidth, int maxHeight) {
283         mMaxWidth = maxWidth;
284         mMaxHeight = maxHeight;
285     }
286 
SetCaptureFormat(int format)287     public void SetCaptureFormat(int format)
288     {
289         mPreviewFormat = format;
290         if (mListener instanceof VpCameraViewListenerAdapter) {
291             VpCameraViewListenerAdapter adapter = (VpCameraViewListenerAdapter) mListener;
292             adapter.setFrameFormat(mPreviewFormat);
293         }
294     }
295 
296     /**
297      * Called when mSyncObject lock is held
298      */
checkCurrentState()299     private void checkCurrentState() {
300         Log.d(TAG, "call checkCurrentState");
301         int targetState;
302 
303         if (mEnabled && mSurfaceExist && getVisibility() == VISIBLE) {
304             targetState = STARTED;
305         } else {
306             targetState = STOPPED;
307         }
308 
309         if (targetState != mState) {
310             /* The state change detected. Need to exit the current state and enter target state */
311             processExitState(mState);
312             mState = targetState;
313             processEnterState(mState);
314         }
315     }
316 
processEnterState(int state)317     private void processEnterState(int state) {
318         Log.d(TAG, "call processEnterState: " + state);
319         switch(state) {
320         case STARTED:
321             onEnterStartedState();
322             if (mListener != null) {
323                 mListener.onCameraViewStarted(mFrameWidth, mFrameHeight);
324             }
325             break;
326         case STOPPED:
327             onEnterStoppedState();
328             if (mListener != null) {
329                 mListener.onCameraViewStopped();
330             }
331             break;
332         };
333     }
334 
processExitState(int state)335     private void processExitState(int state) {
336         Log.d(TAG, "call processExitState: " + state);
337         switch(state) {
338         case STARTED:
339             onExitStartedState();
340             break;
341         case STOPPED:
342             onExitStoppedState();
343             break;
344         };
345     }
346 
onEnterStoppedState()347     private void onEnterStoppedState() {
348         /* nothing to do */
349     }
350 
onExitStoppedState()351     private void onExitStoppedState() {
352         /* nothing to do */
353     }
354 
355     // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x
356     // Bitmap must be constructed before surface
onEnterStartedState()357     private void onEnterStartedState() {
358         Log.d(TAG, "call onEnterStartedState");
359         /* Connect camera */
360         if (!connectCamera(getWidth(), getHeight())) {
361             AlertDialog ad = new AlertDialog.Builder(getContext()).create();
362             ad.setCancelable(false); // This blocks the 'BACK' button
363             ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed.");
364             ad.setButton(DialogInterface.BUTTON_NEUTRAL,  "OK", new DialogInterface.OnClickListener() {
365                 public void onClick(DialogInterface dialog, int which) {
366                     dialog.dismiss();
367                     ((Activity) getContext()).finish();
368                 }
369             });
370             ad.show();
371 
372         }
373     }
374 
onExitStartedState()375     private void onExitStartedState() {
376         disconnectCamera();
377         if (mCacheBitmap != null) {
378             mCacheBitmap.recycle();
379         }
380     }
381 
382     /**
383      * This method shall be called by the subclasses when they have valid
384      * object and want it to be delivered to external client (via callback) and
385      * then displayed on the screen.
386      * @param frame - the current frame to be delivered
387      */
deliverAndDrawFrame(VpCameraViewFrame frame)388     protected void deliverAndDrawFrame(VpCameraViewFrame frame) {
389         long modified = -1;
390 
391         if (mListener != null) {
392             modified = mListener.onCameraFrame(frame);
393         } else {
394             modified = frame.rgba().nativeObj;
395         }
396 
397         boolean bmpValid = true;
398         if (modified != -1) {
399 			      if (mPreviewFormat == RGBA){
400                 try {
401                     Utils.vpImageUCharToBitmap(modified, mCacheBitmap);
402                 } catch(Exception e) {
403                     Log.e(TAG, "Image type: " + mPreviewFormat);
404                     Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
405                     Log.e(TAG, "Utils.vpImageUCharToBitmap() throws an exception: " + e.getMessage());
406                     bmpValid = false;
407                 }
408             } else if (mPreviewFormat == GRAY){
409                 try {
410                     Utils.vpImageRGBaToBitmap(modified, mCacheBitmap);
411                 } catch(Exception e) {
412                     Log.e(TAG, "Image type: " + mPreviewFormat);
413                     Log.e(TAG, "Bitmap type: " + mCacheBitmap.getWidth() + "*" + mCacheBitmap.getHeight());
414                     Log.e(TAG, "Utils.vpImageRGBaToBitmap() throws an exception: " + e.getMessage());
415                     bmpValid = false;
416                 }
417             }
418         }
419 
420         if (bmpValid && mCacheBitmap != null) {
421             Canvas canvas = getHolder().lockCanvas();
422             if (canvas != null) {
423                 canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR);
424         				Log.d(TAG, "mStretch value: " + mScale);
425 
426                 if (mScale != 0) {
427                     canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
428                          new Rect((int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2),
429                          (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2),
430                          (int)((canvas.getWidth() - mScale*mCacheBitmap.getWidth()) / 2 + mScale*mCacheBitmap.getWidth()),
431                          (int)((canvas.getHeight() - mScale*mCacheBitmap.getHeight()) / 2 + mScale*mCacheBitmap.getHeight())), null);
432                 } else {
433                      canvas.drawBitmap(mCacheBitmap, new Rect(0,0,mCacheBitmap.getWidth(), mCacheBitmap.getHeight()),
434                          new Rect((canvas.getWidth() - mCacheBitmap.getWidth()) / 2,
435                          (canvas.getHeight() - mCacheBitmap.getHeight()) / 2,
436                          (canvas.getWidth() - mCacheBitmap.getWidth()) / 2 + mCacheBitmap.getWidth(),
437                          (canvas.getHeight() - mCacheBitmap.getHeight()) / 2 + mCacheBitmap.getHeight()), null);
438                 }
439 
440                 if (mFpsMeter != null) {
441                     // Using default tick frequency for linux
442                     // Refer: https://stackoverflow.com/questions/28292807/what-is-exactly-a-clock-tick-in-the-context-of-android-cpu-usage
443                     mFpsMeter.measure(100,System.currentTimeMillis()*100);
444                     mFpsMeter.draw(canvas, 20, 30);
445                 }
446                 getHolder().unlockCanvasAndPost(canvas);
447             }
448         }
449     }
450 
451     /**
452      * This method is invoked shall perform concrete operation to initialize the camera.
453      * CONTRACT: as a result of this method variables mFrameWidth and mFrameHeight MUST be
454      * initialized with the size of the Camera frames that will be delivered to external processor.
455      * @param width - the width of this SurfaceView
456      * @param height - the height of this SurfaceView
457      */
connectCamera(int width, int height)458     protected abstract boolean connectCamera(int width, int height);
459 
460     /**
461      * Disconnects and release the particular camera object being connected to this surface view.
462      * Called when syncObject lock is held
463      */
disconnectCamera()464     protected abstract void disconnectCamera();
465 
466     // NOTE: On Android 4.1.x the function must be called before SurfaceTexture constructor!
AllocateCache()467     protected void AllocateCache()
468     {
469         mCacheBitmap = Bitmap.createBitmap(mFrameWidth, mFrameHeight, Bitmap.Config.ARGB_8888);
470     }
471 
472     public interface ListItemAccessor {
getWidth(Object obj)473         public int getWidth(Object obj);
getHeight(Object obj)474         public int getHeight(Object obj);
475     };
476 
477     /**
478      * This helper method can be called by subclasses to select camera preview size.
479      * It goes over the list of the supported preview sizes and selects the maximum one which
480      * fits both values set via setMaxFrameSize() and surface frame allocated for this view
481      * @param supportedSizes
482      * @param surfaceWidth
483      * @param surfaceHeight
484      * @return optimal frame size
485      */
calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight)486     protected int[] calculateCameraFrameSize(List<?> supportedSizes, ListItemAccessor accessor, int surfaceWidth, int surfaceHeight) {
487         int calcWidth = 0;
488         int calcHeight = 0;
489 
490         int maxAllowedWidth = (mMaxWidth != MAX_UNSPECIFIED && mMaxWidth < surfaceWidth)? mMaxWidth : surfaceWidth;
491         int maxAllowedHeight = (mMaxHeight != MAX_UNSPECIFIED && mMaxHeight < surfaceHeight)? mMaxHeight : surfaceHeight;
492 
493         for (Object size : supportedSizes) {
494             int width = accessor.getWidth(size);
495             int height = accessor.getHeight(size);
496 
497             if (width <= maxAllowedWidth && height <= maxAllowedHeight) {
498                 if (width >= calcWidth && height >= calcHeight) {
499                     calcWidth = (int) width;
500                     calcHeight = (int) height;
501                 }
502             }
503         }
504 
505 		int size[] = {calcWidth, calcHeight};
506         return size;
507     }
508 }
509