package org.droidplanner.services.android.impl.api;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import com.o3dr.android.client.R;
import com.o3dr.services.android.lib.drone.connection.ConnectionParameter;
import com.o3dr.services.android.lib.drone.mission.item.complex.CameraDetail;
import com.o3dr.services.android.lib.model.IApiListener;
import com.o3dr.services.android.lib.model.IDroidPlannerServices;
import org.droidplanner.services.android.impl.core.drone.DroneManager;
import org.droidplanner.services.android.impl.core.survey.CameraInfo;
import org.droidplanner.services.android.impl.utils.Utils;
import org.droidplanner.services.android.impl.utils.file.IO.CameraInfoLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import timber.log.Timber;
/**
* DroneKit-Android background service implementation.
*/
public class DroidPlannerService extends Service {
/**
* Status bar notification id
*/
private static final int FOREGROUND_ID = 101;
/**
* Set of actions to notify the local app's components of the service events.
*/
public static final String ACTION_DRONE_CREATED = Utils.PACKAGE_NAME + ".ACTION_DRONE_CREATED";
public static final String ACTION_DRONE_DESTROYED = Utils.PACKAGE_NAME + ".ACTION_DRONE_DESTROYED";
public static final String ACTION_RELEASE_API_INSTANCE = Utils.PACKAGE_NAME + ".action.RELEASE_API_INSTANCE";
public static final String EXTRA_API_INSTANCE_APP_ID = "extra_api_instance_app_id";
/**
* Used to broadcast service events.
*/
private LocalBroadcastManager lbm;
/**
* Stores drone api instances per connected client. The client are denoted by their app id.
*/
final ConcurrentHashMap<String, DroneApi> droneApiStore = new ConcurrentHashMap<>();
/**
* Caches drone managers per connection type.
*/
final ConcurrentHashMap<ConnectionParameter, DroneManager> droneManagers = new ConcurrentHashMap<>();
private DPServices dpServices;
private CameraInfoLoader cameraInfoLoader;
private List<CameraDetail> cachedCameraDetails;
/**
* Generate a drone api instance for the client denoted by the given app id.
*
* @param listener Used to retrieve api information.
* @param appId Application id of the connecting client.
* @return a IDroneApi instance
*/
DroneApi registerDroneApi(IApiListener listener, String appId) {
if (listener == null)
return null;
DroneApi droneApi = new DroneApi(this, listener, appId);
droneApiStore.put(appId, droneApi);
lbm.sendBroadcast(new Intent(ACTION_DRONE_CREATED));
updateForegroundNotification();
return droneApi;
}
/**
* Release the drone api instance attached to the given app id.
*
* @param appId Application id of the disconnecting client.
*/
void releaseDroneApi(String appId) {
if (appId == null)
return;
DroneApi droneApi = droneApiStore.remove(appId);
if (droneApi != null) {
Timber.d("Releasing drone api instance for " + appId);
droneApi.destroy();
lbm.sendBroadcast(new Intent(ACTION_DRONE_DESTROYED));
updateForegroundNotification();
}
}
/**
* Establish a connection with a vehicle using the given connection parameter.
*
* @param connParams Parameters used to connect to the vehicle.
* @param appId Application id of the connecting client.
* @param listener Callback to receive drone events.
* @return A DroneManager instance which acts as router between the connected vehicle and the listeneing client(s).
*/
DroneManager connectDroneManager(ConnectionParameter connParams, String appId, DroneApi listener) {
if (connParams == null || TextUtils.isEmpty(appId) || listener == null)
return null;
DroneManager droneMgr = droneManagers.get(connParams);
if (droneMgr == null) {
final DroneManager temp = DroneManager.generateDroneManager(getApplicationContext(), connParams, new Handler(Looper.getMainLooper()));
droneMgr = droneManagers.putIfAbsent(connParams, temp);
if(droneMgr == null){
Timber.d("Generating new drone manager.");
droneMgr = temp;
}
else{
temp.destroy();
}
}
Timber.d("Drone manager connection for " + appId);
droneMgr.connect(appId, listener, connParams);
return droneMgr;
}
/**
* Disconnect the given client from the vehicle managed by the given drone manager.
*
* @param droneMgr Handler for the connected vehicle.
* @param clientInfo Info of the disconnecting client.
*/
void disconnectDroneManager(DroneManager droneMgr, DroneApi.ClientInfo clientInfo) {
if (droneMgr == null || clientInfo == null || TextUtils.isEmpty(clientInfo.appId))
return;
String appId = clientInfo.appId;
Timber.d("Drone manager disconnection for " + appId);
droneMgr.disconnect(clientInfo);
if (droneMgr.getConnectedAppsCount() == 0) {
Timber.d("Destroying drone manager.");
droneMgr.destroy();
droneManagers.remove(droneMgr.getConnectionParameter());
}
}
/**
* Retrieves the set of camera info provided by the app.
*
* @return a list of {@link CameraDetail} objects.
*/
synchronized List<CameraDetail> getCameraDetails() {
if (cachedCameraDetails == null) {
List<String> cameraInfoNames = cameraInfoLoader.getCameraInfoList();
List<CameraInfo> cameraInfos = new ArrayList<>(cameraInfoNames.size());
for (String infoName : cameraInfoNames) {
try {
cameraInfos.add(cameraInfoLoader.openFile(infoName));
} catch (Exception e) {
Timber.e(e, e.getMessage());
}
}
List<CameraDetail> cameraDetails = new ArrayList<>(cameraInfos.size());
for (CameraInfo camInfo : cameraInfos) {
cameraDetails.add(new CameraDetail(camInfo.name, camInfo.sensorWidth,
camInfo.sensorHeight, camInfo.sensorResolution, camInfo.focalLength,
camInfo.overlap, camInfo.sidelap, camInfo.isInLandscapeOrientation));
}
cachedCameraDetails = cameraDetails;
}
return cachedCameraDetails;
}
@Override
public IBinder onBind(Intent intent) {
Timber.d("Binding intent: " + intent);
final String action = intent.getAction();
if (IDroidPlannerServices.class.getName().equals(action)) {
// Return binder to ipc client-server interaction.
return dpServices;
} else {
return null;
}
}
@SuppressLint("NewApi")
@Override
public void onCreate() {
super.onCreate();
Timber.d("Creating DroneKit-Android.");
final Context context = getApplicationContext();
dpServices = new DPServices(this);
lbm = LocalBroadcastManager.getInstance(context);
this.cameraInfoLoader = new CameraInfoLoader(context);
updateForegroundNotification();
}
@SuppressLint("NewApi")
private void updateForegroundNotification() {
final Context context = getApplicationContext();
//Put the service in the foreground
final NotificationCompat.Builder notifBuilder = new NotificationCompat.Builder(context)
.setContentTitle("DroneKit-Android")
.setPriority(NotificationCompat.PRIORITY_MIN)
.setSmallIcon(R.drawable.ic_stat_notify);
final int connectedCount = droneApiStore.size();
if (connectedCount > 1) {
notifBuilder.setContentText(connectedCount + " connected apps");
}
final Notification notification = notifBuilder.build();
startForeground(FOREGROUND_ID, notification);
}
@Override
public void onDestroy() {
super.onDestroy();
Timber.d("Destroying DroneKit-Android.");
for (DroneApi droneApi : droneApiStore.values()) {
droneApi.destroy();
}
droneApiStore.clear();
for (DroneManager droneMgr : droneManagers.values()) {
droneMgr.destroy();
}
droneManagers.clear();
dpServices.destroy();
stopForeground(true);
//Disable this service. It'll be reenabled the next time its local client needs it.
enableDroidPlannerService(getApplicationContext(), false);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
final String action = intent.getAction();
switch (action) {
case ACTION_RELEASE_API_INSTANCE:
final String appId = intent.getStringExtra(EXTRA_API_INSTANCE_APP_ID);
releaseDroneApi(appId);
break;
}
}
stopSelf();
return START_NOT_STICKY;
}
/**
* Toggles the DroidPlannerService component
* @param context
* @param enable
*/
public static void enableDroidPlannerService(Context context, boolean enable){
final ComponentName serviceComp = new ComponentName(context, DroidPlannerService.class);
final int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
context.getPackageManager().setComponentEnabledSetting(serviceComp, newState, PackageManager.DONT_KILL_APP);
}
}