From ab67881d37f80b87a68f842c511bf5d82c5579cf Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Wed, 29 Apr 2015 14:20:31 -0700 Subject: [PATCH 01/11] Fix fragment commit crash. --- .../android/fragments/control/FlightControlManagerFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/src/org/droidplanner/android/fragments/control/FlightControlManagerFragment.java b/Android/src/org/droidplanner/android/fragments/control/FlightControlManagerFragment.java index 6f4775f78e..968bfac8a4 100644 --- a/Android/src/org/droidplanner/android/fragments/control/FlightControlManagerFragment.java +++ b/Android/src/org/droidplanner/android/fragments/control/FlightControlManagerFragment.java @@ -94,7 +94,7 @@ private void selectActionsBar(int droneType) { break; } - fm.beginTransaction().replace(R.id.flight_actions_bar, actionsBarFragment).commit(); + fm.beginTransaction().replace(R.id.flight_actions_bar, actionsBarFragment).commitAllowingStateLoss(); header = (SlidingUpHeader) actionsBarFragment; } From 7ccc82fd2226c71300ad69ea42f8d1da3715a4e2 Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Sun, 3 May 2015 22:00:30 -0700 Subject: [PATCH 02/11] client library update and code cleanup. --- Android/build.gradle | 2 +- .../droidplanner/android/DroidPlannerApp.java | 2 +- .../android/activities/EditorActivity.java | 4 +- .../android/dialogs/EditInputDialog.java | 2 +- .../android/dialogs/YesNoDialog.java | 5 +- .../android/dialogs/YesNoWithPrefsDialog.java | 2 +- .../android/fragments/ParamsFragment.java | 8 +- .../fragments/SensorSetupFragment.java | 7 - .../calibration/mag/FragmentSetupMAG.java | 336 ------------------ 9 files changed, 12 insertions(+), 356 deletions(-) delete mode 100644 Android/src/org/droidplanner/android/fragments/calibration/mag/FragmentSetupMAG.java diff --git a/Android/build.gradle b/Android/build.gradle index b9a1cbf607..eb52c5a86d 100644 --- a/Android/build.gradle +++ b/Android/build.gradle @@ -12,7 +12,7 @@ dependencies { compile 'com.android.support:cardview-v7:22.1.0' compile 'com.android.support:recyclerview-v7:22.1.0' - compile 'com.o3dr.android:dronekit-android:2.3.11' + compile 'com.o3dr.android:dronekit-android:2.3.19' compile 'me.grantland:autofittextview:0.2.1' compile(name:'shimmer-android-release', ext:'aar') diff --git a/Android/src/org/droidplanner/android/DroidPlannerApp.java b/Android/src/org/droidplanner/android/DroidPlannerApp.java index 827b1b2439..4029d952f8 100644 --- a/Android/src/org/droidplanner/android/DroidPlannerApp.java +++ b/Android/src/org/droidplanner/android/DroidPlannerApp.java @@ -137,7 +137,7 @@ public void onCreate() { lbm = LocalBroadcastManager.getInstance(context); controlTower = new ControlTower(context); - drone = new Drone(); + drone = new Drone(context); missionProxy = new MissionProxy(context, this.drone); final Thread.UncaughtExceptionHandler dpExceptionHandler = new Thread.UncaughtExceptionHandler() { diff --git a/Android/src/org/droidplanner/android/activities/EditorActivity.java b/Android/src/org/droidplanner/android/activities/EditorActivity.java index 58f91fefea..6ba5bb5888 100644 --- a/Android/src/org/droidplanner/android/activities/EditorActivity.java +++ b/Android/src/org/droidplanner/android/activities/EditorActivity.java @@ -69,7 +69,7 @@ public class EditorActivity extends DrawerNavigationUI implements OnPathFinished static { eventFilter.addAction(MissionProxy.ACTION_MISSION_PROXY_UPDATE); eventFilter.addAction(AttributeEvent.MISSION_RECEIVED); - eventFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_ENDED); + eventFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_COMPLETED); } private final BroadcastReceiver eventReceiver = new BroadcastReceiver() { @@ -77,7 +77,7 @@ public class EditorActivity extends DrawerNavigationUI implements OnPathFinished public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); switch (action) { - case AttributeEvent.PARAMETERS_REFRESH_ENDED: + case AttributeEvent.PARAMETERS_REFRESH_COMPLETED: case MissionProxy.ACTION_MISSION_PROXY_UPDATE: updateMissionLength(); break; diff --git a/Android/src/org/droidplanner/android/dialogs/EditInputDialog.java b/Android/src/org/droidplanner/android/dialogs/EditInputDialog.java index 66fd9cd76e..5e991962d4 100644 --- a/Android/src/org/droidplanner/android/dialogs/EditInputDialog.java +++ b/Android/src/org/droidplanner/android/dialogs/EditInputDialog.java @@ -1,9 +1,9 @@ package org.droidplanner.android.dialogs; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; +import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.View; import android.widget.EditText; diff --git a/Android/src/org/droidplanner/android/dialogs/YesNoDialog.java b/Android/src/org/droidplanner/android/dialogs/YesNoDialog.java index 9e1e7aa258..0bdbc19281 100644 --- a/Android/src/org/droidplanner/android/dialogs/YesNoDialog.java +++ b/Android/src/org/droidplanner/android/dialogs/YesNoDialog.java @@ -1,11 +1,11 @@ package org.droidplanner.android.dialogs; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; import android.view.View; import android.widget.TextView; @@ -69,8 +69,7 @@ public void onClick(DialogInterface dialog, int which) { } protected View generateContentView(Bundle savedInstanceState){ - final View contentView = getActivity().getLayoutInflater().inflate(R.layout - .dialog_yes_no_content, null); + final View contentView = getActivity().getLayoutInflater().inflate(R.layout.dialog_yes_no_content, null); if(contentView == null){ return contentView; diff --git a/Android/src/org/droidplanner/android/dialogs/YesNoWithPrefsDialog.java b/Android/src/org/droidplanner/android/dialogs/YesNoWithPrefsDialog.java index 4c2c9c888f..8d5caaf059 100644 --- a/Android/src/org/droidplanner/android/dialogs/YesNoWithPrefsDialog.java +++ b/Android/src/org/droidplanner/android/dialogs/YesNoWithPrefsDialog.java @@ -1,11 +1,11 @@ package org.droidplanner.android.dialogs; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; +import android.support.v7.app.AlertDialog; import android.view.View; import android.widget.CheckBox; import android.widget.Toast; diff --git a/Android/src/org/droidplanner/android/fragments/ParamsFragment.java b/Android/src/org/droidplanner/android/fragments/ParamsFragment.java index 18968ca4a4..6d53de8c27 100644 --- a/Android/src/org/droidplanner/android/fragments/ParamsFragment.java +++ b/Android/src/org/droidplanner/android/fragments/ParamsFragment.java @@ -63,8 +63,8 @@ public class ParamsFragment extends ApiListenerListFragment { static { intentFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_STARTED); - intentFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_ENDED); - intentFilter.addAction(AttributeEvent.PARAMETERS_RECEIVED); + intentFilter.addAction(AttributeEvent.PARAMETERS_REFRESH_COMPLETED); + intentFilter.addAction(AttributeEvent.PARAMETER_RECEIVED); intentFilter.addAction(AttributeEvent.STATE_CONNECTED); intentFilter.addAction(AttributeEvent.TYPE_UPDATED); } @@ -78,7 +78,7 @@ public void onReceive(Context context, Intent intent) { startProgress(); break; - case AttributeEvent.PARAMETERS_REFRESH_ENDED: + case AttributeEvent.PARAMETERS_REFRESH_COMPLETED: stopProgress(); /*** FALL - THROUGH ***/ case AttributeEvent.STATE_CONNECTED: @@ -90,7 +90,7 @@ public void onReceive(Context context, Intent intent) { } break; - case AttributeEvent.PARAMETERS_RECEIVED: + case AttributeEvent.PARAMETER_RECEIVED: final int defaultValue = -1; int index = intent.getIntExtra(AttributeEventExtra.EXTRA_PARAMETER_INDEX, defaultValue); int count = intent.getIntExtra(AttributeEventExtra.EXTRA_PARAMETERS_COUNT, defaultValue); diff --git a/Android/src/org/droidplanner/android/fragments/SensorSetupFragment.java b/Android/src/org/droidplanner/android/fragments/SensorSetupFragment.java index 365d6a091f..4da36266e8 100644 --- a/Android/src/org/droidplanner/android/fragments/SensorSetupFragment.java +++ b/Android/src/org/droidplanner/android/fragments/SensorSetupFragment.java @@ -12,7 +12,6 @@ import org.droidplanner.android.R; import org.droidplanner.android.fragments.calibration.imu.FragmentSetupIMU; -import org.droidplanner.android.fragments.calibration.mag.FragmentSetupMAG; import org.droidplanner.android.widgets.viewPager.TabPageIndicator; /** @@ -54,15 +53,11 @@ public Fragment getItem(int i) { case 0: default: return new FragmentSetupIMU(); - case 1: - return new FragmentSetupMAG(); } } @Override public int getCount() { - //Enable mag calibration when it's fully working. -// return 2; return 1; } @@ -72,8 +67,6 @@ public CharSequence getPageTitle(int position) { case 0: default: return FragmentSetupIMU.getTitle(context); - case 1: - return FragmentSetupMAG.getTitle(context); } } } diff --git a/Android/src/org/droidplanner/android/fragments/calibration/mag/FragmentSetupMAG.java b/Android/src/org/droidplanner/android/fragments/calibration/mag/FragmentSetupMAG.java deleted file mode 100644 index 6c58f76c77..0000000000 --- a/Android/src/org/droidplanner/android/fragments/calibration/mag/FragmentSetupMAG.java +++ /dev/null @@ -1,336 +0,0 @@ -package org.droidplanner.android.fragments.calibration.mag; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import com.o3dr.android.client.Drone; -import com.o3dr.services.android.lib.drone.attribute.AttributeEvent; -import com.o3dr.services.android.lib.drone.attribute.AttributeEventExtra; -import com.o3dr.services.android.lib.drone.attribute.AttributeType; -import com.o3dr.services.android.lib.drone.property.State; - -import org.droidplanner.android.R; -import org.droidplanner.android.fragments.helpers.ApiListenerFragment; -import org.droidplanner.android.utils.Point3D; -import org.droidplanner.android.widgets.scatterplot.ScatterPlot; - -import java.util.Arrays; - -public class FragmentSetupMAG extends ApiListenerFragment { - - private static final int MIN_POINTS_COUNT = 250; - - private static final int CALIBRATION_IDLE = 0; - private static final int CALIBRATION_IN_PROGRESS = 1; - private static final int CALIBRATION_COMPLETED = 2; - - private static final String EXTRA_CALIBRATION_STATUS = "extra_calibration_status"; - private static final String EXTRA_CALIBRATION_POINTS = "extra_calibration_points"; - - private static final IntentFilter intentFilter = new IntentFilter(); - static { - intentFilter.addAction(AttributeEvent.STATE_CONNECTED); - intentFilter.addAction(AttributeEvent.STATE_DISCONNECTED); - intentFilter.addAction(AttributeEvent.CALIBRATION_MAG_STARTED); - intentFilter.addAction(AttributeEvent.CALIBRATION_MAG_ESTIMATION); - intentFilter.addAction(AttributeEvent.CALIBRATION_MAG_COMPLETED); - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - switch (action) { - case AttributeEvent.STATE_CONNECTED: - buttonStep.setEnabled(true); - break; - case AttributeEvent.STATE_DISCONNECTED: - cancelCalibration(); - buttonStep.setEnabled(false); - break; - case AttributeEvent.CALIBRATION_MAG_STARTED: { - double[] pointsX = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_X); - double[] pointsY = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_Y); - double[] pointsZ = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_Z); - - inProgressPoints = Point3D.fromDoubleArrays(pointsX, pointsY, pointsZ); - - setCalibrationStatus(CALIBRATION_IN_PROGRESS); - break; - } - case AttributeEvent.CALIBRATION_MAG_ESTIMATION: { - double[] pointsX = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_X); - double[] pointsY = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_Y); - double[] pointsZ = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_POINTS_Z); - - inProgressPoints = Point3D.fromDoubleArrays(pointsX, pointsY, pointsZ); - - final int pointsCount = inProgressPoints == null ? 0 : inProgressPoints.length; - if (pointsCount == 0) { - return; - } - - final double fitness = intent.getDoubleExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_FITNESS, - 0); - final double[] fitCenter = intent.getDoubleArrayExtra(AttributeEventExtra - .EXTRA_CALIBRATION_MAG_FIT_CENTER); - final double[] fitRadii = intent.getDoubleArrayExtra(AttributeEventExtra - .EXTRA_CALIBRATION_MAG_FIT_RADII); - - if (pointsCount < MIN_POINTS_COUNT) { - calibrationFitness.setIndeterminate(true); - calibrationProgress.setText("0 / 100"); - } else { - final int progress = (int) (fitness * 100); - calibrationFitness.setIndeterminate(false); - calibrationFitness.setMax(100); - calibrationFitness.setProgress(progress); - - calibrationProgress.setText(progress + " / 100"); - } - - // Grab the last point - final Point3D point = inProgressPoints[pointsCount - 1]; - - plot1.addData((float) point.x); - plot1.addData((float) point.z); - if (fitCenter == null || fitRadii == null) { - plot1.updateSphere(null); - } else { - plot1.updateSphere(new int[]{(int) fitCenter[0], (int) fitCenter[2], - (int) fitRadii[0], (int) fitRadii[2]}); - } - plot1.invalidate(); - - plot2.addData((float) point.y); - plot2.addData((float) point.z); - if (fitCenter == null || fitRadii == null) { - plot2.updateSphere(null); - } else { - plot2.updateSphere(new int[]{(int) fitCenter[1], (int) fitCenter[2], - (int) fitRadii[1], (int) fitRadii[2]}); - } - plot2.invalidate(); - - break; - } - case AttributeEvent.CALIBRATION_MAG_COMPLETED: - double[] offsets = intent.getDoubleArrayExtra(AttributeEventExtra.EXTRA_CALIBRATION_MAG_OFFSETS); - if (offsets != null) { - String offsetsSummary = Arrays.toString(offsets); - Log.d("MAG", "Calibration Finished: " + offsetsSummary); - Toast.makeText(getActivity(), "Calibration Finished: " + offsetsSummary, - Toast.LENGTH_LONG).show(); - } - - setCalibrationStatus(CALIBRATION_COMPLETED); - break; - } - } - }; - - private View inProgressCalibrationView; - private Button buttonStep; - private TextView calibrationProgress; - private ProgressBar calibrationFitness; - private ScatterPlot plot1, plot2; - - private int calibrationStatus = CALIBRATION_IDLE; - - private Point3D[] startPoints; - private Point3D[] inProgressPoints; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_setup_mag_main, container, false); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - plot1 = (ScatterPlot) view.findViewById(R.id.scatterPlot1); - plot1.setTitle("XZ"); - - plot2 = (ScatterPlot) view.findViewById(R.id.scatterPlot2); - plot2.setTitle("YZ"); - - inProgressCalibrationView = view.findViewById(R.id.in_progress_calibration_container); - - calibrationProgress = (TextView) view.findViewById(R.id.calibration_progress); - - buttonStep = (Button) view.findViewById(R.id.buttonStep); - buttonStep.setEnabled(false); - buttonStep.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (calibrationStatus == CALIBRATION_COMPLETED) { - // Clear the screen. - clearScreen(); - setCalibrationStatus(CALIBRATION_IDLE); - } else { - startCalibration(); - } - } - }); - - Button buttonCancel = (Button) view.findViewById(R.id.buttonCancel); - buttonCancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - cancelCalibration(); - } - }); - - calibrationFitness = (ProgressBar) view.findViewById(R.id.calibration_progress_bar); - - if (savedInstanceState != null) { - final int calibrationStatus = savedInstanceState.getInt(EXTRA_CALIBRATION_STATUS, - CALIBRATION_IDLE); - setCalibrationStatus(calibrationStatus); - - if (calibrationStatus == CALIBRATION_IN_PROGRESS) { - final Point3D[] loadedPoints = (Point3D[]) savedInstanceState - .getParcelableArray(EXTRA_CALIBRATION_POINTS); - if (loadedPoints != null && loadedPoints.length > 0) { - startPoints = loadedPoints; - - for (Point3D point : loadedPoints) { - final double x = point.x; - final double y = point.y; - final double z = point.z; - - plot1.addData((float) x); - plot1.addData((float) z); - - plot2.addData((float) y); - plot2.addData((float) z); - } - - plot1.invalidate(); - plot2.invalidate(); - } - } - } - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putInt(EXTRA_CALIBRATION_STATUS, calibrationStatus); - - if (getDrone().isConnected() && inProgressPoints != null && inProgressPoints.length > 0){ - outState.putParcelableArray(EXTRA_CALIBRATION_POINTS, inProgressPoints); - } - } - - private void pauseCalibration() { - if (getDrone().isConnected()) { - getDrone().stopMagnetometerCalibration(); - } - } - - private void cancelCalibration() { - if (getDrone().isConnected()) { - getDrone().stopMagnetometerCalibration(); - if (calibrationStatus == CALIBRATION_IN_PROGRESS) { - setCalibrationStatus(CALIBRATION_IDLE); - } - } - clearScreen(); - } - - private void clearScreen() { - plot1.reset(); - plot2.reset(); - } - - private void setCalibrationStatus(int status) { - if (calibrationStatus == status) { - return; - } - - calibrationStatus = status; - switch (calibrationStatus) { - case CALIBRATION_IN_PROGRESS: - // Hide the 'start' button - buttonStep.setVisibility(View.GONE); - - // Show the 'in progress view' - inProgressCalibrationView.setVisibility(View.VISIBLE); - calibrationFitness.setIndeterminate(true); - calibrationProgress.setText("0 / 100"); - break; - - case CALIBRATION_COMPLETED: - calibrationFitness.setIndeterminate(false); - calibrationFitness.setMax(100); - calibrationFitness.setProgress(100); - - // Hide the 'in progress view' - inProgressCalibrationView.setVisibility(View.GONE); - - // Show the 'calibrate/done' button - buttonStep.setVisibility(View.VISIBLE); - buttonStep.setText(R.string.button_setup_done); - break; - - default: - // Hide the 'in progress view' - inProgressCalibrationView.setVisibility(View.GONE); - - // Show the 'calibrate/done' button - buttonStep.setVisibility(View.VISIBLE); - buttonStep.setText(R.string.button_setup_calibrate); - break; - } - } - - public void startCalibration() { - Drone dpApi = getDrone(); - if (dpApi.isConnected()) { - double[][] result = Point3D.fromPoint3Ds(startPoints); - dpApi.startMagnetometerCalibration(result[0], result[1], result[2]); - startPoints = null; - } - } - - public static CharSequence getTitle(Context context) { - return context.getText(R.string.setup_mag_title); - } - - @Override - public void onApiConnected() { - Drone drone = getDrone(); - State droneState = drone.getAttribute(AttributeType.STATE); - if (droneState.isConnected() && !droneState.isFlying()) { - buttonStep.setEnabled(true); - } else { - cancelCalibration(); - buttonStep.setEnabled(false); - } - - getBroadcastManager().registerReceiver(broadcastReceiver, intentFilter); - if (calibrationStatus == CALIBRATION_IN_PROGRESS) { - startCalibration(); - } - } - - @Override - public void onApiDisconnected() { - getBroadcastManager().unregisterReceiver(broadcastReceiver); - pauseCalibration(); - } -} From c0e9df1974ce9dd84ac67b0f8a2f56e6474a2b07 Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Thu, 14 May 2015 17:59:33 -0700 Subject: [PATCH 03/11] updated tts language selection logic. --- .../TTSNotificationProvider.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java b/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java index 39d0ecb660..ae97bb1eb0 100644 --- a/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java +++ b/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java @@ -1,5 +1,6 @@ package org.droidplanner.android.notifications; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -31,8 +32,11 @@ import org.droidplanner.android.fragments.SettingsFragment; import org.droidplanner.android.utils.prefs.DroidPlannerPrefs; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -292,19 +296,37 @@ private void scheduleWatchdog() { } } + @SuppressLint("NewApi") @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { // TODO: check if the language is available Locale ttsLanguage; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + final int sdkVersion = Build.VERSION.SDK_INT; + if(sdkVersion >= Build.VERSION_CODES.LOLLIPOP){ + ttsLanguage = tts.getDefaultVoice().getLocale(); + } + else if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { ttsLanguage = tts.getDefaultLanguage(); } else { ttsLanguage = tts.getLanguage(); } - if (ttsLanguage == null) { - ttsLanguage = Locale.US; + if (ttsLanguage == null || tts.isLanguageAvailable(ttsLanguage) == TextToSpeech.LANG_NOT_SUPPORTED) { + final List availableLanguages = new ArrayList<>(tts.getAvailableLanguages()); + + if(!availableLanguages.isEmpty()) { + //Pick the first available language. + ttsLanguage = availableLanguages.get(0); + } + else { + ttsLanguage = Locale.US; + } + } + + if(tts.isLanguageAvailable(ttsLanguage) == TextToSpeech.LANG_MISSING_DATA){ + context.startActivity(new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); } int supportStatus = tts.setLanguage(ttsLanguage); From 902863d17cf5d958c7194c45e8e48d57b8fd4bd2 Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Thu, 14 May 2015 23:26:53 -0700 Subject: [PATCH 04/11] Added mapbox and offline map provider implementations. --- Android/build.gradle | 3 + .../droidplanner/android/DroidPlannerApp.java | 7 + .../android/data/DatabaseState.java | 40 ++ .../providers/mapbox/MapboxTileProvider.java | 42 ++ .../maps/providers/mapbox/MapboxUtils.java | 61 ++ .../providers/mapbox/OfflineTileProvider.java | 42 ++ .../mapbox/offline/MapDownloader.java | 548 ++++++++++++++++++ .../mapbox/offline/MapDownloaderListener.java | 13 + .../offline/OfflineDatabaseHandler.java | 94 +++ .../android/utils/NetworkUtils.java | 59 ++ .../org/droidplanner/android/utils/Utils.java | 19 + 11 files changed, 928 insertions(+) create mode 100644 Android/src/org/droidplanner/android/data/DatabaseState.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxTileProvider.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxUtils.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/OfflineTileProvider.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloader.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloaderListener.java create mode 100644 Android/src/org/droidplanner/android/maps/providers/mapbox/offline/OfflineDatabaseHandler.java create mode 100644 Android/src/org/droidplanner/android/utils/NetworkUtils.java diff --git a/Android/build.gradle b/Android/build.gradle index eb52c5a86d..71b42744bc 100644 --- a/Android/build.gradle +++ b/Android/build.gradle @@ -22,6 +22,9 @@ dependencies { compile files('libs/protobuf-java-2.5.0.jar') compile files('libs/jeromq-0.3.4.jar') compile files('libs/sius-0.3.2-SNAPSHOT.jar') + + compile 'com.squareup.okhttp:okhttp:2.3.0' + compile 'com.squareup.okhttp:okhttp-urlconnection:2.3.0' } android { diff --git a/Android/src/org/droidplanner/android/DroidPlannerApp.java b/Android/src/org/droidplanner/android/DroidPlannerApp.java index 4029d952f8..75e57b5e08 100644 --- a/Android/src/org/droidplanner/android/DroidPlannerApp.java +++ b/Android/src/org/droidplanner/android/DroidPlannerApp.java @@ -23,6 +23,7 @@ import com.o3dr.services.android.lib.drone.connection.DroneSharePrefs; import org.droidplanner.android.activities.helpers.BluetoothDevicesActivity; +import org.droidplanner.android.maps.providers.mapbox.offline.MapDownloader; import org.droidplanner.android.notifications.NotificationHandler; import org.droidplanner.android.proxy.mission.MissionProxy; import org.droidplanner.android.utils.Utils; @@ -127,6 +128,7 @@ public void run() { private DroidPlannerPrefs dpPrefs; private LocalBroadcastManager lbm; private NotificationHandler notificationHandler; + private MapDownloader mapDownloader; @Override public void onCreate() { @@ -135,6 +137,7 @@ public void onCreate() { dpPrefs = new DroidPlannerPrefs(context); lbm = LocalBroadcastManager.getInstance(context); + mapDownloader = new MapDownloader(context); controlTower = new ControlTower(context); drone = new Drone(context); @@ -160,6 +163,10 @@ public void uncaughtException(Thread thread, Throwable ex) { registerReceiver(broadcastReceiver, intentFilter); } + public MapDownloader getMapDownloader() { + return mapDownloader; + } + public void addApiListener(ApiListener listener) { if (listener == null) return; diff --git a/Android/src/org/droidplanner/android/data/DatabaseState.java b/Android/src/org/droidplanner/android/data/DatabaseState.java new file mode 100644 index 0000000000..9eddc4d323 --- /dev/null +++ b/Android/src/org/droidplanner/android/data/DatabaseState.java @@ -0,0 +1,40 @@ +package org.droidplanner.android.data; + +import android.content.Context; +import android.text.TextUtils; + +import org.droidplanner.android.maps.providers.mapbox.offline.OfflineDatabaseHandler; + +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created by Fredia Huya-Kouadio on 5/13/15. + */ +public class DatabaseState { + + private static final ConcurrentHashMap databaseHandlers = new ConcurrentHashMap<>(); + + public static OfflineDatabaseHandler getOfflineDatabaseHandlerForMapId(Context context, String dbName) { + final String lowerMapId = dbName.toLowerCase(Locale.US); + if (databaseHandlers.containsKey(lowerMapId)) { + return databaseHandlers.get(dbName); + } + + OfflineDatabaseHandler dbh = new OfflineDatabaseHandler(context, lowerMapId); + databaseHandlers.put(lowerMapId, dbh); + return dbh; + } + + public static void deleteDatabase(Context context, String dbName){ + if(context == null || TextUtils.isEmpty(dbName)) + return; + + final String lowerMapId = dbName.toLowerCase(Locale.US); + OfflineDatabaseHandler dbHandler = databaseHandlers.remove(lowerMapId); + if(dbHandler != null) + dbHandler.close(); + + context.deleteDatabase(lowerMapId); + } +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxTileProvider.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxTileProvider.java new file mode 100644 index 0000000000..ef468d0d39 --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxTileProvider.java @@ -0,0 +1,42 @@ +package org.droidplanner.android.maps.providers.mapbox; + +import android.util.Log; + +import com.google.android.gms.maps.model.UrlTileProvider; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Created by Fredia Huya-Kouadio on 5/11/15. + */ +public class MapboxTileProvider extends UrlTileProvider { + + private final static String TAG = MapboxTileProvider.class.getSimpleName(); + + private final String mapboxId; + private final String mapboxAccessToken; + private final int maxZoomLevel; + + public MapboxTileProvider(String mapboxId, String mapboxAccessToken, int maxZoomLevel) { + super(MapboxUtils.TILE_WIDTH, MapboxUtils.TILE_HEIGHT); + this.mapboxId = mapboxId; + this.mapboxAccessToken = mapboxAccessToken; + this.maxZoomLevel = maxZoomLevel; + } + + @Override + public URL getTileUrl(int x, int y, int zoom) { + if (zoom <= maxZoomLevel) { + final String tileUrl = MapboxUtils.getMapTileURL(mapboxId, mapboxAccessToken, zoom, x, y); + try { + return new URL(tileUrl); + } catch (MalformedURLException e) { + Log.e(TAG, "Error while building url for mapbox map tile.", e); + } + } + return null; + } + + +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxUtils.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxUtils.java new file mode 100644 index 0000000000..da8c44bc70 --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/MapboxUtils.java @@ -0,0 +1,61 @@ +package org.droidplanner.android.maps.providers.mapbox; + +import android.text.TextUtils; + +import java.util.Locale; + +/** + * Created by Fredia Huya-Kouadio on 5/11/15. + */ +public class MapboxUtils { + + //Private constructor to prevent instantiation. + private MapboxUtils(){} + + public static final int TILE_WIDTH = 512; //pixels + public static final int TILE_HEIGHT = 512; //pixels + + public static final String MAPBOX_BASE_URL_V4 = "https://a.tiles.mapbox.com/v4/"; + + public static String getMapTileURL(String mapID, String accessToken, int zoom, int x, int y) { + return String.format(Locale.US, "https://a.tiles.mapbox.com/v4/%s/%d/%d/%d%s.%s?access_token=%s", + mapID, zoom, x, y, "@2x", "png", accessToken); + } + + public static String markerIconURL(String accessToken, String size, String symbol, String color) { + // Make a string which follows the Mapbox Core API spec for stand-alone markers. This relies on the Mapbox API + // for error checking. + + StringBuffer marker = new StringBuffer("pin-"); + final String lowerCaseSize = size.toLowerCase(Locale.US); + + if (lowerCaseSize.charAt(0) == 'l') { + marker.append("l"); // large + } else if (lowerCaseSize.charAt(0) == 's') { + marker.append("s"); // small + } else { + marker.append("m"); // default to medium + } + + if (!TextUtils.isEmpty(symbol)) { + marker.append(String.format("-%s+", symbol)); + } else { + marker.append("+"); + } + + marker.append(color.replaceAll("#", "")); + +// if (AppUtils.isRunningOn2xOrGreaterScreen(context)) { +// marker.append("@2x"); +// } + marker.append(".png"); + + marker.append("?access_token="); + marker.append(accessToken); + return String.format(Locale.US, MAPBOX_BASE_URL_V4 + "marker/%s", marker); + } + + public static String getUserAgent() { + return "Mapbox Android SDK/0.7.3"; + } +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/OfflineTileProvider.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/OfflineTileProvider.java new file mode 100644 index 0000000000..f6a4d733c6 --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/OfflineTileProvider.java @@ -0,0 +1,42 @@ +package org.droidplanner.android.maps.providers.mapbox; + +import android.content.Context; + +import com.google.android.gms.maps.model.Tile; +import com.google.android.gms.maps.model.TileProvider; + +import org.droidplanner.android.data.DatabaseState; + +/** + * Created by Fredia Huya-Kouadio on 5/11/15. + */ +public class OfflineTileProvider implements TileProvider { + + private static final String TAG = OfflineTileProvider.class.getSimpleName(); + + private final Context context; + private final String mapboxId; + private final String mapboxAccessToken; + private final int maxZoomLevel; + + public OfflineTileProvider(Context context, String mapboxId, String mapboxAccessToken, int maxZoomLevel) { + this.context = context; + this.mapboxId = mapboxId; + this.mapboxAccessToken = mapboxAccessToken; + this.maxZoomLevel = maxZoomLevel; + } + + @Override + public Tile getTile(int x, int y, int zoom) { + if (zoom > maxZoomLevel) { + return TileProvider.NO_TILE; + } + + final String tileUri = MapboxUtils.getMapTileURL(mapboxId, mapboxAccessToken, zoom, x, y); + byte[] data = DatabaseState.getOfflineDatabaseHandlerForMapId(context, mapboxId).dataForURL(tileUri); + if (data == null || data.length == 0) + return TileProvider.NO_TILE; + + return new Tile(MapboxUtils.TILE_WIDTH, MapboxUtils.TILE_HEIGHT, data); + } +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloader.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloader.java new file mode 100644 index 0000000000..a2a379e5bd --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloader.java @@ -0,0 +1,548 @@ +package org.droidplanner.android.maps.providers.mapbox.offline; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; +import android.util.Log; + +import com.google.android.gms.maps.model.VisibleRegion; + +import org.droidplanner.android.data.DatabaseState; +import org.droidplanner.android.maps.providers.mapbox.MapboxUtils; +import org.droidplanner.android.utils.NetworkUtils; +import org.droidplanner.android.utils.Utils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +public class MapDownloader { + + private static final String TAG = MapDownloader.class.getSimpleName(); + + /** + * The possible states of the offline map downloader. + */ + public enum MBXOfflineMapDownloaderState { + /** + * An offline map download job is in progress. + */ + MBXOfflineMapDownloaderStateRunning, + /** + * An offline map download job is being canceled. + */ + MBXOfflineMapDownloaderStateCanceling, + /** + * The offline map downloader is ready to begin a new offline map download job. + */ + MBXOfflineMapDownloaderStateAvailable + } + + private MBXOfflineMapDownloaderState state; + private final AtomicInteger totalFilesWritten = new AtomicInteger(0); + private final AtomicInteger totalFilesExpectedToWrite = new AtomicInteger(0); + + private final Context context; + private ExecutorService downloadsScheduler; + private final ArrayList listeners = new ArrayList<>(); + + public MapDownloader(Context context) { + this.context = context; + setupDownloadScheduler(); + + this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable; + } + + public MBXOfflineMapDownloaderState getState() { + return state; + } + + public boolean addMapDownloaderListener(MapDownloaderListener listener) { + return listeners.add(listener); + } + + public boolean removeMapDownloaderListener(MapDownloaderListener listener) { + return listeners.remove(listener); + } + + public void cancelDownload() { + if (state == MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) { + this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateCanceling; + notifyDelegateOfStateChange(); + } + + setupDownloadScheduler(); + + if (state == MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateCanceling) { + this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable; + notifyDelegateOfStateChange(); + } + } + + private void setupDownloadScheduler() { + if (downloadsScheduler != null) { + downloadsScheduler.shutdownNow(); + } + + final int processorsCount = (int) (Runtime.getRuntime().availableProcessors() * 1.5f); + Log.v(TAG, "Using " + processorsCount + " processors."); + downloadsScheduler = Executors.newFixedThreadPool(processorsCount); + } + +/* + Delegate Notifications +*/ + + public void notifyDelegateOfStateChange() { + for (MapDownloaderListener listener : listeners) { + listener.stateChanged(this.state); + } + } + + public void notifyDelegateOfInitialCount(int totalFilesExpectedToWrite) { + for (MapDownloaderListener listener : listeners) { + listener.initialCountOfFiles(totalFilesExpectedToWrite); + } + } + + public void notifyDelegateOfProgress(int totalFilesWritten, int totalFilesExpectedToWrite) { + for (MapDownloaderListener listener : listeners) { + listener.progressUpdate(totalFilesWritten, totalFilesExpectedToWrite); + } + } + + public void notifyDelegateOfNetworkConnectivityError(Throwable error) { + for (MapDownloaderListener listener : listeners) { + listener.networkConnectivityError(error); + } + } + + public void notifyDelegateOfSqliteError(Throwable error) { + for (MapDownloaderListener listener : listeners) { + listener.sqlLiteError(error); + } + } + + public void notifyDelegateOfHTTPStatusError(int status, String url) { + for (MapDownloaderListener listener : listeners) { + listener.httpStatusError(new Exception(String.format(Locale.US, "HTTP Status Error %d, for url = %s", status, + url))); + } + } + + public void notifyDelegateOfCompletionWithOfflineMapDatabase() { + for (MapDownloaderListener listener : listeners) { + listener.completionOfOfflineDatabaseMap(); + } + } + + public void startDownloading(final String mapId) { + + // Get the actual URLs + ArrayList urls = sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit(mapId, -1); + this.totalFilesExpectedToWrite.set(urls.size()); + this.totalFilesWritten.set(0); + + notifyDelegateOfInitialCount(totalFilesExpectedToWrite.get()); + + Log.d(TAG, String.format(Locale.US, "number of urls to download = %d", urls.size())); + if (this.totalFilesExpectedToWrite.get() == 0) { + finishUpDownloadProcess(); + return; + } + + if (!NetworkUtils.isNetworkAvailable(context)) { + Log.e(TAG, "Network is not available."); + notifyDelegateOfNetworkConnectivityError(new IllegalStateException("Network is not available")); + return; + } + + final CountDownLatch downloadsTracker = new CountDownLatch(this.totalFilesExpectedToWrite.get()); + for (final String url : urls) { + downloadsScheduler.execute(new Runnable() { + @Override + public void run() { + HttpURLConnection conn = null; + try { + conn = NetworkUtils.getHttpURLConnection(new URL(url)); + Log.d(TAG, "URL to download = " + conn.getURL().toString()); + conn.setConnectTimeout(60000); + conn.connect(); + int rc = conn.getResponseCode(); + if (rc != HttpURLConnection.HTTP_OK) { + String msg = String.format(Locale.US, "HTTP Error connection. Response Code = %d for url = %s", rc, conn.getURL().toString()); + Log.w(TAG, msg); + notifyDelegateOfHTTPStatusError(rc, url); + throw new IOException(msg); + } + + ByteArrayOutputStream bais = new ByteArrayOutputStream(); + InputStream is = null; + try { + is = conn.getInputStream(); + // Read 4K at a time + byte[] byteChunk = new byte[4096]; + int n; + + while ((n = is.read(byteChunk)) > 0) { + bais.write(byteChunk, 0, n); + } + } catch (IOException e) { + Log.e(TAG, String.format(Locale.US, "Failed while reading bytes from %s: %s", conn.getURL().toString(), e.getMessage())); + e.printStackTrace(); + } finally { + if (is != null) { + is.close(); + } + conn.disconnect(); + } + sqliteSaveDownloadedData(mapId, bais.toByteArray(), url); + } catch (IOException e) { + Log.e(TAG, "Error occurred while retrieving map data.", e); + } finally { + downloadsTracker.countDown(); + + if (conn != null) { + conn.disconnect(); + } + } + + } + }); + } + + downloadsScheduler.execute(new Runnable() { + @Override + public void run() { + try { + downloadsTracker.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Error while waiting for downloads to complete.", e); + } finally { + finishUpDownloadProcess(); + } + } + }); + } + +/* + Implementation: sqlite stuff +*/ + + public void sqliteSaveDownloadedData(String mapId, byte[] data, String url) { + if (Utils.runningOnMainThread()) { + Log.w(TAG, "trying to run sqliteSaveDownloadedData() on main thread. Return."); + return; + } + + // Bail out if the state has changed to canceling, suspended, or available + if (this.state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) { + Log.w(TAG, "sqliteSaveDownloadedData() is not in a Running state so bailing. State = " + this.state); + return; + } + + // Open the database read-write and multi-threaded. The slightly obscure c-style variable names here and below are + // used to stay consistent with the sqlite documentaion. + // Continue by inserting an image blob into the data table + SQLiteDatabase db = DatabaseState.getOfflineDatabaseHandlerForMapId(context, mapId) + .getWritableDatabase(); + try { + db.beginTransaction(); + + ContentValues values = new ContentValues(); + values.put(OfflineDatabaseHandler.FIELD_DATA_VALUE, data); + db.insert(OfflineDatabaseHandler.TABLE_DATA, null, values); + + db.execSQL(String.format(Locale.US, "UPDATE %s SET %s=200, %s=last_insert_rowid() WHERE %s='%s';", OfflineDatabaseHandler.TABLE_RESOURCES, OfflineDatabaseHandler.FIELD_RESOURCES_STATUS, OfflineDatabaseHandler.FIELD_RESOURCES_ID, OfflineDatabaseHandler.FIELD_RESOURCES_URL, url)); + db.setTransactionSuccessful(); + db.endTransaction(); +// db.close(); + }catch(IllegalStateException e){ + Log.e(TAG, "Error while saving downloader data to the database.", e); + } + + // Update the progress + notifyDelegateOfProgress(this.totalFilesWritten.incrementAndGet(), this.totalFilesExpectedToWrite.get()); + Log.d(TAG, "totalFilesWritten = " + this.totalFilesWritten + "; totalFilesExpectedToWrite = " + this + .totalFilesExpectedToWrite.get()); + } + + private void finishUpDownloadProcess() { + if (this.state == MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning) { + Log.i(TAG, "Just finished downloading all materials. Persist the OfflineMapDatabase, change the state, and call it a day."); + // This is what to do when we've downloaded all the files + notifyDelegateOfCompletionWithOfflineMapDatabase(); + this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable; + notifyDelegateOfStateChange(); + } + } + + public ArrayList sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit(String mapId, int limit) { + ArrayList results = new ArrayList(); + if (Utils.runningOnMainThread()) { + Log.w(TAG, "Attempting to run sqliteReadArrayOfOfflineMapURLsToBeDownloadLimit() on main thread. Returning."); + return results; + } + + // Read up to limit undownloaded urls from the offline map database + String query = String.format(Locale.US, "SELECT %s FROM %s WHERE %s IS NULL", OfflineDatabaseHandler.FIELD_RESOURCES_URL, OfflineDatabaseHandler.TABLE_RESOURCES, OfflineDatabaseHandler.FIELD_RESOURCES_STATUS); + if (limit > 0) { + query = query + String.format(Locale.US, " LIMIT %d", limit); + } + query = query + ";"; + + // Open the database + SQLiteDatabase db = DatabaseState.getOfflineDatabaseHandlerForMapId(context, mapId) + .getReadableDatabase(); + Cursor cursor = db.rawQuery(query, null); + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + results.add(cursor.getString(0)); + } while (cursor.moveToNext()); + } + cursor.close(); + } +// db.close(); + + return results; + } + + + public boolean sqliteCreateDatabaseUsingMetadata(String mapId, List urlStrings) { + if (Utils.runningOnMainThread()) { + Log.w(TAG, "sqliteCreateDatabaseUsingMetadata() running on main thread. Returning."); + return false; + } + + // Build a query to populate the database (map metadata and list of map resource urls) + SQLiteDatabase db = DatabaseState.getOfflineDatabaseHandlerForMapId(context, mapId) + .getWritableDatabase(); + db.beginTransaction(); + + for (String url : urlStrings) { + ContentValues cv = new ContentValues(); + cv.put(OfflineDatabaseHandler.FIELD_RESOURCES_URL, url); + db.insertWithOnConflict(OfflineDatabaseHandler.TABLE_RESOURCES, null, cv, SQLiteDatabase.CONFLICT_IGNORE); + } + + db.setTransactionSuccessful(); + db.endTransaction(); +// db.close(); + + return true; + } + +/* + API: Begin an offline map download +*/ + + public void beginDownloadingMapID(final String mapId, final String accessToken, VisibleRegion mapRegion, int + minimumZ, int maximumZ) { + beginDownloadingMapID(mapId, accessToken, mapRegion, minimumZ, maximumZ, true, true); + } + + public void beginDownloadingMapID(final String mapId, final String accessToken, VisibleRegion mapRegion, int minimumZ, int maximumZ, boolean includeMetadata, + boolean includeMarkers) { + if (state != MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateAvailable) { + Log.w(TAG, "state doesn't equal MBXOfflineMapDownloaderStateAvailable so return. state = " + state); + return; + } + + // Start a download job to retrieve all the resources needed for using the specified map offline + this.state = MBXOfflineMapDownloaderState.MBXOfflineMapDownloaderStateRunning; + + final ArrayList urls = new ArrayList(); + String dataName = "features.json"; // Only using API V4 for now + + // Include URLs for the metadata and markers json if applicable + if (includeMetadata) { + urls.add(String.format(Locale.US, MapboxUtils.MAPBOX_BASE_URL_V4 + "%s.json?secure&access_token=%s", + mapId, accessToken)); + } + if (includeMarkers) { + urls.add(String.format(Locale.US, MapboxUtils.MAPBOX_BASE_URL_V4 + "%s/%s?access_token=%s", mapId, + dataName, accessToken)); + } + + // Loop through the zoom levels and lat/lon bounds to generate a list of urls which should be included in the offline map + // + double minLat = Math.min( + Math.min(mapRegion.farLeft.latitude, mapRegion.nearLeft.latitude), + Math.min(mapRegion.farRight.latitude, mapRegion.nearRight.latitude)); + double maxLat = Math.max( + Math.max(mapRegion.farLeft.latitude, mapRegion.nearLeft.latitude), + Math.max(mapRegion.farRight.latitude, mapRegion.nearRight.latitude)); + + double minLon = Math.min( + Math.min(mapRegion.farLeft.longitude, mapRegion.nearLeft.longitude), + Math.min(mapRegion.farRight.longitude, mapRegion.nearRight.longitude)); + double maxLon = Math.max( + Math.max(mapRegion.farLeft.longitude, mapRegion.nearLeft.longitude), + Math.max(mapRegion.farRight.longitude, mapRegion.nearRight.longitude)); + + int minX; + int maxX; + int minY; + int maxY; + int tilesPerSide; + + Log.d(TAG, "Generating urls for tiles from zoom " + minimumZ + " to zoom " + maximumZ); + + for (int zoom = minimumZ; zoom <= maximumZ; zoom++) { + tilesPerSide = Double.valueOf(Math.pow(2.0, zoom)).intValue(); + minX = Double.valueOf(Math.floor(((minLon + 180.0) / 360.0) * tilesPerSide)).intValue(); + maxX = Double.valueOf(Math.floor(((maxLon + 180.0) / 360.0) * tilesPerSide)).intValue(); + minY = Double.valueOf(Math.floor((1.0 - (Math.log(Math.tan(maxLat * Math.PI / 180.0) + 1.0 / Math.cos(maxLat * Math.PI / 180.0)) / Math.PI)) / 2.0 * tilesPerSide)).intValue(); + maxY = Double.valueOf(Math.floor((1.0 - (Math.log(Math.tan(minLat * Math.PI / 180.0) + 1.0 / Math.cos(minLat * Math.PI / 180.0)) / Math.PI)) / 2.0 * tilesPerSide)).intValue(); + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + urls.add(MapboxUtils.getMapTileURL(mapId, accessToken, zoom, x, y)); + } + } + } + + Log.d(TAG, urls.size() + " urls generated."); + + // Determine if we need to add marker icon urls (i.e. parse markers.geojson/features.json), and if so, add them + if (includeMarkers) { + String dName = "markers.geojson"; + final String geojson = String.format(Locale.US, MapboxUtils.MAPBOX_BASE_URL_V4 + "%s/%s?access_token=%s", + mapId, dName, accessToken); + + if (!NetworkUtils.isNetworkAvailable(context)) { + // We got a session level error which probably indicates a connectivity problem such as airplane mode. + // Since we must fetch and parse markers.geojson/features.json in order to determine which marker icons need to be + // added to the list of urls to download, the lack of network connectivity is a non-recoverable error + // here. + notifyDelegateOfNetworkConnectivityError(new IllegalStateException("Network is unavailable")); + Log.e(TAG, "Network is unavailable."); + return; + } + + downloadsScheduler.execute(new Runnable() { + @Override + public void run() { + try { + HttpURLConnection conn = NetworkUtils.getHttpURLConnection(new URL(geojson)); + conn.setConnectTimeout(60000); + conn.connect(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException(); + } + + BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), Charset.forName("UTF-8"))); + String jsonText = Utils.readAll(rd); + + // The marker geojson was successfully retrieved, so parse it for marker icons. Note that we shouldn't + // try to save it here, because it may already be in the download queue and saving it twice will mess + // up the count of urls to be downloaded! + // + Set markerIconURLStrings = new HashSet(); + markerIconURLStrings.addAll(parseMarkerIconURLStringsFromGeojsonData(accessToken, jsonText)); + Log.i(TAG, "Number of markerIconURLs = " + markerIconURLStrings.size()); + if (markerIconURLStrings.size() > 0) { + urls.addAll(markerIconURLStrings); + } + } catch (IOException e) { + // The url for markers.geojson/features.json didn't work (some maps don't have any markers). Notify the delegate of the + // problem, and stop attempting to add marker icons, but don't bail out on whole the offline map download. + // The delegate can decide for itself whether it wants to continue or cancel. + // + // TODO + e.printStackTrace(); +/* + [self notifyDelegateOfHTTPStatusError:((NSHTTPURLResponse *)response).statusCode url:response.URL]; +*/ + } finally { + startDownloadProcess(mapId, urls); + } + } + }); + } else { + Log.i(TAG, "No marker icons to worry about, so just start downloading."); + // There aren't any marker icons to worry about, so just create database and start downloading + startDownloadProcess(mapId, urls); + } + } + + /** + * Private method for Starting the Whole Download Process + * + * @param urls Map urls + */ + private void startDownloadProcess(final String mapId, final List urls) { + downloadsScheduler.execute(new Runnable() { + @Override + public void run() { + // Do database creation / io on background thread + if (!sqliteCreateDatabaseUsingMetadata(mapId, urls)) { + Log.e(TAG, "Map Database wasn't created"); + return; + } + + startDownloading(mapId); + } + }); + } + + + public Set parseMarkerIconURLStringsFromGeojsonData(String accessToken, String data) { + HashSet iconURLStrings = new HashSet(); + + JSONObject simplestyleJSONDictionary; + try { + simplestyleJSONDictionary = new JSONObject(data); + + // Find point features in the markers dictionary (if there are any) and add them to the map. + JSONArray markers = simplestyleJSONDictionary.getJSONArray("features"); + + if (markers != null && markers.length() > 0) { + for (int lc = 0; lc < markers.length(); lc++) { + JSONObject feature = markers.optJSONObject(lc); + if (feature != null) { + String type = feature.getJSONObject("geometry").getString("type"); + + if ("Point".equals(type)) { + String size = feature.getJSONObject("properties").getString("marker-size"); + String color = feature.getJSONObject("properties").getString("marker-color"); + String symbol = feature.getJSONObject("properties").getString("marker-symbol"); + if (!TextUtils.isEmpty(size) && !TextUtils.isEmpty(color) && !TextUtils.isEmpty(symbol)) { + String markerURL = MapboxUtils.markerIconURL(accessToken, size, symbol, color); + if (!TextUtils.isEmpty(markerURL)) { + iconURLStrings.add(markerURL); + } + } + } + } + // This is the last line of the loop + } + } + } catch (JSONException e) { + Log.e(TAG, e.getMessage(), e); + } + + // Return only the unique icon urls + return iconURLStrings; + } + +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloaderListener.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloaderListener.java new file mode 100644 index 0000000000..7d8b5f11ab --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/MapDownloaderListener.java @@ -0,0 +1,13 @@ +package org.droidplanner.android.maps.providers.mapbox.offline; + +public interface MapDownloaderListener { + + public void stateChanged(MapDownloader.MBXOfflineMapDownloaderState newState); + public void initialCountOfFiles(int numberOfFiles); + public void progressUpdate(int numberOfFilesWritten, int numberOfFilesExcepted); + public void networkConnectivityError(Throwable error); + public void sqlLiteError(Throwable error); + public void httpStatusError(Throwable error); + public void completionOfOfflineDatabaseMap(); + +} diff --git a/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/OfflineDatabaseHandler.java b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/OfflineDatabaseHandler.java new file mode 100644 index 0000000000..e45ea1542a --- /dev/null +++ b/Android/src/org/droidplanner/android/maps/providers/mapbox/offline/OfflineDatabaseHandler.java @@ -0,0 +1,94 @@ +package org.droidplanner.android.maps.providers.mapbox.offline; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +public class OfflineDatabaseHandler extends SQLiteOpenHelper { + + private static final String TAG = OfflineDatabaseHandler.class.getSimpleName(); + + // All Static variables + // Database Version + public static final int DATABASE_VERSION = 1; + + // Table name(s) + public static final String TABLE_DATA = "data"; + public static final String TABLE_RESOURCES = "resources"; + + // Table Fields + public static final String FIELD_DATA_ID = "id"; + public static final String FIELD_DATA_VALUE = "value"; + + public static final String FIELD_RESOURCES_ID = "id"; + public static final String FIELD_RESOURCES_URL = "url"; + public static final String FIELD_RESOURCES_STATUS = "status"; + + /** + * Constructor + * + * @param context Context + */ + public OfflineDatabaseHandler(Context context, String dbName) { + super(context, dbName, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "onCreate() called... Setting up application's database."); + // Create The table(s) + String data = "CREATE TABLE " + TABLE_DATA + " (" + FIELD_DATA_ID + " INTEGER PRIMARY KEY, " + FIELD_DATA_VALUE + " BLOB);"; + String resources = "CREATE TABLE " + TABLE_RESOURCES + " (" + FIELD_RESOURCES_URL + " TEXT UNIQUE, " + FIELD_RESOURCES_STATUS + " TEXT, " + FIELD_RESOURCES_ID + " INTEGER REFERENCES data);"; + + db.execSQL("PRAGMA foreign_keys=ON;"); + db.beginTransaction(); + + try { + db.execSQL(data); + db.execSQL(resources); + db.setTransactionSuccessful(); + } catch (SQLException e) { + Log.e(TAG, "Error creating databases", e); + } finally { + db.endTransaction(); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); + db.execSQL("PRAGMA foreign_keys=OFF;"); + db.execSQL("drop table if exists " + TABLE_DATA); + db.execSQL("drop table if exists " + TABLE_RESOURCES); + onCreate(db); + } + + public byte[] dataForURL(String url) { + return sqliteDataForURL(url); + } + + public byte[] sqliteDataForURL(String url) { + SQLiteDatabase db = getReadableDatabase(); + + String query = "SELECT " + OfflineDatabaseHandler.FIELD_DATA_VALUE + " FROM " + OfflineDatabaseHandler.TABLE_DATA + + " WHERE " + OfflineDatabaseHandler.FIELD_DATA_ID + + "= (SELECT " + OfflineDatabaseHandler.FIELD_RESOURCES_ID + " from " + + OfflineDatabaseHandler.TABLE_RESOURCES + " where " + OfflineDatabaseHandler.FIELD_RESOURCES_URL + + " = '" + url + "');"; + + Cursor cursor = db.rawQuery(query, null); + + byte[] blob = null; + if (cursor != null) { + if (cursor.moveToFirst()) { + blob = cursor.getBlob(cursor.getColumnIndex(OfflineDatabaseHandler.FIELD_DATA_VALUE)); + } + cursor.close(); + } +// db.close(); + return blob; + } +} diff --git a/Android/src/org/droidplanner/android/utils/NetworkUtils.java b/Android/src/org/droidplanner/android/utils/NetworkUtils.java new file mode 100644 index 0000000000..ce555b5d24 --- /dev/null +++ b/Android/src/org/droidplanner/android/utils/NetworkUtils.java @@ -0,0 +1,59 @@ +package org.droidplanner.android.utils; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +import com.squareup.okhttp.Cache; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.OkUrlFactory; + +import org.droidplanner.android.maps.providers.mapbox.MapboxUtils; + +import java.net.HttpURLConnection; +import java.net.URL; + +import javax.net.ssl.SSLSocketFactory; + +/** + * Created by Fredia Huya-Kouadio on 5/11/15. + */ +public class NetworkUtils { + public static boolean isNetworkAvailable(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); + return activeNetworkInfo != null && activeNetworkInfo.isConnected(); + } + + public static String getCurrentWifiLink(Context context) { + final WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + + final WifiInfo connectedWifi = wifiMgr.getConnectionInfo(); + final String connectedSSID = connectedWifi == null ? null : connectedWifi.getSSID().replace("\"", ""); + return connectedSSID; + } + + public static HttpURLConnection getHttpURLConnection(final URL url) { + return getHttpURLConnection(url, null, null); + } + + public static HttpURLConnection getHttpURLConnection(final URL url, final Cache cache) { + return getHttpURLConnection(url, cache, null); + } + + public static HttpURLConnection getHttpURLConnection(final URL url, final Cache cache, final SSLSocketFactory sslSocketFactory) { + OkHttpClient client = new OkHttpClient(); + if (cache != null) { + client.setCache(cache); + } + if (sslSocketFactory != null) { + client.setSslSocketFactory(sslSocketFactory); + } + HttpURLConnection connection = new OkUrlFactory(client).open(url); + connection.setRequestProperty("User-Agent", MapboxUtils.getUserAgent()); + return connection; + } +} diff --git a/Android/src/org/droidplanner/android/utils/Utils.java b/Android/src/org/droidplanner/android/utils/Utils.java index a822fac35f..711cf6a7b0 100644 --- a/Android/src/org/droidplanner/android/utils/Utils.java +++ b/Android/src/org/droidplanner/android/utils/Utils.java @@ -3,10 +3,13 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.os.Looper; import org.droidplanner.android.maps.providers.DPMapProvider; import org.droidplanner.android.utils.prefs.DroidPlannerPrefs; +import java.io.IOException; +import java.io.Reader; import java.util.Locale; /** @@ -47,4 +50,20 @@ public static void updateUILanguage(Context context) { res.updateConfiguration(config, res.getDisplayMetrics()); } } + + public static boolean runningOnMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + + public static String readAll(Reader rd) throws IOException { + StringBuilder sb = new StringBuilder(); + int cp; + while ((cp = rd.read()) != -1) { + sb.append((char) cp); + } + return sb.toString(); + } + + //Private constructor to prevent instantiation. + private Utils(){} } From 18a8d81bdbb21d7eca5ea99210bab5ce9c933205 Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Sun, 24 May 2015 20:53:53 -0700 Subject: [PATCH 05/11] tts language selection bug fix. added callback for `home updated` event to the DroneMap instance. --- .../src/org/droidplanner/android/fragments/DroneMap.java | 2 ++ .../maps/providers/google_map/GoogleMapFragment.java | 6 ++++++ .../android/notifications/TTSNotificationProvider.java | 5 +---- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Android/src/org/droidplanner/android/fragments/DroneMap.java b/Android/src/org/droidplanner/android/fragments/DroneMap.java index 95bece9f3e..bae543de17 100644 --- a/Android/src/org/droidplanner/android/fragments/DroneMap.java +++ b/Android/src/org/droidplanner/android/fragments/DroneMap.java @@ -56,6 +56,7 @@ public abstract class DroneMap extends ApiListenerFragment { eventFilter.addAction(AttributeEvent.STATE_DISCONNECTED); eventFilter.addAction(AttributeEvent.CAMERA_FOOTPRINTS_UPDATED); eventFilter.addAction(AttributeEvent.ATTITUDE_UPDATED); + eventFilter.addAction(AttributeEvent.HOME_UPDATED); eventFilter.addAction(ACTION_UPDATE_MAP); } @@ -70,6 +71,7 @@ public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); switch (action) { case ACTION_UPDATE_MAP: + case AttributeEvent.HOME_UPDATED: case MissionProxy.ACTION_MISSION_PROXY_UPDATE: postUpdate(); break; diff --git a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java b/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java index df3b51f146..269fb1566c 100644 --- a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java +++ b/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java @@ -281,6 +281,12 @@ protected void doRun() { protected DroidPlannerApp dpApp; private Polygon footprintPoly; + /* + Tile overlay + */ + private TileOverlay onlineTileProvider; + private TileOverlay offlineTileProvider; + @Override public void onAttach(Activity activity) { super.onAttach(activity); diff --git a/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java b/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java index ae97bb1eb0..18a4af1d95 100644 --- a/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java +++ b/Android/src/org/droidplanner/android/notifications/TTSNotificationProvider.java @@ -303,10 +303,7 @@ public void onInit(int status) { // TODO: check if the language is available Locale ttsLanguage; final int sdkVersion = Build.VERSION.SDK_INT; - if(sdkVersion >= Build.VERSION_CODES.LOLLIPOP){ - ttsLanguage = tts.getDefaultVoice().getLocale(); - } - else if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (sdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { ttsLanguage = tts.getDefaultLanguage(); } else { ttsLanguage = tts.getLanguage(); From 422cb7772805e1eabbfbe94cf334af3d5edca900 Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Mon, 25 May 2015 11:50:08 -0700 Subject: [PATCH 06/11] removed check for appropriate gps location settings when the map is loaded. added check for appropriate gps location settings when follow me is enabled. upgraded to the latest version of dronekit-android. --- Android/build.gradle | 12 +- Android/res/values/strings.xml | 1 + .../activities/DrawerNavigationUI.java | 6 +- .../control/BaseFlightControlFragment.java | 115 +++++++++++ .../control/CopterFlightControlFragment.java | 13 +- .../control/GenericActionsFragment.java | 3 +- .../control/PlaneFlightControlFragment.java | 12 +- .../control/RoverFlightControlFragment.java | 4 +- .../google_map/GoogleMapFragment.java | 174 ++++++----------- .../utils/location/CheckLocationSettings.java | 184 ++++++++++++++++++ 10 files changed, 381 insertions(+), 143 deletions(-) create mode 100644 Android/src/org/droidplanner/android/fragments/control/BaseFlightControlFragment.java create mode 100644 Android/src/org/droidplanner/android/utils/location/CheckLocationSettings.java diff --git a/Android/build.gradle b/Android/build.gradle index 71b42744bc..2c366965b0 100644 --- a/Android/build.gradle +++ b/Android/build.gradle @@ -1,18 +1,18 @@ apply plugin: 'com.android.application' dependencies { - compile 'com.google.android.gms:play-services-maps:7.0.0' - compile 'com.google.android.gms:play-services-location:7.0.0' - compile 'com.google.android.gms:play-services-analytics:7.0.0' + compile 'com.google.android.gms:play-services-maps:7.3.0' + compile 'com.google.android.gms:play-services-location:7.3.0' + compile 'com.google.android.gms:play-services-analytics:7.3.0' compile 'com.sothree.slidinguppanel:library:2.0.2' - compile 'com.android.support:support-v4:22.1.0' - compile 'com.android.support:appcompat-v7:22.1.0' + compile 'com.android.support:support-v4:22.1.1' + compile 'com.android.support:appcompat-v7:22.1.1' compile 'com.android.support:cardview-v7:22.1.0' compile 'com.android.support:recyclerview-v7:22.1.0' - compile 'com.o3dr.android:dronekit-android:2.3.19' + compile 'com.o3dr.android:dronekit-android:2.3.25' compile 'me.grantland:autofittextview:0.2.1' compile(name:'shimmer-android-release', ext:'aar') diff --git a/Android/res/values/strings.xml b/Android/res/values/strings.xml index 608cc4ff3c..239b3e1820 100644 --- a/Android/res/values/strings.xml +++ b/Android/res/values/strings.xml @@ -549,5 +549,6 @@ Enable to allow the app to periodically ping the udp server. UDP ping receiver ip UDP ping receiver port + Unable to get accurate location. Please update your location settings! diff --git a/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java b/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java index 1c037ff483..d41c6a7a85 100644 --- a/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java +++ b/Android/src/org/droidplanner/android/activities/DrawerNavigationUI.java @@ -21,13 +21,11 @@ import android.widget.FrameLayout; import android.widget.TextView; -import com.google.android.gms.maps.GoogleMap; - import org.droidplanner.android.R; import org.droidplanner.android.activities.helpers.SuperUI; import org.droidplanner.android.fragments.SettingsFragment; import org.droidplanner.android.fragments.actionbar.ActionBarTelemFragment; -import org.droidplanner.android.maps.providers.google_map.GoogleMapFragment; +import org.droidplanner.android.fragments.control.BaseFlightControlFragment; import org.droidplanner.android.widgets.SlidingDrawer; /** @@ -100,7 +98,7 @@ protected View getActionDrawer() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data){ switch(requestCode) { - case GoogleMapFragment.REQUEST_CHECK_SETTINGS: + case BaseFlightControlFragment.FOLLOW_SETTINGS_UPDATE: LocalBroadcastManager.getInstance(getApplicationContext()) .sendBroadcast(new Intent(SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED) .putExtra(SettingsFragment.EXTRA_RESULT_CODE, resultCode)); diff --git a/Android/src/org/droidplanner/android/fragments/control/BaseFlightControlFragment.java b/Android/src/org/droidplanner/android/fragments/control/BaseFlightControlFragment.java new file mode 100644 index 0000000000..420bf74112 --- /dev/null +++ b/Android/src/org/droidplanner/android/fragments/control/BaseFlightControlFragment.java @@ -0,0 +1,115 @@ +package org.droidplanner.android.fragments.control; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.view.View; +import android.widget.Toast; + +import com.google.android.gms.location.LocationRequest; +import com.o3dr.android.client.Drone; +import com.o3dr.services.android.lib.drone.attribute.AttributeType; +import com.o3dr.services.android.lib.gcs.follow.FollowState; +import com.o3dr.services.android.lib.gcs.follow.FollowType; + +import org.droidplanner.android.activities.DrawerNavigationUI; +import org.droidplanner.android.fragments.SettingsFragment; +import org.droidplanner.android.fragments.helpers.ApiListenerFragment; +import org.droidplanner.android.utils.location.CheckLocationSettings; + +/** + * Created by Fredia Huya-Kouadio on 5/25/15. + */ +public abstract class BaseFlightControlFragment extends ApiListenerFragment implements View.OnClickListener, + FlightControlManagerFragment.SlidingUpHeader { + + public static final int FOLLOW_SETTINGS_UPDATE = 147; + + private static final String TAG = BaseFlightControlFragment.class.getSimpleName(); + + private static final int FOLLOW_LOCATION_PRIORITY = LocationRequest.PRIORITY_HIGH_ACCURACY; + private static final long FOLLOW_LOCATION_UPDATE_INTERVAL = 30000; // ms + private static final long FOLLOW_LOCATION_UPDATE_FASTEST_INTERVAL = 5000; // ms + private static final float FOLLOW_LOCATION_UPDATE_MIN_DISPLACEMENT = 0; // m + + private static final IntentFilter filter = new IntentFilter(SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED); + + private final BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch(intent.getAction()){ + case SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED: + final int resultCode = intent.getIntExtra(SettingsFragment.EXTRA_RESULT_CODE, Activity.RESULT_OK); + switch (resultCode) { + case Activity.RESULT_OK: + // All required changes were successfully made. Enable follow me. + enableFollowMe(getDrone()); + break; + + case Activity.RESULT_CANCELED: + // The user was asked to change settings, but chose not to + Toast.makeText(getActivity(), "Please update your location settings!", Toast.LENGTH_LONG).show(); + break; + default: + break; + } + break; + } + } + }; + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + if(!(activity instanceof DrawerNavigationUI)){ + throw new IllegalStateException("Parent activity must be an instance of " + DrawerNavigationUI.class + .getName()); + } + } + + @Override + public void onApiConnected(){ + getBroadcastManager().registerReceiver(receiver, filter); + } + + @Override + public void onApiDisconnected(){ + getBroadcastManager().unregisterReceiver(receiver); + } + + protected void toggleFollowMe() { + final Drone drone = getDrone(); + if (drone == null) + return; + + final FollowState followState = drone.getAttribute(AttributeType.FOLLOW_STATE); + if (followState.isEnabled()) { + drone.disableFollowMe(); + } else { + enableFollowMe(drone); + } + } + + private void enableFollowMe(final Drone drone) { + if(drone == null) + return; + + final LocationRequest locationReq = LocationRequest.create() + .setPriority(FOLLOW_LOCATION_PRIORITY) + .setFastestInterval(FOLLOW_LOCATION_UPDATE_FASTEST_INTERVAL) + .setInterval(FOLLOW_LOCATION_UPDATE_INTERVAL) + .setSmallestDisplacement(FOLLOW_LOCATION_UPDATE_MIN_DISPLACEMENT); + + final CheckLocationSettings locationSettingsChecker = new CheckLocationSettings((DrawerNavigationUI) getActivity(), locationReq, + new Runnable() { + @Override + public void run() { + drone.enableFollowMe(FollowType.LEASH); + } + }); + + locationSettingsChecker.check(); + } +} diff --git a/Android/src/org/droidplanner/android/fragments/control/CopterFlightControlFragment.java b/Android/src/org/droidplanner/android/fragments/control/CopterFlightControlFragment.java index 0e34d7275a..0a8ea974d2 100644 --- a/Android/src/org/droidplanner/android/fragments/control/CopterFlightControlFragment.java +++ b/Android/src/org/droidplanner/android/fragments/control/CopterFlightControlFragment.java @@ -36,8 +36,7 @@ /** * Provide functionality for flight action button specific to copters. */ -public class CopterFlightControlFragment extends ApiListenerFragment implements View.OnClickListener, - FlightControlManagerFragment.SlidingUpHeader { +public class CopterFlightControlFragment extends BaseFlightControlFragment { private static final String TAG = CopterFlightControlFragment.class.getSimpleName(); @@ -203,6 +202,7 @@ public void onViewCreated(View view, Bundle savedInstanceState) { @Override public void onApiConnected() { + super.onApiConnected(); missionProxy = getMissionProxy(); setupButtonsByFlightState(); @@ -214,6 +214,7 @@ public void onApiConnected() { @Override public void onApiDisconnected() { + super.onApiDisconnected(); getBroadcastManager().unregisterReceiver(eventReceiver); } @@ -277,13 +278,7 @@ public void onClick(View v) { break; case R.id.mc_follow: - FollowState followState = drone.getAttribute(AttributeType.FOLLOW_STATE); - if (followState != null) { - if (followState.isEnabled()) - drone.disableFollowMe(); - else - drone.enableFollowMe(FollowType.LEASH); - } + toggleFollowMe(); break; case R.id.mc_dronieBtn: diff --git a/Android/src/org/droidplanner/android/fragments/control/GenericActionsFragment.java b/Android/src/org/droidplanner/android/fragments/control/GenericActionsFragment.java index 21c867b8b7..4750e63428 100644 --- a/Android/src/org/droidplanner/android/fragments/control/GenericActionsFragment.java +++ b/Android/src/org/droidplanner/android/fragments/control/GenericActionsFragment.java @@ -16,8 +16,7 @@ /** * Provides action buttons functionality for generic drone type. */ -public class GenericActionsFragment extends Fragment implements View.OnClickListener, - FlightControlManagerFragment.SlidingUpHeader { +public class GenericActionsFragment extends BaseFlightControlFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, diff --git a/Android/src/org/droidplanner/android/fragments/control/PlaneFlightControlFragment.java b/Android/src/org/droidplanner/android/fragments/control/PlaneFlightControlFragment.java index 489f279957..041f0cf54c 100644 --- a/Android/src/org/droidplanner/android/fragments/control/PlaneFlightControlFragment.java +++ b/Android/src/org/droidplanner/android/fragments/control/PlaneFlightControlFragment.java @@ -33,8 +33,7 @@ /** * Provides functionality for flight action buttons specific to planes. */ -public class PlaneFlightControlFragment extends ApiListenerFragment implements - View.OnClickListener, FlightControlManagerFragment.SlidingUpHeader { +public class PlaneFlightControlFragment extends BaseFlightControlFragment { private static final String ACTION_FLIGHT_ACTION_BUTTON = "Copter flight action button"; @@ -270,6 +269,8 @@ private void setupButtonsForFlying() { @Override public void onApiConnected() { + super.onApiConnected(); + setupButtonsByFlightState(); updateFlightModeButtons(); updateFollowButton(); @@ -278,6 +279,7 @@ public void onApiConnected() { @Override public void onApiDisconnected() { + super.onApiDisconnected(); getBroadcastManager().unregisterReceiver(eventReceiver); } @@ -340,11 +342,7 @@ public void onClick(View v) { break; case R.id.mc_follow: { - final FollowState followState = drone.getAttribute(AttributeType.FOLLOW_STATE); - if (followState.isEnabled()) { - drone.disableFollowMe(); - } else - drone.enableFollowMe(FollowType.LEASH); + toggleFollowMe(); break; } diff --git a/Android/src/org/droidplanner/android/fragments/control/RoverFlightControlFragment.java b/Android/src/org/droidplanner/android/fragments/control/RoverFlightControlFragment.java index 9d8c83ff88..1d63e62a83 100644 --- a/Android/src/org/droidplanner/android/fragments/control/RoverFlightControlFragment.java +++ b/Android/src/org/droidplanner/android/fragments/control/RoverFlightControlFragment.java @@ -25,7 +25,7 @@ /** * Created by Fredia Huya-Kouadio on 3/4/15. */ -public class RoverFlightControlFragment extends ApiListenerFragment implements FlightControlManagerFragment.SlidingUpHeader, View.OnClickListener { +public class RoverFlightControlFragment extends BaseFlightControlFragment { private static final String ACTION_FLIGHT_ACTION_BUTTON = "Rover flight action button"; @@ -147,6 +147,7 @@ private void setupButtonsForConnected() { @Override public void onApiConnected() { + super.onApiConnected(); setupButtonsByFlightState(); updateFlightModeButtons(); getBroadcastManager().registerReceiver(eventReceiver, intentFilter); @@ -154,6 +155,7 @@ public void onApiConnected() { @Override public void onApiDisconnected() { + super.onApiDisconnected(); getBroadcastManager().unregisterReceiver(eventReceiver); } diff --git a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java b/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java index 269fb1566c..13b0c33fdf 100644 --- a/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java +++ b/Android/src/org/droidplanner/android/maps/providers/google_map/GoogleMapFragment.java @@ -24,17 +24,11 @@ import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.api.Api; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; -import com.google.android.gms.location.LocationListener; +import com.google.android.gms.location.LocationAvailability; +import com.google.android.gms.location.LocationCallback; import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationServices; -import com.google.android.gms.location.LocationSettingsRequest; -import com.google.android.gms.location.LocationSettingsResult; -import com.google.android.gms.location.LocationSettingsStates; -import com.google.android.gms.location.LocationSettingsStatusCodes; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; @@ -85,7 +79,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -public class GoogleMapFragment extends SupportMapFragment implements DPMap, LocationListener, GoogleApiClientManager.ManagerListener { +public class GoogleMapFragment extends SupportMapFragment implements DPMap, GoogleApiClientManager.ManagerListener { private static final String TAG = GoogleMapFragment.class.getSimpleName(); @@ -96,13 +90,10 @@ public class GoogleMapFragment extends SupportMapFragment implements DPMap, Loca public static final String MAP_TYPE_NORMAL = "Normal"; public static final String MAP_TYPE_TERRAIN = "Terrain"; - // TODO: update the interval based on the user's current activity. private static final long USER_LOCATION_UPDATE_INTERVAL = 30000; // ms - private static final long USER_LOCATION_UPDATE_FASTEST_INTERVAL = 1000; // ms + private static final long USER_LOCATION_UPDATE_FASTEST_INTERVAL = 5000; // ms private static final float USER_LOCATION_UPDATE_MIN_DISPLACEMENT = 0; // m - public static final int REQUEST_CHECK_SETTINGS = 147; - private static final float GO_TO_MY_LOCATION_ZOOM = 19f; private static final IntentFilter eventFilter = new IntentFilter(); @@ -110,7 +101,6 @@ public class GoogleMapFragment extends SupportMapFragment implements DPMap, Loca static { eventFilter.addAction(AttributeEvent.GPS_POSITION); eventFilter.addAction(SettingsFragment.ACTION_MAP_ROTATION_PREFERENCE_UPDATED); - eventFilter.addAction(SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED); } private final static Api[] apisList = new Api[]{LocationServices.API}; @@ -142,23 +132,6 @@ public void onMapReady(GoogleMap googleMap) { } }); break; - - case SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED: - final int resultCode = intent.getIntExtra(SettingsFragment.EXTRA_RESULT_CODE, Activity.RESULT_OK); - switch (resultCode) { - case Activity.RESULT_OK: - // All required changes were successfully made. Try to acquire user location again - mGApiClientMgr.addTask(mRequestLocationUpdateTask); - break; - - case Activity.RESULT_CANCELED: - // The user was asked to change settings, but chose not to - Toast.makeText(getActivity(), "Please update your location settings!", Toast.LENGTH_LONG).show(); - break; - default: - break; - } - break; } } }; @@ -170,6 +143,52 @@ public void onMapReady(GoogleMap googleMap) { private final AtomicReference mPanMode = new AtomicReference( AutoPanMode.DISABLED); + private final Handler handler = new Handler(); + + private final LocationCallback locationCb = new LocationCallback() { + @Override + public void onLocationAvailability(LocationAvailability locationAvailability) { + super.onLocationAvailability(locationAvailability); + } + + @Override + public void onLocationResult(LocationResult result) { + super.onLocationResult(result); + + final Location location = result.getLastLocation(); + if (location == null) + return; + + //Update the user location icon. + if (userMarker == null) { + final MarkerOptions options = new MarkerOptions() + .position(new LatLng(location.getLatitude(), location.getLongitude())) + .draggable(false) + .flat(true) + .visible(true) + .icon(BitmapDescriptorFactory.fromResource(R.drawable.blue)); + + getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(GoogleMap googleMap) { + userMarker = googleMap.addMarker(options); + } + }); + } else { + userMarker.setPosition(new LatLng(location.getLatitude(), location.getLongitude())); + } + + if (mPanMode.get() == AutoPanMode.USER) { + Log.d(TAG, "User location changed."); + updateCamera(DroneHelper.LocationToCoord(location), (int) getMap().getCameraPosition().zoom); + } + + if (mLocationListener != null) { + mLocationListener.onLocationChanged(location); + } + } + }; + private final GoogleApiClientTask mGoToMyLocationTask = new GoogleApiClientTask() { @Override public void doRun() { @@ -186,7 +205,7 @@ public void doRun() { private final GoogleApiClientTask mRemoveLocationUpdateTask = new GoogleApiClientTask() { @Override public void doRun() { - LocationServices.FusedLocationApi.removeLocationUpdates(getGoogleApiClient(), GoogleMapFragment.this); + LocationServices.FusedLocationApi.removeLocationUpdates(getGoogleApiClient(), locationCb); } }; @@ -194,55 +213,13 @@ public void doRun() { @Override public void doRun() { final LocationRequest locationReq = LocationRequest.create() - .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) + .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY) .setFastestInterval(USER_LOCATION_UPDATE_FASTEST_INTERVAL) .setInterval(USER_LOCATION_UPDATE_INTERVAL) .setSmallestDisplacement(USER_LOCATION_UPDATE_MIN_DISPLACEMENT); - final GoogleApiClient googleApiClient = getGoogleApiClient(); - - LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() - .addLocationRequest(locationReq); - - PendingResult result = - LocationServices.SettingsApi.checkLocationSettings(googleApiClient, builder.build()); - - result.setResultCallback(new ResultCallback() { - @Override - public void onResult(LocationSettingsResult callback) { - final Status status = callback.getStatus(); - - switch (status.getStatusCode()) { - case LocationSettingsStatusCodes.SUCCESS: - // All location settings are satisfied. The client can initialize location - // requests here. - LocationServices.FusedLocationApi.requestLocationUpdates(googleApiClient, locationReq, - GoogleMapFragment.this); - break; - - case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: - // Location settings are not satisfied. But could be fixed by showing the user - // a dialog. - try { - // Show the dialog by calling startResolutionForResult(), - // and check the result in onActivityResult(). - status.startResolutionForResult(getActivity(), REQUEST_CHECK_SETTINGS); - } catch (IntentSender.SendIntentException e) { - // Ignore the error. - Log.e(TAG, e.getMessage(), e); - } - break; - - case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: - // Location settings are not satisfied. However, we have no way to fix the - // settings so we won't show the dialog. - Log.w(TAG, "Unable to get accurate user location."); - Toast.makeText(getActivity(), "Unable to get accurate location. Please update your " + - "location settings!", Toast.LENGTH_LONG).show(); - break; - } - } - }); + LocationServices.FusedLocationApi.requestLocationUpdates(getGoogleApiClient(), locationReq, + locationCb, handler.getLooper()); } }; @@ -752,17 +729,17 @@ public void zoomToFit(List coords) { @Override public void onMapReady(GoogleMap googleMap) { final Activity activity = getActivity(); - if(activity == null) + if (activity == null) return; - final View rootView = ((ViewGroup)activity.findViewById(android.R.id.content)).getChildAt(0); - if(rootView == null) + final View rootView = ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0); + if (rootView == null) return; final int height = rootView.getHeight(); final int width = rootView.getWidth(); Log.d(TAG, String.format(Locale.US, "Screen W %d, H %d", width, height)); - if(height > 0 && width > 0) { + if (height > 0 && width > 0) { CameraUpdate animation = CameraUpdateFactory.newLatLngBounds(bounds, width, height, 100); googleMap.animateCamera(animation); } @@ -871,7 +848,7 @@ public boolean onMarkerClick(Marker marker) { if (mMarkerClickListener != null) { final MarkerInfo markerInfo = mBiMarkersMap.getKey(marker); - if(markerInfo != null) + if (markerInfo != null) return mMarkerClickListener.onMarkerClick(markerInfo); } return false; @@ -956,37 +933,6 @@ public double getMapRotation() { } } - @Override - public void onLocationChanged(Location location) { - //Update the user location icon. - if (userMarker == null) { - final MarkerOptions options = new MarkerOptions() - .position(new LatLng(location.getLatitude(), location.getLongitude())) - .draggable(false) - .flat(true) - .visible(true) - .icon(BitmapDescriptorFactory.fromResource(R.drawable.blue)); - - getMapAsync(new OnMapReadyCallback() { - @Override - public void onMapReady(GoogleMap googleMap) { - userMarker = googleMap.addMarker(options); - } - }); - } else { - userMarker.setPosition(new LatLng(location.getLatitude(), location.getLongitude())); - } - - if (mPanMode.get() == AutoPanMode.USER) { - Log.d(TAG, "User location changed."); - updateCamera(DroneHelper.LocationToCoord(location), (int) getMap().getCameraPosition().zoom); - } - - if (mLocationListener != null) { - mLocationListener.onLocationChanged(location); - } - } - @Override public void skipMarkerClickEvents(boolean skip) { useMarkerClickAsMapClick = skip; diff --git a/Android/src/org/droidplanner/android/utils/location/CheckLocationSettings.java b/Android/src/org/droidplanner/android/utils/location/CheckLocationSettings.java new file mode 100644 index 0000000000..45198e5e67 --- /dev/null +++ b/Android/src/org/droidplanner/android/utils/location/CheckLocationSettings.java @@ -0,0 +1,184 @@ +package org.droidplanner.android.utils.location; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Handler; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesUtil; +import com.google.android.gms.common.api.Api; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResult; +import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.o3dr.services.android.lib.util.googleApi.GoogleApiClientManager; + +import org.droidplanner.android.R; +import org.droidplanner.android.activities.DrawerNavigationUI; +import org.droidplanner.android.fragments.SettingsFragment; +import org.droidplanner.android.fragments.control.BaseFlightControlFragment; + +import java.lang.ref.WeakReference; + +/** + * Created by Fredia Huya-Kouadio on 5/25/15. + */ +public class CheckLocationSettings implements GoogleApiClientManager.ManagerListener { + + private static final String TAG = CheckLocationSettings.class.getSimpleName(); + + private final static Api[] apisList = new Api[]{LocationServices.API}; + + /** + * Used to ensure the correct location settings are set before starting follow me. + */ + private final GoogleApiClientManager.GoogleApiClientTask checkLocationSettings = new GoogleApiClientManager.GoogleApiClientTask() { + @Override + protected void doRun() { + final GoogleApiClient googleApiClient = getGoogleApiClient(); + + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() + .addLocationRequest(locationReq); + + PendingResult result = + LocationServices.SettingsApi.checkLocationSettings(googleApiClient, builder.build()); + + result.setResultCallback(new ResultCallback() { + @Override + public void onResult(LocationSettingsResult locationSettingsResult) { + final Status status = locationSettingsResult.getStatus(); + + final DrawerNavigationUI activity = activityRef.get(); + switch (status.getStatusCode()) { + case LocationSettingsStatusCodes.SUCCESS: + if (onSuccess != null) + onSuccess.run(); + break; + + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + if (activity != null) { + // Location settings are not satisfied. But could be fixed by showing the user + // a dialog. + try { + // Show the dialog by calling startResolutionForResult(), + // and check the result in onActivityResult(). + status.startResolutionForResult(activity, BaseFlightControlFragment.FOLLOW_SETTINGS_UPDATE); + } catch (IntentSender.SendIntentException e) { + // Ignore the error. + Log.e(TAG, e.getMessage(), e); + } + } + break; + + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + if (activity != null) { + // Location settings are not satisfied. However, we have no way to fix the + // settings so we won't show the dialog. + Log.w(TAG, "Unable to get accurate user location."); + Toast.makeText(activity, R.string.invalid_location_settings_warning, Toast.LENGTH_LONG).show(); + } + break; + } + + //Stop the google api client manager + gapiMgr.stopSafely(); + } + }); + } + }; + + private final WeakReference activityRef; + private final LocationRequest locationReq; + private final Runnable onSuccess; + private final GoogleApiClientManager gapiMgr; + + public CheckLocationSettings(DrawerNavigationUI activity, LocationRequest locationReq, Runnable onSuccess) { + activityRef = new WeakReference<>(activity); + this.locationReq = locationReq; + this.onSuccess = onSuccess; + + gapiMgr = new GoogleApiClientManager(activity.getApplicationContext(), new Handler(), apisList); + gapiMgr.setManagerListener(this); + } + + public void check() { + gapiMgr.start(); + } + + public void onReceive(Intent intent) { + switch (intent.getAction()) { + case SettingsFragment.ACTION_LOCATION_SETTINGS_UPDATED: + final int resultCode = intent.getIntExtra(SettingsFragment.EXTRA_RESULT_CODE, Activity.RESULT_OK); + switch (resultCode) { + case Activity.RESULT_OK: + // All required changes were successfully made. Try to acquire user location again + if (onSuccess != null) + onSuccess.run(); + break; + + case Activity.RESULT_CANCELED: + // The user was asked to change settings, but chose not to + final Activity activity = activityRef.get(); + if (activity != null) { + Toast.makeText(activity, "Please update your location settings!", Toast.LENGTH_LONG) + .show(); + } + break; + + default: + break; + } + break; + } + } + + @Override + public void onGoogleApiConnectionError(ConnectionResult connectionResult) { + final Activity activity = activityRef.get(); + if (activity == null) + return; + + if (connectionResult.hasResolution()) { + try { + connectionResult.startResolutionForResult(activity, 0); + } catch (IntentSender.SendIntentException e) { + //There was an error with the resolution intent. Try again. + gapiMgr.start(); + } + } else { + onUnavailableGooglePlayServices(connectionResult.getErrorCode()); + } + } + + @Override + public void onUnavailableGooglePlayServices(int i) { + final Activity activity = activityRef.get(); + if (activity != null) { + GooglePlayServicesUtil.showErrorDialogFragment(i, activity, 0, new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + activity.finish(); + } + }); + } + } + + @Override + public void onManagerStarted() { + gapiMgr.addTask(checkLocationSettings); + } + + @Override + public void onManagerStopped() { + + } +} From a7f5879c48c33f23455a2de029efac0a0cafdc1e Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Mon, 25 May 2015 11:52:17 -0700 Subject: [PATCH 07/11] added callback for altitude update events. --- .../org/droidplanner/android/fragments/TelemetryFragment.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Android/src/org/droidplanner/android/fragments/TelemetryFragment.java b/Android/src/org/droidplanner/android/fragments/TelemetryFragment.java index d350ebf866..7f91ee716b 100644 --- a/Android/src/org/droidplanner/android/fragments/TelemetryFragment.java +++ b/Android/src/org/droidplanner/android/fragments/TelemetryFragment.java @@ -38,6 +38,7 @@ public class TelemetryFragment extends ApiListenerFragment { static { eventFilter.addAction(AttributeEvent.ATTITUDE_UPDATED); eventFilter.addAction(AttributeEvent.SPEED_UPDATED); + eventFilter.addAction(AttributeEvent.ALTITUDE_UPDATED); eventFilter.addAction(AttributeEvent.STATE_UPDATED); } @@ -62,7 +63,9 @@ public void onReceive(Context context, Intent intent) { case AttributeEvent.SPEED_UPDATED: final Speed droneSpeed = drone.getAttribute(AttributeType.SPEED); onSpeedUpdate(droneSpeed); + break; + case AttributeEvent.ALTITUDE_UPDATED: final Altitude droneAltitude = drone.getAttribute(AttributeType.ALTITUDE); onAltitudeUpdate(droneAltitude); break; From 98e823849fc678242f4fdbda153189c8706a8d7e Mon Sep 17 00:00:00 2001 From: ne0fhyk Date: Tue, 26 May 2015 04:01:08 -0700 Subject: [PATCH 08/11] Added the ability to customize the altitude preferences. --- Android/res/values/preferences_keys.xml | 4 ++ Android/res/values/strings.xml | 6 ++ Android/res/xml/preferences.xml | 27 +++++++++ .../android/fragments/SettingsFragment.java | 57 ++++++++++++++++++- .../fragments/mode/ModeFollowFragment.java | 9 ++- .../fragments/mode/ModeGuidedFragment.java | 32 ++++++----- .../utils/prefs/DroidPlannerPrefs.java | 51 ++++++++++++++++- .../length/ImperialLengthUnitProvider.java | 9 +++ .../providers/length/LengthUnitProvider.java | 6 ++ .../length/MetricLengthUnitProvider.java | 6 ++ 10 files changed, 186 insertions(+), 21 deletions(-) diff --git a/Android/res/values/preferences_keys.xml b/Android/res/values/preferences_keys.xml index d8854014a9..bfa3b4e3d4 100644 --- a/Android/res/values/preferences_keys.xml +++ b/Android/res/values/preferences_keys.xml @@ -64,4 +64,8 @@ This file is used to store the preferences keys so that it's accessible and modi pref_udp_ping_receiver_ip pref_udp_ping_receiver_port + pref_alt_max_value + pref_alt_min_value + pref_alt_default_value + diff --git a/Android/res/values/strings.xml b/Android/res/values/strings.xml index 239b3e1820..b27a357ee0 100644 --- a/Android/res/values/strings.xml +++ b/Android/res/values/strings.xml @@ -551,4 +551,10 @@ UDP ping receiver port Unable to get accurate location. Please update your location settings! + + Altitude Preferences + Max altitude value + Min altitude value + Default altitude value + diff --git a/Android/res/xml/preferences.xml b/Android/res/xml/preferences.xml index 3b1284e201..64fa776c05 100644 --- a/Android/res/xml/preferences.xml +++ b/Android/res/xml/preferences.xml @@ -282,6 +282,33 @@ android:title="@string/pref_ui_gps_hdop_title" /> + + + + + + + + + +