package org.droidplanner.android.notifications; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.o3dr.android.client.Drone; 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.property.Battery; 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.Signal; import com.o3dr.services.android.lib.drone.property.State; import com.o3dr.services.android.lib.drone.property.VehicleMode; import com.o3dr.services.android.lib.util.MathUtils; import com.o3dr.services.android.lib.util.SpannableUtils; import org.beyene.sius.unit.length.LengthUnit; import org.droidplanner.android.DroidPlannerApp; import org.droidplanner.android.R; import org.droidplanner.android.activities.FlightActivity; import org.droidplanner.android.utils.prefs.DroidPlannerPrefs; import org.droidplanner.android.utils.unit.UnitManager; /** * Implements DroidPlanner's status bar notifications. */ public class StatusBarNotificationProvider implements NotificationHandler.NotificationProvider { private static final String TAG = StatusBarNotificationProvider.class.getSimpleName(); /** * Android status bar's notification id. */ private static final int NOTIFICATION_ID = 1; /** * This is the period for the flight time update. */ protected final static long FLIGHT_TIMER_PERIOD = 1000l; // 1 second private final Runnable removeNotification = new Runnable() { @Override public void run() { NotificationManagerCompat.from(mContext).cancelAll(); } }; private final Handler mHandler = new Handler(); /** * Application context. */ private final Context mContext; /** * Builder for the app notification. */ private NotificationCompat.Builder mNotificationBuilder; /** * Pending intent for the notification on click behavior. Opens the * FlightActivity screen. */ private final PendingIntent mNotificationIntent; /** * Pending intent for the notification connect/disconnect action. */ private final PendingIntent mToggleConnectionIntent; /** * Uses to generate the inbox style use to populate the notification. */ private InboxStyleBuilder mInboxBuilder; /** * Handle to the app preferences. */ private final DroidPlannerPrefs mAppPrefs; private final Drone drone; StatusBarNotificationProvider(Context context, Drone api) { mContext = context; this.drone = api; mAppPrefs = DroidPlannerPrefs.getInstance(context); mNotificationIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, FlightActivity.class), 0); mToggleConnectionIntent = PendingIntent .getBroadcast(mContext, 0, new Intent(DroidPlannerApp.ACTION_TOGGLE_DRONE_CONNECTION), 0); } @Override public void init(){ mHandler.removeCallbacks(removeNotification); final String summaryText = mContext.getString(R.string.connected); mInboxBuilder = new InboxStyleBuilder().setSummary(summaryText); mNotificationBuilder = new NotificationCompat.Builder(mContext) .addAction(R.drawable.ic_action_io, mContext.getText(R.string.menu_disconnect), mToggleConnectionIntent) .setContentIntent(mNotificationIntent) .setContentText(summaryText) .setOngoing(false) .setSmallIcon(R.drawable.ic_stat_notify) .setColor(mContext.getResources().getColor(R.color.stat_notify_connected)); updateFlightMode(drone); updateDroneState(drone); updateBattery(drone); updateGps(drone); updateHome(drone); updateRadio(drone); showNotification(); LocalBroadcastManager.getInstance(mContext).registerReceiver(eventReceiver, eventFilter); } /** * Dismiss the app status bar notification. */ @Override public void onTerminate() { LocalBroadcastManager.getInstance(mContext).unregisterReceiver(eventReceiver); mInboxBuilder = null; if (mNotificationBuilder != null) { mNotificationBuilder = new NotificationCompat.Builder(mContext) .addAction(R.drawable.ic_action_io, mContext.getText(R.string.menu_connect), mToggleConnectionIntent) .setContentIntent(mNotificationIntent) .setContentTitle(mContext.getString(R.string.disconnected)) .setOngoing(false).setContentText("") .setSmallIcon(R.drawable.ic_stat_notify); } showNotification(); mHandler.postDelayed(removeNotification, 2000L); } private static final IntentFilter eventFilter = new IntentFilter(); static { eventFilter.addAction(AttributeEvent.BATTERY_UPDATED); eventFilter.addAction(AttributeEvent.GPS_POSITION); eventFilter.addAction(AttributeEvent.GPS_FIX); eventFilter.addAction(AttributeEvent.GPS_COUNT); eventFilter.addAction(AttributeEvent.HOME_UPDATED); eventFilter.addAction(AttributeEvent.SIGNAL_UPDATED); eventFilter.addAction(AttributeEvent.STATE_UPDATED); eventFilter.addAction(AttributeEvent.STATE_VEHICLE_MODE); eventFilter.addAction(AttributeEvent.TYPE_UPDATED); } private final BroadcastReceiver eventReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { boolean showNotification = true; final String action = intent.getAction(); switch (action) { case AttributeEvent.GPS_POSITION: updateHome(drone); break; case AttributeEvent.GPS_FIX: case AttributeEvent.GPS_COUNT: updateGps(drone); break; case AttributeEvent.BATTERY_UPDATED: updateBattery(drone); break; case AttributeEvent.HOME_UPDATED: updateHome(drone); break; case AttributeEvent.SIGNAL_UPDATED: updateRadio(drone); break; case AttributeEvent.STATE_UPDATED: updateDroneState(drone); break; case AttributeEvent.STATE_VEHICLE_MODE: case AttributeEvent.TYPE_UPDATED: updateFlightMode(drone); break; default: showNotification = false; break; } if (showNotification) { showNotification(); } } }; /** * Runnable used to update the drone flight time. */ protected final Runnable mFlightTimeUpdater = new Runnable() { @Override public void run() { mHandler.removeCallbacks(this); if (drone == null || !drone.isConnected()) return; if (mInboxBuilder != null) { long timeInSeconds = drone.getFlightTime(); long minutes = timeInSeconds / 60; long seconds = timeInSeconds % 60; mInboxBuilder.setLine(2, SpannableUtils.normal("Air Time: ", SpannableUtils.bold(String.format("%02d:%02d", minutes, seconds)))); } mHandler.postDelayed(this, FLIGHT_TIMER_PERIOD); } }; private void updateRadio(Drone drone) { if (mInboxBuilder == null) return; Signal droneSignal = drone.getAttribute(AttributeType.SIGNAL); String update = droneSignal == null ? "--" : String.format("%d%%", MathUtils.getSignalStrength(droneSignal .getFadeMargin(), droneSignal.getRemFadeMargin())); mInboxBuilder.setLine(4, SpannableUtils.normal("Signal: ", SpannableUtils.bold(update))); } private void updateHome(Drone drone) { if (mInboxBuilder == null) return; String update = "--"; final Gps droneGps = this.drone.getAttribute(AttributeType.GPS); final Home droneHome = this.drone.getAttribute(AttributeType.HOME); if (droneGps != null && droneGps.isValid() && droneHome != null && droneHome.isValid()) { LengthUnit distanceToHome = UnitManager.getUnitSystem(mContext).getLengthUnitProvider() .boxBaseValueToTarget(MathUtils.getDistance2D(droneHome.getCoordinate(), droneGps.getPosition())); update = String.format("Home\n%s", distanceToHome); } mInboxBuilder.setLine(0, SpannableUtils.normal("Home: ", update)); } private void updateGps(Drone drone) { if (mInboxBuilder == null) return; Gps droneGps = drone.getAttribute(AttributeType.GPS); String update = droneGps == null ? "--" : String.format( "%d, %s", droneGps.getSatellitesCount(), droneGps.getFixType()); mInboxBuilder.setLine(1, SpannableUtils.normal("Satellite: ", SpannableUtils.bold(update))); } private void updateBattery(Drone drone) { if (mInboxBuilder == null) return; Battery droneBattery = drone.getAttribute(AttributeType.BATTERY); String update = droneBattery == null ? "--" : String.format( "%2.1fv (%2.0f%%)", droneBattery.getBatteryVoltage(), droneBattery.getBatteryRemain()); mInboxBuilder.setLine(3, SpannableUtils.normal("Battery: ", SpannableUtils.bold(update))); } private void updateDroneState(Drone drone) { if (mInboxBuilder == null) return; mHandler.removeCallbacks(mFlightTimeUpdater); if (drone != null && drone.isConnected()) { mFlightTimeUpdater.run(); } } private void updateFlightMode(Drone drone) { if (mNotificationBuilder == null) return; State droneState = drone.getAttribute(AttributeType.STATE); VehicleMode mode = droneState == null ? null : droneState.getVehicleMode(); String update = mode == null ? "--" : mode.getLabel(); final CharSequence modeSummary = SpannableUtils.normal("Flight Mode: ", SpannableUtils.bold(update)); mNotificationBuilder.setContentTitle(modeSummary); } /** * Build a notification from the notification builder, and display it. */ private void showNotification() { if (mNotificationBuilder == null) { return; } if (mInboxBuilder != null) { mNotificationBuilder.setStyle(mInboxBuilder.generateInboxStyle()); } NotificationManagerCompat.from(mContext).notify(NOTIFICATION_ID, mNotificationBuilder.build()); } private static class InboxStyleBuilder { private static final int MAX_LINES_COUNT = 5; private final CharSequence[] mLines = new CharSequence[MAX_LINES_COUNT]; private CharSequence mSummary; private boolean mHasContent = false; public void setLine(int index, CharSequence content) { if (index >= mLines.length || index < 0) { Log.w(TAG, "Invalid index (" + index + ") for inbox content."); return; } mLines[index] = content; mHasContent = true; } public InboxStyleBuilder setSummary(CharSequence summary) { mSummary = summary; mHasContent = true; return this; } public NotificationCompat.InboxStyle generateInboxStyle() { if (!mHasContent) { return null; } NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); if (mSummary != null) { inboxStyle.setSummaryText(mSummary); } for (CharSequence line : mLines) { if (line != null) { inboxStyle.addLine(line); } } return inboxStyle; } } }