>>
14 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/domain/media/SortingOption.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.domain.media
2 |
3 | enum class SortingOption {
4 | ASCENDING,
5 | DESCENDING
6 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Audio.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | /**
9 | * Audio values indicate whether to record audio stream when record video.
10 | *
11 | * @see CameraView#setAudio(Audio)
12 | */
13 | public enum Audio implements Control {
14 |
15 | /**
16 | * No Audio.
17 | */
18 | OFF(0),
19 |
20 | /**
21 | * With Audio.
22 | */
23 | ON(1);
24 |
25 | public final static Audio DEFAULT = ON;
26 |
27 | private int value;
28 |
29 | Audio(int value) {
30 | this.value = value;
31 | }
32 |
33 | @Nullable
34 | public static Audio fromValue(int value) {
35 | Audio[] list = Audio.values();
36 | for (Audio action : list) {
37 | if (action.value() == value) {
38 | return action;
39 | }
40 | }
41 | return null;
42 | }
43 |
44 | public int value() {
45 | return value;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Control.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 | /**
4 | * Base interface for controls like {@link Audio},
5 | * {@link Facing}, {@link Flash} and so on.
6 | */
7 | public interface Control {
8 | }
9 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Facing.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import android.content.Context;
5 |
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 |
9 | import com.lassi.presentation.cameraview.controls.CameraView;
10 | import com.lassi.presentation.cameraview.utils.CameraUtils;
11 |
12 | /**
13 | * Facing value indicates which camera sensor should be used for the current session.
14 | *
15 | * @see CameraView#setFacing(Facing)
16 | */
17 | public enum Facing implements Control {
18 |
19 | /**
20 | * Back-facing camera sensor.
21 | */
22 | BACK(0),
23 |
24 | /**
25 | * Front-facing camera sensor.
26 | */
27 | FRONT(1);
28 |
29 | private int value;
30 |
31 | Facing(int value) {
32 | this.value = value;
33 | }
34 |
35 | @NonNull
36 | public static Facing DEFAULT(@Nullable Context context) {
37 | if (context == null) {
38 | return BACK;
39 | } else if (CameraUtils.hasCameraFacing(context, BACK)) {
40 | return BACK;
41 | } else if (CameraUtils.hasCameraFacing(context, FRONT)) {
42 | return FRONT;
43 | } else {
44 | // The controller will throw a CameraException.
45 | // This device has no cameras.
46 | return BACK;
47 | }
48 | }
49 |
50 | @Nullable
51 | public static Facing fromValue(int value) {
52 | Facing[] list = Facing.values();
53 | for (Facing action : list) {
54 | if (action.value() == value) {
55 | return action;
56 | }
57 | }
58 | return null;
59 | }
60 |
61 | public int value() {
62 | return value;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Flash.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraOptions;
7 | import com.lassi.presentation.cameraview.controls.CameraView;
8 |
9 | /**
10 | * Flash value indicates the flash mode to be used.
11 | *
12 | * @see CameraView#setFlash(Flash)
13 | */
14 | public enum Flash implements Control {
15 |
16 | /**
17 | * Flash is always off.
18 | */
19 | OFF(0),
20 |
21 | /**
22 | * Flash will be on when capturing.
23 | * This is not guaranteed to be supported.
24 | *
25 | * @see CameraOptions#getSupportedFlash()
26 | */
27 | ON(1),
28 |
29 |
30 | /**
31 | * Flash mode is chosen by the camera.
32 | * This is not guaranteed to be supported.
33 | *
34 | * @see CameraOptions#getSupportedFlash()
35 | */
36 | AUTO(2),
37 |
38 |
39 | /**
40 | * Flash is always on, working as a torch.
41 | * This is not guaranteed to be supported.
42 | *
43 | * @see CameraOptions#getSupportedFlash()
44 | */
45 | TORCH(3);
46 |
47 | public static final Flash DEFAULT = OFF;
48 |
49 | private int value;
50 |
51 | Flash(int value) {
52 | this.value = value;
53 | }
54 |
55 | @Nullable
56 | public static Flash fromValue(int value) {
57 | Flash[] list = Flash.values();
58 | for (Flash action : list) {
59 | if (action.value() == value) {
60 | return action;
61 | }
62 | }
63 | return null;
64 | }
65 |
66 | public int value() {
67 | return value;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Gesture.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.NonNull;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | import java.util.Arrays;
9 | import java.util.List;
10 |
11 |
12 | /**
13 | * Gestures listen to finger gestures over the {@link CameraView} bounds and can be mapped
14 | * to one or more camera controls using XML attributes or {@link CameraView#mapGesture(Gesture, GestureAction)}.
15 | *
16 | * Not every gesture can control a certain action. For example, pinch gestures can only control
17 | * continuous values, such as zoom or AE correction. Single point gestures, on the other hand,
18 | * can only control point actions such as focusing or capturing a picture.
19 | */
20 | public enum Gesture {
21 |
22 | /**
23 | * Pinch gesture, typically assigned to the zoom control.
24 | * This gesture can be mapped to:
25 | *
26 | * - {@link GestureAction#ZOOM}
27 | * - {@link GestureAction#EXPOSURE_CORRECTION}
28 | * - {@link GestureAction#NONE}
29 | */
30 | PINCH(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION),
31 |
32 | /**
33 | * Single tap gesture, typically assigned to the focus control.
34 | * This gesture can be mapped to:
35 | *
36 | * - {@link GestureAction#FOCUS}
37 | * - {@link GestureAction#FOCUS_WITH_MARKER}
38 | * - {@link GestureAction#CAPTURE}
39 | * - {@link GestureAction#NONE}
40 | */
41 | TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE),
42 | // DOUBLE_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE),
43 |
44 | /**
45 | * Long tap gesture.
46 | * This gesture can be mapped to:
47 | *
48 | * - {@link GestureAction#FOCUS}
49 | * - {@link GestureAction#FOCUS_WITH_MARKER}
50 | * - {@link GestureAction#CAPTURE}
51 | * - {@link GestureAction#NONE}
52 | */
53 | LONG_TAP(GestureAction.FOCUS, GestureAction.FOCUS_WITH_MARKER, GestureAction.CAPTURE),
54 |
55 | /**
56 | * Horizontal scroll gesture.
57 | * This gesture can be mapped to:
58 | *
59 | * - {@link GestureAction#ZOOM}
60 | * - {@link GestureAction#EXPOSURE_CORRECTION}
61 | * - {@link GestureAction#NONE}
62 | */
63 | SCROLL_HORIZONTAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION),
64 |
65 | /**
66 | * Vertical scroll gesture.
67 | * This gesture can be mapped to:
68 | *
69 | * - {@link GestureAction#ZOOM}
70 | * - {@link GestureAction#EXPOSURE_CORRECTION}
71 | * - {@link GestureAction#NONE}
72 | */
73 | SCROLL_VERTICAL(GestureAction.ZOOM, GestureAction.EXPOSURE_CORRECTION);
74 |
75 | private List mControls;
76 |
77 | Gesture(GestureAction... controls) {
78 | mControls = Arrays.asList(controls);
79 | }
80 |
81 | public boolean isAssignableTo(@NonNull GestureAction control) {
82 | return control == GestureAction.NONE || mControls.contains(control);
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/GestureAction.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | /**
9 | * Gestures actions are actions over camera controls that can be mapped to certain gestures over
10 | * the screen, using XML attributes or {@link CameraView#mapGesture(Gesture, GestureAction)}.
11 | *
12 | * Not every gesture can control a certain action. For example, pinch gestures can only control
13 | * continuous values, such as zoom or AE correction. Single point gestures, on the other hand,
14 | * can only control point actions such as focusing or capturing a picture.
15 | */
16 | public enum GestureAction {
17 |
18 | /**
19 | * No action. This can be mapped to any gesture to disable it.
20 | */
21 | NONE(0),
22 |
23 | /**
24 | * Auto focus control, typically assigned to the tap gesture.
25 | * This action can be mapped to:
26 | *
27 | * - {@link Gesture#TAP}
28 | * - {@link Gesture#LONG_TAP}
29 | */
30 | FOCUS(1),
31 |
32 | /**
33 | * Auto focus control, typically assigned to the tap gesture.
34 | * On top of {@link #FOCUS}, this will draw a default marker on screen.
35 | * This action can be mapped to:
36 | *
37 | * - {@link Gesture#TAP}
38 | * - {@link Gesture#LONG_TAP}
39 | */
40 | FOCUS_WITH_MARKER(2),
41 |
42 | /**
43 | * When triggered, this action will fire a picture shoot.
44 | * This action can be mapped to:
45 | *
46 | * - {@link Gesture#TAP}
47 | * - {@link Gesture#LONG_TAP}
48 | */
49 | CAPTURE(3),
50 |
51 | /**
52 | * Zoom control, typically assigned to the pinch gesture.
53 | * This action can be mapped to:
54 | *
55 | * - {@link Gesture#PINCH}
56 | * - {@link Gesture#SCROLL_HORIZONTAL}
57 | * - {@link Gesture#SCROLL_VERTICAL}
58 | */
59 | ZOOM(4),
60 |
61 | /**
62 | * Exposure correction control.
63 | * This action can be mapped to:
64 | *
65 | * - {@link Gesture#PINCH}
66 | * - {@link Gesture#SCROLL_HORIZONTAL}
67 | * - {@link Gesture#SCROLL_VERTICAL}
68 | */
69 | EXPOSURE_CORRECTION(5);
70 |
71 |
72 | public final static GestureAction DEFAULT_PINCH = NONE;
73 | public final static GestureAction DEFAULT_TAP = NONE;
74 | public final static GestureAction DEFAULT_LONG_TAP = NONE;
75 | public final static GestureAction DEFAULT_SCROLL_HORIZONTAL = NONE;
76 | public final static GestureAction DEFAULT_SCROLL_VERTICAL = NONE;
77 |
78 | private int value;
79 |
80 | GestureAction(int value) {
81 | this.value = value;
82 | }
83 |
84 | @Nullable
85 | public static GestureAction fromValue(int value) {
86 | GestureAction[] list = GestureAction.values();
87 | for (GestureAction action : list) {
88 | if (action.value() == value) {
89 | return action;
90 | }
91 | }
92 | return null;
93 | }
94 |
95 | public int value() {
96 | return value;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Grid.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | /**
9 | * Grid values can be used to draw grid lines over the camera preview.
10 | *
11 | * @see CameraView#setGrid(Grid)
12 | */
13 | public enum Grid implements Control {
14 |
15 | /**
16 | * No grid is drawn.
17 | */
18 | OFF(0),
19 |
20 | /**
21 | * Draws a regular, 3x3 grid.
22 | */
23 | DRAW_3X3(1),
24 |
25 | /**
26 | * Draws a regular, 4x4 grid.
27 | */
28 | DRAW_4X4(2),
29 |
30 | /**
31 | * Draws a grid respecting the 'phi' constant proportions,
32 | * often referred as to the golden ratio.
33 | */
34 | DRAW_PHI(3);
35 |
36 | public static final Grid DEFAULT = OFF;
37 |
38 | private int value;
39 |
40 | Grid(int value) {
41 | this.value = value;
42 | }
43 |
44 | @Nullable
45 | public static Grid fromValue(int value) {
46 | Grid[] list = Grid.values();
47 | for (Grid action : list) {
48 | if (action.value() == value) {
49 | return action;
50 | }
51 | }
52 | return null;
53 | }
54 |
55 | public int value() {
56 | return value;
57 | }
58 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Hdr.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import com.lassi.presentation.cameraview.controls.CameraView;
6 |
7 |
8 | /**
9 | * Hdr values indicate whether to use high dynamic range techniques when capturing pictures.
10 | *
11 | * @see CameraView#setHdr(Hdr)
12 | */
13 | public enum Hdr implements Control {
14 |
15 | /**
16 | * No HDR.
17 | */
18 | OFF(0),
19 |
20 | /**
21 | * Using HDR.
22 | */
23 | ON(1);
24 |
25 | public final static Hdr DEFAULT = OFF;
26 |
27 | private int value;
28 |
29 | Hdr(int value) {
30 | this.value = value;
31 | }
32 |
33 | @Nullable
34 | public static Hdr fromValue(int value) {
35 | Hdr[] list = Hdr.values();
36 | for (Hdr action : list) {
37 | if (action.value() == value) {
38 | return action;
39 | }
40 | }
41 | return null;
42 | }
43 |
44 | public int value() {
45 | return value;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Mode.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | import java.io.File;
9 |
10 | /**
11 | * Type of the session to be opened or to move to.
12 | * Session modes have influence over the capture and preview size, ability to shoot pictures,
13 | * focus modes, runtime permissions needed.
14 | *
15 | * @see CameraView#setMode(Mode)
16 | */
17 | public enum Mode implements Control {
18 |
19 | /**
20 | * Session used to capture pictures.
21 | *
22 | * - {@link CameraView#takeVideo(File)} will throw an exception
23 | * - Only the camera permission is requested
24 | * - Capture size is chosen according to the current picture size selector
25 | */
26 | PICTURE(0),
27 |
28 | /**
29 | * Session used to capture videos.
30 | *
31 | * - {@link CameraView#takePicture()} will throw an exception
32 | * - Camera and audio record permissions are requested
33 | * - Capture size is chosen according to the current video size selector
34 | */
35 | VIDEO(1);
36 |
37 | public static final Mode DEFAULT = PICTURE;
38 |
39 | private int value;
40 |
41 | Mode(int value) {
42 | this.value = value;
43 | }
44 |
45 | @Nullable
46 | public static Mode fromValue(int value) {
47 | Mode[] list = Mode.values();
48 | for (Mode action : list) {
49 | if (action.value() == value) {
50 | return action;
51 | }
52 | }
53 | return null;
54 | }
55 |
56 | public int value() {
57 | return value;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/Preview.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | /**
9 | * The preview engine to be used.
10 | *
11 | * @see CameraView#setPreview(Preview)
12 | */
13 | public enum Preview implements Control {
14 |
15 | /**
16 | * Preview engine based on {@link android.view.SurfaceView}.
17 | * Not recommended.
18 | */
19 | SURFACE(0),
20 |
21 | /**
22 | * Preview engine based on {@link android.view.TextureView}.
23 | * Stable, but does not support all features (like video snapshots,
24 | * or picture snapshot while taking videos).
25 | */
26 | TEXTURE(1),
27 |
28 | /**
29 | * Preview engine based on {@link android.opengl.GLSurfaceView}.
30 | * This is the best engine available. Supports video snapshots,
31 | * and picture snapshots while taking videos.
32 | */
33 | GL_SURFACE(2);
34 |
35 | public final static Preview DEFAULT = GL_SURFACE;
36 |
37 | private int value;
38 |
39 | Preview(int value) {
40 | this.value = value;
41 | }
42 |
43 | @Nullable
44 | public static Preview fromValue(int value) {
45 | Preview[] list = Preview.values();
46 | for (Preview action : list) {
47 | if (action.value() == value) {
48 | return action;
49 | }
50 | }
51 | return null;
52 | }
53 |
54 | public int value() {
55 | return value;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/VideoCodec.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraView;
7 |
8 | /**
9 | * Constants for selecting the encoder of video recordings.
10 | * https://developer.android.com/guide/topics/media/media-formats.html#video-formats
11 | *
12 | * @see CameraView#setVideoCodec(VideoCodec)
13 | */
14 | public enum VideoCodec implements Control {
15 |
16 |
17 | /**
18 | * Let the device choose its codec.
19 | */
20 | DEVICE_DEFAULT(0),
21 |
22 | /**
23 | * The H.263 codec.
24 | */
25 | H_263(1),
26 |
27 | /**
28 | * The H.264 codec.
29 | */
30 | H_264(2);
31 |
32 | public static final VideoCodec DEFAULT = DEVICE_DEFAULT;
33 |
34 | private int value;
35 |
36 | VideoCodec(int value) {
37 | this.value = value;
38 | }
39 |
40 | @Nullable
41 | public static VideoCodec fromValue(int value) {
42 | VideoCodec[] list = VideoCodec.values();
43 | for (VideoCodec action : list) {
44 | if (action.value() == value) {
45 | return action;
46 | }
47 | }
48 | return null;
49 | }
50 |
51 | public int value() {
52 | return value;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/audio/WhiteBalance.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.audio;
2 |
3 |
4 | import androidx.annotation.Nullable;
5 |
6 | import com.lassi.presentation.cameraview.controls.CameraOptions;
7 | import com.lassi.presentation.cameraview.controls.CameraView;
8 |
9 | /**
10 | * White balance values control the white balance settings.
11 | *
12 | * @see CameraView#setWhiteBalance(WhiteBalance)
13 | */
14 | public enum WhiteBalance implements Control {
15 |
16 | /**
17 | * Automatic white balance selection (AWB).
18 | * This is not guaranteed to be supported.
19 | *
20 | * @see CameraOptions#getSupportedWhiteBalance()
21 | */
22 | AUTO(0),
23 |
24 | /**
25 | * White balance appropriate for incandescent light.
26 | * This is not guaranteed to be supported.
27 | *
28 | * @see CameraOptions#getSupportedWhiteBalance()
29 | */
30 | INCANDESCENT(1),
31 |
32 | /**
33 | * White balance appropriate for fluorescent light.
34 | * This is not guaranteed to be supported.
35 | *
36 | * @see CameraOptions#getSupportedWhiteBalance()
37 | */
38 | FLUORESCENT(2),
39 |
40 | /**
41 | * White balance appropriate for daylight captures.
42 | * This is not guaranteed to be supported.
43 | *
44 | * @see CameraOptions#getSupportedWhiteBalance()
45 | */
46 | DAYLIGHT(3),
47 |
48 | /**
49 | * White balance appropriate for pictures in cloudy conditions.
50 | * This is not guaranteed to be supported.
51 | *
52 | * @see CameraOptions#getSupportedWhiteBalance()
53 | */
54 | CLOUDY(4);
55 |
56 | public static final WhiteBalance DEFAULT = AUTO;
57 |
58 | private int value;
59 |
60 | WhiteBalance(int value) {
61 | this.value = value;
62 | }
63 |
64 | @Nullable
65 | public static WhiteBalance fromValue(int value) {
66 | WhiteBalance[] list = WhiteBalance.values();
67 | for (WhiteBalance action : list) {
68 | if (action.value() == value) {
69 | return action;
70 | }
71 | }
72 | return null;
73 | }
74 |
75 | public int value() {
76 | return value;
77 | }
78 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/CamcorderProfiles.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.media.CamcorderProfile;
5 | import android.os.Build;
6 |
7 | import androidx.annotation.NonNull;
8 |
9 | import java.util.ArrayList;
10 | import java.util.Collections;
11 | import java.util.Comparator;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | /**
17 | * Wraps the {@link CamcorderProfile} static utilities.
18 | */
19 | class CamcorderProfiles {
20 |
21 | @SuppressLint("UseSparseArrays")
22 | private static Map sizeToProfileMap = new HashMap<>();
23 |
24 | static {
25 | sizeToProfileMap.put(new Size(176, 144), CamcorderProfile.QUALITY_QCIF);
26 | sizeToProfileMap.put(new Size(320, 240), CamcorderProfile.QUALITY_QVGA);
27 | sizeToProfileMap.put(new Size(352, 288), CamcorderProfile.QUALITY_CIF);
28 | sizeToProfileMap.put(new Size(720, 480), CamcorderProfile.QUALITY_480P);
29 | sizeToProfileMap.put(new Size(1280, 720), CamcorderProfile.QUALITY_720P);
30 | sizeToProfileMap.put(new Size(1920, 1080), CamcorderProfile.QUALITY_1080P);
31 | if (Build.VERSION.SDK_INT >= 21) {
32 | sizeToProfileMap.put(new Size(3840, 2160), CamcorderProfile.QUALITY_2160P);
33 | }
34 | }
35 |
36 | /**
37 | * Returns a CamcorderProfile that's somewhat coherent with the target size,
38 | * to ensure we get acceptable video/audio parameters for MediaRecorders (most notably the bitrate).
39 | *
40 | * @param cameraId the camera id
41 | * @param targetSize the target video size
42 | * @return a profile
43 | */
44 | @NonNull
45 | static CamcorderProfile get(int cameraId, @NonNull Size targetSize) {
46 | final int targetArea = targetSize.getWidth() * targetSize.getHeight();
47 | List sizes = new ArrayList<>(sizeToProfileMap.keySet());
48 | Collections.sort(sizes, new Comparator() {
49 | @Override
50 | public int compare(Size s1, Size s2) {
51 | int a1 = Math.abs(s1.getWidth() * s1.getHeight() - targetArea);
52 | int a2 = Math.abs(s2.getWidth() * s2.getHeight() - targetArea);
53 | //noinspection UseCompareMethod
54 | return (a1 < a2) ? -1 : ((a1 == a2) ? 0 : 1);
55 | }
56 | });
57 | while (sizes.size() > 0) {
58 | Size candidate = sizes.remove(0);
59 | //noinspection ConstantConditions
60 | int quality = sizeToProfileMap.get(candidate);
61 | if (CamcorderProfile.hasProfile(cameraId, quality)) {
62 | return CamcorderProfile.get(cameraId, quality);
63 | }
64 | }
65 | // Should never happen, but fallback to low.
66 | return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/CameraException.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import com.lassi.presentation.cameraview.audio.Facing;
4 |
5 | /**
6 | * Holds an error with the camera configuration.
7 | */
8 | public class CameraException extends RuntimeException {
9 |
10 | /**
11 | * Unknown error. No further info available.
12 | */
13 | public static final int REASON_UNKNOWN = 0;
14 |
15 | /**
16 | * We failed to connect to the camera service.
17 | * The camera might be in use by another app.
18 | */
19 | public static final int REASON_FAILED_TO_CONNECT = 1;
20 |
21 | /**
22 | * Failed to start the camera preview.
23 | * Again, the camera might be in use by another app.
24 | */
25 | public static final int REASON_FAILED_TO_START_PREVIEW = 2;
26 |
27 | /**
28 | * Camera was forced to disconnect.
29 | * In Camera1, this is thrown when android.hardware.Camera.CAMERA_ERROR_EVICTED
30 | * is caught.
31 | */
32 | public static final int REASON_DISCONNECTED = 3;
33 |
34 | /**
35 | * Could not take a picture or a picture snapshot,
36 | * for some not specified reason.
37 | */
38 | public static final int REASON_PICTURE_FAILED = 4;
39 |
40 | /**
41 | * Could not take a video or a video snapshot,
42 | * for some not specified reason.
43 | */
44 | public static final int REASON_VIDEO_FAILED = 5;
45 |
46 | /**
47 | * Indicates that we could not find a camera for the current {@link Facing}
48 | * value.
49 | * This can be solved by changing the facing value and starting again.
50 | */
51 | public static final int REASON_NO_CAMERA = 6;
52 |
53 | private int reason = REASON_UNKNOWN;
54 |
55 | CameraException(Throwable cause) {
56 | super(cause);
57 | }
58 |
59 | CameraException(Throwable cause, int reason) {
60 | super(cause);
61 | this.reason = reason;
62 | }
63 |
64 | CameraException(int reason) {
65 | super();
66 | this.reason = reason;
67 | }
68 |
69 | public int getReason() {
70 | return reason;
71 | }
72 |
73 | /**
74 | * Whether this error is unrecoverable. If this function returns true,
75 | * the Camera has been closed and it is likely showing a black preview.
76 | * This is the right moment to show an error dialog to the user.
77 | *
78 | * @return true if this error is unrecoverable
79 | */
80 | public boolean isUnrecoverable() {
81 | switch (getReason()) {
82 | case REASON_FAILED_TO_CONNECT:
83 | return true;
84 | case REASON_FAILED_TO_START_PREVIEW:
85 | return true;
86 | case REASON_DISCONNECTED:
87 | return true;
88 | default:
89 | return false;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/CameraMapper.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import android.hardware.Camera;
4 | import android.os.Build;
5 |
6 | import com.lassi.presentation.cameraview.audio.Facing;
7 | import com.lassi.presentation.cameraview.audio.Flash;
8 | import com.lassi.presentation.cameraview.audio.Hdr;
9 | import com.lassi.presentation.cameraview.audio.WhiteBalance;
10 |
11 | import java.util.HashMap;
12 |
13 |
14 | @SuppressWarnings("unchecked")
15 | public class CameraMapper extends Mapper {
16 |
17 | private static final HashMap FLASH = new HashMap<>();
18 | private static final HashMap WB = new HashMap<>();
19 | private static final HashMap FACING = new HashMap<>();
20 | private static final HashMap HDR = new HashMap<>();
21 |
22 | static {
23 | FLASH.put(Flash.OFF, Camera.Parameters.FLASH_MODE_OFF);
24 | FLASH.put(Flash.ON, Camera.Parameters.FLASH_MODE_ON);
25 | FLASH.put(Flash.AUTO, Camera.Parameters.FLASH_MODE_AUTO);
26 | FLASH.put(Flash.TORCH, Camera.Parameters.FLASH_MODE_TORCH);
27 | FACING.put(Facing.BACK, Camera.CameraInfo.CAMERA_FACING_BACK);
28 | FACING.put(Facing.FRONT, Camera.CameraInfo.CAMERA_FACING_FRONT);
29 | WB.put(WhiteBalance.AUTO, Camera.Parameters.WHITE_BALANCE_AUTO);
30 | WB.put(WhiteBalance.INCANDESCENT, Camera.Parameters.WHITE_BALANCE_INCANDESCENT);
31 | WB.put(WhiteBalance.FLUORESCENT, Camera.Parameters.WHITE_BALANCE_FLUORESCENT);
32 | WB.put(WhiteBalance.DAYLIGHT, Camera.Parameters.WHITE_BALANCE_DAYLIGHT);
33 | WB.put(WhiteBalance.CLOUDY, Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT);
34 | HDR.put(Hdr.OFF, Camera.Parameters.SCENE_MODE_AUTO);
35 | if (Build.VERSION.SDK_INT >= 17) {
36 | HDR.put(Hdr.ON, Camera.Parameters.SCENE_MODE_HDR);
37 | } else {
38 | HDR.put(Hdr.ON, "hdr");
39 | }
40 | }
41 |
42 | @Override
43 | T map(Flash flash) {
44 | return (T) FLASH.get(flash);
45 | }
46 |
47 | @Override
48 | public T map(Facing facing) {
49 | return (T) FACING.get(facing);
50 | }
51 |
52 | @Override
53 | T map(WhiteBalance whiteBalance) {
54 | return (T) WB.get(whiteBalance);
55 | }
56 |
57 | @Override
58 | T map(Hdr hdr) {
59 | return (T) HDR.get(hdr);
60 | }
61 |
62 | private T reverseLookup(HashMap map, Object object) {
63 | for (T value : map.keySet()) {
64 | if (map.get(value).equals(object)) {
65 | return value;
66 | }
67 | }
68 | return null;
69 | }
70 |
71 | @Override
72 | Flash unmapFlash(T cameraConstant) {
73 | return reverseLookup(FLASH, cameraConstant);
74 | }
75 |
76 | @Override
77 | Facing unmapFacing(T cameraConstant) {
78 | return reverseLookup(FACING, cameraConstant);
79 | }
80 |
81 | @Override
82 | WhiteBalance unmapWhiteBalance(T cameraConstant) {
83 | return reverseLookup(WB, cameraConstant);
84 | }
85 |
86 | @Override
87 | Hdr unmapHdr(T cameraConstant) {
88 | return reverseLookup(HDR, cameraConstant);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/FrameProcessor.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.WorkerThread;
5 |
6 | /**
7 | * A FrameProcessor will process {@link Frame}s coming from the camera preview.
8 | * It must be passed to {@link CameraView#addFrameProcessor(FrameProcessor)}.
9 | */
10 | public interface FrameProcessor {
11 |
12 | /**
13 | * Processes the given frame. The frame will hold the correct values only for the
14 | * duration of this method. When it returns, the frame contents will be replaced.
15 | *
16 | * To keep working with the Frame in an async manner, please use {@link Frame#freeze()},
17 | * which will return an immutable Frame. In that case you can pass / hold the frame for
18 | * as long as you want, and then release its contents using {@link Frame#release()}.
19 | *
20 | * @param frame the new frame
21 | */
22 | @WorkerThread
23 | void process(@NonNull Frame frame);
24 | }
25 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/FullPictureRecorder.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import android.hardware.Camera;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 | import androidx.exifinterface.media.ExifInterface;
8 |
9 | import com.lassi.presentation.cameraview.utils.CameraLogger;
10 | import com.lassi.presentation.cameraview.utils.CameraUtils;
11 |
12 | import java.io.ByteArrayInputStream;
13 | import java.io.IOException;
14 |
15 | /**
16 | * A {@link PictureResult} that uses standard APIs.
17 | */
18 | class FullPictureRecorder extends PictureRecorder {
19 |
20 | private static final String TAG = FullPictureRecorder.class.getSimpleName();
21 | private static final CameraLogger LOG = CameraLogger.create(TAG);
22 |
23 | private Camera mCamera;
24 |
25 | FullPictureRecorder(@NonNull PictureResult stub, @Nullable PictureRecorder.PictureResultListener listener, @NonNull Camera camera) {
26 | super(stub, listener);
27 | mCamera = camera;
28 |
29 | // We set the rotation to the camera parameters, but we don't know if the result will be
30 | // already rotated with 0 exif, or original with non zero exif. we will have to read EXIF.
31 | Camera.Parameters params = mCamera.getParameters();
32 | params.setRotation(mResult.rotation);
33 | mCamera.setParameters(params);
34 | }
35 |
36 | // Camera2 constructor here...
37 |
38 | @Override
39 | void take() {
40 | mCamera.takePicture(
41 | new Camera.ShutterCallback() {
42 | @Override
43 | public void onShutter() {
44 | dispatchOnShutter(true);
45 | }
46 | },
47 | null,
48 | null,
49 | new Camera.PictureCallback() {
50 | @Override
51 | public void onPictureTaken(byte[] data, final Camera camera) {
52 | int exifRotation;
53 | try {
54 | ExifInterface exif = new ExifInterface(new ByteArrayInputStream(data));
55 | int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
56 | exifRotation = CameraUtils.readExifOrientation(exifOrientation);
57 | } catch (IOException e) {
58 | exifRotation = 0;
59 | }
60 | mResult.format = PictureResult.FORMAT_JPEG;
61 | mResult.data = data;
62 | mResult.rotation = exifRotation;
63 | camera.startPreview(); // This is needed, read somewhere in the docs.
64 | dispatchResult();
65 | }
66 | }
67 | );
68 | }
69 |
70 | @Override
71 | protected void dispatchResult() {
72 | mCamera = null;
73 | super.dispatchResult();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/Mapper.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 |
4 | import com.lassi.presentation.cameraview.audio.Facing;
5 | import com.lassi.presentation.cameraview.audio.Flash;
6 | import com.lassi.presentation.cameraview.audio.Hdr;
7 | import com.lassi.presentation.cameraview.audio.WhiteBalance;
8 |
9 | abstract class Mapper {
10 |
11 | abstract T map(Flash flash);
12 |
13 | abstract T map(Facing facing);
14 |
15 | abstract T map(WhiteBalance whiteBalance);
16 |
17 | abstract T map(Hdr hdr);
18 |
19 | abstract Flash unmapFlash(T cameraConstant);
20 |
21 | abstract Facing unmapFacing(T cameraConstant);
22 |
23 | abstract WhiteBalance unmapWhiteBalance(T cameraConstant);
24 |
25 | abstract Hdr unmapHdr(T cameraConstant);
26 | }
27 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/PictureRecorder.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | /**
7 | * Interface for picture capturing.
8 | * Don't call start if already started. Don't call stop if already stopped.
9 | * Don't reuse.
10 | */
11 | abstract class PictureRecorder {
12 |
13 | /* tests */ PictureResult mResult;
14 | /* tests */ PictureResultListener mListener;
15 |
16 | PictureRecorder(@NonNull PictureResult stub, @Nullable PictureResultListener listener) {
17 | mResult = stub;
18 | mListener = listener;
19 | }
20 |
21 | abstract void take();
22 |
23 | @SuppressWarnings("WeakerAccess")
24 | protected void dispatchOnShutter(boolean didPlaySound) {
25 | if (mListener != null) mListener.onPictureShutter(didPlaySound);
26 | }
27 |
28 | protected void dispatchResult() {
29 | if (mListener != null) {
30 | mListener.onPictureResult(mResult);
31 | mListener = null;
32 | mResult = null;
33 | }
34 | }
35 |
36 | interface PictureResultListener {
37 | void onPictureShutter(boolean didPlaySound);
38 |
39 | void onPictureResult(@Nullable PictureResult result);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/Size.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | /**
6 | * A simple class representing a size, with width and height values.
7 | */
8 | public class Size implements Comparable {
9 |
10 | private final int mWidth;
11 | private final int mHeight;
12 |
13 | public Size(int width, int height) {
14 | mWidth = width;
15 | mHeight = height;
16 | }
17 |
18 | public int getWidth() {
19 | return mWidth;
20 | }
21 |
22 | public int getHeight() {
23 | return mHeight;
24 | }
25 |
26 | @SuppressWarnings("SuspiciousNameCombination")
27 | Size flip() {
28 | return new Size(mHeight, mWidth);
29 | }
30 |
31 | @Override
32 | public boolean equals(Object o) {
33 | if (o == null) {
34 | return false;
35 | }
36 | if (this == o) {
37 | return true;
38 | }
39 | if (o instanceof Size) {
40 | Size size = (Size) o;
41 | return mWidth == size.mWidth && mHeight == size.mHeight;
42 | }
43 | return false;
44 | }
45 |
46 | @Override
47 | public String toString() {
48 | return mWidth + "x" + mHeight;
49 | }
50 |
51 | @Override
52 | public int hashCode() {
53 | return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));
54 | }
55 |
56 | @Override
57 | public int compareTo(@NonNull Size another) {
58 | return mWidth * mHeight - another.mWidth * another.mHeight;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/SizeSelector.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * A size selector receives a list of {@link Size}s and returns another list with
9 | * sizes that are considered acceptable.
10 | */
11 | public interface SizeSelector {
12 |
13 | /**
14 | * Returns a list of acceptable sizes from the given input.
15 | * The final size will be the first element in the output list.
16 | *
17 | * @param source input list
18 | * @return output list
19 | */
20 | @NonNull
21 | List select(@NonNull List source);
22 | }
23 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/controls/VideoRecorder.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.controls;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.annotation.Nullable;
5 |
6 | /**
7 | * Interface for video recording.
8 | * Don't call start if already started. Don't call stop if already stopped.
9 | * Don't reuse.
10 | */
11 | abstract class VideoRecorder {
12 |
13 | protected Exception mError;
14 | /* tests */ VideoResult mResult;
15 | /* tests */ VideoResultListener mListener;
16 |
17 | VideoRecorder(@NonNull VideoResult stub, @Nullable VideoResultListener listener) {
18 | mResult = stub;
19 | mListener = listener;
20 | }
21 |
22 | abstract void start();
23 |
24 | abstract void stop();
25 |
26 | @SuppressWarnings("WeakerAccess")
27 | protected void dispatchResult() {
28 | if (mListener != null) {
29 | mListener.onVideoResult(mResult, mError);
30 | mListener = null;
31 | mResult = null;
32 | mError = null;
33 | }
34 | }
35 |
36 |
37 | interface VideoResultListener {
38 | void onVideoResult(@Nullable VideoResult result, @Nullable Exception exception);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/preview/GestureLayout.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.preview;
2 |
3 | import android.content.Context;
4 | import android.graphics.PointF;
5 | import android.view.MotionEvent;
6 | import android.widget.FrameLayout;
7 |
8 | import androidx.annotation.NonNull;
9 |
10 | import com.lassi.presentation.cameraview.audio.Gesture;
11 |
12 | public abstract class GestureLayout extends FrameLayout {
13 |
14 | // The number of possible values between minValue and maxValue, for the scaleValue method.
15 | // We could make this non-static (e.g. larger granularity for exposure correction).
16 | private final static int GRANULARITY = 50;
17 |
18 | protected boolean mEnabled;
19 | protected Gesture mType;
20 | protected PointF[] mPoints;
21 |
22 | public GestureLayout(@NonNull Context context) {
23 | super(context);
24 | onInitialize(context);
25 | }
26 |
27 | // Checks for newValue to be between minValue and maxValue,
28 | // and checks that it is 'far enough' from the oldValue, in order
29 | // to reduce useless updates.
30 | protected static float capValue(float oldValue, float newValue, float minValue, float maxValue) {
31 | if (newValue < minValue) newValue = minValue;
32 | if (newValue > maxValue) newValue = maxValue;
33 |
34 | float distance = (maxValue - minValue) / (float) GRANULARITY;
35 | float half = distance / 2;
36 | if (newValue >= oldValue - half && newValue <= oldValue + half) {
37 | // Too close! Return the oldValue.
38 | return oldValue;
39 | }
40 | return newValue;
41 | }
42 |
43 | protected void onInitialize(@NonNull Context context) {
44 | }
45 |
46 | public void enable(boolean enable) {
47 | mEnabled = enable;
48 | }
49 |
50 | public boolean enabled() {
51 | return mEnabled;
52 | }
53 |
54 | public abstract boolean onTouchEvent(MotionEvent event);
55 |
56 | @NonNull
57 | public final Gesture getGestureType() {
58 | return mType;
59 | }
60 |
61 | // For tests.
62 | void setGestureType(@NonNull Gesture type) {
63 | mType = type;
64 | }
65 |
66 | @NonNull
67 | public final PointF[] getPoints() {
68 | return mPoints;
69 | }
70 |
71 | // Implementors should call capValue at the end.
72 | public abstract float scaleValue(float currValue, float minValue, float maxValue);
73 | }
74 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/preview/PinchGestureLayout.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.preview;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.graphics.PointF;
6 | import android.os.Build;
7 | import android.view.MotionEvent;
8 | import android.view.ScaleGestureDetector;
9 |
10 | import androidx.annotation.NonNull;
11 |
12 | import com.lassi.presentation.cameraview.audio.Gesture;
13 |
14 | public class PinchGestureLayout extends GestureLayout {
15 |
16 | private final static float ADD_SENSITIVITY = 2f;
17 |
18 | ScaleGestureDetector mDetector;
19 | /* tests */ float mFactor = 0;
20 | private boolean mNotify;
21 |
22 | public PinchGestureLayout(@NonNull Context context) {
23 | super(context);
24 | }
25 |
26 | @Override
27 | protected void onInitialize(@NonNull Context context) {
28 | super.onInitialize(context);
29 | mPoints = new PointF[]{new PointF(0, 0), new PointF(0, 0)};
30 | mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
31 | @Override
32 | public boolean onScale(ScaleGestureDetector detector) {
33 | mNotify = true;
34 | mFactor = ((detector.getScaleFactor() - 1) * ADD_SENSITIVITY);
35 | return true;
36 | }
37 | });
38 |
39 | if (Build.VERSION.SDK_INT >= 19) {
40 | mDetector.setQuickScaleEnabled(false);
41 | }
42 |
43 | // We listen only to the pinch type.
44 | setGestureType(Gesture.PINCH);
45 | }
46 |
47 |
48 | @SuppressLint("ClickableViewAccessibility")
49 | @Override
50 | public boolean onTouchEvent(MotionEvent event) {
51 | if (!mEnabled) return false;
52 |
53 | // Reset the mNotify flag on a new gesture.
54 | // This is to ensure that the mNotify flag stays on until the
55 | // previous gesture ends.
56 | if (event.getAction() == MotionEvent.ACTION_DOWN) {
57 | mNotify = false;
58 | }
59 |
60 | // Let's see if we detect something. This will call onScale().
61 | mDetector.onTouchEvent(event);
62 |
63 | // Keep notifying CameraView as long as the gesture goes.
64 | if (mNotify) {
65 | getPoints()[0].x = event.getX(0);
66 | getPoints()[0].y = event.getY(0);
67 | if (event.getPointerCount() > 1) {
68 | getPoints()[1].x = event.getX(1);
69 | getPoints()[1].y = event.getY(1);
70 | }
71 | return true;
72 | }
73 | return false;
74 | }
75 |
76 | @Override
77 | public float scaleValue(float currValue, float minValue, float maxValue) {
78 | float add = mFactor;
79 | // ^ This works well if minValue = 0, maxValue = 1.
80 | // Account for the different range:
81 | add *= (maxValue - minValue);
82 |
83 | // ^ This works well if currValue = 0.
84 | // Account for a different starting point:
85 | /* if (add > 0) {
86 | add *= (maxValue - currValue);
87 | } else if (add < 0) {
88 | add *= (currValue - minValue);
89 | } Nope, I don't like this, it slows everything down. */
90 | return GestureLayout.capValue(currValue, currValue + add, minValue, maxValue);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/preview/RendererThread.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.preview;
2 |
3 | public @interface RendererThread {
4 | }
5 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/preview/SurfaceCameraPreview.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.preview;
2 |
3 | import android.content.Context;
4 | import android.view.LayoutInflater;
5 | import android.view.SurfaceHolder;
6 | import android.view.SurfaceView;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | import androidx.annotation.NonNull;
11 | import androidx.annotation.Nullable;
12 |
13 | import com.lassi.R;
14 | import com.lassi.presentation.cameraview.utils.CameraLogger;
15 |
16 | // Fallback preview when hardware acceleration is off.
17 | // Currently this does NOT support cropping (e. g. the crop inside behavior),
18 | // so we return false in supportsCropping() in order to have proper measuring.
19 | // This means that CameraView is forced to be wrap_content.
20 | public class SurfaceCameraPreview extends CameraPreview {
21 |
22 | private final static CameraLogger LOG = CameraLogger.create(SurfaceCameraPreview.class.getSimpleName());
23 |
24 | private boolean mDispatched;
25 | private View mRootView;
26 |
27 | public SurfaceCameraPreview(@NonNull Context context, @NonNull ViewGroup parent, @Nullable CameraPreview.SurfaceCallback callback) {
28 | super(context, parent, callback);
29 | }
30 |
31 | @NonNull
32 | @Override
33 | protected SurfaceView onCreateView(@NonNull Context context, @NonNull ViewGroup parent) {
34 | View root = LayoutInflater.from(context).inflate(R.layout.cameraview_surface_view, parent, false);
35 | parent.addView(root, 0);
36 | SurfaceView surfaceView = root.findViewById(R.id.surface_view);
37 | final SurfaceHolder holder = surfaceView.getHolder();
38 | holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
39 | holder.addCallback(new SurfaceHolder.Callback() {
40 |
41 | @Override
42 | public void surfaceCreated(SurfaceHolder holder) {
43 | // This is too early to call anything.
44 | // surfaceChanged is guaranteed to be called after, with exact dimensions.
45 | }
46 |
47 | @Override
48 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
49 | LOG.i("callback:", "surfaceChanged", "w:", width, "h:", height, "dispatched:", mDispatched);
50 | if (!mDispatched) {
51 | dispatchOnSurfaceAvailable(width, height);
52 | mDispatched = true;
53 | } else {
54 | dispatchOnSurfaceSizeChanged(width, height);
55 | }
56 | }
57 |
58 | @Override
59 | public void surfaceDestroyed(SurfaceHolder holder) {
60 | LOG.i("callback:", "surfaceDestroyed");
61 | dispatchOnSurfaceDestroyed();
62 | mDispatched = false;
63 | }
64 | });
65 | mRootView = root;
66 | return surfaceView;
67 | }
68 |
69 | @NonNull
70 | @Override
71 | public View getRootView() {
72 | return mRootView;
73 | }
74 |
75 | @NonNull
76 | @Override
77 | public SurfaceHolder getOutput() {
78 | return getView().getHolder();
79 | }
80 |
81 | @NonNull
82 | @Override
83 | public Class getOutputClass() {
84 | return SurfaceHolder.class;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/BitmapCallback.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import android.graphics.Bitmap;
4 |
5 | import androidx.annotation.Nullable;
6 | import androidx.annotation.UiThread;
7 |
8 | /**
9 | * Receives callbacks about a bitmap decoding operation.
10 | */
11 | public interface BitmapCallback {
12 |
13 | /**
14 | * Notifies that the bitmap was succesfully decoded.
15 | * This is run on the UI thread.
16 | * Returns a null object if a {@link OutOfMemoryError} was encountered.
17 | *
18 | * @param bitmap decoded bitmap, or null
19 | */
20 | @UiThread
21 | void onBitmapReady(@Nullable Bitmap bitmap);
22 | }
23 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/CropHelper.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import android.graphics.Rect;
4 |
5 | import androidx.annotation.NonNull;
6 |
7 | import com.lassi.presentation.cameraview.controls.AspectRatio;
8 | import com.lassi.presentation.cameraview.controls.Size;
9 |
10 | public class CropHelper {
11 |
12 | // It's important that size and aspect ratio belong to the same reference.
13 | @NonNull
14 | public static Rect computeCrop(@NonNull Size currentSize, @NonNull AspectRatio targetRatio) {
15 | int currentWidth = currentSize.getWidth();
16 | int currentHeight = currentSize.getHeight();
17 | if (targetRatio.matches(currentSize)) {
18 | return new Rect(0, 0, currentWidth, currentHeight);
19 | }
20 |
21 | // They are not equal. Compute.
22 | AspectRatio currentRatio = AspectRatio.Companion.of(currentWidth, currentHeight);
23 | int x, y, width, height;
24 | if (currentRatio.toFloat() > targetRatio.toFloat()) {
25 | height = currentHeight;
26 | width = (int) (height * targetRatio.toFloat());
27 | y = 0;
28 | x = (currentWidth - width) / 2;
29 | } else {
30 | width = currentWidth;
31 | height = (int) (width / targetRatio.toFloat());
32 | y = (currentHeight - height) / 2;
33 | x = 0;
34 | }
35 | return new Rect(x, y, x + width, y + height);
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/FileCallback.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import androidx.annotation.Nullable;
4 | import androidx.annotation.UiThread;
5 |
6 | import java.io.File;
7 |
8 | /**
9 | * Receives callbacks about a file saving operation.
10 | */
11 | public interface FileCallback {
12 |
13 | /**
14 | * Notifies that the data was succesfully written to file.
15 | * This is run on the UI thread.
16 | * Returns a null object if an exception was encountered, for example
17 | * if you don't have permissions to write to file.
18 | *
19 | * @param file the written file, or null
20 | */
21 | @UiThread
22 | void onFileReady(@Nullable File file);
23 | }
24 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/OrientationHelper.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import android.content.Context;
4 | import android.hardware.SensorManager;
5 | import android.view.Display;
6 | import android.view.OrientationEventListener;
7 | import android.view.Surface;
8 | import android.view.WindowManager;
9 |
10 | import androidx.annotation.NonNull;
11 |
12 | public class OrientationHelper {
13 |
14 | final OrientationEventListener mListener;
15 |
16 | private final Callback mCallback;
17 | private int mDeviceOrientation = -1;
18 | private int mDisplayOffset = -1;
19 |
20 | public OrientationHelper(@NonNull Context context, @NonNull Callback callback) {
21 | mCallback = callback;
22 | mListener = new OrientationEventListener(context.getApplicationContext(), SensorManager.SENSOR_DELAY_NORMAL) {
23 |
24 | @SuppressWarnings("ConstantConditions")
25 | @Override
26 | public void onOrientationChanged(int orientation) {
27 | int or = 0;
28 | if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) {
29 | or = mDeviceOrientation != -1 ? mDeviceOrientation : 0;
30 | } else if (orientation >= 315 || orientation < 45) {
31 | or = 0;
32 | } else if (orientation >= 45 && orientation < 135) {
33 | or = 90;
34 | } else if (orientation >= 135 && orientation < 225) {
35 | or = 180;
36 | } else if (orientation >= 225 && orientation < 315) {
37 | or = 270;
38 | }
39 |
40 | if (or != mDeviceOrientation) {
41 | mDeviceOrientation = or;
42 | mCallback.onDeviceOrientationChanged(mDeviceOrientation);
43 | }
44 | }
45 | };
46 | }
47 |
48 | public void enable(@NonNull Context context) {
49 | Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
50 | switch (display.getRotation()) {
51 | case Surface.ROTATION_0:
52 | mDisplayOffset = 0;
53 | break;
54 | case Surface.ROTATION_90:
55 | mDisplayOffset = 90;
56 | break;
57 | case Surface.ROTATION_180:
58 | mDisplayOffset = 180;
59 | break;
60 | case Surface.ROTATION_270:
61 | mDisplayOffset = 270;
62 | break;
63 | default:
64 | mDisplayOffset = 0;
65 | break;
66 | }
67 | mListener.enable();
68 | }
69 |
70 | public void disable() {
71 | mListener.disable();
72 | mDisplayOffset = -1;
73 | mDeviceOrientation = -1;
74 | }
75 |
76 | int getDeviceOrientation() {
77 | return mDeviceOrientation;
78 | }
79 |
80 | public int getDisplayOffset() {
81 | return mDisplayOffset;
82 | }
83 |
84 | public interface Callback {
85 | void onDeviceOrientationChanged(int deviceOrientation);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/RotationHelper.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.lassi.presentation.cameraview.controls.Size;
6 |
7 | /**
8 | * This will only be used on low APIs or when GL surface is not available.
9 | * This risks OOMs and was never a good tool.
10 | */
11 | @SuppressWarnings("DeprecatedIsStillUsed")
12 | @Deprecated
13 | public class RotationHelper {
14 |
15 | public static byte[] rotate(@NonNull final byte[] yuv, @NonNull final Size size, final int rotation) {
16 | if (rotation == 0) return yuv;
17 | if (rotation % 90 != 0 || rotation < 0 || rotation > 270) {
18 | throw new IllegalArgumentException("0 <= rotation < 360, rotation % 90 == 0");
19 | }
20 | final int width = size.getWidth();
21 | final int height = size.getHeight();
22 | final byte[] output = new byte[yuv.length];
23 | final int frameSize = width * height;
24 | final boolean swap = rotation % 180 != 0;
25 | final boolean xflip = rotation % 270 != 0;
26 | final boolean yflip = rotation >= 180;
27 |
28 | for (int j = 0; j < height; j++) {
29 | for (int i = 0; i < width; i++) {
30 | final int yIn = j * width + i;
31 | final int uIn = frameSize + (j >> 1) * width + (i & ~1);
32 | final int vIn = uIn + 1;
33 |
34 | final int wOut = swap ? height : width;
35 | final int hOut = swap ? width : height;
36 | final int iSwapped = swap ? j : i;
37 | final int jSwapped = swap ? i : j;
38 | final int iOut = xflip ? wOut - iSwapped - 1 : iSwapped;
39 | final int jOut = yflip ? hOut - jSwapped - 1 : jSwapped;
40 |
41 | final int yOut = jOut * wOut + iOut;
42 | final int uOut = frameSize + (jOut >> 1) * wOut + (iOut & ~1);
43 | final int vOut = uOut + 1;
44 |
45 | output[yOut] = (byte) (0xff & yuv[yIn]);
46 | output[uOut] = (byte) (0xff & yuv[uIn]);
47 | output[vOut] = (byte) (0xff & yuv[vIn]);
48 | }
49 | }
50 |
51 | return output;
52 | }
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/Task.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import java.util.concurrent.CountDownLatch;
6 | import java.util.concurrent.TimeUnit;
7 |
8 | /**
9 | * A naive implementation of {@link CountDownLatch}
10 | * to help in testing.
11 | */
12 | public class Task {
13 |
14 | private CountDownLatch mLatch;
15 | private T mResult;
16 | private int mCount;
17 |
18 | public Task() {
19 | }
20 |
21 | public Task(boolean startListening) {
22 | if (startListening) listen();
23 | }
24 |
25 | private boolean listening() {
26 | return mLatch != null;
27 | }
28 |
29 | void listen() {
30 | if (listening()) throw new RuntimeException("Should not happen.");
31 | mResult = null;
32 | mLatch = new CountDownLatch(1);
33 | }
34 |
35 | public void start() {
36 | if (!listening()) mCount++;
37 | }
38 |
39 | public void end(T result) {
40 | if (mCount > 0) {
41 | mCount--;
42 | return;
43 | }
44 |
45 | if (listening()) { // Should be always true.
46 | mResult = result;
47 | mLatch.countDown();
48 | }
49 | }
50 |
51 | T await(long millis) {
52 | return await(millis, TimeUnit.MILLISECONDS);
53 | }
54 |
55 | T await() {
56 | return await(1, TimeUnit.MINUTES);
57 | }
58 |
59 | private T await(long time, @NonNull TimeUnit unit) {
60 | try {
61 | mLatch.await(time, unit);
62 | } catch (Exception e) {
63 | e.printStackTrace();
64 | }
65 | T result = mResult;
66 | mResult = null;
67 | mLatch = null;
68 | return result;
69 | }
70 |
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/utils/WorkerHandler.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.utils;
2 |
3 | import android.os.Handler;
4 | import android.os.HandlerThread;
5 | import android.os.Looper;
6 |
7 | import androidx.annotation.NonNull;
8 |
9 | import java.lang.ref.WeakReference;
10 | import java.util.concurrent.ConcurrentHashMap;
11 |
12 | /**
13 | * Class holding a background handler.
14 | * We want them to survive configuration changes if there's still job to do.
15 | */
16 | public class WorkerHandler {
17 |
18 | private final static CameraLogger LOG = CameraLogger.create(WorkerHandler.class.getSimpleName());
19 | private final static ConcurrentHashMap> sCache = new ConcurrentHashMap<>(4);
20 | private HandlerThread mThread;
21 | private Handler mHandler;
22 |
23 | private WorkerHandler(@NonNull String name) {
24 | mThread = new HandlerThread(name);
25 | mThread.setDaemon(true);
26 | mThread.start();
27 | mHandler = new Handler(mThread.getLooper());
28 | }
29 |
30 | @NonNull
31 | public static WorkerHandler get(@NonNull String name) {
32 | if (sCache.containsKey(name)) {
33 | WorkerHandler cached = sCache.get(name).get();
34 | if (cached != null) {
35 | HandlerThread thread = cached.mThread;
36 | if (thread.isAlive() && !thread.isInterrupted()) {
37 | LOG.w("get:", "Reusing cached worker handler.", name);
38 | return cached;
39 | }
40 | }
41 | LOG.w("get:", "Thread reference died, removing.", name);
42 | sCache.remove(name);
43 | }
44 |
45 | LOG.i("get:", "Creating new handler.", name);
46 | WorkerHandler handler = new WorkerHandler(name);
47 | sCache.put(name, new WeakReference<>(handler));
48 | return handler;
49 | }
50 |
51 | // Handy util to perform action in a fallback thread.
52 | // Not to be used for long-running operations since they will
53 | // block the fallback thread.
54 | public static void run(@NonNull Runnable action) {
55 | get("FallbackCameraThread").post(action);
56 | }
57 |
58 | static void destroy() {
59 | for (String key : sCache.keySet()) {
60 | WeakReference ref = sCache.get(key);
61 | WorkerHandler handler = ref.get();
62 | if (handler != null && handler.getThread().isAlive()) {
63 | handler.getThread().interrupt();
64 | // handler.getThread().quit();
65 | }
66 | ref.clear();
67 | }
68 | sCache.clear();
69 | }
70 |
71 | public Handler get() {
72 | return mHandler;
73 | }
74 |
75 | public void post(@NonNull Runnable runnable) {
76 | mHandler.post(runnable);
77 | }
78 |
79 | @NonNull
80 | public HandlerThread getThread() {
81 | return mThread;
82 | }
83 |
84 | @NonNull
85 | public Looper getLooper() {
86 | return mThread.getLooper();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/ByteBufferPool.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | class ByteBufferPool extends Pool {
6 |
7 | ByteBufferPool(final int bufferSize, int maxPoolSize) {
8 | super(maxPoolSize, new Pool.Factory() {
9 | @Override
10 | public ByteBuffer create() {
11 | return ByteBuffer.allocateDirect(bufferSize);
12 | }
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/EncoderThread.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | @interface EncoderThread {
4 | }
5 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/InputBuffer.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import java.nio.ByteBuffer;
4 |
5 | class InputBuffer {
6 | ByteBuffer data;
7 | ByteBuffer source;
8 | int index;
9 | int length;
10 | long timestamp;
11 | boolean isEndOfStream;
12 | }
13 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/InputBufferPool.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | class InputBufferPool extends Pool {
4 |
5 | InputBufferPool() {
6 | super(Integer.MAX_VALUE, new Pool.Factory() {
7 | @Override
8 | public InputBuffer create() {
9 | return new InputBuffer();
10 | }
11 | });
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/MediaCodecBuffers.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import android.media.MediaCodec;
4 | import android.os.Build;
5 |
6 | import java.nio.ByteBuffer;
7 |
8 | /**
9 | * A Wrapper to MediaCodec that facilitates the use of API-dependent get{Input/Output}Buffer methods,
10 | * in order to prevent: http://stackoverflow.com/q/30646885
11 | */
12 | class MediaCodecBuffers {
13 |
14 | private final MediaCodec mMediaCodec;
15 | private final ByteBuffer[] mInputBuffers;
16 | private ByteBuffer[] mOutputBuffers;
17 |
18 | MediaCodecBuffers(MediaCodec mediaCodec) {
19 | mMediaCodec = mediaCodec;
20 |
21 | if (Build.VERSION.SDK_INT < 21) {
22 | mInputBuffers = mediaCodec.getInputBuffers();
23 | mOutputBuffers = mediaCodec.getOutputBuffers();
24 | } else {
25 | mInputBuffers = mOutputBuffers = null;
26 | }
27 | }
28 |
29 | public ByteBuffer getInputBuffer(final int index) {
30 | if (Build.VERSION.SDK_INT >= 21) {
31 | return mMediaCodec.getInputBuffer(index);
32 | }
33 | ByteBuffer buffer = mInputBuffers[index];
34 | buffer.clear();
35 | return buffer;
36 | }
37 |
38 | public ByteBuffer getOutputBuffer(final int index) {
39 | if (Build.VERSION.SDK_INT >= 21) {
40 | return mMediaCodec.getOutputBuffer(index);
41 | }
42 | return mOutputBuffers[index];
43 | }
44 |
45 | public void onOutputBuffersChanged() {
46 | if (Build.VERSION.SDK_INT < 21) {
47 | mOutputBuffers = mMediaCodec.getOutputBuffers();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/OutputBuffer.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import android.media.MediaCodec;
4 |
5 | import java.nio.ByteBuffer;
6 |
7 | class OutputBuffer {
8 | MediaCodec.BufferInfo info;
9 | int trackIndex;
10 | ByteBuffer data;
11 | }
12 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/OutputBufferPool.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import android.media.MediaCodec;
4 |
5 | class OutputBufferPool extends Pool {
6 |
7 | OutputBufferPool(final int trackIndex) {
8 | super(Integer.MAX_VALUE, new Pool.Factory() {
9 | @Override
10 | public OutputBuffer create() {
11 | OutputBuffer buffer = new OutputBuffer();
12 | buffer.trackIndex = trackIndex;
13 | buffer.info = new MediaCodec.BufferInfo();
14 | return buffer;
15 | }
16 | });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cameraview/video/Pool.java:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cameraview.video;
2 |
3 | import androidx.annotation.CallSuper;
4 | import androidx.annotation.NonNull;
5 | import androidx.annotation.Nullable;
6 |
7 | import com.lassi.presentation.cameraview.utils.CameraLogger;
8 |
9 | import java.util.concurrent.LinkedBlockingQueue;
10 |
11 | class Pool {
12 |
13 | private static final String TAG = Pool.class.getSimpleName();
14 | private static final CameraLogger LOG = CameraLogger.create(TAG);
15 |
16 | private int maxPoolSize;
17 | private int activeCount;
18 | private LinkedBlockingQueue mQueue;
19 | private Factory factory;
20 |
21 | Pool(int maxPoolSize, Factory factory) {
22 | this.maxPoolSize = maxPoolSize;
23 | this.mQueue = new LinkedBlockingQueue<>(maxPoolSize);
24 | this.factory = factory;
25 | }
26 |
27 | boolean canGet() {
28 | return count() < maxPoolSize;
29 | }
30 |
31 | @Nullable
32 | T get() {
33 | T buffer = mQueue.poll();
34 | if (buffer != null) {
35 | activeCount++; // poll decreases, this fixes
36 | LOG.v("GET: Reusing recycled item.", this);
37 | return buffer;
38 | }
39 |
40 | if (!canGet()) {
41 | LOG.v("GET: Returning null. Too much items requested.", this);
42 | return null;
43 | }
44 |
45 | activeCount++;
46 | LOG.v("GET: Creating a new item.", this);
47 | return factory.create();
48 | }
49 |
50 | void recycle(@NonNull T item) {
51 | LOG.v("RECYCLE: Recycling item.", this);
52 | if (--activeCount < 0) {
53 | throw new IllegalStateException("Trying to recycle an item which makes activeCount < 0." +
54 | "This means that this or some previous items being recycled were not coming from " +
55 | "this pool, or some item was recycled more than once. " + this);
56 | }
57 | if (!mQueue.offer(item)) {
58 | throw new IllegalStateException("Trying to recycle an item while the queue is full. " +
59 | "This means that this or some previous items being recycled were not coming from " +
60 | "this pool, or some item was recycled more than once. " + this);
61 | }
62 | }
63 |
64 | @NonNull
65 | @Override
66 | public String toString() {
67 | return getClass().getSimpleName() + " -- count:" + count() + ", active:" + activeCount() + ", cached:" + cachedCount();
68 | }
69 |
70 | final int count() {
71 | return activeCount() + cachedCount();
72 | }
73 |
74 | final int activeCount() {
75 | return activeCount;
76 | }
77 |
78 | final int cachedCount() {
79 | return mQueue.size();
80 | }
81 |
82 | @CallSuper
83 | void clear() {
84 | mQueue.clear();
85 | }
86 |
87 | interface Factory {
88 | T create();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/LassiBaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import androidx.annotation.CallSuper
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.viewbinding.ViewBinding
8 | import com.livefront.bridge.Bridge
9 | import io.reactivex.disposables.CompositeDisposable
10 | import io.reactivex.disposables.Disposable
11 |
12 | abstract class LassiBaseActivity : AppCompatActivity() {
13 |
14 | private var _binding: VB? = null
15 | protected val binding get() = _binding!!
16 |
17 | private val compositeDisposable = CompositeDisposable()
18 |
19 | abstract fun inflateLayout(layoutInflater: LayoutInflater): VB
20 |
21 | protected open fun getBundle() = Unit
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | getBundle()
26 | _binding = inflateLayout(layoutInflater)
27 | setContentView(binding.root)
28 | initViews()
29 | Bridge.restoreInstanceState(this, savedInstanceState)
30 | }
31 |
32 | @CallSuper
33 | protected open fun initViews() {
34 | }
35 |
36 | protected fun Disposable.collect() = compositeDisposable.add(this)
37 |
38 | fun hasExtra(key: String): Boolean {
39 | return intent.hasExtra(key)
40 | }
41 |
42 | override fun onSaveInstanceState(outState: Bundle) {
43 | super.onSaveInstanceState(outState)
44 | Bridge.saveInstanceState(this, outState)
45 | outState.clear()
46 | }
47 |
48 | override fun onDestroy() {
49 | super.onDestroy()
50 | Bridge.clear(this)
51 | _binding = null
52 | }
53 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/LassiBaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.CallSuper
8 | import androidx.fragment.app.Fragment
9 | import androidx.viewbinding.ViewBinding
10 | import com.livefront.bridge.Bridge
11 | import io.reactivex.disposables.CompositeDisposable
12 | import io.reactivex.disposables.Disposable
13 |
14 | abstract class LassiBaseFragment : Fragment() {
15 |
16 | private val compositeDisposable = CompositeDisposable()
17 |
18 | abstract fun inflateLayout(layoutInflater: LayoutInflater): VB
19 |
20 | open fun hasOptionMenu(): Boolean = false
21 |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
23 | super.onViewCreated(view, savedInstanceState)
24 | initViews()
25 | }
26 |
27 | @CallSuper
28 | protected open fun initViews() {
29 | }
30 |
31 | protected fun Disposable.collect() = compositeDisposable.add(this)
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | Bridge.restoreInstanceState(this, savedInstanceState)
36 | savedInstanceState?.clear()
37 | }
38 |
39 | override fun onSaveInstanceState(outState: Bundle) {
40 | super.onSaveInstanceState(outState)
41 | Bridge.saveInstanceState(this, outState)
42 | outState.clear()
43 | }
44 |
45 | override fun onDestroy() {
46 | super.onDestroy()
47 | Bridge.clear(this)
48 | }
49 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common
2 |
3 | import androidx.annotation.CallSuper
4 | import androidx.lifecycle.ViewModel
5 | import io.reactivex.disposables.CompositeDisposable
6 | import io.reactivex.disposables.Disposable
7 |
8 | abstract class LassiBaseViewModel : ViewModel() {
9 |
10 | private val compositeDisposable = CompositeDisposable()
11 |
12 | @CallSuper
13 | open fun loadPage() {
14 |
15 | }
16 |
17 | protected fun Disposable.collect() = compositeDisposable.add(this)
18 |
19 | override fun onCleared() {
20 | super.onCleared()
21 | compositeDisposable.dispose()
22 | }
23 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModelActivity.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common
2 |
3 | import android.os.Bundle
4 | import androidx.annotation.CallSuper
5 | import androidx.viewbinding.ViewBinding
6 |
7 | abstract class LassiBaseViewModelActivity : LassiBaseActivity() {
8 |
9 | protected val viewModel by lazy { buildViewModel() }
10 |
11 | protected abstract fun buildViewModel(): T
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | initLiveDataObservers()
16 | viewModel.loadPage()
17 | }
18 |
19 | @CallSuper
20 | protected open fun initLiveDataObservers() = Unit
21 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/LassiBaseViewModelFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.CallSuper
8 | import androidx.viewbinding.ViewBinding
9 |
10 | abstract class LassiBaseViewModelFragment : LassiBaseFragment() {
11 | private var _binding: VB? = null
12 | protected val binding get() = _binding!!
13 |
14 | protected val viewModel by lazy { buildViewModel() }
15 |
16 | protected abstract fun buildViewModel(): T
17 |
18 | protected open fun getBundle() = Unit
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater,
22 | container: ViewGroup?,
23 | savedInstanceState: Bundle?
24 | ): View? {
25 | _binding = inflateLayout(inflater)
26 | setHasOptionsMenu(hasOptionMenu())
27 | getBundle()
28 | return binding.root
29 | }
30 |
31 | override fun onActivityCreated(savedInstanceState: Bundle?) {
32 | super.onActivityCreated(savedInstanceState)
33 | initLiveDataObservers()
34 | viewModel.loadPage()
35 | }
36 |
37 | @CallSuper
38 | protected open fun initLiveDataObservers() {
39 | }
40 |
41 | override fun onDestroy() {
42 | super.onDestroy()
43 | _binding = null
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/common/decoration/GridSpacingItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.common.decoration
2 |
3 | import android.graphics.Rect
4 | import android.view.View
5 | import androidx.annotation.Px
6 | import androidx.recyclerview.widget.RecyclerView
7 |
8 | class GridSpacingItemDecoration(
9 | private val columnCount: Int,
10 | @Px preferredSpace: Int,
11 | private val includeEdge: Boolean = true
12 | ) : RecyclerView.ItemDecoration() {
13 |
14 | /**
15 | * In this algorithm space should divide by 3 without remnant or width of
16 | * items can have a difference and we want them to be exactly the same
17 | */
18 | private val space =
19 | if (preferredSpace % 3 == 0) preferredSpace else (preferredSpace + (3 - preferredSpace % 3))
20 |
21 | override fun getItemOffsets(
22 | outRect: Rect,
23 | view: View,
24 | parent: RecyclerView,
25 | state: RecyclerView.State
26 | ) {
27 | val position = parent.getChildAdapterPosition(view)
28 | if (includeEdge) {
29 | when {
30 | position % columnCount == 0 -> {
31 | outRect.left = space
32 | outRect.right = space / 3
33 | }
34 | position % columnCount == columnCount - 1 -> {
35 | outRect.right = space
36 | outRect.left = space / 3
37 | }
38 | else -> {
39 | outRect.left = space * 2 / 3
40 | outRect.right = space * 2 / 3
41 | }
42 | }
43 | if (position < columnCount) {
44 | outRect.top = space
45 | }
46 | outRect.bottom = space
47 | } else {
48 | when {
49 | position % columnCount == 0 -> outRect.right = space * 2 / 3
50 | position % columnCount == columnCount - 1 -> outRect.left = space * 2 / 3
51 | else -> {
52 | outRect.left = space / 3
53 | outRect.right = space / 3
54 | }
55 | }
56 | if (position >= columnCount) {
57 | outRect.top = space
58 | }
59 | }
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/BitmapLoadingWorkerJob.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.net.Uri
6 | import kotlinx.coroutines.CoroutineScope
7 | import kotlinx.coroutines.Dispatchers
8 | import kotlinx.coroutines.Job
9 | import kotlinx.coroutines.isActive
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 | import java.lang.ref.WeakReference
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | internal class BitmapLoadingWorkerJob internal constructor(
16 | private val context: Context,
17 | cropImageView: CropImageView,
18 | internal val uri: Uri,
19 | ) : CoroutineScope {
20 | private val width: Int
21 | private val height: Int
22 | private val cropImageViewReference = WeakReference(cropImageView)
23 | private var job: Job = Job()
24 |
25 | override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job
26 |
27 | init {
28 | val metrics = cropImageView.resources.displayMetrics
29 | val densityAdjustment = if (metrics.density > 1) (1.0 / metrics.density) else 1.0
30 | width = (metrics.widthPixels * densityAdjustment).toInt()
31 | height = (metrics.heightPixels * densityAdjustment).toInt()
32 | }
33 |
34 | fun start() {
35 | job = launch(Dispatchers.Default) {
36 | try {
37 | if (isActive) {
38 | val decodeResult = BitmapUtils.decodeSampledBitmap(
39 | context = context,
40 | uri = uri,
41 | reqWidth = width,
42 | reqHeight = height,
43 | )
44 |
45 | if (isActive) {
46 | val orientateResult = BitmapUtils.orientateBitmapByExif(
47 | bitmap = decodeResult.bitmap,
48 | context = context,
49 | uri = uri,
50 | )
51 |
52 | onPostExecute(
53 | Result(
54 | uri = uri,
55 | bitmap = orientateResult.bitmap,
56 | loadSampleSize = decodeResult.sampleSize,
57 | degreesRotated = orientateResult.degrees,
58 | flipHorizontally = orientateResult.flipHorizontally,
59 | flipVertically = orientateResult.flipVertically,
60 | error = null,
61 | ),
62 | )
63 | }
64 | }
65 | } catch (e: Exception) {
66 | onPostExecute(
67 | Result(
68 | uri = uri,
69 | bitmap = null,
70 | loadSampleSize = 0,
71 | degreesRotated = 0,
72 | flipHorizontally = false,
73 | flipVertically = false,
74 | error = e,
75 | ),
76 | )
77 | }
78 | }
79 | }
80 |
81 | private suspend fun onPostExecute(result: Result) {
82 | withContext(Dispatchers.Main) {
83 | var completeCalled = false
84 | if (isActive) {
85 | cropImageViewReference.get()?.let {
86 | completeCalled = true
87 | it.onSetImageUriAsyncComplete(result)
88 | }
89 | }
90 |
91 | if (!completeCalled && result.bitmap != null) {
92 | // Fast release of unused bitmap.
93 | result.bitmap.recycle()
94 | }
95 | }
96 | }
97 |
98 | fun cancel() = job.cancel()
99 |
100 | internal data class Result(
101 | val uri: Uri,
102 | val bitmap: Bitmap?,
103 | val loadSampleSize: Int,
104 | val degreesRotated: Int,
105 | val flipHorizontally: Boolean,
106 | val flipVertically: Boolean,
107 | val error: Exception?,
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/CropException.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.net.Uri
4 |
5 | sealed class CropException(message: String) : Exception(message) {
6 | class Cancellation : CropException("$EXCEPTION_PREFIX cropping has been cancelled by the user") {
7 | internal companion object {
8 | private const val serialVersionUID: Long = -6896269134508601990L
9 | }
10 | }
11 |
12 | class FailedToLoadBitmap(uri: Uri, message: String?) : CropException("$EXCEPTION_PREFIX Failed to load sampled bitmap: $uri\r\n$message") {
13 | internal companion object {
14 | private const val serialVersionUID: Long = 7791142932960927332L
15 | }
16 | }
17 |
18 | class FailedToDecodeImage(uri: Uri) : CropException("$EXCEPTION_PREFIX Failed to decode image: $uri") {
19 | internal companion object {
20 | private const val serialVersionUID: Long = 3516154387706407275L
21 | }
22 | }
23 |
24 | internal companion object {
25 | private const val serialVersionUID: Long = 4933890872862969613L
26 | const val EXCEPTION_PREFIX = "crop:"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/CropImageAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.graphics.Matrix
4 | import android.graphics.RectF
5 | import android.view.animation.AccelerateDecelerateInterpolator
6 | import android.view.animation.Animation
7 | import android.view.animation.Animation.AnimationListener
8 | import android.view.animation.Transformation
9 | import android.widget.ImageView
10 | import com.lassi.presentation.cropper.CropOverlayView
11 |
12 | /**
13 | * Animation to handle smooth cropping image matrix transformation change, specifically for
14 | * zoom-in/out.
15 | */
16 | internal class CropImageAnimation(
17 | private val imageView: ImageView,
18 | private val cropOverlayView: CropOverlayView,
19 | ) : Animation(), AnimationListener {
20 |
21 | private val startBoundPoints = FloatArray(8)
22 | private val endBoundPoints = FloatArray(8)
23 | private val startCropWindowRect = RectF()
24 | private val endCropWindowRect = RectF()
25 | private val startImageMatrix = FloatArray(9)
26 | private val endImageMatrix = FloatArray(9)
27 |
28 | init {
29 | duration = 300
30 | fillAfter = true
31 | interpolator = AccelerateDecelerateInterpolator()
32 | setAnimationListener(this)
33 | }
34 |
35 | fun setStartState(boundPoints: FloatArray, imageMatrix: Matrix) {
36 | reset()
37 | System.arraycopy(boundPoints, 0, startBoundPoints, 0, 8)
38 | startCropWindowRect.set(cropOverlayView.cropWindowRect)
39 | imageMatrix.getValues(startImageMatrix)
40 | }
41 |
42 | fun setEndState(boundPoints: FloatArray, imageMatrix: Matrix) {
43 | System.arraycopy(boundPoints, 0, endBoundPoints, 0, 8)
44 | endCropWindowRect.set(cropOverlayView.cropWindowRect)
45 | imageMatrix.getValues(endImageMatrix)
46 | }
47 |
48 | override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
49 | val animRect = RectF().apply {
50 | left = (startCropWindowRect.left + (endCropWindowRect.left - startCropWindowRect.left) * interpolatedTime)
51 | top = (startCropWindowRect.top + (endCropWindowRect.top - startCropWindowRect.top) * interpolatedTime)
52 | right = (startCropWindowRect.right + (endCropWindowRect.right - startCropWindowRect.right) * interpolatedTime)
53 | bottom = (startCropWindowRect.bottom + (endCropWindowRect.bottom - startCropWindowRect.bottom) * interpolatedTime)
54 | }
55 |
56 | val animPoints = FloatArray(8)
57 | for (i in animPoints.indices) {
58 | animPoints[i] = (startBoundPoints[i] + (endBoundPoints[i] - startBoundPoints[i]) * interpolatedTime)
59 | }
60 |
61 | cropOverlayView.apply {
62 | cropWindowRect = animRect
63 | setBounds(animPoints, imageView.width, imageView.height)
64 | invalidate()
65 | }
66 |
67 | val animMatrix = FloatArray(9)
68 | for (i in animMatrix.indices) {
69 | animMatrix[i] = (startImageMatrix[i] + (endImageMatrix[i] - startImageMatrix[i]) * interpolatedTime)
70 | }
71 |
72 | imageView.apply {
73 | imageMatrix.setValues(animMatrix)
74 | invalidate()
75 | }
76 | }
77 |
78 | override fun onAnimationStart(animation: Animation) = Unit
79 | override fun onAnimationEnd(animation: Animation) {
80 | imageView.clearAnimation()
81 | }
82 |
83 | override fun onAnimationRepeat(animation: Animation) = Unit
84 | }
85 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/CropImageContract.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import androidx.activity.result.contract.ActivityResultContract
8 | import com.lassi.common.utils.KeyUtils
9 | import com.lassi.data.media.MiMedia
10 |
11 | /**
12 | * An [ActivityResultContract] to start an activity that allows the user to crop an image.
13 | * The UI can be customized using [CropImageOptions].
14 | * If you do not provide an [CropImageContractOptions.uri] in the input the user will be asked to pick an image before cropping.
15 | */
16 | class CropImageContract : ActivityResultContract() {
17 | override fun createIntent(context: Context, input: CropImageContractOptions) =
18 | Intent(context, CropImageActivity::class.java).apply {
19 | putExtra(
20 | CropImage.CROP_IMAGE_EXTRA_BUNDLE,
21 | Bundle(2).apply {
22 | putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri)
23 | putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.cropImageOptions)
24 | },
25 | )
26 | }
27 |
28 | override fun parseResult(resultCode: Int, intent: Intent?): MiMedia? {
29 | if (resultCode != Activity.RESULT_OK) return null
30 | return intent?.getParcelableExtra(KeyUtils.MEDIA_PREVIEW)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/CropImageContractOptions.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.net.Uri
4 |
5 | data class CropImageContractOptions(
6 | val uri: Uri?,
7 | val cropImageOptions: CropImageOptions,
8 | )
9 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/ParcelableUtils.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.os.Parcelable
6 |
7 | inline fun Bundle.parcelable(key: String): T? = when {
8 | // Does not work yet, https://issuetracker.google.com/issues/240585930
9 | // SDK_INT >= 33 -> getParcelable(key, T::class.java)
10 | else -> @Suppress("DEPRECATION") getParcelable(key) as? T
11 | }
12 |
13 | inline fun Intent.parcelable(key: String): T? = when {
14 | // Does not work yet, https://issuetracker.google.com/issues/240585930
15 | // SDK_INT >= 33 -> getParcelable(key, T::class.java)
16 | else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T
17 | }
18 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/cropper/utils/GetFilePathFromUri.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.cropper.utils
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.webkit.MimeTypeMap
7 | import java.io.File
8 | import java.io.FileOutputStream
9 | import java.io.IOException
10 | import java.io.InputStream
11 | import java.io.OutputStream
12 | import java.text.SimpleDateFormat
13 | import java.util.Date
14 | import java.util.Locale.getDefault
15 |
16 | internal fun getFilePathFromUri(context: Context, uri: Uri, uniqueName: Boolean): String =
17 | if (uri.path?.contains("file://") == true) {
18 | uri.path!!
19 | } else {
20 | getFileFromContentUri(context, uri, uniqueName).path
21 | }
22 |
23 | private fun getFileFromContentUri(context: Context, contentUri: Uri, uniqueName: Boolean): File {
24 | // Preparing Temp file name
25 | val fileExtension = getFileExtension(context, contentUri) ?: ""
26 | val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", getDefault()).format(Date())
27 | val fileName = ("temp_file_" + if (uniqueName) timeStamp else "") + ".$fileExtension"
28 | // Creating Temp file
29 | val tempFile = File(context.cacheDir, fileName)
30 | tempFile.createNewFile()
31 | // Initialize streams
32 | var oStream: FileOutputStream? = null
33 | var inputStream: InputStream? = null
34 |
35 | try {
36 | oStream = FileOutputStream(tempFile)
37 | inputStream = context.contentResolver.openInputStream(contentUri)
38 |
39 | inputStream?.let { copy(inputStream, oStream) }
40 | oStream.flush()
41 | } catch (e: Exception) {
42 | e.printStackTrace()
43 | } finally {
44 | // Close streams
45 | inputStream?.close()
46 | oStream?.close()
47 | }
48 |
49 | return tempFile
50 | }
51 |
52 | private fun getFileExtension(context: Context, uri: Uri): String? =
53 | if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
54 | MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri))
55 | } else {
56 | uri.path?.let { MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(it)).toString()) }
57 | }
58 |
59 | @Throws(IOException::class)
60 | private fun copy(source: InputStream, target: OutputStream) {
61 | val buf = ByteArray(8192)
62 | var length: Int
63 | while (source.read(buf).also { length = it } > 0) {
64 | target.write(buf, 0, length)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.docs
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import androidx.lifecycle.viewModelScope
5 | import com.lassi.data.common.Response
6 | import com.lassi.data.common.Result
7 | import com.lassi.data.media.MiMedia
8 | import com.lassi.domain.media.MediaRepository
9 | import com.lassi.presentation.common.LassiBaseViewModel
10 | import kotlinx.coroutines.flow.collect
11 | import kotlinx.coroutines.flow.onStart
12 | import kotlinx.coroutines.launch
13 | import java.util.*
14 |
15 | class DocsViewModel(private val mediaRepository: MediaRepository) : LassiBaseViewModel() {
16 | var fetchDocsLiveData = MutableLiveData>>()
17 |
18 | fun fetchDocs() {
19 | viewModelScope.launch {
20 | mediaRepository.fetchDocs()
21 | .onStart {
22 | fetchDocsLiveData.postValue(Response.Loading())
23 | }.collect { result ->
24 | when (result) {
25 | is Result.Success -> {
26 | fetchDocsLiveData.postValue(Response.Success(result.data))
27 | }
28 | is Result.Error -> {
29 | fetchDocsLiveData.postValue(Response.Error(result.throwable))
30 | }
31 | else -> {
32 | /**
33 | * no need to implement
34 | */
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/docs/DocsViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.docs
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import com.lassi.data.media.repository.MediaRepositoryImpl
7 | import com.lassi.domain.media.MediaRepository
8 |
9 | @Suppress("UNCHECKED_CAST")
10 | class DocsViewModelFactory(val context: Context) : ViewModelProvider.Factory {
11 | private val mediaRepository: MediaRepository = MediaRepositoryImpl(context)
12 |
13 | override fun create(modelClass: Class): T {
14 | return DocsViewModel(mediaRepository) as T
15 | }
16 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/media/SelectedMediaViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.media
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.viewModelScope
7 | import com.lassi.data.common.Response
8 | import com.lassi.data.common.Result
9 | import com.lassi.data.media.MiMedia
10 | import com.lassi.domain.media.LassiConfig
11 | import com.lassi.domain.media.MediaType
12 | import com.lassi.domain.media.SelectedMediaRepository
13 | import com.lassi.presentation.common.LassiBaseViewModel
14 | import kotlinx.coroutines.flow.onStart
15 | import kotlinx.coroutines.launch
16 |
17 | class SelectedMediaViewModel(
18 | private val selectedMediaRepository: SelectedMediaRepository
19 | ) : LassiBaseViewModel() {
20 | val selectedMediaLiveData = MutableLiveData>()
21 | private var selectedMedias = arrayListOf()
22 |
23 | var fetchedMediaLiveData = MutableLiveData>>()
24 |
25 | private val _currentSortingOption: MediatorLiveData =
26 | MediatorLiveData(LassiConfig.getConfig().ascFlag)
27 | val currentSortingOption: LiveData = _currentSortingOption
28 |
29 | fun currentSortingOptionUpdater(currentSortingOption: Int) {
30 | _currentSortingOption.value = currentSortingOption
31 | }
32 |
33 | fun addAllSelectedMedia(selectedMedias: ArrayList) {
34 | this.selectedMedias = selectedMedias
35 | this.selectedMedias = this.selectedMedias.distinctBy {
36 | it.path
37 | } as ArrayList
38 | selectedMediaLiveData.value = this.selectedMedias
39 | }
40 |
41 | fun addSelectedMedia(selectedMedia: MiMedia) {
42 | this.selectedMedias.add(selectedMedia)
43 | this.selectedMedias = this.selectedMedias.distinctBy { it.path } as ArrayList
44 | selectedMediaLiveData.value = this.selectedMedias
45 | }
46 |
47 | fun getSelectedMediaData(bucket: String) {
48 | viewModelScope.launch {
49 | selectedMediaRepository.getSelectedMediaData(bucket).onStart {
50 | fetchedMediaLiveData.value = Response.Loading()
51 | }.collect { result ->
52 | when (result) {
53 | is Result.Success -> result.data.let {
54 | fetchedMediaLiveData.postValue(Response.Success(it))
55 | }
56 |
57 | is Result.Error -> fetchedMediaLiveData.value = Response.Error(result.throwable)
58 | else -> {}
59 | }
60 | }
61 | }
62 | }
63 |
64 | fun getSortedDataFromDb(bucket: String, isAsc: Int, mediaType: MediaType) {
65 | viewModelScope.launch {
66 | selectedMediaRepository.getSortedDataFromDb(bucket, isAsc, mediaType).onStart {
67 | fetchedMediaLiveData.value = Response.Loading()
68 | }.collect { result ->
69 | when (result) {
70 | is Result.Success -> result.data.let {
71 | fetchedMediaLiveData.postValue(Response.Success(it))
72 | }
73 |
74 | is Result.Error -> {
75 | fetchedMediaLiveData.value = Response.Error(result.throwable)
76 | }
77 |
78 | else -> {}
79 | }
80 | }
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/mediadirectory/CropImageImpl.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.mediadirectory
2 |
3 | import android.net.Uri
4 | import com.lassi.presentation.cropper.CropImageActivity
5 | import com.lassi.presentation.cropper.CropImageView
6 |
7 | abstract class CropImageImpl: CropImageActivity() {
8 | override fun onSetImageUriComplete(view: CropImageView, uri: Uri, error: Exception?) {
9 | super.onSetImageUriComplete(view, uri, error)
10 | }
11 |
12 | override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
13 | super.onCropImageComplete(view, result)
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/mediadirectory/FolderViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.mediadirectory
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import com.lassi.data.media.repository.MediaRepositoryImpl
7 | import com.lassi.domain.media.MediaRepository
8 |
9 | @Suppress("UNCHECKED_CAST")
10 | class FolderViewModelFactory(val context: Context) : ViewModelProvider.Factory {
11 | private val mediaRepository: MediaRepository = MediaRepositoryImpl(context)
12 |
13 | override fun create(modelClass: Class): T {
14 | return FolderViewModel(mediaRepository) as T
15 | }
16 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/mediadirectory/SelectedMediaViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.mediadirectory
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import com.lassi.data.media.repository.SelectedMediaRepositoryImpl
7 | import com.lassi.domain.media.SelectedMediaRepository
8 | import com.lassi.presentation.media.SelectedMediaViewModel
9 |
10 | @Suppress("UNCHECKED_CAST")
11 | class SelectedMediaViewModelFactory(val context: Context) : ViewModelProvider.Factory {
12 | private val selectedMediaRepository: SelectedMediaRepository =
13 | SelectedMediaRepositoryImpl(context)
14 |
15 | override fun create(modelClass: Class): T {
16 | return SelectedMediaViewModel(selectedMediaRepository) as T
17 | }
18 | }
--------------------------------------------------------------------------------
/lassi/src/main/java/com/lassi/presentation/mediadirectory/adapter/FolderAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.lassi.presentation.mediadirectory.adapter
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.lassi.R
6 | import com.lassi.common.extenstions.hide
7 | import com.lassi.common.extenstions.loadImage
8 | import com.lassi.common.extenstions.show
9 | import com.lassi.common.extenstions.toBinding
10 | import com.lassi.data.media.MiItemMedia
11 | import com.lassi.databinding.ItemMediaBinding
12 |
13 | class FolderAdapter(
14 | private val onItemClick: (bucket: MiItemMedia) -> Unit
15 | ) : RecyclerView.Adapter() {
16 |
17 | private var buckets = ArrayList()
18 |
19 | fun setList(buckets: ArrayList?) {
20 | buckets?.let {
21 | this.buckets.clear()
22 | this.buckets.addAll(it)
23 | }
24 | notifyDataSetChanged()
25 | }
26 |
27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
28 | return FolderViewHolder(parent.toBinding())
29 | }
30 |
31 | override fun getItemCount() = buckets.size
32 |
33 | override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
34 | holder.bind(buckets[position])
35 | }
36 |
37 | fun clear() {
38 | val size: Int = buckets.size
39 | buckets.clear()
40 | notifyItemRangeRemoved(0, size)
41 | }
42 |
43 | inner class FolderViewHolder(val binding: ItemMediaBinding) :
44 | RecyclerView.ViewHolder(binding.root) {
45 | fun bind(bucket: MiItemMedia) {
46 | with(bucket) {
47 | binding.apply {
48 | tvFolderName.show()
49 | tvDuration.hide()
50 | ivFolderThumbnail.loadImage(bucket.latestItemPathForBucket)
51 | tvFolderName.text = String.format(
52 | tvFolderName.context.getString(R.string.directory_with_item_count),
53 | bucketName,
54 | totalItemSizeForBucket.toString()
55 | )
56 | itemView.setOnClickListener {
57 | onItemClick(bucket)
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/lassi/src/main/res/anim/right_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/anim/right_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/focus_marker_fill.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/focus_marker_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_back_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_camera_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_checked_media.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_crop_image_menu_flip.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_crop_image_menu_rotate_left.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_crop_image_menu_rotate_right.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_done_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_flash_auto_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_flash_off_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_flash_on_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_flip_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_flip_camera_white.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
22 |
23 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_image_placeholder.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
14 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_rotate_left_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_rotate_right_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_sorting_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/ic_tick_red.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/shape_circle_red.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/drawable/shape_circle_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/activity_media_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
23 |
24 |
33 |
34 |
43 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/activity_video_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
24 |
25 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/cameraview_gl_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/cameraview_layout_focus_marker.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
17 |
18 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/cameraview_surface_view.xml:
--------------------------------------------------------------------------------
1 |
3 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/cameraview_texture_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/crop_image_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/crop_image_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
19 |
25 |
26 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/fragment_media_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
34 |
35 |
44 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/item_media.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
33 |
34 |
47 |
48 |
62 |
63 |
75 |
76 |
--------------------------------------------------------------------------------
/lassi/src/main/res/layout/sorting_option.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
14 |
15 |
20 |
21 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/lassi/src/main/res/menu/crop_image_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/lassi/src/main/res/menu/crop_image_menu_old.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
16 | -
21 |
22 |
25 |
28 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/lassi/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 | -
13 |
14 |
17 |
20 |
21 |
22 |
26 |
27 |
--------------------------------------------------------------------------------
/lassi/src/main/res/menu/media_picker_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
10 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/lassi/src/main/res/menu/video_preview_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
9 |
--------------------------------------------------------------------------------
/lassi/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1f33cb
4 | #041697
5 | #ec2b4e
6 |
7 | #80000000
8 | #50000000
9 | #FFA000
10 |
11 |
--------------------------------------------------------------------------------
/lassi/src/main/res/values/integer.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 400
4 |
--------------------------------------------------------------------------------
/lassi/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
17 |
18 |
19 |
20 |
33 |
34 |
--------------------------------------------------------------------------------
/lassi/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
9 |
12 |
15 |
18 |
--------------------------------------------------------------------------------
/media/image-picker-camera.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/media/image-picker-camera.gif
--------------------------------------------------------------------------------
/media/image-picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mindinventory/Lassi-Android/700cd88bd28fb799d0157d73080f917779e16a52/media/image-picker.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven { url "https://jitpack.io" }
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | maven { url 'https://jitpack.io' }
14 | mavenCentral()
15 | }
16 | }
17 | rootProject.name = "Lassi sample"
18 | include ':app', ':lassi'
--------------------------------------------------------------------------------