1
\$\begingroup\$

I've built a service that records a camera.

The aim of this service is mostly to record video while the app is in the background.

public class RecordingService extends LifecycleService {


    public static final String ACTION_RETURN_TO_ACTIVITY = "ACTION_RETURN_TO_ACTIVITY";
    public static final String ACTION_TOGGLE_RECORD = "ACTION_TOGGLE_RECORD";
    public static final String ACTION_TOGGLE_SOUND = "ACTION_TOGGLE_SOUND";
    public static final String ACTION_TOGGLE_SAVE = "ACTION_TOGGLE_SAVE";
    public static final String ACTION_KILL_SERVICE = "ACTION_KILL_SERVICE";
    public static final String ACTION_FINISH_ACTIVITY = "ACTION_FINISH_ACTIVITY";
    public static final String NO_ACTION = "NO_ACTION";
    public static final String RECORD_AFTER_START = "recordAfterStart";
    private static final int REQUEST_CODE = 341;
    private static final int SENSOR_ORIENTATION_DEFAULT_DEGREES = 90;
    private static final int SENSOR_ORIENTATION_INVERSE_DEGREES = 270;
    private static final SparseIntArray DEFAULT_ORIENTATIONS = new SparseIntArray();
    private static final SparseIntArray INVERSE_ORIENTATIONS = new SparseIntArray();
    private static final String TAG = "Camera2VideoFragment";
    private static boolean hasBeenInitialized = false;

    static {
        DEFAULT_ORIENTATIONS.append(Surface.ROTATION_0, 90);
        DEFAULT_ORIENTATIONS.append(Surface.ROTATION_90, 0);
        DEFAULT_ORIENTATIONS.append(Surface.ROTATION_180, 270);
        DEFAULT_ORIENTATIONS.append(Surface.ROTATION_270, 180);
    }

    static {
        INVERSE_ORIENTATIONS.append(Surface.ROTATION_0, 270);
        INVERSE_ORIENTATIONS.append(Surface.ROTATION_90, 180);
        INVERSE_ORIENTATIONS.append(Surface.ROTATION_180, 90);
        INVERSE_ORIENTATIONS.append(Surface.ROTATION_270, 0);
    }

    // This is the object that receives interactions from clients.
    private final IBinder binder = new RecordingService.RecordBinder();
    @Inject
    IRecordRepository recordRepository;
    @Inject
    IPreferencesRepository preferencesRepository;
    private Notification notif;
    private Handler timerHandler;
    private Runnable timerRunnable;
    private String nextVideoFilePath;
    private RemoteViews contentView;
    private IRecordingInitialization recordingInitialization;
    private RecordViewModel recordViewModel;
    private boolean shouldSaveVideo = false;
    private Surface previewSurface;
    private int oldWidth;
    private int oldHeight;
    private ArrayList<RecordLocalisationEntity> savedLocalisation;
    private MutableLiveData<Boolean> isRecording;
    private boolean recordOnCameraOpening = false;
    private MutableLiveData<String> addressLiveData;
    /**
     * An {@link AutoFitTextureView} for camera preview.
     */
    private AutoFitTextureView mTextureView;
    /**
     * A reference to the opened {@link android.hardware.camera2.CameraDevice}.
     */
    private CameraDevice mCameraDevice;
    /**
     * A reference to the current {@link android.hardware.camera2.CameraCaptureSession} for
     * preview.
     */
    private CameraCaptureSession mPreviewSession;
    /**
     * The {@link android.util.Size} of camera preview.
     */
    private Size mPreviewSize;
    /**
     * The {@link android.util.Size} of video recording.
     */
    private Size mVideoSize;
    /**
     * MediaRecorder
     */
    private MediaRecorder mMediaRecorder;
    /**
     * An additional thread for running tasks that shouldn't block the UI.
     */
    private HandlerThread mBackgroundThread;
    /**
     * A {@link Handler} for running tasks in the background.
     */
    private Handler mBackgroundHandler;
    /**
     * A {@link Semaphore} to prevent the app from exiting before closing the camera.
     */
    private Semaphore mCameraOpenCloseLock = new Semaphore(1);
    private Integer mSensorOrientation;
    private CaptureRequest.Builder mPreviewBuilder;
    private Geocoder geocoder;
    private FusedLocationProviderClient locationProviderClient;
    private Task<LocationSettingsResponse> locationSettingsResponseTask;
    private long startingTime;
    private LocationCallback locationCallback = new LocationCallback() {
        @SuppressLint("MissingPermission")
        @Override
        public void onLocationResult(LocationResult locationResult) {

            Log.d(TAG,"onLocationResult");
            for (Location location : locationResult.getLocations()) {
                try {
                    Address addressObject = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1).get(0);
                    String numeroRue = addressObject.getSubThoroughfare();
                    numeroRue = (numeroRue == null) ? "" : numeroRue;
                    String address = numeroRue + " " + addressObject.getThoroughfare() + ", \n" + addressObject.getPostalCode() + " " + addressObject.getLocality();
                    addressLiveData.setValue(address);
                    if (isRecording.getValue()) {
                        RecordLocalisationEntity entity = new RecordLocalisationEntity();
                        entity.setAddress(address);
                        entity.setTime(SystemClock.elapsedRealtime() - startingTime);
                        savedLocalisation.add(entity);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    };
    /**
     * {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its status.
     */
    private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            Log.d(TAG,"CameraDevice.StateCallback onOpened");
            mCameraDevice = cameraDevice;
            cameraOpening = false;
            mCameraOpenCloseLock.release();
            Log.d(TAG,"statecallback onopened semaphore released");
            if (null != mTextureView) {
                configureTransform(mTextureView.getWidth(), mTextureView.getHeight());
            }
            if(recordingInitialization != null) {
                recordingInitialization.onComplete();
                recordingInitialization = null;
            }
            if(recordOnCameraOpening){
                startRecordingVideo();
                recordOnCameraOpening =false;
            }
            else{
                startPreview();
            }
        }

        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            Log.d(TAG,"CameraDevice.StateCallback onClosed");
            super.onClosed(camera);
            mCameraOpenCloseLock.release();
            Log.d(TAG,"statecallback onclosed semaphore released");
            mCameraDevice = null;
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {
            Log.d(TAG,"CameraDevice.StateCallback onDisconnected");
            mCameraOpenCloseLock.release();
            Log.d(TAG,"statecallback ondisconnected semaphore released");
            cameraDevice.close();
            mCameraDevice = null;
            if(mTextureView != null) {
                oldWidth = mTextureView.getWidth();
                oldHeight = mTextureView.getHeight();
                openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());
            }
            else{
                openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
            }

        }

        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            Log.d(TAG,"CameraDevice.StateCallback onError");
            mCameraOpenCloseLock.release();
            Log.d(TAG,"semaphore released");
            cameraDevice.close();
            mCameraDevice = null;
            if(error == ERROR_CAMERA_DEVICE){
                if(mTextureView != null) {
                    oldWidth = mTextureView.getWidth();
                    oldHeight = mTextureView.getHeight();
                    openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());
                }
                else{
                    openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
                }
            }
        }

    };
    private Surface recorderSurface;
    private SurfaceTexture previewSurfaceTexture;
    private boolean cameraOpening =false;


    public RecordingService() {
    }

    /**
     * In this sample, we choose a video size with 3x4 aspect ratio. Also, we don't use sizes
     * larger than 1080p, since MediaRecorder cannot handle such a high-resolution video.
     *
     * @param choices The list of available sizes
     * @return The video size
     */
    private static Size chooseVideoSize(Size[] choices, Size aspectRatio) {
        Log.d(TAG,"chooseVideoSize");
        for (Size size : choices) {
            if (size.getWidth() == size.getHeight() * aspectRatio.getHeight() / aspectRatio.getWidth() && size.getWidth() <= 1080) {
                return size;
            }
            if (size.getWidth() == size.getHeight() * aspectRatio.getWidth() / aspectRatio.getHeight() && size.getWidth() <= 1080) {
                return size;
            }
        }
        return choices[0];
    }

    /**
     * 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 width       The minimum desired width
     * @param height      The minimum desired height
     * @param aspectRatio The aspect ratio
     * @return The optimal {@code Size}, or an arbitrary one if none were big enough
     */
    private static Size chooseOptimalSize(Size[] choices, int width, int height, Size aspectRatio) {
        Log.d(TAG,"chooseOptimalSize");
        // Collect the supported resolutions that are at least as big as the preview Surface
        // Collect the supported resolutions that are at least as big as the preview Surface
        List<Size> bigEnough = new ArrayList<>();
        int w = aspectRatio.getWidth();
        int h = aspectRatio.getHeight();
        for (Size option : choices) {
            if (option.getHeight()== width * h / w && option.getWidth() >= width && option.getHeight() >= height) {
                bigEnough.add(option);
            }
        }

        // Pick the smallest of those, assuming we found any
        if (bigEnough.size() > 0) {
            return Collections.min(bigEnough, new CompareSizesByArea());
        } else {
            return aspectRatio;
        }
    }

    public MutableLiveData<String> getAddressLiveData() {
        return addressLiveData;
    }

    public void setCurrentCamera(int currentCamera) {
        Log.d(TAG,"setCurrentCamera");
        if(this.getIsRecordingVideo().getValue() || mTextureView == null){
            return;
        }

        this.closeCamera();
        if(mTextureView != null) {
            oldWidth = mTextureView.getWidth();
            oldHeight = mTextureView.getHeight();
            openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());
        }
        else{
            openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
        }
    }

    public void toggleSound() {
        Log.d(TAG,"toggleSound");
        if(this.getIsRecordingVideo().getValue()){
            return;
        }
        recordViewModel.toggleSound();
        AudioManager myAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);

        myAudioManager.setMode(AudioManager.MODE_NORMAL);
        if(myAudioManager.isMicrophoneMute() != recordViewModel.getIsSoundEnable().getValue()) {
            myAudioManager.setMicrophoneMute(recordViewModel.getIsSoundEnable().getValue());
        }
    }

    public void launchNotif() {
        Log.d(TAG,"launchNotif");
        Intent toggleRecordingIntent = new Intent(getApplicationContext(), RecordingService.class);
        toggleRecordingIntent.setAction(ACTION_TOGGLE_RECORD);
        PendingIntent piToggleRecording = PendingIntent.getService(getApplicationContext(), REQUEST_CODE, toggleRecordingIntent, 0);

        Intent toggleSaveCurrentRecordIntent = new Intent(getApplicationContext(), RecordingService.class);
        toggleSaveCurrentRecordIntent.setAction(ACTION_TOGGLE_SAVE);
        PendingIntent piToggleSaveCurrentRecordIntent = PendingIntent.getService(getApplicationContext(), REQUEST_CODE, toggleSaveCurrentRecordIntent, 0);


        contentView = new RemoteViews(getPackageName(),R.layout.notification_layout);
        contentView.setOnClickPendingIntent(R.id.toggle_recording,piToggleRecording);
        contentView.setOnClickPendingIntent(R.id.toggle_save_current_record,piToggleSaveCurrentRecordIntent);

        notif = getNotificationBuilder()
                .build();
        final NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(01, notif);
    }

    public NotificationCompat.Builder getNotificationBuilder() {
        Log.d(TAG,"getNotificationBuilder");
        Intent returnToActivityIntent = new Intent(getApplicationContext(), RecordingService.class);
        returnToActivityIntent.setAction(ACTION_RETURN_TO_ACTIVITY);
        PendingIntent piReturnToActivity = PendingIntent.getService(getApplicationContext(), REQUEST_CODE, returnToActivityIntent, 0);

        return new NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notification_icon)
                .setContent(contentView)
                .setPriority(Notification.PRIORITY_MAX)
                .setCustomBigContentView(contentView)
                .setContentIntent(piReturnToActivity)
                .setUsesChronometer(true);
    }

    public void onSurfaceTextureDestroyed() {
        Timber.e("onSurfaceTextureDestroyed");
        this.mTextureView = null;
        this.closePreviewSession();
        try {

            mCameraOpenCloseLock.acquire();
            Log.d(TAG,"semaphore acquire");
            closeCamera();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            mCameraOpenCloseLock.release();
            Log.d(TAG,"onsurfacetexture semaphore released");
        }

    }

    public void reloadCamera() {

        Log.d(TAG,"reloadCamera");
        boolean shouldRecordAfterReload = false;
        if(this.getIsRecordingVideo().getValue()){
            shouldRecordAfterReload = true;
            this.toggleRecording();
        }
        closeCamera();

        this.recordOnCameraOpening = shouldRecordAfterReload;

        if(mTextureView != null) {
            oldWidth = mTextureView.getWidth();
            oldHeight = mTextureView.getHeight();
            openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());

        }
        else{
            openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
        }


    }

    public MutableLiveData<Boolean> getIsRecordingVideo() {
        return isRecording;
    }

    public void beforeSendToBackground() {
        Log.d(TAG,"beforeSendToBackground");
        closeCamera();

    }

    public void afterSendToBackground() {
        Log.d(TAG,"afterSendToBackground");
        this.recordOnCameraOpening = true;
        if(mTextureView != null) {
            oldWidth = mTextureView.getWidth();
            oldHeight = mTextureView.getHeight();
            openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());
        }
        else{
            openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
        }
    }

    @SuppressLint("MissingPermission")
    @Override
    public void onCreate() {

        Log.d(TAG,"onCreate");
        super.onCreate();

        addressLiveData = new MutableLiveData<>();
        addressLiveData.setValue(getApplication().getResources().getString(R.string.disabled_gps));

        isRecording = new MutableLiveData<>();
        isRecording.setValue(false);
        geocoder = new Geocoder(getApplication(), Locale.getDefault());
        locationProviderClient = LocationServices.getFusedLocationProviderClient(getApplication());
        initLocationTask();
        LocationManager lm = (LocationManager) getApplication().getSystemService(Context.LOCATION_SERVICE);
        lm.addGpsStatusListener(event -> {
            switch (event) {
                case GPS_EVENT_STARTED:
                    initLocationTask();
                    break;
                case GPS_EVENT_STOPPED:
                    locationSettingsResponseTask = null;
                    locationProviderClient.removeLocationUpdates(locationCallback);
                    addressLiveData.setValue(getApplication().getResources().getString(R.string.disabled_gps));
                    break;
            }
        });
    }

    @SuppressLint("MissingPermission")
    private void initLocationTask() {

        Log.d(TAG,"initLocationTask");
        LocationRequest locationRequest = createLocationRequest();
        LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
                .addLocationRequest(locationRequest);
        SettingsClient client = LocationServices.getSettingsClient(getApplication());
        locationSettingsResponseTask = client.checkLocationSettings(builder.build());


        locationSettingsResponseTask.addOnSuccessListener(locationSettingsResponse -> {
            locationProviderClient.requestLocationUpdates(locationRequest,
                    locationCallback,
                    null /* Looper */);
        });
    }

    private LocationRequest createLocationRequest() {

        Log.d(TAG,"createLocationRequest");
        LocationRequest mLocationRequest = new LocationRequest();
        mLocationRequest.setInterval(10000);
        mLocationRequest.setFastestInterval(5000);
        mLocationRequest.setMaxWaitTime(10000);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
        return mLocationRequest;
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG,"onBind");
        super.onBind(intent);
        AndroidInjection.inject(this);
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG,"onStartCommand");
        super.onStartCommand(intent,flags,startId);
        if(intent == null || intent.getAction() == null) return START_STICKY;
        switch (intent.getAction()){
            case ACTION_RETURN_TO_ACTIVITY:
                if(this.recordViewModel != null && this.getIsRecordingVideo() != null && this.getIsRecordingVideo().getValue()){
                    this.toggleRecording();
                }
                beforeSendToBackground();
                Intent goToActivityIntent = new Intent(getApplicationContext(), MainActivity.class);
                goToActivityIntent.addFlags(FLAG_ACTIVITY_NEW_TASK);
                goToActivityIntent.addCategory(CATEGORY_LAUNCHER);
                startActivity(goToActivityIntent);

                break;
            case ACTION_TOGGLE_RECORD:
                this.toggleRecording();
                Toast.makeText(this, R.string.video_saved, Toast.LENGTH_SHORT).show();
                break;
            case ACTION_TOGGLE_SAVE:
                this.toggleSaveRecord();
                break;
            case ACTION_TOGGLE_SOUND:
                this.recordViewModel.toggleSound();
                this.toggleSound();
                break;
            case ACTION_KILL_SERVICE:
                if(!isRecording.getValue()) {
                    this.onUnbind(intent);
                }
                break;
            case NO_ACTION:
            default:
                break;
        }
        return START_STICKY;
    }

    private void toggleSaveRecord() {
        Log.d(TAG,"toggleSaveRecord");
        shouldSaveVideo = !shouldSaveVideo;
        if(shouldSaveVideo){
            Toast.makeText(this, R.string.video_starred, Toast.LENGTH_SHORT).show();
        }
        else{
            Toast.makeText(this, R.string.video_not_starred, Toast.LENGTH_SHORT).show();
        }
        this.updateNotification();
    }


    public void initialize(AutoFitTextureView textureView, RecordViewModel recordViewModel, IRecordingInitialization recordingInitialization){
        Log.d(TAG,"initialize");
        this.mTextureView = textureView;
        timerHandler = new Handler();

        savedLocalisation = new ArrayList<>();
        this.recordViewModel = recordViewModel;
        this.launchNotif();
        this.startForeground(01,notif);
        startBackgroundThread();
        int currentCamera = recordViewModel.getCurrentCamera().getValue();
        oldHeight = mTextureView.getHeight();
        oldWidth = mTextureView.getWidth();
        openCamera(mTextureView.getWidth(), mTextureView.getHeight(), currentCamera);

        if(this.recordViewModel.getAutoRecordingValue() && !hasBeenInitialized ){
            hasBeenInitialized = true;
            this.recordingInitialization = recordingInitialization;
        }
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG,"onUnbind");
        this.stopForeground(true);
        final NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.cancel(01);
        locationProviderClient.removeLocationUpdates(locationCallback);
        stopBackgroundThread();
        return true;
    }

    public void toggleRecording() {
        Log.d(TAG,"toggleRecording");
        if (this.getIsRecordingVideo().getValue()) {
            stopRecordingVideo();
        } else {
            startRecordingVideo();
        }
        updateNotification();
    }

    public long getStartingTime() {
        Log.d(TAG,"getStartingTime");
        return startingTime;
    }

    private void updateNotification() {
        Log.d(TAG,"updateNotification");
        String notificationButton = this.getIsRecordingVideo().getValue()? getResources().getString(R.string.stop):getResources().getString(R.string.start);
        contentView.setTextViewText(R.id.toggle_recording,notificationButton);

        int starResourceId = shouldSaveVideo? R.drawable.ic_star_black_24dp:R.drawable.ic_star_border_black_24dp;
        contentView.setImageViewResource(R.id.toggle_save_current_record, starResourceId);

        String notificationTitle = this.getIsRecordingVideo().getValue()? getResources().getString(R.string.notification_recording):getResources().getString(R.string.notification_stopped);
        contentView.setTextViewText(R.id.title,notificationTitle);

        contentView.setViewVisibility(R.id.touch_here, this.getIsRecordingVideo().getValue()?View.GONE:View.VISIBLE);

        String duration = new SimpleDateFormat("mm:ss").format(new Date(preferencesRepository.getVideoDuration().getValue()));
        contentView.setChronometer(R.id.chronometer,this.getStartingTime(),"%s / "+duration,true);

        if(this.getIsRecordingVideo().getValue()){
            this.contentView.setViewVisibility(R.id.toggle_save_current_record, View.VISIBLE);
            this.contentView.setViewVisibility(R.id.toggle_recording, View.VISIBLE);
            this.contentView.setViewVisibility(R.id.chronometer, View.VISIBLE);
        }
        else{
            this.contentView.setViewVisibility(R.id.toggle_save_current_record, View.INVISIBLE);
            this.contentView.setViewVisibility(R.id.toggle_recording, View.INVISIBLE);
            this.contentView.setViewVisibility(R.id.chronometer, View.INVISIBLE);
        }

        final NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(01, notif);

    }

    /**
     * Starts a background thread and its {@link Handler}.
     */
    private void startBackgroundThread() {
        Log.d(TAG,"startBackgroundThread");
        mBackgroundThread = new HandlerThread("CameraBackground");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    }

    /**
     * Stops the background thread and its {@link Handler}.
     */
    private void stopBackgroundThread() {
        Log.d(TAG,"stopBackgroundThread");
        try {
            mBackgroundThread.quitSafely();
            mBackgroundThread.join();
            mBackgroundThread = null;
            mBackgroundHandler = null;
        } catch (InterruptedException | NullPointerException e) {
            e.printStackTrace();
        }
    }

    /**
     * Tries to open a {@link CameraDevice}. The result is listened by `mStateCallback`.
     */
    @SuppressWarnings("MissingPermission")
    private void openCamera(int width, int height, int currentCamera) {
        if(cameraOpening) return;
        if(mCameraDevice != null) return;

        Log.d(TAG,"openCamera");
        cameraOpening = true;
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            Log.d(TAG, "tryAcquire");
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            Log.d(TAG,"semaphore locked");
            String cameraId;
            if(manager.getCameraIdList().length > 1 ){
                cameraId = manager.getCameraIdList()[currentCamera];
            }
            else {
                cameraId = manager.getCameraIdList()[0];
            }

            // 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);
            if (map == null) {
                throw new RuntimeException("Cannot get available preview/video sizes");
            }

            Size aspectRatio = new Size(width,height);
            mVideoSize = chooseVideoSize(map.getOutputSizes(MediaRecorder.class), aspectRatio);
            mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class),
                    width, height, mVideoSize);

            int orientation = getResources().getConfiguration().orientation;
            if(mTextureView != null) {
                if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
                    mTextureView.setAspectRatio(mPreviewSize.getWidth(), mPreviewSize.getHeight());
                } else {
                    mTextureView.setAspectRatio(mPreviewSize.getHeight(), mPreviewSize.getWidth());
                }
            }
            configureTransform(width, height);
            mMediaRecorder = new MediaRecorder();
            manager.openCamera(cameraId, mStateCallback, null);
        } catch (CameraAccessException | NullPointerException e) {
            Timber.e(e);
            cameraOpening = false;
            mCameraOpenCloseLock.release();
            Log.d(TAG,"open camera semaphore released");
        } catch (InterruptedException e) {
            cameraOpening = false;
            mCameraOpenCloseLock.release();
            Log.d(TAG,"open camera semaphore released");
        }
    }

    private void closeCamera() {
        Log.d(TAG,"closeCamera");
        try {

            if (null != mCameraDevice) {
                mCameraOpenCloseLock.acquire();
                Log.d(TAG,"semaphore acquire");
                mCameraDevice.close();
                mCameraDevice = null;
            }
            closePreviewSession();

        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera closing.");
        } finally {
            mCameraOpenCloseLock.release();
            Log.d(TAG,"close camera semaphore released");
        }
    }

    /**
     * Start the camera preview.
     */
    private void startPreview() {
        Log.d(TAG,"Start Preview");
        if (null == mCameraDevice || mTextureView == null || !mTextureView.isAvailable() || null == mPreviewSize) {
            return;
        }
        try {
            this.mCameraOpenCloseLock.acquire();
            Log.d(TAG,"semaphore acquire");
            closePreviewSession();
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            assert texture != null;
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

            previewSurface = new Surface(texture);
            mPreviewBuilder.addTarget(previewSurface);

            mCameraDevice.createCaptureSession(Collections.singletonList(previewSurface),
                    new CameraCaptureSession.StateCallback() {

                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession session) {
                            mPreviewSession = session;
                            updatePreview();

                        }

                        @Override
                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                            Timber.e("Exception while creating captureSession");
                        }
                    }, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            this.mCameraOpenCloseLock.release();
            Log.d(TAG,"startpreview semaphore released");
        }
    }

    /**
     * Update the camera preview. {@link #startPreview()} needs to be called in advance.
     */
    private void updatePreview() {
        Log.d(TAG,"updatePreview");
        if (null == mCameraDevice) {
            return;
        }
        try {
            setUpCaptureRequestBuilder(mPreviewBuilder);
            HandlerThread thread = new HandlerThread("CameraPreview");
            thread.start();
            mPreviewSession.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);
    }

    /**
     * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`.
     * This method should not to be called until the camera preview size is determined in
     * openCamera, or until the size of `mTextureView` is fixed.
     *
     * @param viewWidth  The width of `mTextureView`
     * @param viewHeight The height of `mTextureView`
     */
    public void configureTransform(int viewWidth, int viewHeight) {
        Log.d(TAG,"configureTransform");
        WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE);

        int rotation = window.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);
        }
        if(mTextureView != null) {
            mTextureView.setTransform(matrix);
        }
    }

    private void setUpMediaRecorder() throws IOException {
        Log.d(TAG,"setUpMediaRecorder");
        WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        if(this.recordViewModel.getIsSoundEnable().getValue()) {
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        }
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mMediaRecorder.setOutputFile(getNextVideoFilePath(this));
        mMediaRecorder.setVideoEncodingBitRate(10000000);
        mMediaRecorder.setVideoFrameRate(30);
        mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        if(this.recordViewModel.getIsSoundEnable().getValue()) {
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
            mMediaRecorder.setAudioSamplingRate(16000);
        }
        int rotation = window.getDefaultDisplay().getRotation();
        switch (mSensorOrientation) {
            case SENSOR_ORIENTATION_DEFAULT_DEGREES:
                mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
                break;
            case SENSOR_ORIENTATION_INVERSE_DEGREES:
                mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
                break;
        }
        mMediaRecorder.setOnErrorListener((mediaRecorder, i, i1) -> {
            Log.d(TAG, "mMedia Recorder on Error : "+ i);
        });

        mMediaRecorder.setOnInfoListener((mediaRecorder, i, i1) -> {
            Log.d(TAG, "mMedia Recorder on Info : "+ i);
        });
        mMediaRecorder.prepare();
    }

    private void startRecordingVideo() {
        Log.d(TAG,"startRecordingVideo");
        if (null == mCameraDevice || null == mPreviewSize) {
            Log.d(TAG,"startRecordingVideo camera or preview size null");
            if(mTextureView != null) {
                oldWidth = mTextureView.getWidth();
                oldHeight = mTextureView.getHeight();
                openCamera(mTextureView.getWidth(), mTextureView.getHeight(), recordViewModel.getCurrentCamera().getValue());
            }
            else{
                openCamera(oldWidth, oldHeight, recordViewModel.getCurrentCamera().getValue());
            }
            this.recordOnCameraOpening = true;
            return;
        }
        try {
            closePreviewSession();

            setUpMediaRecorder();

            try {
                mCameraOpenCloseLock.acquire();
                Log.d(TAG,"semaphore acquired");
                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
                mCameraOpenCloseLock.release();
                Log.d(TAG,"startrecording video semaphore released");
            } catch (InterruptedException|IllegalArgumentException e) {
                e.printStackTrace();
            }


            List<Surface> surfaces = new ArrayList<>();
            if(mTextureView !=null) {
                previewSurfaceTexture = mTextureView.getSurfaceTexture();
                assert previewSurfaceTexture != null;
                previewSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());            // Set up Surface for the camera preview
                previewSurface = new Surface(previewSurfaceTexture);
                if(previewSurface.isValid())
                    surfaces.add(previewSurface);
                mPreviewBuilder.addTarget(previewSurface);
            }

            // Set up Surface for the MediaRecorder
            recorderSurface = mMediaRecorder.getSurface();
            if(recorderSurface.isValid() )
                surfaces.add(recorderSurface);
            mPreviewBuilder.addTarget(recorderSurface);

            RecordLocalisationEntity entity = new RecordLocalisationEntity();
            entity.setAddress(this.addressLiveData.getValue());
            entity.setTime(0);
            this.savedLocalisation.add(entity);
            startingTime = SystemClock.elapsedRealtime();
            this.isRecording.setValue(true);
            updateNotification();
            timerRunnable = () -> {
                Long elapsedMillis = SystemClock.elapsedRealtime() - this.getStartingTime();
                if (elapsedMillis > recordViewModel.getVideoDuration().getValue()) {
                    toggleRecording();
                }
                timerHandler.postDelayed(timerRunnable, 2000);
            };
            timerHandler.post(timerRunnable);

            // 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) {
                    mPreviewSession = cameraCaptureSession;
                    updatePreview();
                    mMediaRecorder.start();
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    Timber.e("Error when creating captureSession");
                }
            }, mBackgroundHandler);
        } catch (CameraAccessException | IOException e) {
            e.printStackTrace();
        }

    }

    private void closePreviewSession() {
        Log.d(TAG,"closePreviewSession");
        if (mPreviewSession != null) {
            mPreviewSession.close();
            mPreviewSession = null;
        }
    }

    private void stopRecordingVideo() {
        Log.d(TAG,"stoprecordingvideo");
        this.timerHandler.removeCallbacks(timerRunnable);

        this.isRecording.setValue(false);
        recordRepository.insert(this.nextVideoFilePath, shouldSaveVideo,this.savedLocalisation);
        this.savedLocalisation.clear();

        this.nextVideoFilePath = null;
        Long elapsedMillis = SystemClock.elapsedRealtime() - startingTime;
        boolean doesVideoReachMaxDuration = false;
        if (preferencesRepository.getVideoDuration().getValue() != null && elapsedMillis > preferencesRepository.getVideoDuration().getValue()) {
            doesVideoReachMaxDuration = true;
        }
        startingTime = SystemClock.elapsedRealtime();
        this.stopForeground(false);
        this.shouldSaveVideo = false;
        this.updateNotification();

        try {
            if(mPreviewSession != null) {
                mPreviewSession.stopRepeating();
                mPreviewSession.abortCaptures();
            }
            startPreview();
            mMediaRecorder.stop();
            mMediaRecorder.reset();
        }
        catch(CameraAccessException | RuntimeException stopException){
            //handle cleanup here
            Timber.e(stopException);
        }

        if(preferencesRepository.getShouldRestartRecording().getValue() && doesVideoReachMaxDuration){
            new Handler().postDelayed(
                    this::toggleRecording, 1000
            );
        }
    }

    public String getNextVideoFilePath(Context context){
        this.nextVideoFilePath = getVideoFilePath(context);
        return this.nextVideoFilePath;
    }

    private String getVideoFilePath(Context context) {
        final File dir = context.getExternalFilesDir(null);
        return (dir == null ? "" : (dir.getAbsolutePath() + "/"))
                + System.currentTimeMillis() + ".mp4";
    }

    /**
     * Compares two {@code Size}s based on their areas.
     */
    static class CompareSizesByArea implements Comparator<Size> {

        @Override
        public int compare(Size lhs, Size rhs) {
            // We cast here to ensure the multiplications won't overflow
            return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
                    (long) rhs.getWidth() * rhs.getHeight());
        }

    }

    /**
     * Class for clients to access.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with
     * IPC.
     */
    public class RecordBinder extends Binder {
        public RecordingService getService() {
            return RecordingService.this;
        }
    }


}
  1. I'd like to have more insights about what I did and if I made any mistakes.
  2. I'd like to know if these are good practices using camera2 API?
  3. How can I improve it ?
\$\endgroup\$
1
\$\begingroup\$

one thing is for sure - this class is way too big...

  • considering a factory class to create callbacks
  • considering using a seperate class for determing the proper size
  • considering using a seperate class for the camera (seperate service from camera) and delegate calls (start record, stop record etc to the camera class)
  • considering using a class for file access (think of a file manager)

one thing in background: your class is the implementation of the Service (public class RecordingService extends LifecycleService) and should be only responsible for the Service relevant things - remove anything from this class what is not part of the service

\$\endgroup\$
  • \$\begingroup\$ Ok got your point! \$\endgroup\$ – Astyan Feb 13 '18 at 11:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.