1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.camera;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.graphics.SurfaceTexture;
22import android.hardware.Camera;
23import android.media.CamcorderProfile;
24import android.media.MediaRecorder;
25import android.os.Handler;
26import android.os.Looper;
27import android.util.Log;
28
29import com.android.gallery3d.R;
30import com.android.gallery3d.common.ApiHelper;
31
32import java.io.FileDescriptor;
33import java.io.IOException;
34import java.io.Serializable;
35import java.lang.reflect.Constructor;
36import java.lang.reflect.InvocationHandler;
37import java.lang.reflect.Method;
38import java.lang.reflect.Proxy;
39
40
41/**
42 * Encapsulates the mobile filter framework components needed to record video
43 * with effects applied. Modeled after MediaRecorder.
44 */
45@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
46public class EffectsRecorder {
47    private static final String TAG = "EffectsRecorder";
48
49    private static Class<?> sClassFilter;
50    private static Method sFilterIsAvailable;
51    private static EffectsRecorder sEffectsRecorder;
52    // The index of the current effects recorder.
53    private static int sEffectsRecorderIndex;
54
55    private static boolean sReflectionInited = false;
56
57    private static Class<?> sClsLearningDoneListener;
58    private static Class<?> sClsOnRunnerDoneListener;
59    private static Class<?> sClsOnRecordingDoneListener;
60    private static Class<?> sClsSurfaceTextureSourceListener;
61
62    private static Method sFilterSetInputValue;
63
64    private static Constructor<?> sCtPoint;
65    private static Constructor<?> sCtQuad;
66
67    private static Method sLearningDoneListenerOnLearningDone;
68
69    private static Method sObjectEquals;
70    private static Method sObjectToString;
71
72    private static Class<?> sClsGraphRunner;
73    private static Method sGraphRunnerGetGraph;
74    private static Method sGraphRunnerSetDoneCallback;
75    private static Method sGraphRunnerRun;
76    private static Method sGraphRunnerGetError;
77    private static Method sGraphRunnerStop;
78
79    private static Method sFilterGraphGetFilter;
80    private static Method sFilterGraphTearDown;
81
82    private static Method sOnRunnerDoneListenerOnRunnerDone;
83
84    private static Class<?> sClsGraphEnvironment;
85    private static Constructor<?> sCtGraphEnvironment;
86    private static Method sGraphEnvironmentCreateGLEnvironment;
87    private static Method sGraphEnvironmentGetRunner;
88    private static Method sGraphEnvironmentAddReferences;
89    private static Method sGraphEnvironmentLoadGraph;
90    private static Method sGraphEnvironmentGetContext;
91
92    private static Method sFilterContextGetGLEnvironment;
93    private static Method sGLEnvironmentIsActive;
94    private static Method sGLEnvironmentActivate;
95    private static Method sGLEnvironmentDeactivate;
96    private static Method sSurfaceTextureTargetDisconnect;
97    private static Method sOnRecordingDoneListenerOnRecordingDone;
98    private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady;
99
100    private Object mLearningDoneListener;
101    private Object mRunnerDoneCallback;
102    private Object mSourceReadyCallback;
103    // A callback to finalize the media after the recording is done.
104    private Object mRecordingDoneListener;
105
106    static {
107        try {
108            sClassFilter = Class.forName("android.filterfw.core.Filter");
109            sFilterIsAvailable = sClassFilter.getMethod("isAvailable",
110                    String.class);
111        } catch (ClassNotFoundException ex) {
112            Log.v(TAG, "Can't find the class android.filterfw.core.Filter");
113        } catch (NoSuchMethodException e) {
114            Log.v(TAG, "Can't find the method Filter.isAvailable");
115        }
116    }
117
118    public static final int  EFFECT_NONE        = 0;
119    public static final int  EFFECT_GOOFY_FACE  = 1;
120    public static final int  EFFECT_BACKDROPPER = 2;
121
122    public static final int  EFFECT_GF_SQUEEZE     = 0;
123    public static final int  EFFECT_GF_BIG_EYES    = 1;
124    public static final int  EFFECT_GF_BIG_MOUTH   = 2;
125    public static final int  EFFECT_GF_SMALL_MOUTH = 3;
126    public static final int  EFFECT_GF_BIG_NOSE    = 4;
127    public static final int  EFFECT_GF_SMALL_EYES  = 5;
128    public static final int  NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
129
130    public static final int  EFFECT_MSG_STARTED_LEARNING = 0;
131    public static final int  EFFECT_MSG_DONE_LEARNING    = 1;
132    public static final int  EFFECT_MSG_SWITCHING_EFFECT = 2;
133    public static final int  EFFECT_MSG_EFFECTS_STOPPED  = 3;
134    public static final int  EFFECT_MSG_RECORDING_DONE   = 4;
135    public static final int  EFFECT_MSG_PREVIEW_RUNNING  = 5;
136
137    private Context mContext;
138    private Handler mHandler;
139
140    private CameraManager.CameraProxy mCameraDevice;
141    private CamcorderProfile mProfile;
142    private double mCaptureRate = 0;
143    private SurfaceTexture mPreviewSurfaceTexture;
144    private int mPreviewWidth;
145    private int mPreviewHeight;
146    private MediaRecorder.OnInfoListener mInfoListener;
147    private MediaRecorder.OnErrorListener mErrorListener;
148
149    private String mOutputFile;
150    private FileDescriptor mFd;
151    private int mOrientationHint = 0;
152    private long mMaxFileSize = 0;
153    private int mMaxDurationMs = 0;
154    private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
155    private int mCameraDisplayOrientation;
156
157    private int mEffect = EFFECT_NONE;
158    private int mCurrentEffect = EFFECT_NONE;
159    private EffectsListener mEffectsListener;
160
161    private Object mEffectParameter;
162
163    private Object mGraphEnv;
164    private int mGraphId;
165    private Object mRunner = null;
166    private Object mOldRunner = null;
167
168    private SurfaceTexture mTextureSource;
169
170    private static final int STATE_CONFIGURE              = 0;
171    private static final int STATE_WAITING_FOR_SURFACE    = 1;
172    private static final int STATE_STARTING_PREVIEW       = 2;
173    private static final int STATE_PREVIEW                = 3;
174    private static final int STATE_RECORD                 = 4;
175    private static final int STATE_RELEASED               = 5;
176    private int mState = STATE_CONFIGURE;
177
178    private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
179    private SoundClips.Player mSoundPlayer;
180
181    /** Determine if a given effect is supported at runtime
182     * Some effects require libraries not available on all devices
183     */
184    public static boolean isEffectSupported(int effectId) {
185        if (sFilterIsAvailable == null)  return false;
186
187        try {
188            switch (effectId) {
189                case EFFECT_GOOFY_FACE:
190                    return (Boolean) sFilterIsAvailable.invoke(null,
191                            "com.google.android.filterpacks.facedetect.GoofyRenderFilter");
192                case EFFECT_BACKDROPPER:
193                    return (Boolean) sFilterIsAvailable.invoke(null,
194                            "android.filterpacks.videoproc.BackDropperFilter");
195                default:
196                    return false;
197            }
198        } catch (Exception ex) {
199            Log.e(TAG, "Fail to check filter", ex);
200        }
201        return false;
202    }
203
204    public EffectsRecorder(Context context) {
205        if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
206
207        if (!sReflectionInited) {
208            try {
209                sFilterSetInputValue = sClassFilter.getMethod("setInputValue",
210                        new Class[] {String.class, Object.class});
211
212                Class<?> clsPoint = Class.forName("android.filterfw.geometry.Point");
213                sCtPoint = clsPoint.getConstructor(new Class[] {float.class,
214                        float.class});
215
216                Class<?> clsQuad = Class.forName("android.filterfw.geometry.Quad");
217                sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint,
218                        clsPoint, clsPoint});
219
220                Class<?> clsBackDropperFilter = Class.forName(
221                        "android.filterpacks.videoproc.BackDropperFilter");
222                sClsLearningDoneListener = Class.forName(
223                        "android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener");
224                sLearningDoneListenerOnLearningDone = sClsLearningDoneListener
225                        .getMethod("onLearningDone", new Class[] {clsBackDropperFilter});
226
227                sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class});
228                sObjectToString = Object.class.getMethod("toString");
229
230                sClsOnRunnerDoneListener = Class.forName(
231                        "android.filterfw.core.GraphRunner$OnRunnerDoneListener");
232                sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod(
233                        "onRunnerDone", new Class[] {int.class});
234
235                sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner");
236                sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph");
237                sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod(
238                        "setDoneCallback", new Class[] {sClsOnRunnerDoneListener});
239                sGraphRunnerRun = sClsGraphRunner.getMethod("run");
240                sGraphRunnerGetError = sClsGraphRunner.getMethod("getError");
241                sGraphRunnerStop = sClsGraphRunner.getMethod("stop");
242
243                Class<?> clsFilterContext = Class.forName("android.filterfw.core.FilterContext");
244                sFilterContextGetGLEnvironment = clsFilterContext.getMethod(
245                        "getGLEnvironment");
246
247                Class<?> clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph");
248                sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter",
249                        new Class[] {String.class});
250                sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown",
251                        new Class[] {clsFilterContext});
252
253                sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment");
254                sCtGraphEnvironment = sClsGraphEnvironment.getConstructor();
255                sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod(
256                        "createGLEnvironment");
257                sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod(
258                        "getRunner", new Class[] {int.class, int.class});
259                sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod(
260                        "addReferences", new Class[] {Object[].class});
261                sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod(
262                        "loadGraph", new Class[] {Context.class, int.class});
263                sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod(
264                        "getContext");
265
266                Class<?> clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment");
267                sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive");
268                sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate");
269                sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate");
270
271                Class<?> clsSurfaceTextureTarget = Class.forName(
272                        "android.filterpacks.videosrc.SurfaceTextureTarget");
273                sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod(
274                        "disconnect", new Class[] {clsFilterContext});
275
276                sClsOnRecordingDoneListener = Class.forName(
277                        "android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener");
278                sOnRecordingDoneListenerOnRecordingDone =
279                        sClsOnRecordingDoneListener.getMethod("onRecordingDone");
280
281                sClsSurfaceTextureSourceListener = Class.forName(
282                        "android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener");
283                sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady =
284                        sClsSurfaceTextureSourceListener.getMethod(
285                                "onSurfaceTextureSourceReady",
286                                new Class[] {SurfaceTexture.class});
287            } catch (Exception ex) {
288                throw new RuntimeException(ex);
289            }
290
291            sReflectionInited = true;
292        }
293
294        sEffectsRecorderIndex++;
295        Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex);
296        sEffectsRecorder = this;
297        SerializableInvocationHandler sih = new SerializableInvocationHandler(
298                sEffectsRecorderIndex);
299        mLearningDoneListener = Proxy.newProxyInstance(
300                sClsLearningDoneListener.getClassLoader(),
301                new Class[] {sClsLearningDoneListener}, sih);
302        mRunnerDoneCallback = Proxy.newProxyInstance(
303                sClsOnRunnerDoneListener.getClassLoader(),
304                new Class[] {sClsOnRunnerDoneListener}, sih);
305        mSourceReadyCallback = Proxy.newProxyInstance(
306                sClsSurfaceTextureSourceListener.getClassLoader(),
307                new Class[] {sClsSurfaceTextureSourceListener}, sih);
308        mRecordingDoneListener =  Proxy.newProxyInstance(
309                sClsOnRecordingDoneListener.getClassLoader(),
310                new Class[] {sClsOnRecordingDoneListener}, sih);
311
312        mContext = context;
313        mHandler = new Handler(Looper.getMainLooper());
314        mSoundPlayer = SoundClips.getPlayer(context);
315    }
316
317    public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) {
318        switch (mState) {
319            case STATE_PREVIEW:
320                throw new RuntimeException("setCamera cannot be called while previewing!");
321            case STATE_RECORD:
322                throw new RuntimeException("setCamera cannot be called while recording!");
323            case STATE_RELEASED:
324                throw new RuntimeException("setCamera called on an already released recorder!");
325            default:
326                break;
327        }
328
329        mCameraDevice = cameraDevice;
330    }
331
332    public void setProfile(CamcorderProfile profile) {
333        switch (mState) {
334            case STATE_RECORD:
335                throw new RuntimeException("setProfile cannot be called while recording!");
336            case STATE_RELEASED:
337                throw new RuntimeException("setProfile called on an already released recorder!");
338            default:
339                break;
340        }
341        mProfile = profile;
342    }
343
344    public void setOutputFile(String outputFile) {
345        switch (mState) {
346            case STATE_RECORD:
347                throw new RuntimeException("setOutputFile cannot be called while recording!");
348            case STATE_RELEASED:
349                throw new RuntimeException("setOutputFile called on an already released recorder!");
350            default:
351                break;
352        }
353
354        mOutputFile = outputFile;
355        mFd = null;
356    }
357
358    public void setOutputFile(FileDescriptor fd) {
359        switch (mState) {
360            case STATE_RECORD:
361                throw new RuntimeException("setOutputFile cannot be called while recording!");
362            case STATE_RELEASED:
363                throw new RuntimeException("setOutputFile called on an already released recorder!");
364            default:
365                break;
366        }
367
368        mOutputFile = null;
369        mFd = fd;
370    }
371
372    /**
373     * Sets the maximum filesize (in bytes) of the recording session.
374     * This will be passed on to the MediaEncoderFilter and then to the
375     * MediaRecorder ultimately. If zero or negative, the MediaRecorder will
376     * disable the limit
377    */
378    public synchronized void setMaxFileSize(long maxFileSize) {
379        switch (mState) {
380            case STATE_RECORD:
381                throw new RuntimeException("setMaxFileSize cannot be called while recording!");
382            case STATE_RELEASED:
383                throw new RuntimeException(
384                    "setMaxFileSize called on an already released recorder!");
385            default:
386                break;
387        }
388        mMaxFileSize = maxFileSize;
389    }
390
391    /**
392    * Sets the maximum recording duration (in ms) for the next recording session
393    * Setting it to zero (the default) disables the limit.
394    */
395    public synchronized void setMaxDuration(int maxDurationMs) {
396        switch (mState) {
397            case STATE_RECORD:
398                throw new RuntimeException("setMaxDuration cannot be called while recording!");
399            case STATE_RELEASED:
400                throw new RuntimeException(
401                    "setMaxDuration called on an already released recorder!");
402            default:
403                break;
404        }
405        mMaxDurationMs = maxDurationMs;
406    }
407
408
409    public void setCaptureRate(double fps) {
410        switch (mState) {
411            case STATE_RECORD:
412                throw new RuntimeException("setCaptureRate cannot be called while recording!");
413            case STATE_RELEASED:
414                throw new RuntimeException(
415                    "setCaptureRate called on an already released recorder!");
416            default:
417                break;
418        }
419
420        if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps");
421        mCaptureRate = fps;
422    }
423
424    public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture,
425                                  int previewWidth,
426                                  int previewHeight) {
427        if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")");
428        switch (mState) {
429            case STATE_RECORD:
430                throw new RuntimeException(
431                    "setPreviewSurfaceTexture cannot be called while recording!");
432            case STATE_RELEASED:
433                throw new RuntimeException(
434                    "setPreviewSurfaceTexture called on an already released recorder!");
435            default:
436                break;
437        }
438
439        mPreviewSurfaceTexture = previewSurfaceTexture;
440        mPreviewWidth = previewWidth;
441        mPreviewHeight = previewHeight;
442
443        switch (mState) {
444            case STATE_WAITING_FOR_SURFACE:
445                startPreview();
446                break;
447            case STATE_STARTING_PREVIEW:
448            case STATE_PREVIEW:
449                initializeEffect(true);
450                break;
451        }
452    }
453
454    public void setEffect(int effect, Object effectParameter) {
455        if (mLogVerbose) Log.v(TAG,
456                               "setEffect: effect ID " + effect +
457                               ", parameter " + effectParameter.toString());
458        switch (mState) {
459            case STATE_RECORD:
460                throw new RuntimeException("setEffect cannot be called while recording!");
461            case STATE_RELEASED:
462                throw new RuntimeException("setEffect called on an already released recorder!");
463            default:
464                break;
465        }
466
467        mEffect = effect;
468        mEffectParameter = effectParameter;
469
470        if (mState == STATE_PREVIEW ||
471                mState == STATE_STARTING_PREVIEW) {
472            initializeEffect(false);
473        }
474    }
475
476    public interface EffectsListener {
477        public void onEffectsUpdate(int effectId, int effectMsg);
478        public void onEffectsError(Exception exception, String filePath);
479    }
480
481    public void setEffectsListener(EffectsListener listener) {
482        mEffectsListener = listener;
483    }
484
485    private void setFaceDetectOrientation() {
486        if (mCurrentEffect == EFFECT_GOOFY_FACE) {
487            Object rotateFilter = getGraphFilter(mRunner, "rotate");
488            Object metaRotateFilter = getGraphFilter(mRunner, "metarotate");
489            setInputValue(rotateFilter, "rotation", mOrientationHint);
490            int reverseDegrees = (360 - mOrientationHint) % 360;
491            setInputValue(metaRotateFilter, "rotation", reverseDegrees);
492        }
493    }
494
495    private void setRecordingOrientation() {
496        if (mState != STATE_RECORD && mRunner != null) {
497            Object bl = newInstance(sCtPoint, new Object[] {0, 0});
498            Object br = newInstance(sCtPoint, new Object[] {1, 0});
499            Object tl = newInstance(sCtPoint, new Object[] {0, 1});
500            Object tr = newInstance(sCtPoint, new Object[] {1, 1});
501            Object recordingRegion;
502            if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
503                // The back camera is not mirrored, so use a identity transform
504                recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr});
505            } else {
506                // Recording region needs to be tweaked for front cameras, since they
507                // mirror their preview
508                if (mOrientationHint == 0 || mOrientationHint == 180) {
509                    // Horizontal flip in landscape
510                    recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl});
511                } else {
512                    // Horizontal flip in portrait
513                    recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br});
514                }
515            }
516            Object recorder = getGraphFilter(mRunner, "recorder");
517            setInputValue(recorder, "inputRegion", recordingRegion);
518        }
519    }
520    public void setOrientationHint(int degrees) {
521        switch (mState) {
522            case STATE_RELEASED:
523                throw new RuntimeException(
524                        "setOrientationHint called on an already released recorder!");
525            default:
526                break;
527        }
528        if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
529        mOrientationHint = degrees;
530        setFaceDetectOrientation();
531        setRecordingOrientation();
532    }
533
534    public void setCameraDisplayOrientation(int orientation) {
535        if (mState != STATE_CONFIGURE) {
536            throw new RuntimeException(
537                "setCameraDisplayOrientation called after configuration!");
538        }
539        mCameraDisplayOrientation = orientation;
540    }
541
542    public void setCameraFacing(int facing) {
543        switch (mState) {
544            case STATE_RELEASED:
545                throw new RuntimeException(
546                    "setCameraFacing called on alrady released recorder!");
547            default:
548                break;
549        }
550        mCameraFacing = facing;
551        setRecordingOrientation();
552    }
553
554    public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
555        switch (mState) {
556            case STATE_RECORD:
557                throw new RuntimeException("setInfoListener cannot be called while recording!");
558            case STATE_RELEASED:
559                throw new RuntimeException(
560                    "setInfoListener called on an already released recorder!");
561            default:
562                break;
563        }
564        mInfoListener = infoListener;
565    }
566
567    public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
568        switch (mState) {
569            case STATE_RECORD:
570                throw new RuntimeException("setErrorListener cannot be called while recording!");
571            case STATE_RELEASED:
572                throw new RuntimeException(
573                    "setErrorListener called on an already released recorder!");
574            default:
575                break;
576        }
577        mErrorListener = errorListener;
578    }
579
580    private void initializeFilterFramework() {
581        mGraphEnv = newInstance(sCtGraphEnvironment);
582        invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment);
583
584        int videoFrameWidth = mProfile.videoFrameWidth;
585        int videoFrameHeight = mProfile.videoFrameHeight;
586        if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) {
587            int tmp = videoFrameWidth;
588            videoFrameWidth = videoFrameHeight;
589            videoFrameHeight = tmp;
590        }
591
592        invoke(mGraphEnv, sGraphEnvironmentAddReferences,
593                new Object[] {new Object[] {
594                "textureSourceCallback", mSourceReadyCallback,
595                "recordingWidth", videoFrameWidth,
596                "recordingHeight", videoFrameHeight,
597                "recordingProfile", mProfile,
598                "learningDoneListener", mLearningDoneListener,
599                "recordingDoneListener", mRecordingDoneListener}});
600        mRunner = null;
601        mGraphId = -1;
602        mCurrentEffect = EFFECT_NONE;
603    }
604
605    private synchronized void initializeEffect(boolean forceReset) {
606        if (forceReset ||
607            mCurrentEffect != mEffect ||
608            mCurrentEffect == EFFECT_BACKDROPPER) {
609
610            invoke(mGraphEnv, sGraphEnvironmentAddReferences,
611                    new Object[] {new Object[] {
612                    "previewSurfaceTexture", mPreviewSurfaceTexture,
613                    "previewWidth", mPreviewWidth,
614                    "previewHeight", mPreviewHeight,
615                    "orientation", mOrientationHint}});
616            if (mState == STATE_PREVIEW ||
617                    mState == STATE_STARTING_PREVIEW) {
618                // Switching effects while running. Inform video camera.
619                sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
620            }
621
622            switch (mEffect) {
623                case EFFECT_GOOFY_FACE:
624                    mGraphId = (Integer) invoke(mGraphEnv,
625                            sGraphEnvironmentLoadGraph,
626                            new Object[] {mContext, R.raw.goofy_face});
627                    break;
628                case EFFECT_BACKDROPPER:
629                    sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
630                    mGraphId = (Integer) invoke(mGraphEnv,
631                            sGraphEnvironmentLoadGraph,
632                            new Object[] {mContext, R.raw.backdropper});
633                    break;
634                default:
635                    throw new RuntimeException("Unknown effect ID" + mEffect + "!");
636            }
637            mCurrentEffect = mEffect;
638
639            mOldRunner = mRunner;
640            mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner,
641                    new Object[] {mGraphId,
642                    getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")});
643            invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback});
644            if (mLogVerbose) {
645                Log.v(TAG, "New runner: " + mRunner
646                      + ". Old runner: " + mOldRunner);
647            }
648            if (mState == STATE_PREVIEW ||
649                    mState == STATE_STARTING_PREVIEW) {
650                // Switching effects while running. Stop existing runner.
651                // The stop callback will take care of starting new runner.
652                mCameraDevice.stopPreview();
653                mCameraDevice.setPreviewTextureAsync(null);
654                invoke(mOldRunner, sGraphRunnerStop);
655            }
656        }
657
658        switch (mCurrentEffect) {
659            case EFFECT_GOOFY_FACE:
660                tryEnableVideoStabilization(true);
661                Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer");
662                setInputValue(goofyFilter, "currentEffect",
663                        ((Integer) mEffectParameter).intValue());
664                break;
665            case EFFECT_BACKDROPPER:
666                tryEnableVideoStabilization(false);
667                Object backgroundSrc = getGraphFilter(mRunner, "background");
668                if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) {
669                    // Set the context first before setting sourceUrl to
670                    // guarantee the content URI get resolved properly.
671                    setInputValue(backgroundSrc, "context", mContext);
672                }
673                setInputValue(backgroundSrc, "sourceUrl", mEffectParameter);
674                // For front camera, the background video needs to be mirrored in the
675                // backdropper filter
676                if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
677                    Object replacer = getGraphFilter(mRunner, "replacer");
678                    setInputValue(replacer, "mirrorBg", true);
679                    if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored");
680                }
681                break;
682            default:
683                break;
684        }
685        setFaceDetectOrientation();
686        setRecordingOrientation();
687    }
688
689    public synchronized void startPreview() {
690        if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
691
692        switch (mState) {
693            case STATE_STARTING_PREVIEW:
694            case STATE_PREVIEW:
695                // Already running preview
696                Log.w(TAG, "startPreview called when already running preview");
697                return;
698            case STATE_RECORD:
699                throw new RuntimeException("Cannot start preview when already recording!");
700            case STATE_RELEASED:
701                throw new RuntimeException("setEffect called on an already released recorder!");
702            default:
703                break;
704        }
705
706        if (mEffect == EFFECT_NONE) {
707            throw new RuntimeException("No effect selected!");
708        }
709        if (mEffectParameter == null) {
710            throw new RuntimeException("No effect parameter provided!");
711        }
712        if (mProfile == null) {
713            throw new RuntimeException("No recording profile provided!");
714        }
715        if (mPreviewSurfaceTexture == null) {
716            if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one");
717            mState = STATE_WAITING_FOR_SURFACE;
718            return;
719        }
720        if (mCameraDevice == null) {
721            throw new RuntimeException("No camera to record from!");
722        }
723
724        if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph.");
725        initializeFilterFramework();
726
727        initializeEffect(true);
728
729        mState = STATE_STARTING_PREVIEW;
730        invoke(mRunner, sGraphRunnerRun);
731        // Rest of preview startup handled in mSourceReadyCallback
732    }
733
734    private Object invokeObjectEquals(Object proxy, Object[] args) {
735        return Boolean.valueOf(proxy == args[0]);
736    }
737
738    private Object invokeObjectToString() {
739        return "Proxy-" + toString();
740    }
741
742    private void invokeOnLearningDone() {
743        if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
744        // Called in a processing thread, so have to post message back to UI
745        // thread
746        sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
747        enable3ALocks(true);
748    }
749
750    private void invokeOnRunnerDone(Object[] args) {
751        int runnerDoneResult = (Integer) args[0];
752        synchronized (EffectsRecorder.this) {
753            if (mLogVerbose) {
754                Log.v(TAG,
755                      "Graph runner done (" + EffectsRecorder.this
756                      + ", mRunner " + mRunner
757                      + ", mOldRunner " + mOldRunner + ")");
758            }
759            if (runnerDoneResult ==
760                    (Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) {
761                // Handle error case
762                Log.e(TAG, "Error running filter graph!");
763                Exception e = null;
764                if (mRunner != null) {
765                    e = (Exception) invoke(mRunner, sGraphRunnerGetError);
766                } else if (mOldRunner != null) {
767                    e = (Exception) invoke(mOldRunner, sGraphRunnerGetError);
768                }
769                raiseError(e);
770            }
771            if (mOldRunner != null) {
772                // Tear down old graph if available
773                if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
774                Object glEnv = getContextGLEnvironment(mGraphEnv);
775                if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
776                    invoke(glEnv, sGLEnvironmentActivate);
777                }
778                getGraphTearDown(mOldRunner,
779                        invoke(mGraphEnv, sGraphEnvironmentGetContext));
780                if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
781                    invoke(glEnv, sGLEnvironmentDeactivate);
782                }
783                mOldRunner = null;
784            }
785            if (mState == STATE_PREVIEW ||
786                    mState == STATE_STARTING_PREVIEW) {
787                // Switching effects, start up the new runner
788                if (mLogVerbose) {
789                    Log.v(TAG, "Previous effect halted. Running graph again. state: "
790                            + mState);
791                }
792                tryEnable3ALocks(false);
793                // In case of an error, the graph restarts from beginning and in case
794                // of the BACKDROPPER effect, the learner re-learns the background.
795                // Hence, we need to show the learning dialogue to the user
796                // to avoid recording before the learning is done. Else, the user
797                // could start recording before the learning is done and the new
798                // background comes up later leading to an end result video
799                // with a heterogeneous background.
800                // For BACKDROPPER effect, this path is also executed sometimes at
801                // the end of a normal recording session. In such a case, the graph
802                // does not restart and hence the learner does not re-learn. So we
803                // do not want to show the learning dialogue then.
804                if (runnerDoneResult == (Integer) getConstant(
805                        sClsGraphRunner, "RESULT_ERROR")
806                        && mCurrentEffect == EFFECT_BACKDROPPER) {
807                    sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
808                }
809                invoke(mRunner, sGraphRunnerRun);
810            } else if (mState != STATE_RELEASED) {
811                // Shutting down effects
812                if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
813                tryEnable3ALocks(false);
814                sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
815            } else {
816                // STATE_RELEASED - camera will be/has been released as well, do nothing.
817            }
818        }
819    }
820
821    private void invokeOnSurfaceTextureSourceReady(Object[] args) {
822        SurfaceTexture source = (SurfaceTexture) args[0];
823        if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
824        synchronized (EffectsRecorder.this) {
825            mTextureSource = source;
826
827            if (mState == STATE_CONFIGURE) {
828                // Stop preview happened while the runner was doing startup tasks
829                // Since we haven't started anything up, don't do anything
830                // Rest of cleanup will happen in onRunnerDone
831                if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping.");
832                return;
833            }
834            if (mState == STATE_RELEASED) {
835                // EffectsRecorder has been released, so don't touch the camera device
836                // or anything else
837                if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping.");
838                return;
839            }
840            if (source == null) {
841                if (mLogVerbose) {
842                    Log.v(TAG, "Ready callback: source null! Looks like graph was closed!");
843                }
844                if (mState == STATE_PREVIEW ||
845                        mState == STATE_STARTING_PREVIEW ||
846                        mState == STATE_RECORD) {
847                    // A null source here means the graph is shutting down
848                    // unexpectedly, so we need to turn off preview before
849                    // the surface texture goes away.
850                    if (mLogVerbose) {
851                        Log.v(TAG, "Ready callback: State: " + mState
852                                + ". stopCameraPreview");
853                    }
854
855                    stopCameraPreview();
856                }
857                return;
858            }
859
860            // Lock AE/AWB to reduce transition flicker
861            tryEnable3ALocks(true);
862
863            mCameraDevice.stopPreview();
864            if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
865            mCameraDevice.setPreviewTextureAsync(mTextureSource);
866
867            mCameraDevice.startPreviewAsync();
868
869            // Unlock AE/AWB after preview started
870            tryEnable3ALocks(false);
871
872            mState = STATE_PREVIEW;
873
874            if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
875
876            // Sending a message to listener that preview is complete
877            sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING);
878        }
879    }
880
881    private void invokeOnRecordingDone() {
882        // Forward the callback to the VideoModule object (as an asynchronous event).
883        if (mLogVerbose) Log.v(TAG, "Recording done callback triggered");
884        sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE);
885    }
886
887    public synchronized void startRecording() {
888        if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
889
890        switch (mState) {
891            case STATE_RECORD:
892                throw new RuntimeException("Already recording, cannot begin anew!");
893            case STATE_RELEASED:
894                throw new RuntimeException(
895                    "startRecording called on an already released recorder!");
896            default:
897                break;
898        }
899
900        if ((mOutputFile == null) && (mFd == null)) {
901            throw new RuntimeException("No output file name or descriptor provided!");
902        }
903
904        if (mState == STATE_CONFIGURE) {
905            startPreview();
906        }
907
908        Object recorder = getGraphFilter(mRunner, "recorder");
909        if (mFd != null) {
910            setInputValue(recorder, "outputFileDescriptor", mFd);
911        } else {
912            setInputValue(recorder, "outputFile", mOutputFile);
913        }
914        // It is ok to set the audiosource without checking for timelapse here
915        // since that check will be done in the MediaEncoderFilter itself
916        setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER);
917        setInputValue(recorder, "recordingProfile", mProfile);
918        setInputValue(recorder, "orientationHint", mOrientationHint);
919        // Important to set the timelapseinterval to 0 if the capture rate is not >0
920        // since the recorder does not get created every time the recording starts.
921        // The recorder infers whether the capture is timelapsed based on the value of
922        // this interval
923        boolean captureTimeLapse = mCaptureRate > 0;
924        if (captureTimeLapse) {
925            double timeBetweenFrameCapture = 1 / mCaptureRate;
926            setInputValue(recorder, "timelapseRecordingIntervalUs",
927                    (long) (1000000 * timeBetweenFrameCapture));
928
929        } else {
930            setInputValue(recorder, "timelapseRecordingIntervalUs", 0L);
931        }
932
933        if (mInfoListener != null) {
934            setInputValue(recorder, "infoListener", mInfoListener);
935        }
936        if (mErrorListener != null) {
937            setInputValue(recorder, "errorListener", mErrorListener);
938        }
939        setInputValue(recorder, "maxFileSize", mMaxFileSize);
940        setInputValue(recorder, "maxDurationMs", mMaxDurationMs);
941        setInputValue(recorder, "recording", true);
942        mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
943        mState = STATE_RECORD;
944    }
945
946    public synchronized void stopRecording() {
947        if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
948
949        switch (mState) {
950            case STATE_CONFIGURE:
951            case STATE_STARTING_PREVIEW:
952            case STATE_PREVIEW:
953                Log.w(TAG, "StopRecording called when recording not active!");
954                return;
955            case STATE_RELEASED:
956                throw new RuntimeException("stopRecording called on released EffectsRecorder!");
957            default:
958                break;
959        }
960        Object recorder = getGraphFilter(mRunner, "recorder");
961        setInputValue(recorder, "recording", false);
962        mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
963        mState = STATE_PREVIEW;
964    }
965
966    // Called to tell the filter graph that the display surfacetexture is not valid anymore.
967    // So the filter graph should not hold any reference to the surface created with that.
968    public synchronized void disconnectDisplay() {
969        if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " +
970            "SurfaceTexture");
971        Object display = getGraphFilter(mRunner, "display");
972        invoke(display, sSurfaceTextureTargetDisconnect, new Object[] {
973                invoke(mGraphEnv, sGraphEnvironmentGetContext)});
974    }
975
976    // The VideoModule will call this to notify that the camera is being
977    // released to the outside world. This call should happen after the
978    // stopRecording call. Else, the effects may throw an exception.
979    // With the recording stopped, the stopPreview call will not try to
980    // release the camera again.
981    // This must be called in onPause() if the effects are ON.
982    public synchronized void disconnectCamera() {
983        if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera");
984        stopCameraPreview();
985        mCameraDevice = null;
986    }
987
988    // In a normal case, when the disconnect is not called, we should not
989    // set the camera device to null, since on return callback, we try to
990    // enable 3A locks, which need the cameradevice.
991    public synchronized void stopCameraPreview() {
992        if (mLogVerbose) Log.v(TAG, "Stopping camera preview.");
993        if (mCameraDevice == null) {
994            Log.d(TAG, "Camera already null. Nothing to disconnect");
995            return;
996        }
997        mCameraDevice.stopPreview();
998        mCameraDevice.setPreviewTextureAsync(null);
999    }
1000
1001    // Stop and release effect resources
1002    public synchronized void stopPreview() {
1003        if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
1004        switch (mState) {
1005            case STATE_CONFIGURE:
1006                Log.w(TAG, "StopPreview called when preview not active!");
1007                return;
1008            case STATE_RELEASED:
1009                throw new RuntimeException("stopPreview called on released EffectsRecorder!");
1010            default:
1011                break;
1012        }
1013
1014        if (mState == STATE_RECORD) {
1015            stopRecording();
1016        }
1017
1018        mCurrentEffect = EFFECT_NONE;
1019
1020        // This will not do anything if the camera has already been disconnected.
1021        stopCameraPreview();
1022
1023        mState = STATE_CONFIGURE;
1024        mOldRunner = mRunner;
1025        invoke(mRunner, sGraphRunnerStop);
1026        mRunner = null;
1027        // Rest of stop and release handled in mRunnerDoneCallback
1028    }
1029
1030    // Try to enable/disable video stabilization if supported; otherwise return false
1031    // It is called from a synchronized block.
1032    boolean tryEnableVideoStabilization(boolean toggle) {
1033        if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization.");
1034        if (mCameraDevice == null) {
1035            Log.d(TAG, "Camera already null. Not enabling video stabilization.");
1036            return false;
1037        }
1038        Camera.Parameters params = mCameraDevice.getParameters();
1039
1040        String vstabSupported = params.get("video-stabilization-supported");
1041        if ("true".equals(vstabSupported)) {
1042            if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
1043            params.set("video-stabilization", toggle ? "true" : "false");
1044            mCameraDevice.setParameters(params);
1045            return true;
1046        }
1047        if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
1048        return false;
1049    }
1050
1051    // Try to enable/disable 3A locks if supported; otherwise return false
1052    @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
1053    synchronized boolean tryEnable3ALocks(boolean toggle) {
1054        if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks");
1055        if (mCameraDevice == null) {
1056            Log.d(TAG, "Camera already null. Not tryenabling 3A locks.");
1057            return false;
1058        }
1059        Camera.Parameters params = mCameraDevice.getParameters();
1060        if (Util.isAutoExposureLockSupported(params) &&
1061            Util.isAutoWhiteBalanceLockSupported(params)) {
1062            params.setAutoExposureLock(toggle);
1063            params.setAutoWhiteBalanceLock(toggle);
1064            mCameraDevice.setParameters(params);
1065            return true;
1066        }
1067        return false;
1068    }
1069
1070    // Try to enable/disable 3A locks if supported; otherwise, throw error
1071    // Use this when locks are essential to success
1072    synchronized void enable3ALocks(boolean toggle) {
1073        if (mLogVerbose) Log.v(TAG, "Enable3ALocks");
1074        if (mCameraDevice == null) {
1075            Log.d(TAG, "Camera already null. Not enabling 3A locks.");
1076            return;
1077        }
1078        Camera.Parameters params = mCameraDevice.getParameters();
1079        if (!tryEnable3ALocks(toggle)) {
1080            throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
1081        }
1082    }
1083
1084    static class SerializableInvocationHandler
1085            implements InvocationHandler, Serializable {
1086        private final int mEffectsRecorderIndex;
1087        public SerializableInvocationHandler(int index) {
1088            mEffectsRecorderIndex = index;
1089        }
1090
1091        @Override
1092        public Object invoke(Object proxy, Method method, Object[] args)
1093                throws Throwable {
1094            if (sEffectsRecorder == null) return null;
1095            if (mEffectsRecorderIndex != sEffectsRecorderIndex) {
1096                Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex);
1097                return null;
1098            }
1099            if (method.equals(sObjectEquals)) {
1100                return sEffectsRecorder.invokeObjectEquals(proxy, args);
1101            } else if (method.equals(sObjectToString)) {
1102                return sEffectsRecorder.invokeObjectToString();
1103            } else if (method.equals(sLearningDoneListenerOnLearningDone)) {
1104                sEffectsRecorder.invokeOnLearningDone();
1105            } else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) {
1106                sEffectsRecorder.invokeOnRunnerDone(args);
1107            } else if (method.equals(
1108                    sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) {
1109                sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args);
1110            } else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) {
1111                sEffectsRecorder.invokeOnRecordingDone();
1112            }
1113            return null;
1114        }
1115    }
1116
1117    // Indicates that all camera/recording activity needs to halt
1118    public synchronized void release() {
1119        if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
1120
1121        switch (mState) {
1122            case STATE_RECORD:
1123            case STATE_STARTING_PREVIEW:
1124            case STATE_PREVIEW:
1125                stopPreview();
1126                // Fall-through
1127            default:
1128                if (mSoundPlayer != null) {
1129                    mSoundPlayer.release();
1130                    mSoundPlayer = null;
1131                }
1132                mState = STATE_RELEASED;
1133                break;
1134        }
1135        sEffectsRecorder = null;
1136    }
1137
1138    private void sendMessage(final int effect, final int msg) {
1139        if (mEffectsListener != null) {
1140            mHandler.post(new Runnable() {
1141                @Override
1142                public void run() {
1143                    mEffectsListener.onEffectsUpdate(effect, msg);
1144                }
1145            });
1146        }
1147    }
1148
1149    private void raiseError(final Exception exception) {
1150        if (mEffectsListener != null) {
1151            mHandler.post(new Runnable() {
1152                @Override
1153                public void run() {
1154                    if (mFd != null) {
1155                        mEffectsListener.onEffectsError(exception, null);
1156                    } else {
1157                        mEffectsListener.onEffectsError(exception, mOutputFile);
1158                    }
1159                }
1160            });
1161        }
1162    }
1163
1164    // invoke method on receiver with no arguments
1165    private Object invoke(Object receiver, Method method) {
1166        try {
1167            return method.invoke(receiver);
1168        } catch (Exception ex) {
1169            throw new RuntimeException(ex);
1170        }
1171    }
1172
1173    // invoke method on receiver with arguments
1174    private Object invoke(Object receiver, Method method, Object[] args) {
1175        try {
1176            return method.invoke(receiver, args);
1177        } catch (Exception ex) {
1178            throw new RuntimeException(ex);
1179        }
1180    }
1181
1182    private void setInputValue(Object receiver, String key, Object value) {
1183        try {
1184            sFilterSetInputValue.invoke(receiver, new Object[] {key, value});
1185        } catch (Exception ex) {
1186            throw new RuntimeException(ex);
1187        }
1188    }
1189
1190    private Object newInstance(Constructor<?> ct, Object[] initArgs) {
1191        try {
1192            return ct.newInstance(initArgs);
1193        } catch (Exception ex) {
1194            throw new RuntimeException(ex);
1195        }
1196    }
1197
1198    private Object newInstance(Constructor<?> ct) {
1199        try {
1200            return ct.newInstance();
1201        } catch (Exception ex) {
1202            throw new RuntimeException(ex);
1203        }
1204    }
1205
1206    private Object getGraphFilter(Object receiver, String name) {
1207        try {
1208            return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph
1209                    .invoke(receiver), new Object[] {name});
1210        } catch (Exception ex) {
1211            throw new RuntimeException(ex);
1212        }
1213    }
1214
1215    private Object getContextGLEnvironment(Object receiver) {
1216        try {
1217            return sFilterContextGetGLEnvironment
1218                    .invoke(sGraphEnvironmentGetContext.invoke(receiver));
1219        } catch (Exception ex) {
1220            throw new RuntimeException(ex);
1221        }
1222    }
1223
1224    private void getGraphTearDown(Object receiver, Object filterContext) {
1225        try {
1226            sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver),
1227                    new Object[]{filterContext});
1228        } catch (Exception ex) {
1229            throw new RuntimeException(ex);
1230        }
1231    }
1232
1233    private Object getConstant(Class<?> cls, String name) {
1234        try {
1235            return cls.getDeclaredField(name).get(null);
1236        } catch (Exception ex) {
1237            throw new RuntimeException(ex);
1238        }
1239    }
1240}
1241