package org.droidplanner.sample.hellodrone;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.o3dr.android.client.ControlTower;
import com.o3dr.android.client.Drone;
import com.o3dr.android.client.apis.ControlApi;
import com.o3dr.android.client.apis.ExperimentalApi;
import com.o3dr.android.client.apis.VehicleApi;
import com.o3dr.android.client.apis.solo.SoloCameraApi;
import com.o3dr.android.client.interfaces.DroneListener;
import com.o3dr.android.client.interfaces.LinkListener;
import com.o3dr.android.client.interfaces.TowerListener;
import com.o3dr.android.client.utils.video.DecoderListener;
import com.o3dr.android.client.utils.video.MediaCodecManager;
import com.o3dr.services.android.lib.coordinate.LatLong;
import com.o3dr.services.android.lib.coordinate.LatLongAlt;
import com.o3dr.services.android.lib.drone.attribute.AttributeEvent;
import com.o3dr.services.android.lib.drone.attribute.AttributeType;
import com.o3dr.services.android.lib.drone.companion.solo.SoloAttributes;
import com.o3dr.services.android.lib.drone.companion.solo.SoloState;
import com.o3dr.services.android.lib.drone.connection.ConnectionParameter;
import com.o3dr.services.android.lib.drone.connection.ConnectionType;
import com.o3dr.services.android.lib.drone.property.Altitude;
import com.o3dr.services.android.lib.drone.property.Gps;
import com.o3dr.services.android.lib.drone.property.Home;
import com.o3dr.services.android.lib.drone.property.Speed;
import com.o3dr.services.android.lib.drone.property.State;
import com.o3dr.services.android.lib.drone.property.Type;
import com.o3dr.services.android.lib.drone.property.VehicleMode;
import com.o3dr.services.android.lib.gcs.link.LinkConnectionStatus;
import com.o3dr.services.android.lib.model.AbstractCommandListener;
import com.o3dr.services.android.lib.model.SimpleCommandListener;
import java.io.IOException;
import java.util.List;
import static com.o3dr.android.client.apis.ExperimentalApi.getApi;
public class MainActivity extends AppCompatActivity implements DroneListener, TowerListener, LinkListener {
private static final String TAG = MainActivity.class.getSimpleName();
private Drone drone;
private int droneType = Type.TYPE_UNKNOWN;
private ControlTower controlTower;
private final Handler handler = new Handler();
private static final int DEFAULT_UDP_PORT = 14550;
private static final int DEFAULT_USB_BAUD_RATE = 57600;
private Spinner modeSelector;
private Button startVideoStream;
private Button stopVideoStream;
private Button startVideoStreamUsingObserver;
private Button stopVideoStreamUsingObserver;
private MediaCodecManager mediaCodecManager;
private TextureView videoView;
private String videoTag = "testvideotag";
Handler mainHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Context context = getApplicationContext();
this.controlTower = new ControlTower(context);
this.drone = new Drone(context);
this.modeSelector = (Spinner) findViewById(R.id.modeSelect);
this.modeSelector.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
onFlightModeSelected(view);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// Do nothing
}
});
final Button takePic = (Button) findViewById(R.id.take_photo_button);
takePic.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
takePhoto();
}
});
final Button toggleVideo = (Button) findViewById(R.id.toggle_video_recording);
toggleVideo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleVideoRecording();
}
});
videoView = (TextureView) findViewById(R.id.video_content);
videoView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
alertUser("Video display is available.");
startVideoStream.setEnabled(true);
startVideoStreamUsingObserver.setEnabled(true);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
startVideoStream.setEnabled(false);
startVideoStreamUsingObserver.setEnabled(false);
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
});
startVideoStream = (Button) findViewById(R.id.start_video_stream);
startVideoStream.setEnabled(false);
startVideoStream.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alertUser("Starting video stream.");
startVideoStream(new Surface(videoView.getSurfaceTexture()));
}
});
stopVideoStream = (Button) findViewById(R.id.stop_video_stream);
stopVideoStream.setEnabled(false);
stopVideoStream.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alertUser("Stopping video stream.");
stopVideoStream();
}
});
startVideoStreamUsingObserver = (Button) findViewById(R.id.start_video_stream_using_observer);
startVideoStreamUsingObserver.setEnabled(false);
startVideoStreamUsingObserver.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alertUser("Starting video stream using observer for video stream packets.");
startVideoStreamForObserver();
}
});
stopVideoStreamUsingObserver = (Button) findViewById(R.id.stop_video_stream_using_observer);
stopVideoStreamUsingObserver.setEnabled(false);
stopVideoStreamUsingObserver.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
alertUser("Stopping video stream using observer for video stream packets.");
stopVideoStreamForObserver();
}
});
// Initialize media codec manager to decode video stream packets.
HandlerThread mediaCodecHandlerThread = new HandlerThread("MediaCodecHandlerThread");
mediaCodecHandlerThread.start();
Handler mediaCodecHandler = new Handler(mediaCodecHandlerThread.getLooper());
mediaCodecManager = new MediaCodecManager(mediaCodecHandler);
mainHandler = new Handler(getApplicationContext().getMainLooper());
}
@Override
public void onStart() {
super.onStart();
this.controlTower.connect(this);
updateVehicleModesForType(this.droneType);
}
@Override
public void onStop() {
super.onStop();
if (this.drone.isConnected()) {
this.drone.disconnect();
updateConnectedButton(false);
}
this.controlTower.unregisterDrone(this.drone);
this.controlTower.disconnect();
}
// DroneKit-Android Listener
// ==========================================================
@Override
public void onTowerConnected() {
alertUser("DroneKit-Android Connected");
this.controlTower.registerDrone(this.drone, this.handler);
this.drone.registerDroneListener(this);
}
@Override
public void onTowerDisconnected() {
alertUser("DroneKit-Android Interrupted");
}
// Drone Listener
// ==========================================================
@Override
public void onDroneEvent(String event, Bundle extras) {
switch (event) {
case AttributeEvent.STATE_CONNECTED:
alertUser("Drone Connected");
updateConnectedButton(this.drone.isConnected());
updateArmButton();
checkSoloState();
break;
case AttributeEvent.STATE_DISCONNECTED:
alertUser("Drone Disconnected");
updateConnectedButton(this.drone.isConnected());
updateArmButton();
break;
case AttributeEvent.STATE_UPDATED:
case AttributeEvent.STATE_ARMING:
updateArmButton();
break;
case AttributeEvent.TYPE_UPDATED:
Type newDroneType = this.drone.getAttribute(AttributeType.TYPE);
if (newDroneType.getDroneType() != this.droneType) {
this.droneType = newDroneType.getDroneType();
updateVehicleModesForType(this.droneType);
}
break;
case AttributeEvent.STATE_VEHICLE_MODE:
updateVehicleMode();
break;
case AttributeEvent.SPEED_UPDATED:
updateSpeed();
break;
case AttributeEvent.ALTITUDE_UPDATED:
updateAltitude();
break;
case AttributeEvent.HOME_UPDATED:
updateDistanceFromHome();
break;
default:
// Log.i("DRONE_EVENT", event); //Uncomment to see events from the drone
break;
}
}
private void checkSoloState() {
final SoloState soloState = drone.getAttribute(SoloAttributes.SOLO_STATE);
if (soloState == null){
alertUser("Unable to retrieve the solo state.");
}
else {
alertUser("Solo state is up to date.");
}
}
@Override
public void onDroneServiceInterrupted(String errorMsg) {
}
// UI Events
// ==========================================================
public void onBtnConnectTap(View view) {
if (this.drone.isConnected()) {
this.drone.disconnect();
} else {
Spinner connectionSelector = (Spinner) findViewById(R.id.selectConnectionType);
int selectedConnectionType = connectionSelector.getSelectedItemPosition();
ConnectionParameter connectionParams = selectedConnectionType == ConnectionType.TYPE_USB
? ConnectionParameter.newUsbConnection(null)
: ConnectionParameter.newUdpConnection(null);
this.drone.connect(connectionParams);
}
}
public void onFlightModeSelected(View view) {
VehicleMode vehicleMode = (VehicleMode) this.modeSelector.getSelectedItem();
VehicleApi.getApi(this.drone).setVehicleMode(vehicleMode, new AbstractCommandListener() {
@Override
public void onSuccess() {
alertUser("Vehicle mode change successful.");
}
@Override
public void onError(int executionError) {
alertUser("Vehicle mode change failed: " + executionError);
}
@Override
public void onTimeout() {
alertUser("Vehicle mode change timed out.");
}
});
}
public void onArmButtonTap(View view) {
State vehicleState = this.drone.getAttribute(AttributeType.STATE);
if (vehicleState.isFlying()) {
// Land
VehicleApi.getApi(this.drone).setVehicleMode(VehicleMode.COPTER_LAND, new SimpleCommandListener() {
@Override
public void onError(int executionError) {
alertUser("Unable to land the vehicle.");
}
@Override
public void onTimeout() {
alertUser("Unable to land the vehicle.");
}
});
} else if (vehicleState.isArmed()) {
// Take off
ControlApi.getApi(this.drone).takeoff(10, new AbstractCommandListener() {
@Override
public void onSuccess() {
alertUser("Taking off...");
}
@Override
public void onError(int i) {
alertUser("Unable to take off.");
}
@Override
public void onTimeout() {
alertUser("Unable to take off.");
}
});
} else if (!vehicleState.isConnected()) {
// Connect
alertUser("Connect to a drone first");
} else {
// Connected but not Armed
VehicleApi.getApi(this.drone).arm(true, false, new SimpleCommandListener() {
@Override
public void onError(int executionError) {
alertUser("Unable to arm vehicle.");
}
@Override
public void onTimeout() {
alertUser("Arming operation timed out.");
}
});
}
}
// UI updating
// ==========================================================
protected void updateConnectedButton(Boolean isConnected) {
Button connectButton = (Button) findViewById(R.id.btnConnect);
if (isConnected) {
connectButton.setText("Disconnect");
} else {
connectButton.setText("Connect");
}
}
protected void updateArmButton() {
State vehicleState = this.drone.getAttribute(AttributeType.STATE);
Button armButton = (Button) findViewById(R.id.btnArmTakeOff);
if (!this.drone.isConnected()) {
armButton.setVisibility(View.INVISIBLE);
} else {
armButton.setVisibility(View.VISIBLE);
}
if (vehicleState.isFlying()) {
// Land
armButton.setText("LAND");
} else if (vehicleState.isArmed()) {
// Take off
armButton.setText("TAKE OFF");
} else if (vehicleState.isConnected()) {
// Connected but not Armed
armButton.setText("ARM");
}
}
protected void updateAltitude() {
TextView altitudeTextView = (TextView) findViewById(R.id.altitudeValueTextView);
Altitude droneAltitude = this.drone.getAttribute(AttributeType.ALTITUDE);
altitudeTextView.setText(String.format("%3.1f", droneAltitude.getAltitude()) + "m");
}
protected void updateSpeed() {
TextView speedTextView = (TextView) findViewById(R.id.speedValueTextView);
Speed droneSpeed = this.drone.getAttribute(AttributeType.SPEED);
speedTextView.setText(String.format("%3.1f", droneSpeed.getGroundSpeed()) + "m/s");
}
protected void updateDistanceFromHome() {
TextView distanceTextView = (TextView) findViewById(R.id.distanceValueTextView);
Altitude droneAltitude = this.drone.getAttribute(AttributeType.ALTITUDE);
double vehicleAltitude = droneAltitude.getAltitude();
Gps droneGps = this.drone.getAttribute(AttributeType.GPS);
LatLong vehiclePosition = droneGps.getPosition();
double distanceFromHome = 0;
if (droneGps.isValid()) {
LatLongAlt vehicle3DPosition = new LatLongAlt(vehiclePosition.getLatitude(), vehiclePosition.getLongitude(), vehicleAltitude);
Home droneHome = this.drone.getAttribute(AttributeType.HOME);
distanceFromHome = distanceBetweenPoints(droneHome.getCoordinate(), vehicle3DPosition);
} else {
distanceFromHome = 0;
}
distanceTextView.setText(String.format("%3.1f", distanceFromHome) + "m");
}
protected void updateVehicleModesForType(int droneType) {
List<VehicleMode> vehicleModes = VehicleMode.getVehicleModePerDroneType(droneType);
ArrayAdapter<VehicleMode> vehicleModeArrayAdapter = new ArrayAdapter<VehicleMode>(this, android.R.layout.simple_spinner_item, vehicleModes);
vehicleModeArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
this.modeSelector.setAdapter(vehicleModeArrayAdapter);
}
protected void updateVehicleMode() {
State vehicleState = this.drone.getAttribute(AttributeType.STATE);
VehicleMode vehicleMode = vehicleState.getVehicleMode();
ArrayAdapter arrayAdapter = (ArrayAdapter) this.modeSelector.getAdapter();
this.modeSelector.setSelection(arrayAdapter.getPosition(vehicleMode));
}
// Helper methods
// ==========================================================
protected void alertUser(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
Log.d(TAG, message);
}
private void runOnMainThread(Runnable runnable) {
mainHandler.post(runnable);
}
protected double distanceBetweenPoints(LatLongAlt pointA, LatLongAlt pointB) {
if (pointA == null || pointB == null) {
return 0;
}
double dx = pointA.getLatitude() - pointB.getLatitude();
double dy = pointA.getLongitude() - pointB.getLongitude();
double dz = pointA.getAltitude() - pointB.getAltitude();
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
private void takePhoto() {
SoloCameraApi.getApi(drone).takePhoto(new AbstractCommandListener() {
@Override
public void onSuccess() {
alertUser("Photo taken.");
}
@Override
public void onError(int executionError) {
alertUser("Error while trying to take the photo: " + executionError);
}
@Override
public void onTimeout() {
alertUser("Timeout while trying to take the photo.");
}
});
}
private void toggleVideoRecording() {
SoloCameraApi.getApi(drone).toggleVideoRecording(new AbstractCommandListener() {
@Override
public void onSuccess() {
alertUser("Video recording toggled.");
}
@Override
public void onError(int executionError) {
alertUser("Error while trying to toggle video recording: " + executionError);
}
@Override
public void onTimeout() {
alertUser("Timeout while trying to toggle video recording.");
}
});
}
private void startVideoStream(Surface videoSurface) {
SoloCameraApi.getApi(drone).startVideoStream(videoSurface, videoTag, true, new AbstractCommandListener() {
@Override
public void onSuccess() {
alertUser("Successfully started the video stream. ");
if (stopVideoStream != null)
stopVideoStream.setEnabled(true);
if (startVideoStream != null)
startVideoStream.setEnabled(false);
if (startVideoStreamUsingObserver != null)
startVideoStreamUsingObserver.setEnabled(false);
if (stopVideoStreamUsingObserver != null)
stopVideoStreamUsingObserver.setEnabled(false);
}
@Override
public void onError(int executionError) {
alertUser("Error while starting the video stream: " + executionError);
}
@Override
public void onTimeout() {
alertUser("Timed out while attempting to start the video stream.");
}
});
}
DecoderListener decoderListener = new DecoderListener() {
@Override
public void onDecodingStarted() {
alertUser("MediaCodecManager: video decoding started...");
}
@Override
public void onDecodingError() {
alertUser("MediaCodecManager: video decoding error...");
}
@Override
public void onDecodingEnded() {
alertUser("MediaCodecManager: video decoding ended...");
}
};
private void startVideoStreamForObserver() {
getApi(drone).startVideoStream(videoTag, new ExperimentalApi.IVideoStreamCallback() {
@Override
public void onVideoStreamConnecting() {
runOnMainThread(new Runnable() {
@Override
public void run() {
alertUser("Successfully obtained lock for drone video stream.");
}
});
}
@Override
public void onVideoStreamConnected() {
runOnMainThread(new Runnable() {
@Override
public void run() {
alertUser("Successfully opened drone video connection.");
if (stopVideoStreamUsingObserver != null)
stopVideoStreamUsingObserver.setEnabled(true);
if (startVideoStreamUsingObserver != null)
startVideoStreamUsingObserver.setEnabled(false);
if (stopVideoStream != null)
stopVideoStream.setEnabled(false);
if (startVideoStream != null)
startVideoStream.setEnabled(false);
}
});
mediaCodecManager.stopDecoding(new DecoderListener() {
@Override
public void onDecodingStarted() {
}
@Override
public void onDecodingError() {
}
@Override
public void onDecodingEnded() {
try {
mediaCodecManager.startDecoding(new Surface(videoView.getSurfaceTexture()),
decoderListener);
} catch (IOException | IllegalStateException e) {
Log.e(TAG, "Unable to create media codec.", e);
if (decoderListener != null)
decoderListener.onDecodingError();
}
}
});
}
@Override
public void onVideoStreamDisconnecting() {
runOnMainThread(new Runnable() {
@Override
public void run() {
alertUser("Successfully released lock for drone video stream.");
}
});
}
@Override
public void onVideoStreamDisconnected() {
runOnMainThread(new Runnable() {
@Override
public void run() {
alertUser("Successfully closed drone video connection.");
if (stopVideoStreamUsingObserver != null)
stopVideoStreamUsingObserver.setEnabled(false);
if (startVideoStreamUsingObserver != null)
startVideoStreamUsingObserver.setEnabled(true);
if (stopVideoStream != null)
stopVideoStream.setEnabled(false);
if (startVideoStream != null)
startVideoStream.setEnabled(true);
}
});
mediaCodecManager.stopDecoding(decoderListener);
}
@Override
public void onError(int executionError) {
alertUser("Error while getting lock to vehicle video stream: " + executionError);
}
@Override
public void onTimeout() {
alertUser("Timed out while attempting to get lock for vehicle video stream.");
}
@Override
public void onAsyncVideoStreamPacketReceived(byte[] data, int dataSize) {
mediaCodecManager.onInputDataReceived(data, dataSize);
}
});
}
private void stopVideoStream() {
SoloCameraApi.getApi(drone).stopVideoStream(videoTag, new AbstractCommandListener() {
@Override
public void onSuccess() {
if (stopVideoStream != null)
stopVideoStream.setEnabled(false);
if (startVideoStream != null)
startVideoStream.setEnabled(true);
if (stopVideoStreamUsingObserver != null)
stopVideoStreamUsingObserver.setEnabled(false);
if (startVideoStreamUsingObserver != null)
startVideoStreamUsingObserver.setEnabled(true);
}
@Override
public void onError(int executionError) {
}
@Override
public void onTimeout() {
}
});
}
private void stopVideoStreamForObserver() {
getApi(drone).stopVideoStream(videoTag);
}
@Override
public void onLinkStateUpdated(@NonNull LinkConnectionStatus connectionStatus) {
switch(connectionStatus.getStatusCode()){
case LinkConnectionStatus.FAILED:
Bundle extras = connectionStatus.getExtras();
String msg = null;
if (extras != null) {
msg = extras.getString(LinkConnectionStatus.EXTRA_ERROR_MSG);
}
alertUser("Connection Failed:" + msg);
break;
}
}
}