Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sensorhub-android-app/res/drawable/ic_zoom_in.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM12,10h-2v2H9v-2H7V9h2V7h1v2h2v1z"/>
</vector>
9 changes: 9 additions & 0 deletions sensorhub-android-app/res/drawable/ic_zoom_out.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14zM7,9h5v1H7V9z"/>
</vector>
55 changes: 45 additions & 10 deletions sensorhub-android-app/res/layout/fragment_dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,6 @@

</LinearLayout>

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_flip_camera"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:visibility="gone"
android:contentDescription="@string/content_desc_switch_camera"
app:icon="@drawable/ic_flip_camera"
app:iconTint="@color/md_theme_onSurface"
app:iconSize="20dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_toggle_video"
Expand Down Expand Up @@ -186,6 +176,51 @@

</LinearLayout>

<LinearLayout
android:id="@+id/video_controls_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="vertical"
android:layout_marginEnd="16dp"
android:layout_marginBottom="80dp"
android:visibility="gone">

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_flip_camera"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:backgroundTint="#80000000"
android:contentDescription="@string/content_desc_switch_camera"
app:icon="@drawable/ic_flip_camera"
app:iconTint="@android:color/white"
app:iconSize="24dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_zoom_in"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:backgroundTint="#80000000"
android:contentDescription="@string/content_desc_zoom_in"
app:icon="@drawable/ic_zoom_in"
app:iconTint="@android:color/white"
app:iconSize="24dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btn_zoom_out"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:backgroundTint="#80000000"
android:contentDescription="@string/content_desc_zoom_out"
app:icon="@drawable/ic_zoom_out"
app:iconTint="@android:color/white"
app:iconSize="24dp" />

</LinearLayout>

</FrameLayout>

<com.google.android.material.floatingactionbutton.FloatingActionButton
Expand Down
2 changes: 2 additions & 0 deletions sensorhub-android-app/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@
<string name="manual_entry_option">Enter name or address manually…</string>
<string name="scan_new_devices">Scan for new devices…</string>
<string name="content_desc_switch_camera">Switch Camera</string>
<string name="content_desc_zoom_in">Zoom In</string>
<string name="content_desc_zoom_out">Zoom Out</string>

<!-- Status Messages -->
<string name="stopping_sensorhub">Stopping SensorHub</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe
private TextureView textureView;
private MaterialCardView videoStatusCard;
private MaterialButton btnToggleVideo;
private MaterialButton btnFlipCamera;
private MaterialButton btnZoomIn;
private MaterialButton btnZoomOut;
private LinearLayout videoControlsOverlay;
private int currentZoomLevel = 0;
private MaterialCardView meshtasticCard;

private View videoStatusDot;
Expand Down Expand Up @@ -119,6 +124,16 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat

btnToggleVideo.setOnClickListener(v -> toggleVideoPreview());

videoControlsOverlay = view.findViewById(R.id.video_controls_overlay);

btnFlipCamera = view.findViewById(R.id.btn_flip_camera);
btnFlipCamera.setOnClickListener(v -> flipCamera());

btnZoomIn = view.findViewById(R.id.btn_zoom_in);
btnZoomIn.setOnClickListener(v -> adjustZoom(1));
btnZoomOut = view.findViewById(R.id.btn_zoom_out);
btnZoomOut.setOnClickListener(v -> adjustZoom(-1));

meshtasticCard = view.findViewById(R.id.meshtastic_card);
view.findViewById(R.id.btn_meshtastic_msg).setOnClickListener(v -> showMeshtasticDialog());

Expand Down Expand Up @@ -177,6 +192,8 @@ private void stopHub() {
hideVideoPreview();
clearTextureView();
videoStatusCard.setVisibility(View.GONE);
if (videoControlsOverlay != null) videoControlsOverlay.setVisibility(View.GONE);
currentZoomLevel = 0;
if (meshtasticCard != null) meshtasticCard.setVisibility(View.GONE);
newStatusMessage(getString(R.string.sensorhub_stopped));
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Expand Down Expand Up @@ -562,6 +579,7 @@ private void updateVideoStatusCard() {
boolean hasVideo = service != null && service.hasVideo();

videoStatusCard.setVisibility(hasVideo ? View.VISIBLE : View.GONE);
updateVideoControlsVisibility();

if (hasVideo && videoInfoText.length() > 0) {
videoInfoArea.setText(videoInfoText.toString());
Expand All @@ -581,15 +599,80 @@ private void toggleVideoPreview() {
textureView.setVisibility(View.VISIBLE);
btnToggleVideo.setText(R.string.btn_hide);
serverStatusContainer.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme()));
updateVideoControlsVisibility();
showVideo();
} else {
hideVideoPreview();
}
}

@SuppressWarnings("deprecation")
private void updateVideoControlsVisibility() {
SensorHubService service = provider.getBoundService();
boolean hasVideo = service != null && service.hasVideo();

if (videoControlsOverlay != null)
videoControlsOverlay.setVisibility(hasVideo && videoPreviewVisible ? View.VISIBLE : View.GONE);

if (btnFlipCamera != null) {
boolean showFlip = hasVideo && android.hardware.Camera.getNumberOfCameras() > 1;
btnFlipCamera.setVisibility(showFlip ? View.VISIBLE : View.GONE);
}
}

@SuppressWarnings("deprecation")
private void flipCamera() {
AndroidSensorsDriver sensors = provider.getAndroidSensors();
if (sensors == null) return;

try {
int currentId = sensors.getConfiguration().selectedCameraId;
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(currentId, info);

String targetFacing = (info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK)
? "FRONT" : "BACK";

int targetId = -1;
int targetFacingInt = "FRONT".equals(targetFacing)
? android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT
: android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;

for (int i = 0; i < android.hardware.Camera.getNumberOfCameras(); i++) {
android.hardware.Camera.CameraInfo camInfo = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(i, camInfo);
if (camInfo.facing == targetFacingInt) {
targetId = i;
break;
}
}

if (targetId >= 0) {
sensors.switchCamera(targetId);
currentZoomLevel = 0;
Toast.makeText(requireContext(), "Switched to " + targetFacing.toLowerCase() + " camera", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Toast.makeText(requireContext(), "Failed to switch camera", Toast.LENGTH_SHORT).show();
}
}

private void adjustZoom(int direction) {
AndroidSensorsDriver sensors = provider.getAndroidSensors();
if (sensors == null) return;

try {
currentZoomLevel = Math.max(0, currentZoomLevel + direction);
sensors.setCameraZoom(currentZoomLevel);
} catch (Exception e) {
Toast.makeText(requireContext(), "Zoom not supported", Toast.LENGTH_SHORT).show();
}
}

private void hideVideoPreview() {
videoPreviewVisible = false;
textureView.setVisibility(View.GONE);
if (videoControlsOverlay != null) videoControlsOverlay.setVisibility(View.GONE);
if (btnToggleVideo != null) btnToggleVideo.setText(R.string.btn_show);
serverStatusContainer.setBackgroundColor(0x00000000);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/***************************** BEGIN LICENSE BLOCK ***************************

The contents of this file are subject to the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file, You can obtain one
at http://mozilla.org/MPL/2.0/.

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.

The Initial Developer is GeoRobotix Innovative Research Inc.. Portions created by the Initial
Developer are Copyright (C) 2026 the Initial Developer. All Rights Reserved.

******************************* END LICENSE BLOCK ***************************/

package org.sensorhub.impl.sensor.android;


import android.hardware.Camera;

import net.opengis.swe.v20.DataBlock;
import net.opengis.swe.v20.DataComponent;

import org.sensorhub.api.command.CommandException;
import org.sensorhub.api.sensor.SensorException;
import org.sensorhub.impl.sensor.AbstractSensorControl;
import org.vast.swe.SWEHelper;


public class AndroidCameraControl extends AbstractSensorControl<AndroidSensorsDriver>
{
private final DataComponent commandDescription;

// field indices in the command DataBlock
private static final int CAMERA_FACING_IDX = 0;
private static final int ZOOM_LEVEL_IDX = 1;


public AndroidCameraControl(AndroidSensorsDriver parentSensor)
{
super("cameraControl", parentSensor);

SWEHelper fac = new SWEHelper();
commandDescription = fac.createRecord()
.name("cameraControl")
.addField("cameraFacing", fac.createCategory()
.definition(SWEHelper.getPropertyUri("CameraSelector"))
.label("Camera Facing Direction")
.description("Select front or back camera")
.addAllowedValues("FRONT", "BACK")
.build())
.addField("zoomLevel", fac.createCount()
.definition(SWEHelper.getPropertyUri("ZoomLevel"))
.label("Zoom Level")
.description("Camera zoom level (0 = no zoom, max depends on device)")
.build())
.build();
}


@Override
public DataComponent getCommandDescription()
{
return commandDescription;
}


@Override
protected boolean execCommand(DataBlock command) throws CommandException
{
try
{
// handle camera facing change
String facing = command.getStringValue(CAMERA_FACING_IDX);
if (facing != null && !facing.isEmpty())
{
int newCameraId = findCameraId(facing);
parentSensor.switchCamera(newCameraId);
}

// handle zoom level change
int zoomLevel = command.getIntValue(ZOOM_LEVEL_IDX);
if (zoomLevel >= 0)
{
parentSensor.setCameraZoom(zoomLevel);
}

return true;
}
catch (SensorException e)
{
throw new CommandException("Failed to execute camera command", e);
}
}


private int findCameraId(String facing) throws CommandException
{
int targetFacing = "FRONT".equals(facing)
? Camera.CameraInfo.CAMERA_FACING_FRONT
: Camera.CameraInfo.CAMERA_FACING_BACK;

for (int i = 0; i < Camera.getNumberOfCameras(); i++)
{
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == targetFacing)
return i;
}

throw new CommandException("No " + facing + " camera found on this device");
}
}
Loading
Loading