@@ -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()
}

}

Large diffs are not rendered by default.

@@ -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()));
}
}
}

Large diffs are not rendered by default.

@@ -50,7 +50,7 @@ protected void populateViewHolder(ClickableScriptCardViewHolder scriptCardViewHo
.load(R.mipmap.card_bg)
.into(scriptCardViewHolder.scriptImageView);
}
if (MovieState.RECORDED == movie.getState()) {
if (MovieState.RECORDED == movie.getState() || MovieState.SHARED == movie.getState()) {
scriptCardViewHolder.shareButton.setVisibility(View.VISIBLE);
scriptCardViewHolder.shareButton.setTag(movie);
scriptCardViewHolder.shareButton.setOnClickListener(
@@ -3,6 +3,9 @@
import android.os.Parcel;
import android.os.Parcelable;

import com.google.firebase.database.Exclude;
import com.twominuteplays.exceptions.MappingError;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -32,6 +35,17 @@ public Contributions(Map<String, Object> map) {
this.clips = (Map<String, String>) map.get("clips");
}

@Exclude
public static Contributions fromMap(Object value) throws MappingError {
if (value != null && value instanceof Map) {
Map<String, Object> map = (Map<String, Object>) value;
if (!map.containsKey("cloned"))
throw new MappingError("Contributions map must have a cloned property.");
return new Contributions(map);
}
throw new MappingError("Value is not an instance of Map.");
}

private String mapValue(Object value) {
if (value == null)
return null;
@@ -75,10 +75,6 @@ private Movie beginClipsDownload(final Contributions contributions, Movie movie)
final Part part = movie.findPart(contributions.getPartId());
// Downloads the clips one at a time.
for(Line line : part.getLines()) {
// if(!line.needsMovieClip()) {
// Log.i(TAG, "Line " + line.getId() + " already has a downloaded video. Skipping.");
// continue;
// }
File outputFile = new File(getExternalFilesDir(Environment.DIRECTORY_MOVIES),
movie.getId() + "-" + line.getId() + ".mp4");
downloadClip(outputFile, contributions.getClips().get(line.getId()));

This file was deleted.

This file was deleted.

@@ -35,6 +35,8 @@
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="60dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</RelativeLayout>

@@ -16,7 +16,7 @@
<string name="picture">Picture</string>
<string name="camera_error">Looks like none of your device\'s cameras are compatible.</string>
<string name="intro_message">Camera intro message</string>
<string name="permission_request">PERMISSION REQUEST</string>
<string name="permission_request">Two Minute Plays requires access to the video camera to record your clips.</string>
<string name="play_or_pause_toggle_button">Play or pause toggle button.</string>
<string name="recoding_indicator">recoding indicator</string>
<string name="current_line">Current line</string>

This file was deleted.

@@ -6,7 +6,7 @@ buildscript {
mavenLocal()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.google.gms:google-services:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files