| @@ -0,0 +1,208 @@ | ||
| package com.twominuteplays.camera; | ||
|
|
||
| import android.media.MediaRecorder; | ||
| import android.util.Log; | ||
| import android.util.Size; | ||
| import android.view.Surface; | ||
|
|
||
| public class MediaRecorderWrapper { | ||
|
|
||
| private static final String TAG = MediaRecorderWrapper.class.getName(); | ||
|
|
||
| private final MediaRecorder mMediaRecorder; | ||
| private MediaRecorderState mState = MediaRecorderState.INITIAL; | ||
|
|
||
| private enum MediaRecorderState { | ||
| INITIAL, | ||
| ERROR, | ||
| INITIALIZED, | ||
| DATA_SOURCE_CONFIGURED, | ||
| PREPARED, | ||
| RECORDING, | ||
| RELEASED; | ||
| } | ||
|
|
||
| public MediaRecorderWrapper() { | ||
| mMediaRecorder = new MediaRecorder(); | ||
| mState = MediaRecorderState.INITIAL; | ||
| } | ||
|
|
||
| private synchronized void logAndThrowStateException(MediaRecorderState...allowedStates) { | ||
| StringBuilder msg = new StringBuilder("State must be in "); | ||
| for (MediaRecorderState state : allowedStates) { | ||
| msg.append(state.name()).append(" "); | ||
| } | ||
| msg.append(". But found ").append(mState.name()); | ||
| Log.e(TAG, msg.toString()); | ||
| throw new IllegalStateException(msg.toString()); | ||
| } | ||
|
|
||
| public synchronized Surface getSurface() { | ||
| Log.d(TAG, "Get Surface"); | ||
| if (mState == MediaRecorderState.PREPARED) { | ||
| return mMediaRecorder.getSurface(); | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.PREPARED); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| public synchronized void setAudioVideoSource() { | ||
| Log.d(TAG, "Set AV Source"); | ||
| if (mState == MediaRecorderState.INITIAL || mState == MediaRecorderState.INITIALIZED) { | ||
| try { | ||
| mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // before setOutputFormat, recording parameters or encoders. First. | ||
| mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // ditto above | ||
| mState = MediaRecorderState.INITIALIZED; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while SETTING AV sources.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.INITIAL, MediaRecorderState.INITIALIZED); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void setOutputFormat() { | ||
| Log.d(TAG, "Set output format."); | ||
| if (mState == MediaRecorderState.INITIALIZED) { | ||
| try { | ||
| mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // after setAudio/VideoSource, before prepare | ||
| mState = MediaRecorderState.DATA_SOURCE_CONFIGURED; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while setting output format.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.INITIALIZED); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void configureDataSource(Size mVideoSize, int sensorOrientation, String outputFileName) { | ||
| Log.d(TAG, "Configure data source."); | ||
| if (mState == MediaRecorderState.DATA_SOURCE_CONFIGURED) { | ||
| try { | ||
| mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // after setOutputFormat, before prepare | ||
| mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); // ditto above | ||
|
|
||
| mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight()); // after setVideoSource, setOutFormat, before prepare | ||
| mMediaRecorder.setVideoFrameRate(24); // after setVideoSource, setOutFormat, before prepare | ||
| mMediaRecorder.setOutputFile(outputFileName); // after setOutputFormat, before prepare | ||
| mMediaRecorder.setVideoEncodingBitRate(1228800); // just before prepare (LD == 350Kbps == 35840, SD == 1200 == 1228800) | ||
|
|
||
| int rotation = Surface.ROTATION_0; // activity.getWindowManager().getDefaultDisplay().getRotation(); | ||
| switch (sensorOrientation) { | ||
| case CameraHelper.SENSOR_ORIENTATION_DEFAULT_DEGREES: | ||
| mMediaRecorder.setOrientationHint(CameraHelper.DEFAULT_ORIENTATIONS.get(rotation)); // before prepare | ||
| break; | ||
| case CameraHelper.SENSOR_ORIENTATION_INVERSE_DEGREES: | ||
| mMediaRecorder.setOrientationHint(CameraHelper.INVERSE_ORIENTATIONS.get(rotation)); // before prepare | ||
| break; | ||
| } | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error configuring data source.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.DATA_SOURCE_CONFIGURED); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void reset() { | ||
| Log.d(TAG, "Reset"); | ||
| if (mState == MediaRecorderState.INITIAL) { | ||
| Log.w(TAG, "Ignoring reset, state is INITIAL"); | ||
| return; | ||
| } | ||
|
|
||
| if (mState != MediaRecorderState.RELEASED) { | ||
| try { | ||
| mMediaRecorder.reset(); | ||
| mState = MediaRecorderState.INITIAL; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while resetting media recorder.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.INITIAL, MediaRecorderState.RELEASED); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void start() { | ||
| Log.d(TAG, "Start"); | ||
| if (mState == MediaRecorderState.PREPARED) { | ||
| try { | ||
| mMediaRecorder.start(); | ||
| mState = MediaRecorderState.RECORDING; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while starting media recorder.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.PREPARED); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void stop() { | ||
| Log.d(TAG, "Stop"); | ||
| if (mState == MediaRecorderState.RECORDING) { | ||
| try { | ||
| mMediaRecorder.stop(); | ||
| mState = MediaRecorderState.INITIAL; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while stopping media recorder.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.RECORDING); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void release() { | ||
| Log.d(TAG, "Release"); | ||
| if (mState == MediaRecorderState.INITIAL) { | ||
| try { | ||
| mMediaRecorder.release(); | ||
| mState = MediaRecorderState.RELEASED; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while releasing media recorder.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| logAndThrowStateException(MediaRecorderState.INITIAL); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void prepare() { | ||
| Log.d(TAG, "Prepare"); | ||
| if (mState == MediaRecorderState.DATA_SOURCE_CONFIGURED) { | ||
| try { | ||
| mMediaRecorder.prepare(); | ||
| mState = MediaRecorderState.PREPARED; | ||
| } | ||
| catch (Throwable t) { | ||
| Log.e(TAG, "Error while preparing media recorder.", t); | ||
| mState = MediaRecorderState.ERROR; | ||
| } | ||
| } | ||
| else { | ||
| throw new IllegalStateException("State must be DATA_SOURCE_CONFIGURED. Is " + mState.name()); | ||
| } | ||
| } | ||
|
|
||
| } |
| @@ -0,0 +1,190 @@ | ||
| package com.twominuteplays.camera; | ||
|
|
||
| import android.app.Activity; | ||
| import android.content.res.Configuration; | ||
| import android.graphics.Matrix; | ||
| import android.graphics.RectF; | ||
| import android.graphics.SurfaceTexture; | ||
| import android.util.Log; | ||
| import android.util.Size; | ||
| import android.view.Surface; | ||
| import android.view.TextureView; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.math.MathContext; | ||
| import java.math.RoundingMode; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public class TextureViewManager implements TextureView.SurfaceTextureListener { | ||
|
|
||
| private static final String TAG = TextureViewManager.class.getName(); | ||
|
|
||
| private AutoFitTextureView mTextureView; | ||
| private Size mPreviewSize; | ||
| private Activity mActivity; | ||
| private CameraCallback mCameraCallback; | ||
|
|
||
| public interface CameraCallback { | ||
| void openCamera(int width, int height); | ||
| } | ||
|
|
||
| public void setCameraCallback(CameraCallback cameraCallback) { | ||
| mCameraCallback = cameraCallback; | ||
| } | ||
|
|
||
| private void callOpenCameraCallback(int width, int height) { | ||
| if (mCameraCallback != null) { | ||
| mCameraCallback.openCamera(width, height); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Called when the texture view changes shape. | ||
| */ | ||
| public void configureTransform() { | ||
| if (null == mTextureView) { | ||
| return; | ||
| } | ||
| configureTransform(mTextureView.getWidth(), mTextureView.getHeight()); | ||
| } | ||
| public void configureTransform(int viewWidth, int viewHeight) { | ||
| if (null == mTextureView || null == mPreviewSize || null == mActivity) { | ||
| return; | ||
| } | ||
| int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation(); | ||
| Matrix matrix = new Matrix(); | ||
| RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); | ||
| RectF bufferRect = new RectF(0, 0, mPreviewSize.getHeight(), mPreviewSize.getWidth()); | ||
| float centerX = viewRect.centerX(); | ||
| float centerY = viewRect.centerY(); | ||
| if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { | ||
| bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); | ||
| matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); | ||
| float scale = Math.max( | ||
| (float) viewHeight / mPreviewSize.getHeight(), | ||
| (float) viewWidth / mPreviewSize.getWidth()); | ||
| matrix.postScale(scale, scale, centerX, centerY); | ||
| matrix.postRotate(90 * (rotation - 2), centerX, centerY); | ||
| } | ||
| mTextureView.setTransform(matrix); | ||
| } | ||
|
|
||
| public void setTextureView(Activity activity, AutoFitTextureView textureView) { | ||
| this.mTextureView = textureView; | ||
| this.mTextureView.setSurfaceTextureListener(this); | ||
| this.mActivity = activity; | ||
| } | ||
|
|
||
| // Surface Texture Listener methods | ||
| @Override | ||
| public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { | ||
| callOpenCameraCallback(width, height); | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { | ||
| configureTransform(width, height); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { | ||
|
|
||
| } | ||
|
|
||
| public boolean isAvailable() { | ||
| return mTextureView.isAvailable(); | ||
| } | ||
|
|
||
| public int getWidth() { | ||
| return mTextureView.getWidth(); | ||
| } | ||
|
|
||
| public int getHeight() { | ||
| return mTextureView.getHeight(); | ||
| } | ||
|
|
||
| public void setOrientation(int orientation, int width, int height) { | ||
| if (orientation == Configuration.ORIENTATION_LANDSCAPE) { | ||
| mTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight()); | ||
| } else { | ||
| mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth()); | ||
| } | ||
| configureTransform(width, height); | ||
| } | ||
|
|
||
| public Surface getSurface() { | ||
| SurfaceTexture texture = mTextureView.getSurfaceTexture(); | ||
| texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); | ||
|
|
||
| return new Surface(texture); | ||
| } | ||
|
|
||
| public void setPreviewSize(Size size) { | ||
| this.mPreviewSize = size; | ||
| } | ||
|
|
||
| public Size getPreviewSize() { | ||
| return this.mPreviewSize; | ||
| } | ||
|
|
||
| /** | ||
| * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose | ||
| * width and height are at least as large as the respective requested values, and whose aspect | ||
| * ratio matches with the specified value. | ||
| * | ||
| * @param choices The list of sizes that the camera supports for the intended output class | ||
| * @param targetMinWidth The minimum desired width | ||
| * @param targetMinHeight The minimum desired height | ||
| * @param aspectRatio The aspect ratio | ||
| * @return The optimal {@code Size}, or an arbitrary one if none were big enough | ||
| */ | ||
| public void setOptimalPreviewSize(Size[] choices, int targetMinWidth, int targetMinHeight, Size aspectRatio) { | ||
| // Collect the supported resolutions that are at least as big as the preview Surface | ||
| List<Size> matchingAspectRatioSizes = new ArrayList<Size>(); | ||
| List<Size> minimumDensitySizes = new ArrayList<Size>(); | ||
| int w = aspectRatio.getWidth(); | ||
| int h = aspectRatio.getHeight(); | ||
| MathContext mc = new MathContext(3, RoundingMode.UP); | ||
| BigDecimal targetAspectRatio = new BigDecimal(h, mc); | ||
| targetAspectRatio = targetAspectRatio.divide(new BigDecimal(w, mc), mc); | ||
| int targetMinDensity = targetMinHeight*targetMinWidth; | ||
| Log.d(TAG, "Target aspect ratio is " + targetAspectRatio + " looking for min of " + targetMinWidth + "x" + targetMinHeight); | ||
| for (Size option : choices) { | ||
| int density = option.getHeight() * option.getWidth(); | ||
| BigDecimal optionAspectRatio = (new BigDecimal(option.getHeight(), mc)) | ||
| .divide(new BigDecimal(option.getWidth(), mc), mc); | ||
| Log.d(TAG, "Found preview size of " + option.toString() + " has a ratio of " + optionAspectRatio); | ||
| if (targetAspectRatio.equals(optionAspectRatio)) { | ||
| matchingAspectRatioSizes.add(option); | ||
| if (density >= targetMinDensity) { | ||
| minimumDensitySizes.add(option); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (minimumDensitySizes.size() > 0) { // If any match the minimum size requirement, and the aspect ratio requirement, use the smallest | ||
| Size size = Collections.min(minimumDensitySizes, new CompareSizesByArea()); | ||
| Log.i(TAG, "Using a good match for preview size. " + size.toString()); | ||
| setPreviewSize(size); | ||
| return; | ||
| } | ||
| // Otherwise use the largest | ||
| if (matchingAspectRatioSizes.size() > 0) { | ||
| Size size = Collections.max(matchingAspectRatioSizes, new CompareSizesByArea()); | ||
| Log.w(TAG, "No preview sizes match the size of the screen, some scaling will occur. " + size.toString()); | ||
| setPreviewSize(size); | ||
| return; | ||
| } | ||
| // Otherwise, use the first one | ||
| Log.w(TAG, "Couldn't find any suitable preview size. Using " + choices[0].toString()); | ||
| setPreviewSize(choices[0]); | ||
| } | ||
| } |
| @@ -0,0 +1,351 @@ | ||
| package com.twominuteplays.camera; | ||
|
|
||
| import android.graphics.SurfaceTexture; | ||
| import android.hardware.camera2.CameraAccessException; | ||
| import android.hardware.camera2.CameraCaptureSession; | ||
| import android.hardware.camera2.CameraCharacteristics; | ||
| import android.hardware.camera2.CameraDevice; | ||
| import android.hardware.camera2.CameraManager; | ||
| import android.hardware.camera2.CameraMetadata; | ||
| import android.hardware.camera2.CaptureRequest; | ||
| import android.hardware.camera2.params.StreamConfigurationMap; | ||
| import android.media.MediaRecorder; | ||
| import android.os.Handler; | ||
| import android.os.HandlerThread; | ||
| import android.support.annotation.NonNull; | ||
| import android.util.Log; | ||
| import android.util.Size; | ||
| import android.view.Surface; | ||
|
|
||
| import com.twominuteplays.TwoMinutePlaysApp; | ||
| import com.twominuteplays.camera.TextureViewManager.CameraCallback; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.concurrent.Semaphore; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| public class VideoCameraManager implements CameraCallback { | ||
| private static final String TAG = VideoCameraManager.class.getName(); | ||
|
|
||
| private CameraDevice mCameraDevice; | ||
| private TextureViewManager mTextureViewManager; | ||
| private Semaphore mCameraOpenCloseLock = new Semaphore(1); | ||
| private HandlerThread mBackgroundThread; | ||
| private Handler mBackgroundHandler; | ||
| private CaptureRequest.Builder mPreviewBuilder; | ||
| private PermissionsCallback mPermissionsCallback; | ||
| private String mClipPath; | ||
| private RecordingStateCallback mRecordingStateCallback; | ||
| private Integer mSensorOrientation; | ||
| private Size mVideoSize; | ||
| private MediaRecorderWrapper mMediaRecorder; | ||
|
|
||
| public VideoCameraManager(TextureViewManager textureViewManager) { | ||
| this.mTextureViewManager = textureViewManager; | ||
| mTextureViewManager.setCameraCallback(this); | ||
| } | ||
|
|
||
| /** | ||
| * Call after cameraConfigured calls you. | ||
| */ | ||
| public void startMediaRecorder() { | ||
| mMediaRecorder.start(); | ||
| } | ||
|
|
||
| public interface PermissionsCallback { | ||
| boolean hasPermissions(); | ||
| } | ||
|
|
||
| public interface RecordingStateCallback { | ||
| void recordingStopped(String fileName); | ||
| void cameraConfigured(); | ||
| void configurationFailed(); | ||
| void error(String s); | ||
| } | ||
|
|
||
| public void setPermissionsCallback(PermissionsCallback callback) { | ||
| this.mPermissionsCallback = callback; | ||
| } | ||
|
|
||
| public void setRecordingStateCallback(RecordingStateCallback recordingStateCallback) { | ||
| this.mRecordingStateCallback = recordingStateCallback; | ||
| } | ||
|
|
||
| public void beginPreview() { | ||
| startBackgroundThread(); | ||
| if (mTextureViewManager.isAvailable()) { | ||
| // This is the entry point | ||
| openCamera(mTextureViewManager.getWidth(), mTextureViewManager.getHeight()); | ||
| } | ||
| } | ||
|
|
||
| public void pause() { | ||
| closeCamera(); | ||
| stopBackgroundThread(); | ||
| } | ||
|
|
||
| // public void stopRecordingVideo() { | ||
| // Log.d(TAG, "Stopping recording."); | ||
| // // Stop recording | ||
| // try { | ||
| // mMediaRecorder.stop(); | ||
| // if (mRecordingStateCallback != null) { | ||
| // mRecordingStateCallback.recordingStopped(mClipPath); | ||
| // } | ||
| // } | ||
| // catch (IllegalStateException e) { | ||
| // mMediaRecorder.reset(); | ||
| // reportError("Didn't catch that. Try again."); | ||
| // } | ||
| // } | ||
| public void stopRecordingVideo(boolean start) { | ||
| Log.d(TAG, "Stopping recording."); | ||
| // Stop recording | ||
| try { | ||
| mMediaRecorder.stop(); | ||
| if (mRecordingStateCallback != null) { | ||
| mRecordingStateCallback.recordingStopped(mClipPath); | ||
| } | ||
| } | ||
| finally { | ||
| closeCamera(); | ||
| if(start) | ||
| openCamera(mTextureViewManager.getWidth(), mTextureViewManager.getHeight()); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void startRecordingVideo(String path) { | ||
| Log.d(TAG, "Attempting to start recording."); | ||
| if (null == mCameraDevice || !mTextureViewManager.isAvailable() || null == mTextureViewManager.getPreviewSize()) { | ||
| Log.w(TAG, "Camera or texture not ready. Bailing out of attempt to start recording."); | ||
| Log.d(TAG, "Camera device is null? " + (null == mCameraDevice)); | ||
| Log.d(TAG, "Texture view manager is available " + mTextureViewManager.isAvailable() + " has preview size of " + mTextureViewManager.getPreviewSize()); | ||
| throw new IllegalStateException("Camera to texture not ready."); | ||
| } | ||
| try { | ||
| mClipPath = path; | ||
| mMediaRecorder.reset(); | ||
| setUpMediaRecorder(); | ||
|
|
||
| mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); | ||
| List<Surface> surfaces = new ArrayList<>(); | ||
|
|
||
| // Set up Surface for the camera preview | ||
| Surface textureViewSurface = mTextureViewManager.getSurface(); | ||
| surfaces.add(textureViewSurface); | ||
| mPreviewBuilder.addTarget(textureViewSurface); | ||
|
|
||
| // Set up Surface for the MediaRecorder | ||
| Surface mRecorderSurface = mMediaRecorder.getSurface(); | ||
| surfaces.add(mRecorderSurface); | ||
| mPreviewBuilder.addTarget(mRecorderSurface); | ||
|
|
||
| Log.d(TAG, "Surfaces all configured, trying to start video capture session."); | ||
| // Start a capture session | ||
| // Once the session starts, we can update the UI and start recording | ||
| mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { | ||
| @Override | ||
| public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { | ||
| updatePreview(cameraCaptureSession); | ||
| Log.d(TAG, "Everything's ok for now, recording started."); | ||
| mRecordingStateCallback.cameraConfigured(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { | ||
| Log.e(TAG, "Camera device capture session configuration failed."); | ||
| mRecordingStateCallback.configurationFailed(); | ||
| } | ||
| }, mBackgroundHandler); | ||
| } catch (CameraAccessException e) { | ||
| Log.e(TAG, "Could not access camera. Reason: " + e.getReason(), e); | ||
| } catch (IOException e) { | ||
| Log.e(TAG, "IO Error while spinning up camera.", e); | ||
| } | ||
| } | ||
|
|
||
| private void reportError(String message) { | ||
| Log.e(TAG, message); | ||
| if (mRecordingStateCallback != null) { | ||
| mRecordingStateCallback.error(message); | ||
| } | ||
| } | ||
|
|
||
| private void reportError(String message, Throwable t) { | ||
| Log.e(TAG, message, t); | ||
| reportError(message); | ||
| } | ||
|
|
||
| private void startPreview() { | ||
| if (null == mCameraDevice || !mTextureViewManager.isAvailable() || null == mTextureViewManager.getPreviewSize()) { | ||
| return; | ||
| } | ||
| try { | ||
| mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); | ||
| Surface previewSurface = mTextureViewManager.getSurface(); | ||
| mPreviewBuilder.addTarget(previewSurface); | ||
| mCameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() { | ||
| @Override | ||
| public void onConfigured(CameraCaptureSession cameraCaptureSession) { | ||
| updatePreview(cameraCaptureSession); | ||
| } | ||
|
|
||
| @Override | ||
| public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { | ||
| Log.e(TAG, "Configure camera failed."); // TODO: now what? | ||
| } | ||
| }, mBackgroundHandler); | ||
| } catch (CameraAccessException e) { | ||
| Log.e(TAG, "The camera is no longer connected or has encountered a fatal error", e); | ||
| } | ||
| } | ||
|
|
||
| private final class CameraStateCallback extends CameraDevice.StateCallback { | ||
| // Camera State Callback Methods | ||
| @Override | ||
| public void onOpened(CameraDevice cameraDevice) { | ||
| mCameraDevice = cameraDevice; | ||
| startPreview(); | ||
| mCameraOpenCloseLock.release(); | ||
| mTextureViewManager.configureTransform(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onDisconnected(CameraDevice cameraDevice) { | ||
| mCameraOpenCloseLock.release(); | ||
| cameraDevice.close(); | ||
| mCameraDevice = null; | ||
| } | ||
|
|
||
| @Override | ||
| public void onError(CameraDevice cameraDevice, int error) { | ||
| mCameraOpenCloseLock.release(); | ||
| cameraDevice.close(); | ||
| mCameraDevice = null; | ||
| Log.e(TAG, "Camera error " + error); // TODO: bail out of activity? Recover? | ||
| } | ||
|
|
||
| @Override | ||
| public void onClosed(CameraDevice camera) { | ||
| Log.i(TAG, "Camera closed."); | ||
| stopBackgroundThread(); | ||
| } | ||
| } | ||
|
|
||
| private void startBackgroundThread() { | ||
| mBackgroundThread = new HandlerThread("CameraBackground"); | ||
| mBackgroundThread.start(); | ||
| mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); | ||
| } | ||
|
|
||
| private void stopBackgroundThread() { | ||
| if (mBackgroundThread != null) { | ||
| mBackgroundThread.quitSafely(); | ||
| try { | ||
| mBackgroundThread.join(); | ||
| mBackgroundThread = null; | ||
| mBackgroundHandler = null; | ||
| } catch (InterruptedException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private synchronized void updatePreview(CameraCaptureSession cameraCaptureSession) { | ||
| if (null == mCameraDevice || cameraCaptureSession == null) { | ||
| return; | ||
| } | ||
| try { | ||
| setUpCaptureRequestBuilder(mPreviewBuilder); | ||
| cameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler); | ||
| } catch (CameraAccessException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
|
|
||
| private void setUpCaptureRequestBuilder(CaptureRequest.Builder builder) { | ||
| builder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); | ||
| } | ||
|
|
||
| @Override | ||
| public void openCamera(int width, int height) { | ||
| if (mPermissionsCallback == null || !mPermissionsCallback.hasPermissions()) | ||
| return; | ||
|
|
||
| CameraManager manager = TwoMinutePlaysApp.getCameraManager(); | ||
| try { | ||
| Log.d(TAG, "tryAcquire"); | ||
| if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { | ||
| throw new RuntimeException("Time out waiting to lock camera opening."); | ||
| } | ||
| String cameraId = CameraHelper.findFrontFacingCamera(manager); | ||
| if (cameraId == null) { | ||
| cameraId = CameraHelper.findFallbackCamera(manager); | ||
| } | ||
|
|
||
| if (cameraId == null) { | ||
| reportError("Could not find a suitable camera to use."); | ||
| return; | ||
| } | ||
|
|
||
| // Choose the sizes for camera preview and video recording | ||
| CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); | ||
| StreamConfigurationMap map = characteristics | ||
| .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); | ||
| mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); | ||
| mVideoSize = CameraHelper.chooseVideoSize(map.getOutputSizes(MediaRecorder.class)); | ||
| mTextureViewManager.setOptimalPreviewSize(map.getOutputSizes(SurfaceTexture.class), | ||
| width, height, mVideoSize); | ||
| mTextureViewManager.setOrientation(TwoMinutePlaysApp.getOrientation(), width, height); | ||
| mMediaRecorder = new MediaRecorderWrapper(); | ||
| manager.openCamera(cameraId, new CameraStateCallback(), null); // This is how mCameraDevice is created | ||
| } catch (CameraAccessException e) { | ||
| reportError("Cannot access the camera.", e); | ||
| } catch (NullPointerException e) { | ||
| Log.e(TAG, "Currently an NPE is thrown when the Camera2API is used but not supported on the device.", e); | ||
| reportError("Cannot access the camera on your device."); | ||
| } catch (InterruptedException e) { | ||
| reportError("Cannot access the camera on your device."); | ||
| } catch (SecurityException e) { | ||
| reportError("Two Minute Plays does not have access the video camera."); | ||
| } | ||
|
|
||
| } | ||
|
|
||
|
|
||
| private void closeCamera() { | ||
| try { | ||
| mCameraOpenCloseLock.acquire(); | ||
| if (null != mCameraDevice) { | ||
| mCameraDevice.close(); | ||
| mCameraDevice = null; | ||
| } | ||
| if (null != mMediaRecorder) { | ||
| try { | ||
| mMediaRecorder.release(); | ||
| } | ||
| finally { | ||
| mMediaRecorder = null; | ||
| } | ||
| } | ||
| } catch (InterruptedException e) { | ||
| reportError("Interrupted while trying to lock camera closing."); | ||
| } finally { | ||
| mCameraOpenCloseLock.release(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Where the bulk of the media recorder is configured. Specific state machine rules apply. | ||
| */ | ||
| private void setUpMediaRecorder() throws IOException { | ||
| mMediaRecorder.setAudioVideoSource(); | ||
| mMediaRecorder.setOutputFormat(); | ||
| mMediaRecorder.configureDataSource(mVideoSize, mSensorOrientation, mClipPath); | ||
| mMediaRecorder.prepare(); // after sources, eccoders, etc., before start() | ||
| } | ||
|
|
||
| } |
| @@ -0,0 +1,83 @@ | ||
| package com.twominuteplays.db; | ||
|
|
||
| import android.content.Context; | ||
| import android.util.Log; | ||
|
|
||
| import com.google.firebase.database.DataSnapshot; | ||
| import com.google.firebase.database.DatabaseError; | ||
| import com.google.firebase.database.DatabaseReference; | ||
| import com.google.firebase.database.ValueEventListener; | ||
| import com.twominuteplays.exceptions.MappingError; | ||
| import com.twominuteplays.model.Contributions; | ||
| import com.twominuteplays.model.Movie; | ||
| import com.twominuteplays.services.ClipDownloadService; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class ContributedManager { | ||
| private static final String TAG = ContributorsManager.class.getName(); | ||
|
|
||
| private Map<String,ValueEventListener> contributionsListenerMap; | ||
|
|
||
| private Map<String,ValueEventListener> getContributionsListenerMap() { | ||
| if (contributionsListenerMap == null) { | ||
| contributionsListenerMap = new HashMap<>(); | ||
| } | ||
| return contributionsListenerMap; | ||
| } | ||
|
|
||
| private String getKey(Long shareId, String contributor) { | ||
| return shareId.toString() + "/" + contributor; | ||
| } | ||
|
|
||
| public synchronized void registerListener(final Movie movie, final Context context) { | ||
| Long shareId = movie.getShareId(); | ||
| String contributor = movie.getContributor(); | ||
| if(shareId != null && !getContributionsListenerMap().containsKey(getKey(shareId, contributor))) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(shareId.toString()) | ||
| .child("contributors").child(contributor); | ||
| if (db == null) { | ||
| Log.w(TAG, "Movies database reference is null. Cannot add listeners."); | ||
| return; | ||
| } | ||
| Log.d(TAG, "Looking for contributions to " + db.toString()); | ||
|
|
||
| ValueEventListener listener = | ||
| new ValueEventListener() { | ||
| @Override | ||
| public void onDataChange(DataSnapshot dataSnapshot) { | ||
| Log.d(TAG, "Found contributor contributions for my share clone. Downloading to " + movie.getId()); | ||
| try { | ||
| Contributions contributions = Contributions.fromMap(dataSnapshot.getValue()); | ||
| ClipDownloadService.startActionDownloadClips(context, contributions, movie); | ||
| } catch (MappingError mappingError) { | ||
| Log.e(TAG, "Error mapping contribution: " + mappingError.getMessage(), mappingError); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public void onCancelled(DatabaseError databaseError) { | ||
|
|
||
| } | ||
| }; | ||
| getContributionsListenerMap().put(getKey(shareId, contributor), listener); | ||
| db.addValueEventListener(listener); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void removeListener(Long shareId, String contributor) { | ||
| try { | ||
| if (shareId != null && contributor != null && | ||
| getContributionsListenerMap().containsKey(getKey(shareId, contributor))) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(shareId.toString()) | ||
| .child("contributors").child(contributor); | ||
| db.removeEventListener(getContributionsListenerMap().get(getKey(shareId, contributor))); | ||
| } | ||
| } | ||
| catch(Throwable t) { | ||
| Log.e(TAG, "Error removing ContributedListener from share. May have leaked.", t); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,97 @@ | ||
| package com.twominuteplays.db; | ||
|
|
||
| import android.util.Log; | ||
|
|
||
| import com.google.firebase.database.ChildEventListener; | ||
| import com.google.firebase.database.DataSnapshot; | ||
| import com.google.firebase.database.DatabaseError; | ||
| import com.google.firebase.database.MutableData; | ||
| import com.google.firebase.database.Transaction; | ||
| import com.twominuteplays.exceptions.MappingError; | ||
| import com.twominuteplays.model.Contributions; | ||
| import com.twominuteplays.model.Movie; | ||
|
|
||
| public class ContributorsListener implements ChildEventListener { | ||
| public static final String TAG = ContributorsListener.class.getName(); | ||
|
|
||
| private final Movie sharedMovie; | ||
|
|
||
| public ContributorsListener(Movie movie) { | ||
| sharedMovie = movie; | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public void onChildAdded(DataSnapshot dataSnapshot, String s) { | ||
| cloneMovieFromShareContributions(dataSnapshot, sharedMovie); | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildChanged(DataSnapshot dataSnapshot, String s) { | ||
| cloneMovieFromShareContributions(dataSnapshot, sharedMovie); | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildRemoved(DataSnapshot dataSnapshot) { | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildMoved(DataSnapshot dataSnapshot, String s) { | ||
| } | ||
|
|
||
| @Override | ||
| public void onCancelled(DatabaseError databaseError) { | ||
| Log.w(TAG, "Listener for contributions cancelled: " + databaseError.getMessage()); | ||
| } | ||
|
|
||
| private void cloneMovieFromShareContributions(DataSnapshot dataSnapshot, final Movie sharedMovie) { | ||
| final String contributorUid = dataSnapshot.getKey(); | ||
| try { | ||
| Contributions contributions = Contributions.fromMap(dataSnapshot.getValue()); | ||
| cloneMovie(dataSnapshot, contributorUid, contributions, sharedMovie); | ||
| } catch (MappingError mappingError) { | ||
| Log.e(TAG, "Mapping error " + mappingError.getMessage(), mappingError); | ||
| } | ||
| } | ||
|
|
||
| private void cloneMovie(DataSnapshot dataSnapshot, final String contributorUid, final Contributions contributions, final Movie sharedMovie) { | ||
| Log.d(TAG, "Found contributions changes for " + sharedMovie.getId()); | ||
| if(contributions != null && contributions.getClips() != null && !contributions.isCloned()) { | ||
| Log.d(TAG, "Got a live one! Try to clone clips."); | ||
| dataSnapshot.getRef().runTransaction(new Transaction.Handler() { | ||
| @Override | ||
| public Transaction.Result doTransaction(MutableData mutableData) { | ||
| try { | ||
| Contributions currentValue = Contributions.fromMap(mutableData.getValue()); | ||
| Log.d(TAG, "Found candidate for cloning from contributor " + contributorUid); | ||
| if (currentValue != null && !currentValue.isCloned()) { | ||
| Log.d(TAG, "Contribution has not yet spawned a movie clone. Cloning..."); | ||
| currentValue.setCloned(true); | ||
| mutableData.setValue(currentValue); | ||
| return Transaction.success(mutableData); | ||
| } | ||
| } catch (MappingError mappingError) { | ||
| Log.e(TAG, "Error cloning: " + mappingError.getMessage(), mappingError); | ||
| } | ||
| return Transaction.abort(); | ||
| } | ||
|
|
||
| @Override | ||
| public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { | ||
| Log.i(TAG, "Possible clone begin."); | ||
| Contributions contributionsCurrentValue = null; | ||
| try { | ||
| contributionsCurrentValue = Contributions.fromMap(dataSnapshot.getValue()); | ||
| } | ||
| catch (Throwable t) { | ||
| Log.w(TAG, "Could not read contributions from data snapshot while cloning shared movie.", t); | ||
| } | ||
| if (committed && contributionsCurrentValue != null && contributionsCurrentValue.isCloned()) { | ||
| sharedMovie.state.shareClone(contributorUid, sharedMovie); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| } |
| @@ -0,0 +1,57 @@ | ||
| package com.twominuteplays.db; | ||
|
|
||
| import android.util.Log; | ||
|
|
||
| import com.google.firebase.database.DatabaseReference; | ||
| import com.twominuteplays.R; | ||
| import com.twominuteplays.TwoMinutePlaysApp; | ||
| import com.twominuteplays.model.Movie; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class ContributorsManager { | ||
|
|
||
| private static final String TAG = ContributorsManager.class.getName(); | ||
|
|
||
| private Map<String,ContributorsListener> contributorsListenerMap; | ||
|
|
||
| private Map<String,ContributorsListener> getContributorsListenerMap() { | ||
| if (contributorsListenerMap == null) { | ||
| contributorsListenerMap = new HashMap<>(); | ||
| } | ||
| return contributorsListenerMap; | ||
| } | ||
|
|
||
| public synchronized void registerListener(Movie movie) { | ||
| Long shareId = movie.getShareId(); | ||
| if(shareId != null && !getContributorsListenerMap().containsKey(shareId.toString())) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(shareId.toString()) | ||
| .child(TwoMinutePlaysApp.getResourceString(R.string.contributorsNode)); | ||
|
|
||
| Log.d(TAG, "Attempting to listen for contributions for " + db.toString()); | ||
| if (db == null) { | ||
| Log.w(TAG, "Movies database reference is null. Cannot add listeners."); | ||
| return; | ||
| } | ||
| ContributorsListener listener = new ContributorsListener(movie); | ||
| getContributorsListenerMap().put(shareId.toString(), listener); | ||
| db.addChildEventListener(listener); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void removeListener(Long shareId) { | ||
| try { | ||
| if (shareId != null && getContributorsListenerMap().containsKey(shareId.toString())) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(shareId.toString()) | ||
| .child(TwoMinutePlaysApp.getResourceString(R.string.contributorsNode)); | ||
| db.removeEventListener(getContributorsListenerMap().remove(shareId.toString())); | ||
| getContributorsListenerMap().remove(shareId.toString()); | ||
| } | ||
| } | ||
| catch(Throwable t) { | ||
| Log.e(TAG, "Error removing ContributorsListener from share. May have leaked.", t); | ||
| } | ||
| } | ||
|
|
||
| } |
| @@ -0,0 +1,140 @@ | ||
| package com.twominuteplays.db; | ||
|
|
||
| import android.content.Context; | ||
| import android.util.Log; | ||
|
|
||
| import com.google.firebase.database.ChildEventListener; | ||
| import com.google.firebase.database.DataSnapshot; | ||
| import com.google.firebase.database.DatabaseError; | ||
| import com.twominuteplays.model.Movie; | ||
| import com.twominuteplays.model.MovieBuilder; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| class MyMoviesEventListener implements ChildEventListener { | ||
| private static final String TAG = MyMoviesEventListener.class.getName(); | ||
| private final Context context; | ||
|
|
||
| private final ContributorsManager contributorsManager = new ContributorsManager(); | ||
| private final OwnerContributionsManager ownerContributionsManager = new OwnerContributionsManager(); | ||
| private final ContributedManager contributedManager = new ContributedManager(); | ||
|
|
||
| public MyMoviesEventListener(Context context) { | ||
| this.context = context; | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) { | ||
| // Called once initially, then again for each movie added | ||
| Map<String,Object> jsonSnapshot = (Map<String, Object>) dataSnapshot.getValue(); | ||
| MovieBuilder builder = new MovieBuilder(); | ||
| Movie movie = builder.withJson(jsonSnapshot).build(); | ||
|
|
||
| if (movie != null) { | ||
| Log.d(TAG, "Listening for changes to my added movie " + movie.getId()); | ||
| onMovieChange(context, movie); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) { | ||
| // Same as onChildAdded | ||
| Map<String,Object> jsonSnapshot = (Map<String, Object>) dataSnapshot.getValue(); | ||
| MovieBuilder builder = new MovieBuilder(); | ||
| Movie movie = builder.withJson(jsonSnapshot).build(); | ||
|
|
||
| if (movie != null) { | ||
| Log.d(TAG, "Listening for changes to my modified movie " + movie.getId()); | ||
| onMovieChange(context, movie); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildRemoved(DataSnapshot dataSnapshot) { | ||
| Map<String,Object> jsonSnapshot = (Map<String, Object>) dataSnapshot.getValue(); | ||
| MovieBuilder builder = new MovieBuilder(); | ||
| Movie movie = builder.withJson(jsonSnapshot).build(); | ||
|
|
||
| Log.d(TAG, "Removing movie " + movie.getId()); | ||
| } | ||
|
|
||
| @Override | ||
| public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) { | ||
| // Dont' care | ||
| } | ||
|
|
||
| @Override | ||
| public void onCancelled(DatabaseError databaseError) { | ||
| Log.w(TAG, "Listener for movies cancelled: " + databaseError.getMessage()); | ||
| } | ||
|
|
||
| private void onMovieChange(Context context, final Movie movie) { | ||
| if (movie.getShareId() != null && movie.getState() != null) { | ||
| switch (movie.getState()) { | ||
| case SHARED: | ||
| listenForContributions(context, movie); | ||
| break; | ||
| case TEMPLATE: | ||
| break; | ||
| case SELECTED: | ||
| break; | ||
| case PART_SELECTED: | ||
| break; | ||
| case RECORDING_STARTED: | ||
| break; | ||
| case CONTRIBUTE: | ||
| break; | ||
| case RECORDED: | ||
| break; | ||
| case CONTRIBUTED: | ||
| listenForOwnerContributions(context, movie); | ||
| break; | ||
| case SINGLE_USER: | ||
| break; | ||
| case SINGLE_USER_MERGED: | ||
| break; | ||
| case SHARE_CLONED: | ||
| listenForContributed(context, movie); | ||
| break; | ||
| case DOWNLOADING_OWNER: | ||
| break; | ||
| case DOWNLOADING_CONTRIBUTOR: | ||
| break; | ||
| case DOWNLOADED: | ||
| removeListeners(movie); | ||
| break; | ||
| case MERGED: | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void removeListeners(Movie movie) { | ||
| ownerContributionsManager.removeListener(movie); | ||
| } | ||
|
|
||
| /** | ||
| * If a movie is shared and then clips are uploaded for it, we need to create a SHARE_CLONED copy. | ||
| */ | ||
| private synchronized void listenForContributions(final Context context, final Movie sharedMovie) { | ||
| contributorsManager.registerListener(sharedMovie); | ||
| } | ||
|
|
||
| /** | ||
| * Download clips when an owner contributes their own clips. | ||
| */ | ||
| private synchronized void listenForOwnerContributions(final Context context, final Movie contributedMovie) { | ||
| ownerContributionsManager.registerListener(contributedMovie, context); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Download clips when they are uploaded from a contributor. | ||
| */ | ||
| private synchronized void listenForContributed(final Context context, final Movie movie) { | ||
| contributedManager.registerListener(movie, context); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| } |
| @@ -0,0 +1,67 @@ | ||
| package com.twominuteplays.db; | ||
|
|
||
| import android.content.Context; | ||
| import android.util.Log; | ||
|
|
||
| import com.google.firebase.database.DataSnapshot; | ||
| import com.google.firebase.database.DatabaseError; | ||
| import com.google.firebase.database.DatabaseReference; | ||
| import com.google.firebase.database.ValueEventListener; | ||
| import com.twominuteplays.exceptions.MappingError; | ||
| import com.twominuteplays.model.Contributions; | ||
| import com.twominuteplays.model.Movie; | ||
| import com.twominuteplays.services.ClipDownloadService; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| public class OwnerContributionsManager { | ||
| private static final String TAG = OwnerContributionsManager.class.getName(); | ||
|
|
||
| private Map<String,ValueEventListener> ownerContributionsListenerMap; | ||
|
|
||
| private Map<String,ValueEventListener> getOwnerContributionsListenerMap() { | ||
| if (ownerContributionsListenerMap == null) { | ||
| ownerContributionsListenerMap = new HashMap<>(); | ||
| } | ||
| return ownerContributionsListenerMap; | ||
| } | ||
|
|
||
|
|
||
| public synchronized void registerListener(final Movie movie, final Context context) { | ||
| if(!getOwnerContributionsListenerMap().containsKey(movie.getId())) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(movie.getShareId().toString()).child("ownersClips"); | ||
| if (db == null) { | ||
| Log.w(TAG, "Movies database reference is null. Cannot add listeners."); | ||
| return; | ||
| } | ||
| ValueEventListener listener = | ||
| new ValueEventListener() { | ||
| @Override | ||
| public void onDataChange(DataSnapshot dataSnapshot) { | ||
| Log.d(TAG, "Found owner contributions for my contributed movie. Downloading to " + movie.getId()); | ||
| try { | ||
| Contributions ownerContributions = Contributions.fromMap(dataSnapshot.getValue()); | ||
| ClipDownloadService.startActionDownloadClips(context, ownerContributions, movie); | ||
| } catch (MappingError mappingError) { | ||
| Log.e(TAG, "Error mapping owner contributions: " + mappingError.getMessage(), mappingError); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onCancelled(DatabaseError databaseError) { | ||
|
|
||
| } | ||
| }; | ||
| getOwnerContributionsListenerMap().put(movie.getId(), listener); | ||
| db.addValueEventListener(listener); | ||
| } | ||
| } | ||
|
|
||
| public synchronized void removeListener(Movie movie) { | ||
| if(getOwnerContributionsListenerMap().containsKey(movie.getId())) { | ||
| DatabaseReference db = FirebaseStuff.getShareRef(movie.getShareId().toString()).child("ownersClips"); | ||
| db.removeEventListener(getOwnerContributionsListenerMap().get(movie.getId())); | ||
| } | ||
| } | ||
| } |