Skip to content

Commit c5169ec

Browse files
committed
refactor: small quality fixes + feat: Front End Error Handling for Dashcam
1 parent 9157122 commit c5169ec

9 files changed

Lines changed: 181 additions & 38 deletions

File tree

‎app/src/main/java/com/drivehub/kamera/BootReceiver.java‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,24 @@
33
import android.content.BroadcastReceiver;
44
import android.content.Context;
55
import android.content.Intent;
6+
import android.util.Log;
67

78
public class BootReceiver extends BroadcastReceiver {
89
@Override
910
public void onReceive(Context context, Intent intent) {
10-
if (intent == null || intent.getAction() == null) return;
11-
if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) return;
11+
if (intent == null || intent.getAction() == null)
12+
return;
13+
if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()))
14+
return;
1215

13-
// On boot, start both recording (if enabled) and the signal listener.
14-
// The `overlayOnSignal` setting controls overlay behavior; without the signal service, the overlay cannot trigger.
15-
RecordingService.startIfNeeded(context);
16+
RecordingService.startIfDashcamEnabled(context);
17+
18+
// SignalService startup binds the Car API and may fall back to polling — guard
19+
// so a binding failure surfaces in logcat instead of crashing the boot receiver.
1620
try {
1721
SignalService.start(context);
18-
} catch (Throwable ignored) {
22+
} catch (Exception e) {
23+
Log.w("BootReceiver", "Failed to start SignalService on boot", e);
1924
}
2025
}
2126
}

‎app/src/main/java/com/drivehub/kamera/CameraProbe.java‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ public final class CameraProbe {
55
System.loadLibrary("cameraprobe");
66
}
77

8-
private CameraProbe() {}
8+
private CameraProbe() {
9+
}
910

1011
/** Probes /dev/video0..maxIndex-1 and returns a human-readable summary. */
1112
public static native String probeAll(int maxIndex);
@@ -16,27 +17,29 @@ private CameraProbe() {}
1617
/** Detaches the preview consumer from the given /dev/video index. */
1718
public static native void detachPreview(int videoIndex);
1819

19-
/** Detaches all preview consumers managed by the native camera stream manager. */
20+
/**
21+
* Detaches all preview consumers managed by the native camera stream manager.
22+
*/
2023
public static native void detachAllPreviews();
2124

2225
/**
2326
* Starts MP4 recording from a specific /dev/videoX device.
2427
* slot: 0..3 to allow multiple concurrent recorders.
2528
*/
2629
public static native boolean startMp4Record(int slot, int videoIndex, String outputPath,
27-
int width, int height, int fps, int bitrate);
30+
int width, int height, int fps, int bitrate);
2831

2932
/** Stops MP4 recording for the given slot. */
3033
public static native boolean stopMp4Record(int slot);
3134

32-
/** Starts a single 2x2 grid MP4 recording that combines all four cameras. */
35+
/** Starts a grid MP4 recording that combines all four cameras. */
3336
public static native boolean startCombinedMp4Record(String outputPath,
34-
int cellWidth, int cellHeight,
35-
int fps, int bitrate,
36-
String signature,
37-
boolean showSpeed);
37+
int cellWidth, int cellHeight,
38+
int fps, int bitrate,
39+
String signature,
40+
boolean showSpeed);
3841

39-
/** Stops the currently running combined 2x2 grid recording. */
42+
/** Stops the currently running combined grid recording. */
4043
public static native boolean stopCombinedMp4Record();
4144

4245
/** Updates the current speed shown in the active combined recording overlay. */

‎app/src/main/java/com/drivehub/kamera/ColorHueStripView.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ interface OnHueChangedListener {
1818
void onHueChanged(float hue);
1919
}
2020

21+
// Six primary/secondary hue stops with red repeated at the end so the gradient closes
22+
// the wheel back to red instead of fading out at magenta.
2123
private static final int[] HUE_COLORS = new int[]{
2224
0xFFFF0000,
2325
0xFFFFFF00,

‎app/src/main/java/com/drivehub/kamera/ColorSaturationValueView.java‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public ColorSaturationValueView(Context context, @Nullable AttributeSet attrs, i
4646

4747
private void init() {
4848
markerPaint.setStyle(Paint.Style.FILL);
49-
markerPaint.setColor(0x00FFFFFF);
49+
markerPaint.setColor(0x00FFFFFF); // Transparent
5050
markerOutlinePaint.setStyle(Paint.Style.STROKE);
5151
markerOutlinePaint.setStrokeWidth(dp(2f));
5252
markerOutlinePaint.setColor(0xFFFFFFFF);
@@ -76,7 +76,8 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
7676
protected void onDraw(Canvas canvas) {
7777
super.onDraw(canvas);
7878
float radius = dp(12f);
79-
// Build the picker field in layers: pure hue, then wash it with white and black gradients.
79+
// Build the picker field in layers: pure hue, then wash it with white and black
80+
// gradients.
8081
canvas.drawRoundRect(contentRect, radius, radius, fieldPaint);
8182
canvas.drawRoundRect(contentRect, radius, radius, saturationOverlayPaint);
8283
canvas.drawRoundRect(contentRect, radius, radius, valueOverlayPaint);
@@ -90,7 +91,8 @@ protected void onDraw(Canvas canvas) {
9091

9192
@Override
9293
public boolean onTouchEvent(MotionEvent event) {
93-
if (event == null) return false;
94+
if (event == null)
95+
return false;
9496
switch (event.getActionMasked()) {
9597
case MotionEvent.ACTION_DOWN:
9698
case MotionEvent.ACTION_MOVE:
@@ -103,8 +105,10 @@ public boolean onTouchEvent(MotionEvent event) {
103105
}
104106

105107
private void updateFromTouch(float x, float y) {
106-
if (contentRect.width() <= 0f || contentRect.height() <= 0f) return;
107-
// Horizontal movement changes saturation, vertical movement changes brightness/value.
108+
if (contentRect.width() <= 0f || contentRect.height() <= 0f)
109+
return;
110+
// Horizontal movement changes saturation, vertical movement changes
111+
// brightness/value.
108112
saturation = clamp((x - contentRect.left) / contentRect.width(), 0f, 1f);
109113
value = clamp(1f - ((y - contentRect.top) / contentRect.height()), 0f, 1f);
110114
invalidate();
@@ -114,8 +118,9 @@ private void updateFromTouch(float x, float y) {
114118
}
115119

116120
private void updateShader() {
117-
if (contentRect.width() <= 0f || contentRect.height() <= 0f) return;
118-
int hueColor = android.graphics.Color.HSVToColor(new float[]{hue, 1f, 1f});
121+
if (contentRect.width() <= 0f || contentRect.height() <= 0f)
122+
return;
123+
int hueColor = android.graphics.Color.HSVToColor(new float[] { hue, 1f, 1f });
119124
fieldPaint.setShader(null);
120125
fieldPaint.setColor(hueColor);
121126

@@ -127,8 +132,7 @@ private void updateShader() {
127132
contentRect.top,
128133
0xFFFFFFFF,
129134
0x00FFFFFF,
130-
Shader.TileMode.CLAMP
131-
);
135+
Shader.TileMode.CLAMP);
132136
saturationOverlayPaint.setShader(saturationShader);
133137

134138
// Fade from transparent to black to cover value/brightness on the Y axis.
@@ -139,8 +143,7 @@ private void updateShader() {
139143
contentRect.bottom,
140144
0x00FFFFFF,
141145
0xFF000000,
142-
Shader.TileMode.CLAMP
143-
);
146+
Shader.TileMode.CLAMP);
144147
valueOverlayPaint.setShader(valueShader);
145148
}
146149

‎app/src/main/java/com/drivehub/kamera/DashcamEventOverlayService.java‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,39 @@ public class DashcamEventOverlayService extends Service {
3535
private android.widget.TextView titleView;
3636
private android.widget.TextView subtitleView;
3737

38+
// Show the standard dashcam event confirmation banner using string resources.
3839
public static void showConfirmation(Context context) {
3940
showBanner(
4041
context,
4142
R.string.dashcam_event_overlay_title,
4243
R.string.dashcam_event_overlay_subtitle,
43-
R.string.notification_dashcam_event_overlay_text
44-
);
44+
R.string.notification_dashcam_event_overlay_text);
4545
}
4646

47+
// Show the OEM pause banner when recording must pause while the OEM app is
48+
// foregrounded.
4749
public static void showOemPause(Context context) {
4850
showBanner(
4951
context,
5052
R.string.dashcam_oem_pause_overlay_title,
5153
R.string.dashcam_oem_pause_overlay_subtitle,
52-
R.string.notification_dashcam_oem_pause_overlay_text
53-
);
54+
R.string.notification_dashcam_oem_pause_overlay_text);
55+
}
56+
57+
public static void showRecordingError(Context context, int subtitleResId, int notificationTextResId) {
58+
showBanner(
59+
context,
60+
R.string.dashcam_recording_error_overlay_title,
61+
subtitleResId,
62+
notificationTextResId);
63+
}
64+
65+
public static void showRecordingRecovered(Context context) {
66+
showBanner(
67+
context,
68+
R.string.dashcam_recording_recovered_overlay_title,
69+
R.string.dashcam_recording_recovered_overlay_subtitle,
70+
R.string.notification_dashcam_recording_recovered_text);
5471
}
5572

5673
private static void showBanner(Context context, int titleResId, int subtitleResId, int notificationTextResId) {
@@ -108,8 +125,7 @@ private void showOverlayWindow() {
108125
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
109126
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
110127
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
111-
PixelFormat.TRANSLUCENT
112-
);
128+
PixelFormat.TRANSLUCENT);
113129
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
114130
params.y = dpToPx(32);
115131

@@ -149,10 +165,8 @@ private void createNotificationChannel() {
149165
NotificationChannel channel = new NotificationChannel(
150166
CHANNEL_ID,
151167
getString(R.string.notification_channel_dashcam_event_overlay),
152-
NotificationManager.IMPORTANCE_LOW
153-
);
154-
NotificationManager notificationManager =
155-
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
168+
NotificationManager.IMPORTANCE_LOW);
169+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
156170
if (notificationManager != null) {
157171
notificationManager.createNotificationChannel(channel);
158172
}

‎app/src/main/java/com/drivehub/kamera/DashcamSettingsController.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ void bind(
7171
.show();
7272
}
7373
saveFields(prefs, etSegmentMin, etTotalMin, etRecordingFps, etSignature, false);
74-
RecordingService.startIfNeeded(activity);
74+
RecordingService.startIfDashcamEnabled(activity);
7575
} else {
7676
RecordingService.stopIfRunning(activity);
7777
}

‎app/src/main/java/com/drivehub/kamera/RecordingService.java‎

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import android.content.Context;
88
import android.content.Intent;
99
import android.content.SharedPreferences;
10+
import android.os.Handler;
1011
import android.os.IBinder;
12+
import android.os.Looper;
1113
import android.os.SystemClock;
1214
import android.util.Log;
1315

@@ -60,21 +62,27 @@ public class RecordingService extends Service {
6062
private static final long TEST_RECORDING_MS = 10_000L;
6163
private static final long EVENT_PRE_ROLL_MS = 60_000L;
6264
private static final long EVENT_POST_ROLL_MS = 60_000L;
65+
private static final long ERROR_OVERLAY_DELAY_MS = 5_000L;
6366

6467
private static volatile boolean sServiceRunning = false;
6568

6669
private final Object eventLock = new Object();
6770
private final Object stateLock = new Object();
71+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
6872
private Thread worker;
6973
private volatile boolean stopRequested = false;
7074
private volatile boolean segmentStopRequested = false;
7175
private volatile boolean oemPauseRequested = false;
76+
private volatile int pendingErrorSubtitleResId = 0;
77+
private volatile int pendingErrorNotificationResId = 0;
78+
private volatile int pendingErrorGeneration = 0;
79+
private volatile boolean errorOverlayShown = false;
7280

7381
public static boolean isRunning() {
7482
return sServiceRunning;
7583
}
7684

77-
public static void startIfNeeded(Context context) {
85+
public static void startIfDashcamEnabled(Context context) {
7886
SharedPreferences prefs = UiPrefs.getPrefs(context);
7987
boolean enabled = prefs.getBoolean(DashcamSettingsController.KEY_ENABLED, false);
8088
if (!enabled)
@@ -646,6 +654,8 @@ private void publishStatus(String status, int activeCameras, int totalCameras, S
646654
}
647655
}
648656

657+
updateDashcamOverlayState(status, lastError);
658+
649659
Intent intent = new Intent(ACTION_STATUS_CHANGED);
650660
intent.setPackage(getPackageName());
651661
intent.putExtra(EXTRA_STATUS, status);
@@ -655,6 +665,77 @@ private void publishStatus(String status, int activeCameras, int totalCameras, S
655665
sendBroadcast(intent);
656666
}
657667

668+
private void updateDashcamOverlayState(String status, String lastError) {
669+
if (STATUS_ERROR.equals(status) || STATUS_PARTIAL.equals(status)) {
670+
scheduleDelayedErrorOverlay(lastError);
671+
return;
672+
}
673+
674+
cancelPendingErrorOverlay();
675+
if (errorOverlayShown && STATUS_RECORDING.equals(status)) {
676+
errorOverlayShown = false;
677+
DashcamEventOverlayService.showRecordingRecovered(this);
678+
} else if (!STATUS_RECORDING.equals(status)) {
679+
errorOverlayShown = false;
680+
}
681+
}
682+
683+
private void scheduleDelayedErrorOverlay(String lastError) {
684+
OverlayMessageSpec spec = mapRecordingError(lastError);
685+
if (spec == null) {
686+
cancelPendingErrorOverlay();
687+
return;
688+
}
689+
if (errorOverlayShown
690+
&& pendingErrorSubtitleResId == spec.subtitleResId
691+
&& pendingErrorNotificationResId == spec.notificationTextResId) {
692+
return;
693+
}
694+
695+
pendingErrorSubtitleResId = spec.subtitleResId;
696+
pendingErrorNotificationResId = spec.notificationTextResId;
697+
final int generation = ++pendingErrorGeneration;
698+
mainHandler.removeCallbacksAndMessages(this);
699+
mainHandler.postAtTime(() -> {
700+
if (generation != pendingErrorGeneration) {
701+
return;
702+
}
703+
errorOverlayShown = true;
704+
DashcamEventOverlayService.showRecordingError(
705+
RecordingService.this,
706+
pendingErrorSubtitleResId,
707+
pendingErrorNotificationResId);
708+
}, this, SystemClock.uptimeMillis() + ERROR_OVERLAY_DELAY_MS);
709+
}
710+
711+
private void cancelPendingErrorOverlay() {
712+
pendingErrorGeneration++;
713+
pendingErrorSubtitleResId = 0;
714+
pendingErrorNotificationResId = 0;
715+
mainHandler.removeCallbacksAndMessages(this);
716+
}
717+
718+
private OverlayMessageSpec mapRecordingError(String lastError) {
719+
if ("storage not writable".equals(lastError)) {
720+
return new OverlayMessageSpec(
721+
R.string.dashcam_recording_error_overlay_subtitle_storage,
722+
R.string.notification_dashcam_recording_error_storage_text);
723+
}
724+
if ("grid start failed".equals(lastError)) {
725+
return new OverlayMessageSpec(
726+
R.string.dashcam_recording_error_overlay_subtitle_start_failed,
727+
R.string.notification_dashcam_recording_error_start_failed_text);
728+
}
729+
if ("grid stop timeout".equals(lastError)) {
730+
return new OverlayMessageSpec(
731+
R.string.dashcam_recording_error_overlay_subtitle_stop_failed,
732+
R.string.notification_dashcam_recording_error_stop_failed_text);
733+
}
734+
return new OverlayMessageSpec(
735+
R.string.dashcam_recording_error_overlay_subtitle_generic,
736+
R.string.notification_dashcam_recording_error_text);
737+
}
738+
658739
private SharedPreferences prefs() {
659740
return UiPrefs.getPrefs(this);
660741
}
@@ -679,6 +760,7 @@ public IBinder onBind(Intent intent) {
679760
public void onDestroy() {
680761
sServiceRunning = false;
681762
stopRequested = true;
763+
cancelPendingErrorOverlay();
682764
if (worker != null) {
683765
worker.interrupt();
684766
try {
@@ -729,4 +811,14 @@ private static final class EventCopyJob {
729811
this.baseNames = baseNames;
730812
}
731813
}
814+
815+
private static final class OverlayMessageSpec {
816+
final int subtitleResId;
817+
final int notificationTextResId;
818+
819+
OverlayMessageSpec(int subtitleResId, int notificationTextResId) {
820+
this.subtitleResId = subtitleResId;
821+
this.notificationTextResId = notificationTextResId;
822+
}
823+
}
732824
}

0 commit comments

Comments
 (0)