From: Mike Beattie Date: Wed, 18 Sep 2019 22:04:58 +0000 (+1200) Subject: Move java source, and resources to new paths for gradle X-Git-Tag: 1.9.1~1^2~37^2~12 X-Git-Url: https://git.gag.com/?p=fw%2Faltos;a=commitdiff_plain;h=8b53f860eb3171cd43e4bd0e440f2889bd810662 Move java source, and resources to new paths for gradle Signed-off-by: Mike Beattie --- diff --git a/altosdroid/AndroidManifest.xml.in b/altosdroid/AndroidManifest.xml.in deleted file mode 100644 index 690f1990..00000000 --- a/altosdroid/AndroidManifest.xml.in +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/altosdroid/app/src/main/AndroidManifest.xml.in b/altosdroid/app/src/main/AndroidManifest.xml.in new file mode 100644 index 00000000..690f1990 --- /dev/null +++ b/altosdroid/app/src/main/AndroidManifest.xml.in @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosBluetooth.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosBluetooth.java new file mode 100644 index 00000000..0d09abd8 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosBluetooth.java @@ -0,0 +1,228 @@ +/* + * Copyright © 2011 Keith Packard + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +//import android.os.Bundle; +import android.os.Handler; +//import android.os.Message; + +import org.altusmetrum.altoslib_13.*; + +public class AltosBluetooth extends AltosDroidLink { + + private ConnectThread connect_thread = null; + + private BluetoothDevice device; + private BluetoothSocket socket; + private InputStream input; + private OutputStream output; + private boolean pause; + + // Constructor + public AltosBluetooth(BluetoothDevice device, Handler handler, boolean pause) { + super(handler); + this.device = device; + this.handler = handler; + this.pause = pause; + + connect_thread = new ConnectThread(); + connect_thread.start(); + } + + void connected() { + if (closed()) { + AltosDebug.debug("connected after closed"); + return; + } + + AltosDebug.check_ui("connected\n"); + try { + synchronized(this) { + if (socket != null) { + input = socket.getInputStream(); + output = socket.getOutputStream(); + super.connected(); + } + } + } catch (InterruptedException ie) { + connect_failed(); + } catch (IOException io) { + connect_failed(); + } + } + + private void connect_failed() { + if (closed()) { + AltosDebug.debug("connect_failed after closed"); + return; + } + + close_device(); + input = null; + output = null; + handler.obtainMessage(TelemetryService.MSG_CONNECT_FAILED, this).sendToTarget(); + AltosDebug.error("ConnectThread: Failed to establish connection"); + } + + void close_device() { + BluetoothSocket tmp_socket; + + synchronized(this) { + tmp_socket = socket; + socket = null; + } + + if (tmp_socket != null) { + try { + tmp_socket.close(); + } catch (IOException e) { + AltosDebug.error("close_socket failed"); + } + } + } + + public void close() { + super.close(); + input = null; + output = null; + } + + private final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + private void create_socket(BluetoothDevice device) { + + BluetoothSocket tmp_socket = null; + + AltosDebug.check_ui("create_socket\n"); + try { + tmp_socket = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID); + } catch (IOException e) { + e.printStackTrace(); + } + if (socket != null) { + AltosDebug.debug("Socket already allocated %s", socket.toString()); + close_device(); + } + synchronized (this) { + socket = tmp_socket; + } + } + + private class ConnectThread extends Thread { + + public void run() { + AltosDebug.debug("ConnectThread: BEGIN (pause %b)", pause); + setName("ConnectThread"); + + if (pause) { + try { + Thread.sleep(4000); + } catch (InterruptedException e) { + } + } + + create_socket(device); + // Always cancel discovery because it will slow down a connection + try { + BluetoothAdapter.getDefaultAdapter().cancelDiscovery(); + } catch (Exception e) { + AltosDebug.debug("cancelDiscovery exception %s", e.toString()); + } + + BluetoothSocket local_socket = null; + + synchronized (AltosBluetooth.this) { + if (!closed()) + local_socket = socket; + } + + if (local_socket != null) { + try { + // Make a connection to the BluetoothSocket + // This is a blocking call and will only return on a + // successful connection or an exception + local_socket.connect(); + } catch (Exception e) { + AltosDebug.debug("Connect exception %s", e.toString()); + try { + local_socket.close(); + } catch (Exception ce) { + AltosDebug.debug("Close exception %s", ce.toString()); + } + local_socket = null; + } + } + + if (local_socket != null) { + connected(); + } else { + connect_failed(); + } + + AltosDebug.debug("ConnectThread: completed"); + } + } + + private synchronized void wait_connected() throws InterruptedException, IOException { + AltosDebug.check_ui("wait_connected\n"); + if (input == null && socket != null) { + AltosDebug.debug("wait_connected..."); + wait(); + AltosDebug.debug("wait_connected done"); + } + if (socket == null) + throw new IOException(); + } + + int write(byte[] buffer, int len) { + if (output == null) + return -1; + try { + output.write(buffer, 0, len); + } catch (IOException ie) { + return -1; + } + return len; + } + + int read(byte[] buffer, int len) { + if (input == null) + return -1; + try { + return input.read(buffer, 0, len); + } catch (IOException ie) { + return -1; + } + } + + // Stubs of required methods when extending AltosLink + public boolean can_cancel_reply() { return false; } + public boolean show_reply_timeout() { return true; } + public void hide_reply_timeout() { } + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDebug.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDebug.java new file mode 100644 index 00000000..2ae06522 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDebug.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.altusmetrum.AltosDroid; + +import java.util.Arrays; +import java.io.*; +import java.lang.*; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.graphics.*; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.*; +import android.widget.*; +import android.location.Location; +import android.content.*; +import android.util.Log; +import android.os.*; +import android.content.pm.*; + +public class AltosDebug { + // Debugging + static final String TAG = "AltosDroid"; + + static boolean D = true; + + static void init(Context context) { + ApplicationInfo app_info = context.getApplicationInfo(); + + if ((app_info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + Log.d(TAG, "Enable debugging\n"); + D = true; + } else { + Log.d(TAG, "Disable debugging\n"); + D = false; + } + } + + + static void info(String format, Object ... arguments) { + Log.i(TAG, String.format(format, arguments)); + } + + static void debug(String format, Object ... arguments) { + if (D) + Log.d(TAG, String.format(format, arguments)); + } + + static void error(String format, Object ... arguments) { + Log.e(TAG, String.format(format, arguments)); + } + + static void check_ui(String format, Object ... arguments) { + if (Looper.myLooper() == Looper.getMainLooper()) { + Log.e(TAG, String.format("ON UI THREAD " + format, arguments)); + for (StackTraceElement el : Thread.currentThread().getStackTrace()) + Log.e(TAG, "\t" + el.toString() + "\n"); + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroid.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroid.java new file mode 100644 index 00000000..1bcb67ef --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroid.java @@ -0,0 +1,1224 @@ +/* + * Copyright © 2012-2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.lang.ref.WeakReference; +import java.text.*; +import java.util.*; +import java.io.*; + +import android.app.Activity; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Intent; +import android.content.Context; +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.content.DialogInterface; +import android.os.IBinder; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.content.res.Resources; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.util.DisplayMetrics; +import android.view.*; +import android.widget.*; +import android.app.AlertDialog; +import android.location.Location; +import android.location.LocationManager; +import android.location.LocationListener; +import android.hardware.usb.*; +import android.graphics.*; +import android.graphics.drawable.*; + +import org.altusmetrum.altoslib_13.*; + +class SavedState { + long received_time; + int state; + boolean locked; + String callsign; + int serial; + int flight; + int rssi; + + SavedState(AltosState state) { + received_time = state.received_time; + this.state = state.state(); + if (state.gps != null) + locked = state.gps.locked; + else + locked = false; + callsign = state.cal_data().callsign; + serial = state.cal_data().serial; + flight = state.cal_data().flight; + rssi = state.rssi; + } +} + +class Tracker implements CharSequence, Comparable { + int serial; + String call; + double frequency; + + String display; + + public Tracker(int serial, String call, double frequency) { + if (call == null) + call = "none"; + + this.serial = serial; + this.call = call; + this.frequency = frequency; + if (frequency == 0.0) + display = "Auto"; + else if (frequency == AltosLib.MISSING) { + display = String.format("%-8.8s %6d", call, serial); + } else { + display = String.format("%-8.8s %7.3f %6d", call, frequency, serial); + } + } + + public Tracker(AltosState s) { + this(s == null ? 0 : s.cal_data().serial, + s == null ? null : s.cal_data().callsign, + s == null ? 0.0 : s.frequency); + } + + /* CharSequence */ + public char charAt(int index) { + return display.charAt(index); + } + + public int length() { + return display.length(); + } + + public CharSequence subSequence(int start, int end) throws IndexOutOfBoundsException { + return display.subSequence(start, end); + } + + public String toString() { + return display.toString(); + } + + /* Comparable */ + public int compareTo (Object other) { + Tracker o = (Tracker) other; + if (frequency == 0.0) { + if (o.frequency == 0.0) + return 0; + return -1; + } + if (o.frequency == 0.0) + return 1; + + int a = serial - o.serial; + int b = call.compareTo(o.call); + int c = (int) Math.signum(frequency - o.frequency); + + if (b != 0) + return b; + if (c != 0) + return c; + return a; + } +} + +public class AltosDroid extends FragmentActivity implements AltosUnitsListener, LocationListener { + + // Actions sent to the telemetry server at startup time + + public static final String ACTION_BLUETOOTH = "org.altusmetrum.AltosDroid.BLUETOOTH"; + public static final String ACTION_USB = "org.altusmetrum.AltosDroid.USB"; + + // Message types received by our Handler + + public static final int MSG_STATE = 1; + public static final int MSG_UPDATE_AGE = 2; + public static final int MSG_IDLE_MODE = 3; + public static final int MSG_IGNITER_STATUS = 4; + + // Intent request codes + public static final int REQUEST_CONNECT_DEVICE = 1; + public static final int REQUEST_ENABLE_BT = 2; + public static final int REQUEST_PRELOAD_MAPS = 3; + public static final int REQUEST_IDLE_MODE = 5; + public static final int REQUEST_IGNITERS = 6; + public static final int REQUEST_SETUP = 7; + + public static final String EXTRA_IDLE_MODE = "idle_mode"; + public static final String EXTRA_IDLE_RESULT = "idle_result"; + public static final String EXTRA_TELEMETRY_SERVICE = "telemetry_service"; + + // Setup result bits + public static final int SETUP_BAUD = 1; + public static final int SETUP_UNITS = 2; + public static final int SETUP_MAP_SOURCE = 4; + public static final int SETUP_MAP_TYPE = 8; + + public static FragmentManager fm; + + private BluetoothAdapter mBluetoothAdapter = null; + + // Flight state values + private TextView mCallsignView; + private TextView mRSSIView; + private TextView mSerialView; + private TextView mFlightView; + private RelativeLayout mStateLayout; + private TextView mStateView; + private TextView mAgeView; + private boolean mAgeViewOld; + private int mAgeNewColor; + private int mAgeOldColor; + + public static final String tab_pad_name = "pad"; + public static final String tab_flight_name = "flight"; + public static final String tab_recover_name = "recover"; + public static final String tab_map_name = "map"; + + // field to display the version at the bottom of the screen + private TextView mVersion; + + private boolean idle_mode = false; + + public Location location = null; + + private AltosState state; + private SavedState saved_state; + + // Tabs + TabHost mTabHost; + AltosViewPager mViewPager; + TabsAdapter mTabsAdapter; + ArrayList mTabs = new ArrayList(); + int tabHeight; + + // Timer and Saved flight state for Age calculation + private Timer timer; + + TelemetryState telemetry_state; + Tracker[] trackers; + + + UsbDevice pending_usb_device; + boolean start_with_usb; + + // Service + private boolean mIsBound = false; + private Messenger mService = null; + final Messenger mMessenger = new Messenger(new IncomingHandler(this)); + + // Text to Speech + private AltosVoice mAltosVoice = null; + + // The Handler that gets information back from the Telemetry Service + static class IncomingHandler extends Handler { + private final WeakReference mAltosDroid; + IncomingHandler(AltosDroid ad) { mAltosDroid = new WeakReference(ad); } + + @Override + public void handleMessage(Message msg) { + AltosDroid ad = mAltosDroid.get(); + + switch (msg.what) { + case MSG_STATE: + if (msg.obj == null) { + AltosDebug.debug("telemetry_state null!"); + return; + } + ad.update_state((TelemetryState) msg.obj); + break; + case MSG_UPDATE_AGE: + ad.update_age(); + break; + case MSG_IDLE_MODE: + ad.idle_mode = (Boolean) msg.obj; + ad.update_state(null); + break; + } + } + }; + + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + mService = new Messenger(service); + try { + Message msg = Message.obtain(null, TelemetryService.MSG_REGISTER_CLIENT); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException e) { + // In this case the service has crashed before we could even do anything with it + } + if (pending_usb_device != null) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, pending_usb_device)); + pending_usb_device = null; + } catch (RemoteException e) { + } + } + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been unexpectedly disconnected - process crashed. + mService = null; + } + }; + + void doBindService() { + bindService(new Intent(this, TelemetryService.class), mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + } + + void doUnbindService() { + if (mIsBound) { + // If we have received the service, and hence registered with it, then now is the time to unregister. + if (mService != null) { + try { + Message msg = Message.obtain(null, TelemetryService.MSG_UNREGISTER_CLIENT); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException e) { + // There is nothing special we need to do if the service has crashed. + } + } + // Detach our existing connection. + unbindService(mConnection); + mIsBound = false; + } + } + + public void registerTab(AltosDroidTab mTab) { + mTabs.add(mTab); + } + + public void unregisterTab(AltosDroidTab mTab) { + mTabs.remove(mTab); + } + + public void units_changed(boolean imperial_units) { + for (AltosDroidTab mTab : mTabs) + mTab.units_changed(imperial_units); + } + + void update_title(TelemetryState telemetry_state) { + switch (telemetry_state.connect) { + case TelemetryState.CONNECT_CONNECTED: + if (telemetry_state.config != null) { + String str = String.format("S/N %d %6.3f MHz%s", telemetry_state.config.serial, + telemetry_state.frequency, idle_mode ? " (idle)" : ""); + if (telemetry_state.telemetry_rate != AltosLib.ao_telemetry_rate_38400) + str = str.concat(String.format(" %d bps", + AltosLib.ao_telemetry_rate_values[telemetry_state.telemetry_rate])); + setTitle(str); + } else { + setTitle(R.string.title_connected_to); + } + break; + case TelemetryState.CONNECT_CONNECTING: + if (telemetry_state.address != null) + setTitle(String.format("Connecting to %s...", telemetry_state.address.name)); + else + setTitle("Connecting to something..."); + break; + case TelemetryState.CONNECT_DISCONNECTED: + case TelemetryState.CONNECT_NONE: + setTitle(R.string.title_not_connected); + break; + } + } + + void start_timer() { + if (timer == null) { + timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 1000L, 1000L); + } + } + + void stop_timer() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + } + + int selected_serial = 0; + int current_serial; + long switch_time; + + void set_switch_time() { + switch_time = System.currentTimeMillis(); + selected_serial = 0; + } + + boolean registered_units_listener; + + void update_state(TelemetryState new_telemetry_state) { + + if (new_telemetry_state != null) + telemetry_state = new_telemetry_state; + + if (selected_serial != 0) + current_serial = selected_serial; + + if (current_serial == 0) + current_serial = telemetry_state.latest_serial; + + if (!registered_units_listener) { + registered_units_listener = true; + AltosPreferences.register_units_listener(this); + } + + int num_trackers = 0; + for (AltosState s : telemetry_state.states.values()) { + num_trackers++; + } + + trackers = new Tracker[num_trackers + 1]; + + int n = 0; + trackers[n++] = new Tracker(0, "auto", 0.0); + + for (AltosState s : telemetry_state.states.values()) + trackers[n++] = new Tracker(s); + + Arrays.sort(trackers); + + update_title(telemetry_state); + + AltosState state = null; + boolean aged = true; + + if (telemetry_state.states.containsKey(current_serial)) { + state = telemetry_state.states.get(current_serial); + int age = state_age(state.received_time); + if (age < 20) + aged = false; + if (current_serial == selected_serial) + aged = false; + else if (switch_time != 0 && (switch_time - state.received_time) > 0) + aged = true; + } + + if (aged) { + AltosState newest_state = null; + int newest_age = 0; + + for (int serial : telemetry_state.states.keySet()) { + AltosState existing = telemetry_state.states.get(serial); + int existing_age = state_age(existing.received_time); + + if (newest_state == null || existing_age < newest_age) { + newest_state = existing; + newest_age = existing_age; + } + } + + if (newest_state != null) + state = newest_state; + } + + update_ui(telemetry_state, state, telemetry_state.quiet); + + start_timer(); + } + + boolean same_string(String a, String b) { + if (a == null) { + if (b == null) + return true; + return false; + } else { + if (b == null) + return false; + return a.equals(b); + } + } + + + private int blend_component(int a, int b, double r, int shift, int mask) { + return ((int) (((a >> shift) & mask) * r + ((b >> shift) & mask) * (1 - r)) & mask) << shift; + } + private int blend_color(int a, int b, double r) { + return (blend_component(a, b, r, 0, 0xff) | + blend_component(a, b, r, 8, 0xff) | + blend_component(a, b, r, 16, 0xff) | + blend_component(a, b, r, 24, 0xff)); + } + + int state_age(long received_time) { + return (int) ((System.currentTimeMillis() - received_time + 500) / 1000); + } + + void set_screen_on(int age) { + if (age < 60) + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + void update_age() { + if (saved_state != null) { + int age = state_age(saved_state.received_time); + + double age_scale = age / 100.0; + + if (age_scale > 1.0) + age_scale = 1.0; + + mAgeView.setTextColor(blend_color(mAgeOldColor, mAgeNewColor, age_scale)); + + set_screen_on(age); + + String text; + if (age < 60) + text = String.format("%ds", age); + else if (age < 60 * 60) + text = String.format("%dm", age / 60); + else if (age < 60 * 60 * 24) + text = String.format("%dh", age / (60 * 60)); + else + text = String.format("%dd", age / (24 * 60 * 60)); + mAgeView.setText(text); + } + } + + void update_ui(TelemetryState telem_state, AltosState state, boolean quiet) { + + this.state = state; + + int prev_state = AltosLib.ao_flight_invalid; + + AltosGreatCircle from_receiver = null; + + if (saved_state != null) + prev_state = saved_state.state; + + if (state != null) { + set_screen_on(state_age(state.received_time)); + + if (state.state() == AltosLib.ao_flight_stateless) { + boolean prev_locked = false; + boolean locked = false; + + if(state.gps != null) + locked = state.gps.locked; + if (saved_state != null) + prev_locked = saved_state.locked; + if (prev_locked != locked) { + String currentTab = mTabHost.getCurrentTabTag(); + if (locked) { + if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name); + } else { + if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_pad_name); + } + } + } else { + if (prev_state != state.state()) { + String currentTab = mTabHost.getCurrentTabTag(); + switch (state.state()) { + case AltosLib.ao_flight_boost: + if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name); + break; + case AltosLib.ao_flight_landed: + if (currentTab.equals(tab_flight_name)) mTabHost.setCurrentTabByTag(tab_recover_name); + break; + case AltosLib.ao_flight_stateless: + if (currentTab.equals(tab_pad_name)) mTabHost.setCurrentTabByTag(tab_flight_name); + break; + } + } + } + + if (location != null && state.gps != null && state.gps.locked) { + double altitude = 0; + if (location.hasAltitude()) + altitude = location.getAltitude(); + from_receiver = new AltosGreatCircle(location.getLatitude(), + location.getLongitude(), + altitude, + state.gps.lat, + state.gps.lon, + state.gps.alt); + } + + if (saved_state == null || !same_string(saved_state.callsign, state.cal_data().callsign)) { + mCallsignView.setText(state.cal_data().callsign); + } + if (saved_state == null || state.cal_data().serial != saved_state.serial) { + if (state.cal_data().serial == AltosLib.MISSING) + mSerialView.setText(""); + else + mSerialView.setText(String.format("%d", state.cal_data().serial)); + } + if (saved_state == null || state.cal_data().flight != saved_state.flight) { + if (state.cal_data().flight == AltosLib.MISSING) + mFlightView.setText(""); + else + mFlightView.setText(String.format("%d", state.cal_data().flight)); + } + if (saved_state == null || state.state() != saved_state.state) { + if (state.state() == AltosLib.ao_flight_stateless) { + mStateLayout.setVisibility(View.GONE); + } else { + mStateView.setText(state.state_name()); + mStateLayout.setVisibility(View.VISIBLE); + } + } + if (saved_state == null || state.rssi != saved_state.rssi) { + if (state.rssi == AltosLib.MISSING) + mRSSIView.setText(""); + else + mRSSIView.setText(String.format("%d", state.rssi)); + } + saved_state = new SavedState(state); + } + + for (AltosDroidTab mTab : mTabs) + mTab.update_ui(telem_state, state, from_receiver, location, mTab == mTabsAdapter.currentItem()); + + AltosDebug.debug("quiet %b\n", quiet); + if (mAltosVoice != null) + mAltosVoice.tell(telem_state, state, from_receiver, location, (AltosDroidTab) mTabsAdapter.currentItem(), quiet); + + } + + private void onTimerTick() { + try { + mMessenger.send(Message.obtain(null, MSG_UPDATE_AGE)); + } catch (RemoteException e) { + } + } + + static String pos(double p, String pos, String neg) { + String h = pos; + if (p == AltosLib.MISSING) + return ""; + if (p < 0) { + h = neg; + p = -p; + } + int deg = (int) Math.floor(p); + double min = (p - Math.floor(p)) * 60.0; + return String.format("%d°%9.4f\" %s", deg, min, h); + } + + static String number(String format, double value) { + if (value == AltosLib.MISSING) + return ""; + return String.format(format, value); + } + + static String integer(String format, int value) { + if (value == AltosLib.MISSING) + return ""; + return String.format(format, value); + } + + private View create_tab_view(String label) { + LayoutInflater inflater = (LayoutInflater) this.getLayoutInflater(); + View tab_view = inflater.inflate(R.layout.tab_layout, null); + TextView text_view = (TextView) tab_view.findViewById (R.id.tabLabel); + text_view.setText(label); + return tab_view; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + AltosDebug.init(this); + AltosDebug.debug("+++ ON CREATE +++"); + + // Initialise preferences + AltosDroidPreferences.init(this); + + fm = getSupportFragmentManager(); + + // Set up the window layout + setContentView(R.layout.altosdroid); + + // Create the Tabs and ViewPager + mTabHost = (TabHost)findViewById(android.R.id.tabhost); + mTabHost.setup(); + + mViewPager = (AltosViewPager)findViewById(R.id.pager); + mViewPager.setOffscreenPageLimit(4); + + mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager); + + mTabsAdapter.addTab(mTabHost.newTabSpec(tab_pad_name).setIndicator(create_tab_view("Pad")), TabPad.class, null); + mTabsAdapter.addTab(mTabHost.newTabSpec(tab_flight_name).setIndicator(create_tab_view("Flight")), TabFlight.class, null); + mTabsAdapter.addTab(mTabHost.newTabSpec(tab_recover_name).setIndicator(create_tab_view("Recover")), TabRecover.class, null); + mTabsAdapter.addTab(mTabHost.newTabSpec(tab_map_name).setIndicator(create_tab_view("Map")), TabMap.class, null); + + // Display the Version + mVersion = (TextView) findViewById(R.id.version); + mVersion.setText("Version: " + BuildInfo.version + + " Built: " + BuildInfo.builddate + " " + BuildInfo.buildtime + " " + BuildInfo.buildtz + + " (" + BuildInfo.branch + "-" + BuildInfo.commitnum + "-" + BuildInfo.commithash + ")"); + + mCallsignView = (TextView) findViewById(R.id.callsign_value); + mRSSIView = (TextView) findViewById(R.id.rssi_value); + mSerialView = (TextView) findViewById(R.id.serial_value); + mFlightView = (TextView) findViewById(R.id.flight_value); + mStateLayout = (RelativeLayout) findViewById(R.id.state_container); + mStateView = (TextView) findViewById(R.id.state_value); + mAgeView = (TextView) findViewById(R.id.age_value); + mAgeNewColor = mAgeView.getTextColors().getDefaultColor(); + mAgeOldColor = getResources().getColor(R.color.old_color); + } + + private void ensureBluetooth() { + // Get local Bluetooth adapter + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + /* if there is a BT adapter and it isn't turned on, then turn it on */ + if (mBluetoothAdapter != null && !mBluetoothAdapter.isEnabled()) { + Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableIntent, AltosDroid.REQUEST_ENABLE_BT); + } + } + + private boolean check_usb() { + UsbDevice device = AltosUsb.find_device(this, AltosLib.product_basestation); + + if (device != null) { + Intent i = new Intent(this, AltosDroid.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, new Intent("hello world", null, this, AltosDroid.class), 0); + + if (AltosUsb.request_permission(this, device, pi)) { + connectUsb(device); + } + start_with_usb = true; + return true; + } + + start_with_usb = false; + + return false; + } + + private void noticeIntent(Intent intent) { + + /* Ok, this is pretty convenient. + * + * When a USB device is plugged in, and our 'hotplug' + * intent registration fires, we get an Intent with + * EXTRA_DEVICE set. + * + * When we start up and see a usb device and request + * permission to access it, that queues a + * PendingIntent, which has the EXTRA_DEVICE added in, + * along with the EXTRA_PERMISSION_GRANTED field as + * well. + * + * So, in both cases, we get the device name using the + * same call. We check to see if access was granted, + * in which case we ignore the device field and do our + * usual startup thing. + */ + + UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true); + + AltosDebug.debug("intent %s device %s granted %s", intent, device, granted); + + if (!granted) + device = null; + + if (device != null) { + AltosDebug.debug("intent has usb device " + device.toString()); + connectUsb(device); + } else { + + /* 'granted' is only false if this intent came + * from the request_permission call and + * permission was denied. In which case, we + * don't want to loop forever... + */ + if (granted) { + AltosDebug.debug("check for a USB device at startup"); + if (check_usb()) + return; + } + AltosDebug.debug("Starting by looking for bluetooth devices"); + ensureBluetooth(); + } + } + + @Override + public void onStart() { + super.onStart(); + AltosDebug.debug("++ ON START ++"); + + set_switch_time(); + + noticeIntent(getIntent()); + + // Start Telemetry Service + String action = start_with_usb ? ACTION_USB : ACTION_BLUETOOTH; + + startService(new Intent(action, null, AltosDroid.this, TelemetryService.class)); + + doBindService(); + + if (mAltosVoice == null) + mAltosVoice = new AltosVoice(this); + + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + AltosDebug.debug("onNewIntent"); + noticeIntent(intent); + } + + @Override + public void onResume() { + super.onResume(); + AltosDebug.debug("+ ON RESUME +"); + + // Listen for GPS and Network position updates + LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this); + + location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + + if (location != null) + AltosDebug.debug("Resume, location is %f,%f\n", + location.getLatitude(), + location.getLongitude()); + + update_ui(telemetry_state, state, true); + } + + @Override + public void onPause() { + super.onPause(); + AltosDebug.debug("- ON PAUSE -"); + // Stop listening for location updates + ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this); + } + + @Override + public void onStop() { + super.onStop(); + AltosDebug.debug("-- ON STOP --"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + AltosDebug.debug("--- ON DESTROY ---"); + + doUnbindService(); + if (mAltosVoice != null) { + mAltosVoice.stop(); + mAltosVoice = null; + } + stop_timer(); + } + + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + AltosDebug.debug("onActivityResult " + resultCode); + switch (requestCode) { + case REQUEST_CONNECT_DEVICE: + // When DeviceListActivity returns with a device to connect to + if (resultCode == Activity.RESULT_OK) { + connectDevice(data); + } + break; + case REQUEST_ENABLE_BT: + // When the request to enable Bluetooth returns + if (resultCode == Activity.RESULT_OK) { + // Bluetooth is now enabled, so set up a chat session + //setupChat(); + AltosDebug.debug("BT enabled"); + bluetoothEnabled(data); + } else { + // User did not enable Bluetooth or an error occured + AltosDebug.debug("BT not enabled"); + } + break; + case REQUEST_IDLE_MODE: + if (resultCode == Activity.RESULT_OK) + idle_mode(data); + break; + case REQUEST_IGNITERS: + break; + case REQUEST_SETUP: + if (resultCode == Activity.RESULT_OK) + note_setup_changes(data); + break; + } + } + + private void note_setup_changes(Intent data) { + int changes = data.getIntExtra(SetupActivity.EXTRA_SETUP_CHANGES, 0); + + if ((changes & SETUP_BAUD) != 0) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, + AltosPreferences.telemetry_rate(1))); + } catch (RemoteException re) { + } + } + if ((changes & SETUP_UNITS) != 0) { + /* nothing to do here */ + } + if ((changes & SETUP_MAP_SOURCE) != 0) { + /* nothing to do here */ + } + if ((changes & SETUP_MAP_TYPE) != 0) { + /* nothing to do here */ + } + set_switch_time(); + } + + private void connectUsb(UsbDevice device) { + if (mService == null) + pending_usb_device = device; + else { + // Attempt to connect to the device + try { + mService.send(Message.obtain(null, TelemetryService.MSG_OPEN_USB, device)); + AltosDebug.debug("Sent OPEN_USB message"); + } catch (RemoteException e) { + AltosDebug.debug("connect device message failed"); + } + } + } + + private void bluetoothEnabled(Intent data) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_BLUETOOTH_ENABLED, null)); + } catch (RemoteException e) { + AltosDebug.debug("send BT enabled message failed"); + } + } + + private void connectDevice(Intent data) { + // Attempt to connect to the device + try { + String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); + String name = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_NAME); + + AltosDebug.debug("Connecting to " + address + " " + name); + DeviceAddress a = new DeviceAddress(address, name); + mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, a)); + AltosDebug.debug("Sent connecting message"); + } catch (RemoteException e) { + AltosDebug.debug("connect device message failed"); + } + } + + private void disconnectDevice(boolean remember) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_DISCONNECT, (Boolean) remember)); + } catch (RemoteException e) { + } + } + + private void idle_mode(Intent data) { + int type = data.getIntExtra(IdleModeActivity.EXTRA_IDLE_RESULT, -1); + Message msg; + + AltosDebug.debug("intent idle_mode %d", type); + switch (type) { + case IdleModeActivity.IDLE_MODE_CONNECT: + msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_START); + try { + mService.send(msg); + } catch (RemoteException re) { + } + break; + case IdleModeActivity.IDLE_MODE_DISCONNECT: + msg = Message.obtain(null, TelemetryService.MSG_MONITOR_IDLE_STOP); + try { + mService.send(msg); + } catch (RemoteException re) { + } + break; + case IdleModeActivity.IDLE_MODE_REBOOT: + msg = Message.obtain(null, TelemetryService.MSG_REBOOT); + try { + mService.send(msg); + } catch (RemoteException re) { + } + break; + case IdleModeActivity.IDLE_MODE_IGNITERS: + Intent serverIntent = new Intent(this, IgniterActivity.class); + startActivityForResult(serverIntent, REQUEST_IGNITERS); + break; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.option_menu, menu); + return true; + } + + void setFrequency(double freq) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq)); + set_switch_time(); + } catch (RemoteException e) { + } + } + + void setFrequency(AltosFrequency frequency) { + setFrequency (frequency.frequency); + } + + void setBaud(int baud) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud)); + set_switch_time(); + } catch (RemoteException e) { + } + } + + void setBaud(String baud) { + try { + int value = Integer.parseInt(baud); + int rate = AltosLib.ao_telemetry_rate_38400; + switch (value) { + case 2400: + rate = AltosLib.ao_telemetry_rate_2400; + break; + case 9600: + rate = AltosLib.ao_telemetry_rate_9600; + break; + case 38400: + rate = AltosLib.ao_telemetry_rate_38400; + break; + } + setBaud(rate); + } catch (NumberFormatException e) { + } + } + + void select_tracker(int serial) { + int i; + + AltosDebug.debug("select tracker %d\n", serial); + + if (serial == selected_serial) { + AltosDebug.debug("%d already selected\n", serial); + return; + } + + if (serial != 0) { + for (i = 0; i < trackers.length; i++) + if (trackers[i].serial == serial) + break; + + if (i == trackers.length) { + AltosDebug.debug("attempt to select unknown tracker %d\n", serial); + return; + } + } + + current_serial = selected_serial = serial; + update_state(null); + } + + void touch_trackers(Integer[] serials) { + AlertDialog.Builder builder_tracker = new AlertDialog.Builder(this); + builder_tracker.setTitle("Select Tracker"); + + final Tracker[] my_trackers = new Tracker[serials.length + 1]; + + my_trackers[0] = new Tracker(null); + + for (int i = 0; i < serials.length; i++) { + AltosState s = telemetry_state.states.get(serials[i]); + my_trackers[i+1] = new Tracker(s); + } + builder_tracker.setItems(my_trackers, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + if (item == 0) + select_tracker(0); + else + select_tracker(my_trackers[item].serial); + } + }); + AlertDialog alert_tracker = builder_tracker.create(); + alert_tracker.show(); + } + + void delete_track(int serial) { + try { + mService.send(Message.obtain(null, TelemetryService.MSG_DELETE_SERIAL, (Integer) serial)); + } catch (Exception ex) { + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Intent serverIntent = null; + switch (item.getItemId()) { + case R.id.connect_scan: + ensureBluetooth(); + // Launch the DeviceListActivity to see devices and do scan + serverIntent = new Intent(this, DeviceListActivity.class); + startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE); + return true; + case R.id.disconnect: + /* Disconnect the device + */ + disconnectDevice(false); + return true; + case R.id.quit: + AltosDebug.debug("R.id.quit"); + disconnectDevice(true); + finish(); + return true; + case R.id.setup: + serverIntent = new Intent(this, SetupActivity.class); + startActivityForResult(serverIntent, REQUEST_SETUP); + return true; + case R.id.select_freq: + // Set the TBT radio frequency + + final AltosFrequency[] frequencies = AltosPreferences.common_frequencies(); + String[] frequency_strings = new String[frequencies.length]; + for (int i = 0; i < frequencies.length; i++) + frequency_strings[i] = frequencies[i].toString(); + + AlertDialog.Builder builder_freq = new AlertDialog.Builder(this); + builder_freq.setTitle("Pick a frequency"); + builder_freq.setItems(frequency_strings, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + setFrequency(frequencies[item]); + } + }); + AlertDialog alert_freq = builder_freq.create(); + alert_freq.show(); + return true; + case R.id.select_tracker: + if (trackers != null) { + AlertDialog.Builder builder_serial = new AlertDialog.Builder(this); + builder_serial.setTitle("Select a tracker"); + builder_serial.setItems(trackers, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + System.out.printf("select item %d %s\n", item, trackers[item].display); + if (item == 0) + select_tracker(0); + else + select_tracker(trackers[item].serial); + } + }); + AlertDialog alert_serial = builder_serial.create(); + alert_serial.show(); + + } + return true; + case R.id.delete_track: + if (trackers != null) { + AlertDialog.Builder builder_serial = new AlertDialog.Builder(this); + builder_serial.setTitle("Delete a track"); + final Tracker[] my_trackers = new Tracker[trackers.length - 1]; + for (int i = 0; i < trackers.length - 1; i++) + my_trackers[i] = trackers[i+1]; + builder_serial.setItems(my_trackers, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + delete_track(my_trackers[item].serial); + } + }); + AlertDialog alert_serial = builder_serial.create(); + alert_serial.show(); + + } + return true; + case R.id.idle_mode: + serverIntent = new Intent(this, IdleModeActivity.class); + serverIntent.putExtra(EXTRA_IDLE_MODE, idle_mode); + startActivityForResult(serverIntent, REQUEST_IDLE_MODE); + return true; + } + return false; + } + + static String direction(AltosGreatCircle from_receiver, + Location receiver) { + if (from_receiver == null) + return null; + + if (receiver == null) + return null; + + if (!receiver.hasBearing()) + return null; + + float bearing = receiver.getBearing(); + float heading = (float) from_receiver.bearing - bearing; + + while (heading <= -180.0f) + heading += 360.0f; + while (heading > 180.0f) + heading -= 360.0f; + + int iheading = (int) (heading + 0.5f); + + if (-1 < iheading && iheading < 1) + return "ahead"; + else if (iheading < -179 || 179 < iheading) + return "backwards"; + else if (iheading < 0) + return String.format("left %d°", -iheading); + else + return String.format("right %d°", iheading); + } + + public void onLocationChanged(Location location) { + this.location = location; + AltosDebug.debug("Location changed to %f,%f", + location.getLatitude(), + location.getLongitude()); + update_ui(telemetry_state, state, false); + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + AltosDebug.debug("Location status now %d\n", status); + } + + public void onProviderEnabled(String provider) { + AltosDebug.debug("Location provider enabled %s\n", provider); + } + + public void onProviderDisabled(String provider) { + AltosDebug.debug("Location provider disabled %s\n", provider); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidLink.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidLink.java new file mode 100644 index 00000000..a443b6e9 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidLink.java @@ -0,0 +1,219 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; + +import android.os.Handler; + +import org.altusmetrum.altoslib_13.*; + +public abstract class AltosDroidLink extends AltosLink { + + Handler handler; + + Thread input_thread = null; + + public double frequency() { + return frequency; + } + + public int telemetry_rate() { + return telemetry_rate; + } + + public void save_frequency() { + AltosPreferences.set_frequency(0, frequency); + } + + public void save_telemetry_rate() { + AltosPreferences.set_telemetry_rate(0, telemetry_rate); + } + + Object closed_lock = new Object(); + boolean closing = false; + boolean closed = false; + + public boolean closed() { + synchronized(closed_lock) { + return closing; + } + } + + void connected() throws InterruptedException { + input_thread = new Thread(this); + input_thread.start(); + + // Configure the newly connected device for telemetry + print("~\nE 0\n"); + set_monitor(false); + AltosDebug.debug("ConnectThread: connected"); + + /* Let TelemetryService know we're connected + */ + handler.obtainMessage(TelemetryService.MSG_CONNECTED, this).sendToTarget(); + + /* Notify other waiting threads that we're connected now + */ + notifyAll(); + } + + public void closing() { + synchronized(closed_lock) { + AltosDebug.debug("Marked closing true"); + closing = true; + } + } + + private boolean actually_closed() { + synchronized(closed_lock) { + return closed; + } + } + + abstract void close_device(); + + public void close() { + AltosDebug.debug("close(): begin"); + + closing(); + + flush_output(); + + synchronized (closed_lock) { + AltosDebug.debug("Marked closed true"); + closed = true; + } + + close_device(); + + synchronized(this) { + + if (input_thread != null) { + AltosDebug.debug("close(): stopping input_thread"); + try { + AltosDebug.debug("close(): input_thread.interrupt()....."); + input_thread.interrupt(); + AltosDebug.debug("close(): input_thread.join()....."); + input_thread.join(); + } catch (Exception e) {} + input_thread = null; + } + notifyAll(); + } + } + + abstract int write(byte[] buffer, int len); + + abstract int read(byte[] buffer, int len); + + private static final int buffer_size = 64; + + private byte[] in_buffer = new byte[buffer_size]; + private byte[] out_buffer = new byte[buffer_size]; + private int buffer_len = 0; + private int buffer_off = 0; + private int out_buffer_off = 0; + + private byte[] debug_chars = new byte[buffer_size]; + private int debug_off; + + private void debug_input(byte b) { + if (b == '\n') { + AltosDebug.debug(" " + new String(debug_chars, 0, debug_off)); + debug_off = 0; + } else { + if (debug_off < buffer_size) + debug_chars[debug_off++] = b; + } + } + + private void disconnected() { + if (closed()) { + AltosDebug.debug("disconnected after closed"); + return; + } + + AltosDebug.debug("Sending disconnected message"); + handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, this).sendToTarget(); + } + + public int getchar() { + + if (actually_closed()) + return ERROR; + + while (buffer_off == buffer_len) { + buffer_len = read(in_buffer, buffer_size); + if (buffer_len < 0) { + AltosDebug.debug("ERROR returned from getchar()"); + disconnected(); + return ERROR; + } + buffer_off = 0; + } +// if (AltosDebug.D) +// debug_input(in_buffer[buffer_off]); + return in_buffer[buffer_off++]; + } + + public void flush_output() { + super.flush_output(); + + if (actually_closed()) { + out_buffer_off = 0; + return; + } + + while (out_buffer_off != 0) { + int sent = write(out_buffer, out_buffer_off); + + if (sent <= 0) { + AltosDebug.debug("flush_output() failed"); + out_buffer_off = 0; + break; + } + + if (sent < out_buffer_off) + System.arraycopy(out_buffer, 0, out_buffer, sent, out_buffer_off - sent); + + out_buffer_off -= sent; + } + } + + public void putchar(byte c) { + out_buffer[out_buffer_off++] = c; + if (out_buffer_off == buffer_size) + flush_output(); + } + + public void print(String data) { + byte[] bytes = data.getBytes(); +// AltosDebug.debug(data.replace('\n', '\\')); + for (byte b : bytes) + putchar(b); + } + + public AltosDroidLink(Handler handler) { + this.handler = handler; + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapInterface.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapInterface.java new file mode 100644 index 00000000..6da90a3d --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapInterface.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import java.io.*; +import android.location.Location; +import org.altusmetrum.altoslib_13.*; + +public interface AltosDroidMapInterface { + public void onCreateView(AltosDroid altos_droid); + + public void onDestroyView(); + + public void set_visible(boolean visible); + + public void center(double lat, double lon, double accuracy); + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver); +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapSourceListener.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapSourceListener.java new file mode 100644 index 00000000..294094c2 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidMapSourceListener.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2016 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +public interface AltosDroidMapSourceListener { + public void map_source_changed(int map_source); +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferences.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferences.java new file mode 100644 index 00000000..8bb78c00 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferences.java @@ -0,0 +1,112 @@ +/* + * Copyright © 2014 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.altusmetrum.AltosDroid; + +import java.io.*; +import java.util.*; +import java.text.*; + +import android.content.Context; +import org.altusmetrum.altoslib_13.*; + +public class AltosDroidPreferences extends AltosPreferences { + + /* Active device preference name */ + final static String activeDeviceAddressPreference = "ACTIVE-DEVICE-ADDRESS"; + final static String activeDeviceNamePreference = "ACTIVE-DEVICE-NAME"; + + static DeviceAddress active_device_address; + + /* Map source preference name */ + final static String mapSourcePreference = "MAP-SOURCE"; + + static final int MAP_SOURCE_OFFLINE = 0; + static final int MAP_SOURCE_ONLINE = 1; + + static int map_source; + + public static void init(Context context) { + if (backend != null) + return; + + AltosPreferences.init(new AltosDroidPreferencesBackend(context)); + + String address = backend.getString(activeDeviceAddressPreference, null); + String name = backend.getString(activeDeviceNamePreference, null); + + if (address != null && name != null) + active_device_address = new DeviceAddress (address, name); + + map_source = backend.getInt(mapSourcePreference, MAP_SOURCE_ONLINE); + } + + public static void set_active_device(DeviceAddress address) { + synchronized(backend) { + active_device_address = address; + if (active_device_address != null) { + backend.putString(activeDeviceAddressPreference, active_device_address.address); + backend.putString(activeDeviceNamePreference, active_device_address.name); + } else { + backend.remove(activeDeviceAddressPreference); + backend.remove(activeDeviceNamePreference); + } + flush_preferences(); + } + } + + public static DeviceAddress active_device() { + synchronized(backend) { + return active_device_address; + } + } + + static LinkedList map_source_listeners; + + public static void set_map_source(int map_source) { + synchronized(backend) { + AltosDroidPreferences.map_source = map_source; + backend.putInt(mapSourcePreference, map_source); + flush_preferences(); + } + if (map_source_listeners != null) { + for (AltosDroidMapSourceListener l : map_source_listeners) { + l.map_source_changed(map_source); + } + } + } + + public static int map_source() { + synchronized(backend) { + return map_source; + } + } + + public static void register_map_source_listener(AltosDroidMapSourceListener l) { + synchronized(backend) { + if (map_source_listeners == null) + map_source_listeners = new LinkedList(); + map_source_listeners.add(l); + } + } + + public static void unregister_map_source_listener(AltosDroidMapSourceListener l) { + synchronized(backend) { + map_source_listeners.remove(l); + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferencesBackend.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferencesBackend.java new file mode 100644 index 00000000..854fe86d --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidPreferencesBackend.java @@ -0,0 +1,144 @@ +/* + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.io.File; +import java.util.Map; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; +import android.util.*; + +import org.altusmetrum.altoslib_13.*; + +public class AltosDroidPreferencesBackend extends AltosPreferencesBackend { + public final static String NAME = "org.altusmetrum.AltosDroid"; + private Context context = null; + private SharedPreferences prefs = null; + private SharedPreferences.Editor editor = null; + + public AltosDroidPreferencesBackend(Context in_context) { + this(in_context, NAME); + } + + public AltosDroidPreferencesBackend(Context in_context, String in_prefs) { + context = in_context; + prefs = context.getSharedPreferences(in_prefs, 0); + editor = prefs.edit(); + } + + public String[] keys() { + Map all = prefs.getAll(); + Object[] ao = all.keySet().toArray(); + + String[] as = new String[ao.length]; + for (int i = 0; i < ao.length; i++) + as[i] = (String) ao[i]; + return as; + } + + public AltosPreferencesBackend node(String key) { + if (!nodeExists(key)) + putBoolean(key, true); + return new AltosDroidPreferencesBackend(context, key); + } + + public boolean nodeExists(String key) { + return prefs.contains(key); + } + + public boolean getBoolean(String key, boolean def) { + return prefs.getBoolean(key, def); + } + + public double getDouble(String key, double def) { + Float f = Float.valueOf(prefs.getFloat(key, (float)def)); + return f.doubleValue(); + } + + public int getInt(String key, int def) { + return prefs.getInt(key, def); + } + + public String getString(String key, String def) { + String ret; + ret = prefs.getString(key, def); +// AltosDebug.debug("AltosDroidPreferencesBackend get string %s:\n", key); +// if (ret == null) +// AltosDebug.debug(" (null)\n"); +// else { +// String[] lines = ret.split("\n"); +// for (String l : lines) +// AltosDebug.debug(" %s\n", l); +// } + return ret; + } + + public byte[] getBytes(String key, byte[] def) { + String save = prefs.getString(key, null); + + if (save == null) + return def; + + byte[] bytes = Base64.decode(save, Base64.DEFAULT); + return bytes; + } + + public void putBoolean(String key, boolean value) { + editor.putBoolean(key, value); + } + + public void putDouble(String key, double value) { + editor.putFloat(key, (float)value); + } + + public void putInt(String key, int value) { + editor.putInt(key, value); + } + + public void putString(String key, String value) { +// AltosDebug.debug("AltosDroidPreferencesBackend put string %s:\n", key); +// String[] lines = value.split("\n"); +// for (String l : lines) +// AltosDebug.debug(" %s\n", l); + editor.putString(key, value); + } + + public void putBytes(String key, byte[] bytes) { + String save = Base64.encodeToString(bytes, Base64.DEFAULT); + editor.putString(key, save); + } + + public void remove(String key) { + AltosDebug.debug("remove preference %s\n", key); + editor.remove(key); + } + + public void flush() { + editor.apply(); + } + + public File homeDirectory() { + return Environment.getExternalStorageDirectory(); + } + + public void debug(String format, Object ... arguments) { + AltosDebug.debug(format, arguments); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidTab.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidTab.java new file mode 100644 index 00000000..9594f85f --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosDroidTab.java @@ -0,0 +1,105 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import org.altusmetrum.altoslib_13.*; +import android.location.Location; +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.FragmentManager; +import android.location.Location; +import android.widget.TextView; + +public abstract class AltosDroidTab extends Fragment implements AltosUnitsListener { + TelemetryState last_telem_state; + AltosState last_state; + AltosGreatCircle last_from_receiver; + Location last_receiver; + AltosDroid altos_droid; + + public abstract void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver); + + public abstract String tab_name(); + + public void units_changed(boolean imperial_units) { + if (!isHidden()) + show(last_telem_state, last_state, last_from_receiver, last_receiver); + } + + public void set_value(TextView text_view, + AltosUnits units, + int width, + double value) { + if (value == AltosLib.MISSING) + text_view.setText(""); + else + text_view.setText(units.show(width, value)); + } + + public void set_visible(boolean visible) { + FragmentTransaction ft = AltosDroid.fm.beginTransaction(); + AltosDebug.debug("set visible %b %s\n", visible, tab_name()); + if (visible) { + ft.show(this); + show(last_telem_state, last_state, last_from_receiver, last_receiver); + } else + ft.hide(this); + try { + ft.commitAllowingStateLoss(); + } catch (IllegalStateException ie) { + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + altos_droid = (AltosDroid) activity; + altos_droid.registerTab(this); + } + + @Override + public void onDetach() { + super.onDetach(); + altos_droid.unregisterTab(this); + altos_droid = null; + } + + @Override + public void onResume() { + super.onResume(); + AltosDebug.debug("onResume tab %s\n", tab_name()); + set_visible(true); + } + + public void update_ui(TelemetryState telem_state, AltosState state, + AltosGreatCircle from_receiver, Location receiver, boolean is_current) + { + last_telem_state = telem_state; + last_state = state; + last_from_receiver = from_receiver; + last_receiver = receiver; + if (is_current) + show(telem_state, state, from_receiver, receiver); + else + return; + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOffline.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOffline.java new file mode 100644 index 00000000..1aebcd36 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOffline.java @@ -0,0 +1,532 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import java.io.*; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.graphics.*; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.*; +import android.widget.*; +import android.location.Location; +import android.content.*; +import android.util.*; + +class Rocket implements Comparable { + AltosLatLon position; + String name; + int serial; + long last_packet; + boolean active; + AltosMapOffline map_offline; + + void paint() { + map_offline.draw_bitmap(position, map_offline.rocket_bitmap, map_offline.rocket_off_x, map_offline.rocket_off_y); + map_offline.draw_text(position, name, 0, 3*map_offline.rocket_bitmap.getHeight()/4); + } + + void set_position(AltosLatLon position, long last_packet) { + this.position = position; + this.last_packet = last_packet; + } + + void set_active(boolean active) { + this.active = active; + } + + public int compareTo(Object o) { + Rocket other = (Rocket) o; + + if (active && !other.active) + return 1; + if (other.active && !active) + return -1; + + long diff = last_packet - other.last_packet; + + if (diff > 0) + return 1; + if (diff < 0) + return -1; + return 0; + } + + Rocket(int serial, AltosMapOffline map_offline) { + this.serial = serial; + this.name = String.format("%d", serial); + this.map_offline = map_offline; + } +} + +public class AltosMapOffline extends View implements ScaleGestureDetector.OnScaleGestureListener, AltosMapInterface, AltosDroidMapInterface, AltosMapTypeListener { + ScaleGestureDetector scale_detector; + boolean scaling; + AltosMap map; + AltosDroid altos_droid; + + static int scale = 1; + + AltosLatLon here; + AltosLatLon there; + AltosLatLon pad; + + Canvas canvas; + Paint paint; + + Bitmap pad_bitmap; + int pad_off_x, pad_off_y; + Bitmap rocket_bitmap; + int rocket_off_x, rocket_off_y; + Bitmap here_bitmap; + int here_off_x, here_off_y; + + static final int WHITE = 0xffffffff; + static final int RED = 0xffff0000; + static final int PINK = 0xffff8080; + static final int YELLOW= 0xffffff00; + static final int CYAN = 0xff00ffff; + static final int BLUE = 0xff0000ff; + static final int BLACK = 0xff000000; + + public static final int stateColors[] = { + WHITE, // startup + WHITE, // idle + WHITE, // pad + RED, // boost + PINK, // fast + YELLOW, // coast + CYAN, // drogue + BLUE, // main + BLACK, // landed + BLACK, // invalid + CYAN, // stateless + }; + + /* AltosMapInterface */ + public void debug(String format, Object ... arguments) { + AltosDebug.debug(format, arguments); + } + + class MapTile extends AltosMapTile { + public void paint(AltosMapTransform t) { + AltosPointInt pt = new AltosPointInt(t.screen(upper_left)); + + if (canvas.quickReject(pt.x, pt.y, pt.x + px_size, pt.y + px_size, Canvas.EdgeType.AA)) + return; + + AltosImage altos_image = this.get_image(); + + MapImage map_image = (MapImage) altos_image; + + Bitmap bitmap = null; + + if (map_image != null) + bitmap = map_image.bitmap; + + if (bitmap != null) { + canvas.drawBitmap(bitmap, pt.x, pt.y, paint); + } else { + paint.setColor(0xff808080); + canvas.drawRect(pt.x, pt.y, pt.x + px_size, pt.y + px_size, paint); + if (t.has_location()) { + String message = null; + switch (status) { + case AltosMapTile.fetching: + message = "Fetching..."; + break; + case AltosMapTile.bad_request: + message = "Internal error"; + break; + case AltosMapTile.failed: + message = "Network error"; + break; + case AltosMapTile.forbidden: + message = "Outside of known launch areas"; + break; + } + if (message != null) { + Rect bounds = new Rect(); + paint.getTextBounds(message, 0, message.length(), bounds); + + int width = bounds.right - bounds.left; + int height = bounds.bottom - bounds.top; + + float x = pt.x + px_size / 2.0f; + float y = pt.y + px_size / 2.0f; + x = x - width / 2.0f; + y = y + height / 2.0f; + paint.setColor(0xff000000); + canvas.drawText(message, 0, message.length(), x, y, paint); + } + } + } + } + + public MapTile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) { + super(cache, upper_left, center, zoom, maptype, px_size, scale); + } + + } + + public AltosMapTile new_tile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) { + return new MapTile(cache, upper_left, center, zoom, maptype, px_size, scale); + } + + public AltosMapPath new_path() { + return null; + } + + public AltosMapLine new_line() { + return null; + } + + class MapImage implements AltosImage { + public Bitmap bitmap; + + public void flush() { + if (bitmap != null) { + bitmap.recycle(); + bitmap = null; + } + } + + public MapImage(File file) { + bitmap = BitmapFactory.decodeFile(file.getPath()); + } + } + + public AltosImage load_image(File file) throws Exception { + return new MapImage(file); + } + + class MapMark extends AltosMapMark { + public void paint(AltosMapTransform t) { + } + + MapMark(double lat, double lon, int state) { + super(lat, lon, state); + } + } + + public AltosMapMark new_mark(double lat, double lon, int state) { + return new MapMark(lat, lon, state); + } + + public int width() { + return getWidth(); + } + + public int height() { + return getHeight(); + } + + public void repaint() { + postInvalidate(); + } + + public void repaint(AltosRectangle damage) { + postInvalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height); + } + + public void set_zoom_label(String label) { + } + + public void select_object(AltosLatLon latlon) { + if (map.transform == null) + return; + ArrayList near = new ArrayList(); + + for (Rocket rocket : sorted_rockets()) { + if (rocket.position == null) { + debug("rocket %d has no position\n", rocket.serial); + continue; + } + double distance = map.transform.hypot(latlon, rocket.position); + debug("check select %d distance %g width %d\n", rocket.serial, distance, rocket_bitmap.getWidth()); + if (distance < rocket_bitmap.getWidth() * 2.0) { + debug("selecting %d\n", rocket.serial); + near.add(rocket.serial); + } + } + if (near.size() != 0) + altos_droid.touch_trackers(near.toArray(new Integer[0])); + } + + class Line { + AltosLatLon a, b; + + void paint() { + if (a != null && b != null) { + AltosPointDouble a_screen = map.transform.screen(a); + AltosPointDouble b_screen = map.transform.screen(b); + paint.setColor(0xff8080ff); + canvas.drawLine((float) a_screen.x, (float) a_screen.y, + (float) b_screen.x, (float) b_screen.y, + paint); + } + } + + void set_a(AltosLatLon a) { + this.a = a; + } + + void set_b(AltosLatLon b) { + this.b = b; + } + + Line() { + } + } + + Line line = new Line(); + + int stroke_width = 20; + + void draw_text(AltosLatLon lat_lon, String text, int off_x, int off_y) { + if (lat_lon != null && map != null && map.transform != null) { + AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon)); + + Rect bounds = new Rect(); + paint.getTextBounds(text, 0, text.length(), bounds); + + int width = bounds.right - bounds.left; + int height = bounds.bottom - bounds.top; + + float x = pt.x; + float y = pt.y; + x = x - width / 2.0f - off_x; + y = y + height / 2.0f - off_y; + paint.setColor(0xff000000); + canvas.drawText(text, 0, text.length(), x, y, paint); + } + } + + HashMap rockets = new HashMap(); + + void draw_bitmap(AltosLatLon lat_lon, Bitmap bitmap, int off_x, int off_y) { + if (lat_lon != null && map != null && map.transform != null) { + AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon)); + + canvas.drawBitmap(bitmap, pt.x - off_x, pt.y - off_y, paint); + } + } + + private Rocket[] sorted_rockets() { + Rocket[] rocket_array = rockets.values().toArray(new Rocket[0]); + + Arrays.sort(rocket_array); + return rocket_array; + } + + private void draw_positions() { + line.set_a(there); + line.set_b(here); + line.paint(); + draw_bitmap(pad, pad_bitmap, pad_off_x, pad_off_y); + + for (Rocket rocket : sorted_rockets()) + rocket.paint(); + draw_bitmap(here, here_bitmap, here_off_x, here_off_y); + } + + @Override public void invalidate() { + Rect r = new Rect(); + getDrawingRect(r); + super.invalidate(); + } + + @Override public void invalidate(int l, int t, int r, int b) { + Rect rect = new Rect(); + getDrawingRect(rect); + super.invalidate(); + } + + @Override + protected void onDraw(Canvas view_canvas) { + if (map == null) { + debug("MapView draw without map\n"); + return; + } + canvas = view_canvas; + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStrokeWidth(stroke_width); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setTextSize(40); + map.paint(); + draw_positions(); + canvas = null; + } + + public boolean onScale(ScaleGestureDetector detector) { + float f = detector.getScaleFactor(); + + if (f <= 0.8) { + map.set_zoom_centre(map.get_zoom() - 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY())); + return true; + } + if (f >= 1.2) { + map.set_zoom_centre(map.get_zoom() + 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY())); + return true; + } + return false; + } + + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + public void onScaleEnd(ScaleGestureDetector detector) { + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + scale_detector.onTouchEvent(event); + + if (scale_detector.isInProgress()) { + scaling = true; + } + + if (scaling) { + if (event.getAction() == MotionEvent.ACTION_UP) { + scaling = false; + } + return true; + } + + if (event.getAction() == MotionEvent.ACTION_DOWN) { + map.touch_start((int) event.getX(), (int) event.getY(), true); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + map.touch_continue((int) event.getX(), (int) event.getY(), true); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + map.touch_stop((int) event.getX(), (int) event.getY(), true); + } + return true; + } + + double mapAccuracy; + + public void center(double lat, double lon, double accuracy) { + if (mapAccuracy <= 0 || accuracy < mapAccuracy/10 || (map != null && !map.has_centre())) { + if (map != null) + map.maybe_centre(lat, lon); + mapAccuracy = accuracy; + } + } + + public void set_visible(boolean visible) { + if (visible) + setVisibility(VISIBLE); + else + setVisibility(GONE); + } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + boolean changed = false; + + if (state != null) { + map.show(state, null); + if (state.pad_lat != AltosLib.MISSING && pad == null) + pad = new AltosLatLon(state.pad_lat, state.pad_lon); + } + + if (telem_state != null) { + Integer[] old_serial = rockets.keySet().toArray(new Integer[0]); + Integer[] new_serial = telem_state.states.keySet().toArray(new Integer[0]); + + /* remove deleted keys */ + for (int serial : old_serial) { + if (!telem_state.states.containsKey(serial)) + rockets.remove(serial); + } + + /* set remaining keys */ + + for (int serial : new_serial) { + Rocket rocket; + AltosState t_state = telem_state.states.get(serial); + if (rockets.containsKey(serial)) + rocket = rockets.get(serial); + else { + rocket = new Rocket(serial, this); + rockets.put(serial, rocket); + } + if (t_state.gps != null) { + AltosLatLon latlon = new AltosLatLon(t_state.gps.lat, t_state.gps.lon); + rocket.set_position(latlon, t_state.received_time); + if (state.cal_data().serial == serial) + there = latlon; + } + if (state != null) + rocket.set_active(state.cal_data().serial == serial); + } + } + if (receiver != null) { + AltosLatLon new_here = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude()); + if (!new_here.equals(here)) { + here = new_here; + AltosDebug.debug("Location changed, redraw"); + repaint(); + } + } + } + + public void onCreateView(AltosDroid altos_droid) { + this.altos_droid = altos_droid; + map = new AltosMap(this, scale); + AltosPreferences.register_map_type_listener(this); + map.set_maptype(AltosPreferences.map_type()); + + pad_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pad); + /* arrow at the bottom of the launchpad image */ + pad_off_x = pad_bitmap.getWidth() / 2; + pad_off_y = pad_bitmap.getHeight(); + + rocket_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rocket); + /* arrow at the bottom of the rocket image */ + rocket_off_x = rocket_bitmap.getWidth() / 2; + rocket_off_y = rocket_bitmap.getHeight(); + + here_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_maps_indicator_current_position); + /* Center of the dot */ + here_off_x = here_bitmap.getWidth() / 2; + here_off_y = here_bitmap.getHeight() / 2; + } + + public void onDestroyView() { + AltosPreferences.unregister_map_type_listener(this); + } + + public void map_type_changed(int map_type) { + if (map != null) + map.set_maptype(map_type); + } + + public AltosMapOffline(Context context, AttributeSet attrs) { + super(context, attrs); + this.altos_droid = altos_droid; + scale_detector = new ScaleGestureDetector(context, this); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOnline.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOnline.java new file mode 100644 index 00000000..37e4435a --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosMapOnline.java @@ -0,0 +1,329 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; + +import org.altusmetrum.altoslib_13.*; + +import com.google.android.gms.maps.*; +import com.google.android.gms.maps.model.*; + +import android.app.Activity; +import android.graphics.Color; +import android.graphics.*; +import android.os.Bundle; +import android.support.v4.app.Fragment; +//import android.support.v4.app.FragmentTransaction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.location.Location; +import android.content.*; + +class RocketOnline implements Comparable { + Marker marker; + int serial; + long last_packet; + int size; + + void set_position(AltosLatLon position, long last_packet) { + marker.setPosition(new LatLng(position.lat, position.lon)); + this.last_packet = last_packet; + } + + private Bitmap rocket_bitmap(Context context, String text) { + + /* From: http://mapicons.nicolasmollet.com/markers/industry/military/missile-2/ + */ + Bitmap orig_bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.rocket); + Bitmap bitmap = orig_bitmap.copy(Bitmap.Config.ARGB_8888, true); + + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setTextSize(40); + paint.setColor(0xff000000); + + Rect bounds = new Rect(); + paint.getTextBounds(text, 0, text.length(), bounds); + + int width = bounds.right - bounds.left; + int height = bounds.bottom - bounds.top; + + float x = bitmap.getWidth() / 2.0f - width / 2.0f; + float y = bitmap.getHeight() / 2.0f - height / 2.0f; + + size = bitmap.getWidth(); + + canvas.drawText(text, 0, text.length(), x, y, paint); + return bitmap; + } + + public void remove() { + marker.remove(); + } + + public int compareTo(Object o) { + RocketOnline other = (RocketOnline) o; + + long diff = last_packet - other.last_packet; + + if (diff > 0) + return 1; + if (diff < 0) + return -1; + return 0; + } + + RocketOnline(Context context, int serial, GoogleMap map, double lat, double lon, long last_packet) { + this.serial = serial; + String name = String.format("%d", serial); + this.marker = map.addMarker(new MarkerOptions() + .icon(BitmapDescriptorFactory.fromBitmap(rocket_bitmap(context, name))) + .position(new LatLng(lat, lon)) + .visible(true)); + this.last_packet = last_packet; + } +} + +public class AltosMapOnline implements AltosDroidMapInterface, GoogleMap.OnMarkerClickListener, GoogleMap.OnMapClickListener, AltosMapTypeListener { + public SupportMapFragment mMapFragment; + private GoogleMap mMap; + private boolean mapLoaded = false; + Context context; + + private HashMap rockets = new HashMap(); + private Marker mPadMarker; + private boolean pad_set; + private Polyline mPolyline; + + private View map_view; + + private double mapAccuracy = -1; + + private AltosLatLon my_position = null; + private AltosLatLon target_position = null; + + private AltosDroid altos_droid; + + public void onCreateView(AltosDroid altos_droid) { + this.altos_droid = altos_droid; + final int map_type = AltosPreferences.map_type(); + AltosPreferences.register_map_type_listener(this); + mMapFragment = new SupportMapFragment() { + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setupMap(map_type); + } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + map_view = super.onCreateView(inflater, container, savedInstanceState); + return map_view; + } + @Override + public void onDestroyView() { + super.onDestroyView(); + map_view = null; + } + }; + } + + public void onDestroyView() { + AltosPreferences.unregister_map_type_listener(this); + } + + private double pixel_distance(LatLng a, LatLng b) { + Projection projection = mMap.getProjection(); + + Point a_pt = projection.toScreenLocation(a); + Point b_pt = projection.toScreenLocation(b); + + return Math.hypot((double) (a_pt.x - b_pt.x), (double) (a_pt.y - b_pt.y)); + } + + private RocketOnline[] sorted_rockets() { + RocketOnline[] rocket_array = rockets.values().toArray(new RocketOnline[0]); + + Arrays.sort(rocket_array); + return rocket_array; + } + + public void onMapClick(LatLng lat_lng) { + ArrayList near = new ArrayList(); + + for (RocketOnline rocket : sorted_rockets()) { + LatLng pos = rocket.marker.getPosition(); + + if (pos == null) + continue; + + double distance = pixel_distance(lat_lng, pos); + if (distance < rocket.size * 2) + near.add(rocket.serial); + } + + if (near.size() != 0) + altos_droid.touch_trackers(near.toArray(new Integer[0])); + } + + public boolean onMarkerClick(Marker marker) { + onMapClick(marker.getPosition()); + return true; + } + + public void setupMap(int map_type) { + mMap = mMapFragment.getMap(); + if (mMap != null) { + map_type_changed(map_type); + mMap.setMyLocationEnabled(true); + mMap.getUiSettings().setTiltGesturesEnabled(false); + mMap.getUiSettings().setZoomControlsEnabled(false); + mMap.setOnMarkerClickListener(this); + mMap.setOnMapClickListener(this); + + mPadMarker = mMap.addMarker( + new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.pad)) + .position(new LatLng(0,0)) + .visible(false) + ); + + mPolyline = mMap.addPolyline( + new PolylineOptions().add(new LatLng(0,0), new LatLng(0,0)) + .width(20) + .color(Color.BLUE) + .visible(false) + ); + + mapLoaded = true; + } + } + + public void center(double lat, double lon, double accuracy) { + if (mMap == null) + return; + + if (mapAccuracy < 0 || accuracy < mapAccuracy/10) { + mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lon),14)); + mapAccuracy = accuracy; + } + } + + private void set_rocket(int serial, AltosState state) { + RocketOnline rocket; + + if (state.gps == null || state.gps.lat == AltosLib.MISSING) + return; + + if (mMap == null) + return; + + if (rockets.containsKey(serial)) { + rocket = rockets.get(serial); + rocket.set_position(new AltosLatLon(state.gps.lat, state.gps.lon), state.received_time); + } else { + rocket = new RocketOnline(context, + serial, + mMap, state.gps.lat, state.gps.lon, + state.received_time); + rockets.put(serial, rocket); + } + } + + private void remove_rocket(int serial) { + RocketOnline rocket = rockets.get(serial); + rocket.remove(); + rockets.remove(serial); + } + + public void set_visible(boolean visible) { + if (map_view == null) + return; + if (visible) + map_view.setVisibility(View.VISIBLE); + else + map_view.setVisibility(View.GONE); + } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + + if (telem_state != null) { + for (int serial : rockets.keySet()) { + if (!telem_state.states.containsKey(serial)) + remove_rocket(serial); + } + + for (int serial : telem_state.states.keySet()) { + set_rocket(serial, telem_state.states.get(serial)); + } + } + + if (state != null) { + if (mapLoaded) { + if (!pad_set && state.pad_lat != AltosLib.MISSING) { + pad_set = true; + mPadMarker.setPosition(new LatLng(state.pad_lat, state.pad_lon)); + mPadMarker.setVisible(true); + } + } + if (state.gps != null && state.gps.lat != AltosLib.MISSING) { + + target_position = new AltosLatLon(state.gps.lat, state.gps.lon); + if (state.gps.locked && state.gps.nsat >= 4) + center (state.gps.lat, state.gps.lon, 10); + } + } + + if (receiver != null) { + double accuracy; + + if (receiver.hasAccuracy()) + accuracy = receiver.getAccuracy(); + else + accuracy = 1000; + + my_position = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude()); + center (my_position.lat, my_position.lon, accuracy); + } + + if (my_position != null && target_position != null && mPolyline != null) { + mPolyline.setPoints(Arrays.asList(new LatLng(my_position.lat, my_position.lon), new LatLng(target_position.lat, target_position.lon))); + mPolyline.setVisible(true); + } + + } + + public void map_type_changed(int map_type) { + if (mMap != null) { + if (map_type == AltosMap.maptype_hybrid) + mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + else if (map_type == AltosMap.maptype_satellite) + mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); + else if (map_type == AltosMap.maptype_terrain) + mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN); + else + mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); + } + } + + public AltosMapOnline(Context context) { + this.context = context; + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosUsb.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosUsb.java new file mode 100644 index 00000000..0b235f28 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosUsb.java @@ -0,0 +1,231 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.UUID; +import java.util.HashMap; + +import android.content.Context; +import android.hardware.usb.*; +import android.app.*; +import android.os.Handler; + +import org.altusmetrum.altoslib_13.*; + +public class AltosUsb extends AltosDroidLink { + + private Thread input_thread = null; + + private Handler handler; + + private UsbManager manager; + private UsbDevice device; + private UsbDeviceConnection connection; + private UsbInterface iface; + private UsbEndpoint in, out; + + private InputStream input; + private OutputStream output; + + // Constructor + public AltosUsb(Context context, UsbDevice device, Handler handler) { + super(handler); +// set_debug(D); + this.handler = handler; + + iface = null; + in = null; + out = null; + + int niface = device.getInterfaceCount(); + + for (int i = 0; i < niface; i++) { + + iface = device.getInterface(i); + + in = null; + out = null; + + int nendpoints = iface.getEndpointCount(); + + for (int e = 0; e < nendpoints; e++) { + UsbEndpoint endpoint = iface.getEndpoint(e); + + if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) { + switch (endpoint.getDirection()) { + case UsbConstants.USB_DIR_OUT: + out = endpoint; + break; + case UsbConstants.USB_DIR_IN: + in = endpoint; + break; + } + } + } + + if (in != null && out != null) + break; + } + + if (in != null && out != null) { + AltosDebug.debug("\tin %s out %s\n", in.toString(), out.toString()); + + manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + + if (manager == null) { + AltosDebug.debug("USB_SERVICE failed"); + return; + } + + connection = manager.openDevice(device); + + if (connection == null) { + AltosDebug.debug("openDevice failed"); + return; + } + + connection.claimInterface(iface, true); + + input_thread = new Thread(this); + input_thread.start(); + + // Configure the newly connected device for telemetry + print("~\nE 0\n"); + set_monitor(false); + } + } + + static private boolean isAltusMetrum(UsbDevice device) { + if (device.getVendorId() != AltosLib.vendor_altusmetrum) + return false; + if (device.getProductId() < AltosLib.product_altusmetrum_min) + return false; + if (device.getProductId() > AltosLib.product_altusmetrum_max) + return false; + return true; + } + + static boolean matchProduct(int want_product, UsbDevice device) { + + if (!isAltusMetrum(device)) + return false; + + if (want_product == AltosLib.product_any) + return true; + + int have_product = device.getProductId(); + + if (want_product == AltosLib.product_basestation) + return have_product == AltosLib.product_teledongle || + have_product == AltosLib.product_teleterra || + have_product == AltosLib.product_telebt || + have_product == AltosLib.product_megadongle; + + if (want_product == AltosLib.product_altimeter) + return have_product == AltosLib.product_telemetrum || + have_product == AltosLib.product_telemega || + have_product == AltosLib.product_easymega || + have_product == AltosLib.product_telegps || + have_product == AltosLib.product_easymini || + have_product == AltosLib.product_telemini; + + if (have_product == AltosLib.product_altusmetrum) /* old devices match any request */ + return true; + + if (want_product == have_product) + return true; + + return false; + } + + static public boolean request_permission(Context context, UsbDevice device, PendingIntent pi) { + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + +// if (manager.hasPermission(device)) +// return true; + + AltosDebug.debug("request permission for USB device " + device.toString()); + + manager.requestPermission(device, pi); + return false; + } + + static public UsbDevice find_device(Context context, int match_product) { + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + + HashMap devices = manager.getDeviceList(); + + for (UsbDevice device : devices.values()) { + int vendor = device.getVendorId(); + int product = device.getProductId(); + + if (matchProduct(match_product, device)) { + AltosDebug.debug("found USB device " + device.toString()); + return device; + } + } + + return null; + } + + private void disconnected() { + if (closed()) { + AltosDebug.debug("disconnected after closed"); + return; + } + + AltosDebug.debug("Sending disconnected message"); + handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, this).sendToTarget(); + } + + void close_device() { + UsbDeviceConnection tmp_connection; + + synchronized(this) { + tmp_connection = connection; + connection = null; + } + + if (tmp_connection != null) { + AltosDebug.debug("Closing USB device"); + tmp_connection.close(); + } + } + + int read(byte[] buffer, int len) { + int ret = connection.bulkTransfer(in, buffer, len, -1); + AltosDebug.debug("read(%d) = %d\n", len, ret); + return ret; + } + + int write(byte[] buffer, int len) { + int ret = connection.bulkTransfer(out, buffer, len, -1); + AltosDebug.debug("write(%d) = %d\n", len, ret); + return ret; + } + + // Stubs of required methods when extending AltosLink + public boolean can_cancel_reply() { return false; } + public boolean show_reply_timeout() { return true; } + public void hide_reply_timeout() { } + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosViewPager.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosViewPager.java new file mode 100644 index 00000000..039ba149 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosViewPager.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.View; + +public class AltosViewPager extends ViewPager { + + public AltosViewPager(Context context) { + super(context); + } + + public AltosViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + + if (v.getClass() != null && + v.getClass().getName() != null && + v.getClass().getName().endsWith("MapOffline")) + return true; + + if(v.getClass() != null && + v.getClass().getPackage() != null && + v.getClass().getPackage().getName() != null && + v.getClass().getPackage().getName().startsWith("maps.")) + return true; + + return super.canScroll(v, checkV, dx, x, y); + } + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosVoice.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosVoice.java new file mode 100644 index 00000000..ae3299fa --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/AltosVoice.java @@ -0,0 +1,331 @@ +/* + * Copyright © 2011 Keith Packard + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.location.Location; + +import org.altusmetrum.altoslib_13.*; + +public class AltosVoice { + + private TextToSpeech tts = null; + private boolean tts_enabled = false; + + static final int TELL_MODE_NONE = 0; + static final int TELL_MODE_PAD = 1; + static final int TELL_MODE_FLIGHT = 2; + static final int TELL_MODE_RECOVER = 3; + + static final int TELL_FLIGHT_NONE = 0; + static final int TELL_FLIGHT_STATE = 1; + static final int TELL_FLIGHT_SPEED = 2; + static final int TELL_FLIGHT_HEIGHT = 3; + static final int TELL_FLIGHT_TRACK = 4; + + private int last_tell_mode; + private int last_tell_serial = AltosLib.MISSING; + private int last_state; + private AltosGPS last_gps; + private double last_height = AltosLib.MISSING; + private Location last_receiver; + private long last_speak_time; + private int last_flight_tell = TELL_FLIGHT_NONE; + private boolean quiet = false; + + private long now() { + return System.currentTimeMillis(); + } + + private void reset_last() { + last_tell_mode = TELL_MODE_NONE; + last_speak_time = now() - 100 * 1000; + last_gps = null; + last_height = AltosLib.MISSING; + last_receiver = null; + last_state = AltosLib.ao_flight_invalid; + last_flight_tell = TELL_FLIGHT_NONE; + } + + public AltosVoice(AltosDroid a) { + tts = new TextToSpeech(a, new OnInitListener() { + public void onInit(int status) { + if (status == TextToSpeech.SUCCESS) tts_enabled = true; + } + }); + reset_last(); + } + + public synchronized void set_enable(boolean enable) { + tts_enabled = enable; + } + + public synchronized void speak(String s) { + if (!tts_enabled) return; + last_speak_time = now(); + if (!quiet) + tts.speak(s, TextToSpeech.QUEUE_ADD, null); + } + + public synchronized long time_since_speak() { + return now() - last_speak_time; + } + + public synchronized void speak(String format, Object ... arguments) { + speak(String.format(format, arguments)); + } + + public synchronized boolean is_speaking() { + return tts.isSpeaking(); + } + + public void stop() { + if (tts != null) { + tts.stop(); + tts.shutdown(); + } + } + + private boolean last_apogee_good; + private boolean last_main_good; + private boolean last_gps_good; + + private boolean tell_gonogo(String name, + boolean current, + boolean previous, + boolean new_mode) { + if (current != previous || new_mode) + speak("%s %s.", name, current ? "ready" : "not ready"); + return current; + } + + private boolean tell_pad(TelemetryState telem_state, AltosState state, + AltosGreatCircle from_receiver, Location receiver) { + + if (state == null) + return false; + + AltosDebug.debug("tell_pad lag %b ltm %d\n", last_apogee_good, last_tell_mode); + + if (state.apogee_voltage != AltosLib.MISSING) + last_apogee_good = tell_gonogo("apogee", + state.apogee_voltage >= AltosLib.ao_igniter_good, + last_apogee_good, + last_tell_mode != TELL_MODE_PAD); + + if (state.main_voltage != AltosLib.MISSING) + last_main_good = tell_gonogo("main", + state.main_voltage >= AltosLib.ao_igniter_good, + last_main_good, + last_tell_mode != TELL_MODE_PAD); + + if (state.gps != null) + last_gps_good = tell_gonogo("G P S", + state.gps_ready, + last_gps_good, + last_tell_mode != TELL_MODE_PAD); + return true; + } + + + private boolean descending(int state) { + return AltosLib.ao_flight_drogue <= state && state <= AltosLib.ao_flight_landed; + } + + private boolean target_moved(AltosState state) { + if (last_gps != null && state != null && state.gps != null) { + AltosGreatCircle moved = new AltosGreatCircle(last_gps.lat, last_gps.lon, last_gps.alt, + state.gps.lat, state.gps.lon, state.gps.alt); + double height_change = 0; + double height = state.height(); + + if (height != AltosLib.MISSING && last_height != AltosLib.MISSING) + height_change = Math.abs(last_height - height); + + if (moved.range < 10 && height_change < 10) + return false; + } + return true; + } + + private boolean receiver_moved(Location receiver) { + if (last_receiver != null && receiver != null) { + AltosGreatCircle moved = new AltosGreatCircle(last_receiver.getLatitude(), + last_receiver.getLongitude(), + last_receiver.getAltitude(), + receiver.getLatitude(), + receiver.getLongitude(), + receiver.getAltitude()); + if (moved.range < 10) + return false; + } + return true; + } + + private boolean tell_flight(TelemetryState telem_state, AltosState state, + AltosGreatCircle from_receiver, Location receiver) { + + boolean spoken = false; + + if (state == null) + return false; + + if (last_tell_mode != TELL_MODE_FLIGHT) + last_flight_tell = TELL_FLIGHT_NONE; + + if (state.state() != last_state && AltosLib.ao_flight_boost <= state.state() && state.state() <= AltosLib.ao_flight_landed) { + speak(state.state_name()); + if (descending(state.state()) && !descending(last_state)) { + if (state.max_height() != AltosLib.MISSING) { + speak("max height: %s.", + AltosConvert.height.say_units(state.max_height())); + } + } + last_flight_tell = TELL_FLIGHT_STATE; + return true; + } + + if (last_tell_mode == TELL_MODE_FLIGHT && last_flight_tell == TELL_FLIGHT_TRACK) { + if (time_since_speak() < 10 * 1000) + return false; + if (!target_moved(state) && !receiver_moved(receiver)) + return false; + } + + double speed; + double height; + + if (last_flight_tell == TELL_FLIGHT_NONE || last_flight_tell == TELL_FLIGHT_STATE || last_flight_tell == TELL_FLIGHT_TRACK) { + last_flight_tell = TELL_FLIGHT_SPEED; + + if (state.state() <= AltosLib.ao_flight_coast) { + speed = state.speed(); + } else { + speed = state.gps_speed(); + if (speed == AltosLib.MISSING) + speed = state.speed(); + } + + if (speed != AltosLib.MISSING) { + speak("speed: %s.", AltosConvert.speed.say_units(speed)); + return true; + } + } + + if (last_flight_tell == TELL_FLIGHT_SPEED) { + last_flight_tell = TELL_FLIGHT_HEIGHT; + height = state.height(); + + if (height != AltosLib.MISSING) { + speak("height: %s.", AltosConvert.height.say_units(height)); + return true; + } + } + + if (last_flight_tell == TELL_FLIGHT_HEIGHT) { + last_flight_tell = TELL_FLIGHT_TRACK; + if (from_receiver != null) { + speak("bearing %s %d, elevation %d, distance %s.", + from_receiver.bearing_words( + AltosGreatCircle.BEARING_VOICE), + (int) (from_receiver.bearing + 0.5), + (int) (from_receiver.elevation + 0.5), + AltosConvert.distance.say(from_receiver.distance)); + return true; + } + } + + return spoken; + } + + private boolean tell_recover(TelemetryState telem_state, AltosState state, + AltosGreatCircle from_receiver, Location receiver) { + + if (from_receiver == null) + return false; + + if (last_tell_mode == TELL_MODE_RECOVER) { + if (!target_moved(state) && !receiver_moved(receiver)) + return false; + if (time_since_speak() <= 10 * 1000) + return false; + } + + String direction = AltosDroid.direction(from_receiver, receiver); + if (direction == null) + direction = String.format("Bearing %d", (int) (from_receiver.bearing + 0.5)); + + speak("%s, distance %s.", direction, + AltosConvert.distance.say_units(from_receiver.distance)); + + return true; + } + + public void tell(TelemetryState telem_state, AltosState state, + AltosGreatCircle from_receiver, Location receiver, + AltosDroidTab tab, boolean quiet) { + + this.quiet = quiet; + + boolean spoken = false; + + if (!tts_enabled) return; + + if (is_speaking()) return; + + int tell_serial = last_tell_serial; + + if (state != null) + tell_serial = state.cal_data().serial; + + if (tell_serial != last_tell_serial) + reset_last(); + + int tell_mode = TELL_MODE_NONE; + + if (tab.tab_name().equals(AltosDroid.tab_pad_name)) + tell_mode = TELL_MODE_PAD; + else if (tab.tab_name().equals(AltosDroid.tab_flight_name)) + tell_mode = TELL_MODE_FLIGHT; + else + tell_mode = TELL_MODE_RECOVER; + + if (tell_mode == TELL_MODE_PAD) + spoken = tell_pad(telem_state, state, from_receiver, receiver); + else if (tell_mode == TELL_MODE_FLIGHT) + spoken = tell_flight(telem_state, state, from_receiver, receiver); + else + spoken = tell_recover(telem_state, state, from_receiver, receiver); + + if (spoken) { + last_tell_mode = tell_mode; + last_tell_serial = tell_serial; + if (state != null) { + last_state = state.state(); + last_height = state.height(); + if (state.gps != null) + last_gps = state.gps; + } + if (receiver != null) + last_receiver = receiver; + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/BuildInfo.java.in b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/BuildInfo.java.in new file mode 100644 index 00000000..aa6c9a74 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/BuildInfo.java.in @@ -0,0 +1,31 @@ +/* + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +public class BuildInfo { + public static final String version = "@VERSION@"; + public static final String git_describe = "@DESCRIBE@"; + public static final String branch = "@BRANCH@"; + public static final String commitnum = "@COMMITNUM@"; + public static final String commithash = "@COMMITHASH@"; + public static final String builddate = "@BUILDDATE@"; + public static final String buildtime = "@BUILDTIME@"; + public static final String buildtz = "@BUILDTZ@"; +} + diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceAddress.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceAddress.java new file mode 100644 index 00000000..6f845566 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceAddress.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +public class DeviceAddress { + public String address; + public String name; + + public DeviceAddress(String address, String name) { + this.address = address; + this.name = name; + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceListActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceListActivity.java new file mode 100644 index 00000000..f36ef267 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/DeviceListActivity.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.Set; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView.OnItemClickListener; + +/** + * This Activity appears as a dialog. It lists any paired devices and + * devices detected in the area after discovery. When a device is chosen + * by the user, the MAC address of the device is sent back to the parent + * Activity in the result Intent. + */ +public class DeviceListActivity extends Activity { + + // Return Intent extra + public static final String EXTRA_DEVICE_ADDRESS = "device_address"; + public static final String EXTRA_DEVICE_NAME = "device_name"; + + // Member fields + private BluetoothAdapter mBtAdapter; + private ArrayAdapter mPairedDevicesArrayAdapter; + private ArrayAdapter mNewDevicesArrayAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.device_list); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + + // Initialize the button to perform device discovery + Button scanButton = (Button) findViewById(R.id.button_scan); + scanButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + doDiscovery(); + v.setVisibility(View.GONE); + } + }); + + // Initialize array adapters. One for already paired devices and + // one for newly discovered devices + mPairedDevicesArrayAdapter = new ArrayAdapter(this, R.layout.device_name); + mNewDevicesArrayAdapter = new ArrayAdapter(this, R.layout.device_name); + + // Find and set up the ListView for paired devices + ListView pairedListView = (ListView) findViewById(R.id.paired_devices); + pairedListView.setAdapter(mPairedDevicesArrayAdapter); + pairedListView.setOnItemClickListener(mDeviceClickListener); + + // Find and set up the ListView for newly discovered devices + ListView newDevicesListView = (ListView) findViewById(R.id.new_devices); + newDevicesListView.setAdapter(mNewDevicesArrayAdapter); + newDevicesListView.setOnItemClickListener(mDeviceClickListener); + + // Register for broadcasts when a device is discovered + IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); + this.registerReceiver(mReceiver, filter); + + // Register for broadcasts when discovery has finished + filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + this.registerReceiver(mReceiver, filter); + + // Get the local Bluetooth adapter + mBtAdapter = BluetoothAdapter.getDefaultAdapter(); + + // Get a set of currently paired devices + Set pairedDevices = mBtAdapter.getBondedDevices(); + + // If there are paired devices, add each one to the ArrayAdapter + if (pairedDevices.size() > 0) { + findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE); + for (BluetoothDevice device : pairedDevices) + if (device.getName().startsWith("TeleBT")) + mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + + } else { + String noDevices = getResources().getText(R.string.none_paired).toString(); + mPairedDevicesArrayAdapter.add(noDevices); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Make sure we're not doing discovery anymore + if (mBtAdapter != null) { + mBtAdapter.cancelDiscovery(); + } + + // Unregister broadcast listeners + this.unregisterReceiver(mReceiver); + } + + /** + * Start device discover with the BluetoothAdapter + */ + private void doDiscovery() { + AltosDebug.debug("doDiscovery()"); + + // Indicate scanning in the title + setProgressBarIndeterminateVisibility(true); + setTitle(R.string.scanning); + + // Turn on sub-title for new devices + findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE); + + // If we're already discovering, stop it + if (mBtAdapter.isDiscovering()) { + mBtAdapter.cancelDiscovery(); + } + + // Request discover from BluetoothAdapter + mBtAdapter.startDiscovery(); + } + + // The on-click listener for all devices in the ListViews + private OnItemClickListener mDeviceClickListener = new OnItemClickListener() { + public void onItemClick(AdapterView av, View v, int arg2, long arg3) { + // Cancel discovery because it's costly and we're about to connect + mBtAdapter.cancelDiscovery(); + + // Get the device MAC address, which is the last 17 chars in the View + String info = ((TextView) v).getText().toString(); + String address = info.substring(info.length() - 17); + + int newline = info.indexOf('\n'); + + String name = null; + if (newline > 0) + name = info.substring(0, newline); + else + name = info; + + AltosDebug.debug("******* selected item '%s'", info); + + // Create the result Intent and include the MAC address + Intent intent = new Intent(); + intent.putExtra(EXTRA_DEVICE_ADDRESS, address); + intent.putExtra(EXTRA_DEVICE_NAME, name); + + // Set result and finish this Activity + setResult(Activity.RESULT_OK, intent); + finish(); + } + }; + + // The BroadcastReceiver that listens for discovered devices and + // changes the title when discovery is finished + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + // When discovery finds a device + if (BluetoothDevice.ACTION_FOUND.equals(action)) { + + /* Get the BluetoothDevice object from the Intent + */ + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + + /* If it's already paired, skip it, because it's been listed already + */ + if (device != null && device.getBondState() != BluetoothDevice.BOND_BONDED) + { + String name = device.getName(); + if (name != null && name.startsWith("TeleBT")) + mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress()); + } + + /* When discovery is finished, change the Activity title + */ + } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { + setProgressBarIndeterminateVisibility(false); + setTitle(R.string.select_device); + if (mNewDevicesArrayAdapter.getCount() == 0) { + String noDevices = getResources().getText(R.string.none_found).toString(); + mNewDevicesArrayAdapter.add(noDevices); + } + } + } + }; + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/Dumper.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/Dumper.java new file mode 100644 index 00000000..2797fc5e --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/Dumper.java @@ -0,0 +1,183 @@ +package org.altusmetrum.AltosDroid; + + import java.lang.reflect.Array; + import java.lang.reflect.Field; + import java.util.HashMap; + + public class Dumper { + private static Dumper instance = new Dumper(); + + protected static Dumper getInstance() { + return instance; + } + + class DumpContext { + int maxDepth = 0; + int maxArrayElements = 0; + int callCount = 0; + HashMap ignoreList = new HashMap(); + HashMap visited = new HashMap(); + } + + public static String dump(Object o) { + return dump(o, 0, 0, null); + } + + public static String dump(Object o, int maxDepth, int maxArrayElements, String[] ignoreList) { + DumpContext ctx = Dumper.getInstance().new DumpContext(); + ctx.maxDepth = maxDepth; + ctx.maxArrayElements = maxArrayElements; + + if (ignoreList != null) { + for (int i = 0; i < Array.getLength(ignoreList); i++) { + int colonIdx = ignoreList[i].indexOf(':'); + if (colonIdx == -1) + ignoreList[i] = ignoreList[i] + ":"; + ctx.ignoreList.put(ignoreList[i], ignoreList[i]); + } + } + + return dump(o, ctx); + } + + protected static String dump(Object o, DumpContext ctx) { + if (o == null) { + return ""; + } + + ctx.callCount++; + StringBuffer tabs = new StringBuffer(); + for (int k = 0; k < ctx.callCount; k++) { + tabs.append("\t"); + } + StringBuffer buffer = new StringBuffer(); + @SuppressWarnings("rawtypes") + Class oClass = o.getClass(); + + String oSimpleName = getSimpleNameWithoutArrayQualifier(oClass); + + if (ctx.ignoreList.get(oSimpleName + ":") != null) + return ""; + + if (oClass.isArray()) { + buffer.append("\n"); + buffer.append(tabs.toString().substring(1)); + buffer.append("[\n"); + int rowCount = ctx.maxArrayElements == 0 ? Array.getLength(o) : Math.min(ctx.maxArrayElements, Array.getLength(o)); + for (int i = 0; i < rowCount; i++) { + buffer.append(tabs.toString()); + try { + Object value = Array.get(o, i); + buffer.append(dumpValue(value, ctx)); + } catch (Exception e) { + buffer.append(e.getMessage()); + } + if (i < Array.getLength(o) - 1) + buffer.append(","); + buffer.append("\n"); + } + if (rowCount < Array.getLength(o)) { + buffer.append(tabs.toString()); + buffer.append(Array.getLength(o) - rowCount + " more array elements..."); + buffer.append("\n"); + } + buffer.append(tabs.toString().substring(1)); + buffer.append("]"); + } else { + buffer.append("\n"); + buffer.append(tabs.toString().substring(1)); + buffer.append("{\n"); + buffer.append(tabs.toString()); + buffer.append("hashCode: " + o.hashCode()); + buffer.append("\n"); + while (oClass != null && oClass != Object.class) { + Field[] fields = oClass.getDeclaredFields(); + + if (ctx.ignoreList.get(oClass.getSimpleName()) == null) { + if (oClass != o.getClass()) { + buffer.append(tabs.toString().substring(1)); + buffer.append(" Inherited from superclass " + oSimpleName + ":\n"); + } + + for (int i = 0; i < fields.length; i++) { + + String fSimpleName = getSimpleNameWithoutArrayQualifier(fields[i].getType()); + String fName = fields[i].getName(); + + fields[i].setAccessible(true); + buffer.append(tabs.toString()); + buffer.append(fName + "(" + fSimpleName + ")"); + buffer.append("="); + + if (ctx.ignoreList.get(":" + fName) == null && + ctx.ignoreList.get(fSimpleName + ":" + fName) == null && + ctx.ignoreList.get(fSimpleName + ":") == null) { + + try { + Object value = fields[i].get(o); + buffer.append(dumpValue(value, ctx)); + } catch (Exception e) { + buffer.append(e.getMessage()); + } + buffer.append("\n"); + } else { + buffer.append(""); + buffer.append("\n"); + } + } + oClass = oClass.getSuperclass(); + oSimpleName = oClass.getSimpleName(); + } else { + oClass = null; + oSimpleName = ""; + } + } + buffer.append(tabs.toString().substring(1)); + buffer.append("}"); + } + ctx.callCount--; + return buffer.toString(); + } + + protected static String dumpValue(Object value, DumpContext ctx) { + if (value == null) { + return ""; + } + if (value.getClass().isPrimitive() || + value.getClass() == java.lang.Short.class || + value.getClass() == java.lang.Long.class || + value.getClass() == java.lang.String.class || + value.getClass() == java.lang.Integer.class || + value.getClass() == java.lang.Float.class || + value.getClass() == java.lang.Byte.class || + value.getClass() == java.lang.Character.class || + value.getClass() == java.lang.Double.class || + value.getClass() == java.lang.Boolean.class) { + + return value.toString(); + + } else { + + Integer visitedIndex = ctx.visited.get(value); + if (visitedIndex == null) { + ctx.visited.put(value, ctx.callCount); + if (ctx.maxDepth == 0 || ctx.callCount < ctx.maxDepth) { + return dump(value, ctx); + } else { + return ""; + } + } else { + return ""; + } + } + } + + + private static String getSimpleNameWithoutArrayQualifier(@SuppressWarnings("rawtypes") Class clazz) { + String simpleName = clazz.getSimpleName(); + int indexOfBracket = simpleName.indexOf('['); + if (indexOfBracket != -1) + return simpleName.substring(0, indexOfBracket); + return simpleName; + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/GoNoGoLights.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/GoNoGoLights.java new file mode 100644 index 00000000..c18d7309 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/GoNoGoLights.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.widget.ImageView; +import android.view.View; + +public class GoNoGoLights { + private Boolean state; + private Boolean missing; + private Boolean set; + + private ImageView red; + private ImageView green; + + private Drawable dRed; + private Drawable dGreen; + private Drawable dGray; + + public GoNoGoLights(ImageView in_red, ImageView in_green, Resources r) { + red = in_red; + green = in_green; + state = false; + missing = true; + set = false; + + dRed = r.getDrawable(R.drawable.redled); + dGreen = r.getDrawable(R.drawable.greenled); + dGray = r.getDrawable(R.drawable.grayled); + } + + public void set(Boolean s, Boolean m) { + if (set && s == state && m == missing) return; + state = s; + missing = m; + set = true; + if (missing) { + red.setImageDrawable(dGray); + green.setImageDrawable(dGray); + } else if (state) { + red.setImageDrawable(dGray); + green.setImageDrawable(dGreen); + } else { + red.setImageDrawable(dRed); + green.setImageDrawable(dGray); + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IdleModeActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IdleModeActivity.java new file mode 100644 index 00000000..3130d2a1 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IdleModeActivity.java @@ -0,0 +1,131 @@ +/* + * Copyright © 2016 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.widget.*; +import android.widget.AdapterView.*; + +import org.altusmetrum.altoslib_13.*; + +public class IdleModeActivity extends Activity { + private EditText callsign; + private Button connect; + private Button disconnect; + private Button reboot; + private Button igniters; + + public static final String EXTRA_IDLE_MODE = "idle_mode"; + public static final String EXTRA_IDLE_RESULT = "idle_result"; + + public static final int IDLE_MODE_CONNECT = 1; + public static final int IDLE_MODE_REBOOT = 2; + public static final int IDLE_MODE_IGNITERS = 3; + public static final int IDLE_MODE_DISCONNECT = 4; + + private void done(int type) { + AltosPreferences.set_callsign(callsign()); + Intent intent = new Intent(); + intent.putExtra(EXTRA_IDLE_RESULT, type); + setResult(Activity.RESULT_OK, intent); + finish(); + } + + private String callsign() { + return callsign.getEditableText().toString(); + } + + public void connect_idle() { + done(IDLE_MODE_CONNECT); + } + + public void disconnect_idle() { + AltosDebug.debug("Disconnect idle button pressed"); + done(IDLE_MODE_DISCONNECT); + } + + public void reboot_idle() { + done(IDLE_MODE_REBOOT); + } + + public void igniters_idle() { + done(IDLE_MODE_IGNITERS); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.idle_mode); + + callsign = (EditText) findViewById(R.id.set_callsign); + callsign.setText(new StringBuffer(AltosPreferences.callsign())); + + connect = (Button) findViewById(R.id.connect_idle); + connect.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + connect_idle(); + } + }); + disconnect = (Button) findViewById(R.id.disconnect_idle); + disconnect.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + disconnect_idle(); + } + }); + + boolean idle_mode = getIntent().getBooleanExtra(AltosDroid.EXTRA_IDLE_MODE, false); + + if (idle_mode) + connect.setVisibility(View.GONE); + else + disconnect.setVisibility(View.GONE); + + reboot = (Button) findViewById(R.id.reboot_idle); + reboot.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + reboot_idle(); + } + }); + igniters = (Button) findViewById(R.id.igniters_idle); + igniters.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + igniters_idle(); + } + }); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IgniterActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IgniterActivity.java new file mode 100644 index 00000000..a172451d --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/IgniterActivity.java @@ -0,0 +1,414 @@ +/* + * Copyright © 2016 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.lang.ref.WeakReference; +import java.util.*; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.*; +import android.graphics.*; +import android.os.*; +import android.view.*; +import android.view.View.*; +import android.widget.*; +import android.widget.AdapterView.*; + +import org.altusmetrum.altoslib_13.*; + +class IgniterItem { + public String name; + public String pretty; + public String status; + public LinearLayout igniter_view = null; + public TextView pretty_view = null; + public TextView status_view = null; + + private void update() { + if (pretty_view != null) + pretty_view.setText(pretty); + if (status_view != null) + status_view.setText(status); + } + + public void set(String name, String pretty, String status) { + if (!name.equals(this.name) || + !pretty.equals(this.pretty) || + !status.equals(this.status)) + { + this.name = name; + this.pretty = pretty; + this.status = status; + update(); + } + } + + public void realize(LinearLayout igniter_view, + TextView pretty_view, + TextView status_view) { + if (igniter_view != this.igniter_view || + pretty_view != this.pretty_view || + status_view != this.status_view) + { + this.igniter_view = igniter_view; + this.pretty_view = pretty_view; + this.status_view = status_view; + update(); + } + } + + public IgniterItem() { + } +} + +class IgniterAdapter extends ArrayAdapter { + int resource; + int selected_item = -1; + + public IgniterAdapter(Context context, int in_resource) { + super(context, in_resource); + resource = in_resource; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + IgniterItem item = getItem(position); + if (item.igniter_view == null) { + LinearLayout igniter_view = new LinearLayout(getContext()); + String inflater = Context.LAYOUT_INFLATER_SERVICE; + LayoutInflater li = (LayoutInflater) getContext().getSystemService(inflater); + li.inflate(resource, igniter_view, true); + + item.realize(igniter_view, + (TextView) igniter_view.findViewById(R.id.igniter_name), + (TextView) igniter_view.findViewById(R.id.igniter_status)); + } + if (position == selected_item) + item.igniter_view.setBackgroundColor(Color.RED); + else + item.igniter_view.setBackgroundColor(Color.BLACK); + return item.igniter_view; + } +} + +public class IgniterActivity extends Activity { + private ListView igniters_view; + private ToggleButton arm; + private Button fire; + + private HashMap igniters = new HashMap();; + + private IgniterAdapter igniters_adapter; + + private boolean is_bound; + private Messenger service = null; + private final Messenger messenger = new Messenger(new IncomingHandler(this)); + + private Timer query_timer; + private boolean query_timer_running; + + private Timer arm_timer; + private int arm_remaining; + + public static final int IGNITER_QUERY = 1; + public static final int IGNITER_FIRE = 2; + + // The Handler that gets information back from the Telemetry Service + static class IncomingHandler extends Handler { + private final WeakReference igniter_activity; + IncomingHandler(IgniterActivity ia) { igniter_activity = new WeakReference(ia); } + + @Override + public void handleMessage(Message msg) { + IgniterActivity ia = igniter_activity.get(); + + switch (msg.what) { + case AltosDroid.MSG_IGNITER_STATUS: + ia.igniter_status((HashMap ) msg.obj); + break; + } + } + }; + + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder binder) { + service = new Messenger(binder); + query_timer_tick(); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been unexpectedly disconnected - process crashed. + service = null; + } + }; + + void doBindService() { + bindService(new Intent(this, TelemetryService.class), connection, Context.BIND_AUTO_CREATE); + is_bound = true; + } + + void doUnbindService() { + if (is_bound) { + // If we have received the service, and hence registered with it, then now is the time to unregister. + unbindService(connection); + is_bound = false; + } + } + + private void done() { + Intent intent = new Intent(); + setResult(Activity.RESULT_OK, intent); + finish(); + } + + class FireThread extends Thread { + private final String igniter; + + @Override + public void run() { + Message msg = Message.obtain(null, TelemetryService.MSG_IGNITER_FIRE, igniter); + try { + service.send(msg); + } catch (RemoteException re) { + } + } + + public FireThread(String igniter) { + this.igniter = igniter; + } + } + + private void fire_igniter() { + if (igniters_adapter.selected_item >= 0) { + IgniterItem item = igniters_adapter.getItem(igniters_adapter.selected_item); + FireThread ft = new FireThread(item.name); + ft.run(); + arm.setChecked(false); + } + } + + private void arm_igniter(boolean is_checked) { + if (is_checked) { + arm_timer_stop(); + arm_timer = new Timer(); + arm_remaining = 10; + arm_set_text(); + fire.setEnabled(true); + arm_timer.scheduleAtFixedRate(new TimerTask() { + public void run() { + arm_timer_tick(); + }}, + 1000L, 1000L); + } else { + arm_timer_stop(); + fire.setEnabled(false); + } + } + + private synchronized void query_timer_tick() { + if (query_timer_running) + return; + if (service == null) + return; + query_timer_running = true; + Thread thread = new Thread(new Runnable() { + public void run() { + try { + Message msg = Message.obtain(null, TelemetryService.MSG_IGNITER_QUERY); + msg.replyTo = messenger; + if (service == null) { + synchronized(IgniterActivity.this) { + query_timer_running = false; + } + } else + service.send(msg); + } catch (RemoteException re) { + AltosDebug.debug("igniter query thread failed"); + synchronized(IgniterActivity.this) { + query_timer_running = false; + } + } + } + }); + thread.start(); + } + + private boolean set_igniter(HashMap status, String name, String pretty) { + if (!status.containsKey(name)) + return false; + + IgniterItem item; + if (!igniters.containsKey(name)) { + item = new IgniterItem(); + igniters.put(name, item); + igniters_adapter.add(item); + } else + item = igniters.get(name); + + item.set(name, pretty, AltosIgnite.status_string(status.get(name))); + return true; + } + + private synchronized void igniter_status(HashMap status) { + query_timer_running = false; + if (status == null) { + AltosDebug.debug("no igniter status"); + return; + } + set_igniter(status, "drogue", "Apogee"); + set_igniter(status, "main", "Main"); + for (int extra = 0;; extra++) { + String name = String.format("%d", extra); + String pretty = String.format("%c", 'A' + extra); + if (!set_igniter(status, name, pretty)) + break; + } + } + + private synchronized void arm_timer_stop() { + if (arm_timer != null) { + arm_timer.cancel(); + arm_timer = null; + } + arm_remaining = 0; + } + + private void arm_set_text() { + String text = String.format("Armed %d", arm_remaining); + + if (arm.isChecked()) + arm.setText(text); + arm.setTextOn(text); + } + + private void arm_timer_tick() { + --arm_remaining; + if (arm_remaining <= 0) { + arm_timer_stop(); + runOnUiThread(new Runnable() { + public void run() { + arm.setChecked(false); + fire.setEnabled(false); + } + }); + } else { + runOnUiThread(new Runnable() { + public void run() { + arm_set_text(); + } + }); + } + } + + private void select_item(int position) { + if (position != igniters_adapter.selected_item) { + if (igniters_adapter.selected_item >= 0) + igniters_view.setItemChecked(igniters_adapter.selected_item, false); + if (position >= 0) { + igniters_view.setItemChecked(position, true); + arm.setEnabled(true); + } else + arm.setEnabled(false); + igniters_adapter.selected_item = position; + } + } + + private class IgniterItemClickListener implements ListView.OnItemClickListener { + @Override + public void onItemClick(AdapterView av, View v, int position, long id) { + AltosDebug.debug("select %d\n", position); + select_item(position); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.igniters); + + igniters_view = (ListView) findViewById(R.id.igniters); + igniters_view.setClickable(true); + + igniters_adapter = new IgniterAdapter(this, R.layout.igniter_status); + + igniters_view.setAdapter(igniters_adapter); + igniters_view.setOnItemClickListener(new IgniterItemClickListener()); + + fire = (Button) findViewById(R.id.igniter_fire); + fire.setEnabled(false); + fire.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + fire_igniter(); + } + }); + + arm = (ToggleButton) findViewById(R.id.igniter_arm); + arm.setEnabled(false); + arm.setOnCheckedChangeListener(new ToggleButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton v, boolean is_checked) { + arm_igniter(is_checked); + } + }); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + } + + @Override + protected void onStart() { + super.onStart(); + doBindService(); + } + + @Override + protected void onResume() { + super.onResume(); + query_timer = new Timer(true); + query_timer.scheduleAtFixedRate(new TimerTask() { + public void run() { + query_timer_tick(); + }}, + 0L, 5000L); + } + + @Override + protected void onPause() { + super.onPause(); + if (query_timer != null) { + query_timer.cancel(); + query_timer = null; + } + arm_timer_stop(); + arm.setChecked(false); + fire.setEnabled(false); + } + + @Override + protected void onStop() { + super.onStop(); + doUnbindService(); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/ManageFrequenciesActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/ManageFrequenciesActivity.java new file mode 100644 index 00000000..d5101166 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/ManageFrequenciesActivity.java @@ -0,0 +1,307 @@ +/* + * Copyright © 2016 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.lang.ref.WeakReference; +import java.util.*; +import java.text.*; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.content.*; +import android.graphics.*; +import android.os.*; +import android.view.*; +import android.view.View.*; +import android.view.inputmethod.*; +import android.widget.*; +import android.widget.AdapterView.*; + +import org.altusmetrum.altoslib_13.*; + +class FrequencyItem { + public AltosFrequency frequency; + public LinearLayout frequency_view = null; + public TextView pretty_view = null; + + private void update() { + if (pretty_view != null && frequency != null) + pretty_view.setText(frequency.toString()); + } + + public void realize(LinearLayout frequency_view, + TextView pretty_view) { + if (frequency_view != this.frequency_view || + pretty_view != this.pretty_view) + { + this.frequency_view = frequency_view; + this.pretty_view = pretty_view; + update(); + } + } + + public void set_frequency(AltosFrequency frequency) { + this.frequency = frequency; + update(); + } + + public FrequencyItem(AltosFrequency frequency) { + this.frequency = frequency; + } +} + +class FrequencyAdapter extends ArrayAdapter { + int resource; + int selected_item = -1; + + public FrequencyAdapter(Context context, int in_resource) { + super(context, in_resource); + resource = in_resource; + } + + public int count() { + int count; + + for (count = 0;; count++) { + try { + getItem(count); + } catch (IndexOutOfBoundsException ie) { + return count; + } + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + FrequencyItem item = getItem(position); + if (item.frequency_view == null) { + LinearLayout frequency_view = new LinearLayout(getContext()); + String inflater = Context.LAYOUT_INFLATER_SERVICE; + LayoutInflater li = (LayoutInflater) getContext().getSystemService(inflater); + li.inflate(resource, frequency_view, true); + + item.realize(frequency_view, + (TextView) frequency_view.findViewById(R.id.frequency)); + } + if (position == selected_item) + item.frequency_view.setBackgroundColor(Color.RED); + else + item.frequency_view.setBackgroundColor(Color.BLACK); + return item.frequency_view; + } +} + +public class ManageFrequenciesActivity extends Activity { + private ListView frequencies_view; + + private Button set; + private Button remove; + private Button done; + + private EditText set_frequency; + private EditText set_description; + + private HashMap frequencies = new HashMap();; + + private FrequencyAdapter frequencies_adapter; + + private boolean is_bound; + private boolean changed = false; + + private void done() { + + set(); + + if (changed) { + AltosFrequency[] frequencies = new AltosFrequency[frequencies_adapter.count()]; + for (int i = 0; i < frequencies.length; i++) + frequencies[i] = frequencies_adapter.getItem(i).frequency; + AltosPreferences.set_common_frequencies(frequencies); + } + + Intent intent = new Intent(); + setResult(Activity.RESULT_OK, intent); + finish(); + } + + private void load_item() { + if (frequencies_adapter.selected_item >= 0) { + FrequencyItem item = frequencies_adapter.getItem(frequencies_adapter.selected_item); + + set_frequency.setText(item.frequency.frequency_string()); + set_description.setText(item.frequency.description); + } else { + set_frequency.setText(""); + set_description.setText(""); + } + } + + private void select_item(int position) { + if (position != frequencies_adapter.selected_item) { + if (frequencies_adapter.selected_item >= 0) + frequencies_view.setItemChecked(frequencies_adapter.selected_item, false); + if (position >= 0) + frequencies_view.setItemChecked(position, true); + frequencies_adapter.selected_item = position; + } else { + if (frequencies_adapter.selected_item >= 0) + frequencies_view.setItemChecked(frequencies_adapter.selected_item, false); + frequencies_adapter.selected_item = -1; + } + load_item(); + } + + private int find(AltosFrequency frequency) { + for (int pos = 0; pos < frequencies_adapter.getCount(); pos++) { + FrequencyItem item = frequencies_adapter.getItem(pos); + if (item.frequency.frequency == frequency.frequency && + item.frequency.description.equals(frequency.description)) + return pos; + } + return -1; + } + + private int insert_item(AltosFrequency frequency) { + FrequencyItem new_item = new FrequencyItem(frequency); + int pos; + for (pos = 0; pos < frequencies_adapter.getCount(); pos++) { + FrequencyItem item = frequencies_adapter.getItem(pos); + if (item.frequency.frequency == new_item.frequency.frequency) { + item.set_frequency(frequency); + return pos; + } + if (item.frequency.frequency > new_item.frequency.frequency) + break; + } + frequencies_adapter.insert(new_item, pos); + return pos; + } + + private class FrequencyItemClickListener implements ListView.OnItemClickListener { + @Override + public void onItemClick(AdapterView av, View v, int position, long id) { + select_item(position); + } + } + + private void hide_keyboard() { + InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); + View view = getCurrentFocus(); + if (view != null) + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void set() { + String frequency_text = set_frequency.getEditableText().toString(); + String description_text = set_description.getEditableText().toString(); + + try { + double f = AltosParse.parse_double_locale(frequency_text); + AltosFrequency frequency = new AltosFrequency(f, description_text); + int pos; + + pos = find(frequency); + if (pos < 0) { + pos = insert_item(frequency); + changed = true; + } + frequencies_adapter.selected_item = -1; + select_item(pos); + } catch (ParseException pe) { + } + hide_keyboard(); + } + + private void remove() { + if (frequencies_adapter.selected_item >= 0) { + frequencies_adapter.remove(frequencies_adapter.getItem(frequencies_adapter.selected_item)); + select_item(-1); + frequencies_view.setAdapter(frequencies_adapter); + changed = true; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.manage_frequencies); + + frequencies_view = (ListView) findViewById(R.id.frequencies); + frequencies_view.setClickable(true); + + frequencies_adapter = new FrequencyAdapter(this, R.layout.frequency); + + frequencies_view.setAdapter(frequencies_adapter); + frequencies_view.setOnItemClickListener(new FrequencyItemClickListener()); + + AltosFrequency[] frequencies = AltosPreferences.common_frequencies(); + for (AltosFrequency frequency : frequencies) + insert_item(frequency); + + set_frequency = (EditText) findViewById(R.id.set_frequency); + set_description = (EditText) findViewById(R.id.set_description); + + set = (Button) findViewById(R.id.set); + set.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + set(); + } + }); + + remove = (Button) findViewById(R.id.remove); + remove.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + remove(); + } + }); + + done = (Button) findViewById(R.id.done); + done.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + done(); + } + }); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/MapTypeActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/MapTypeActivity.java new file mode 100644 index 00000000..ec88d2d6 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/MapTypeActivity.java @@ -0,0 +1,85 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.widget.*; +import android.widget.AdapterView.*; + +import org.altusmetrum.altoslib_13.*; + +public class MapTypeActivity extends Activity { + private Button hybrid; + private Button satellite; + private Button roadmap; + private Button terrain; + private int selected_type; + + public static final String EXTRA_MAP_TYPE = "map_type"; + + private void done(int type) { + + Intent intent = new Intent(); + intent.putExtra(EXTRA_MAP_TYPE, type); + setResult(Activity.RESULT_OK, intent); + finish(); + } + + public void selectType(View view) { + AltosDebug.debug("selectType %s", view.toString()); + if (view == hybrid) + done(AltosMap.maptype_hybrid); + if (view == satellite) + done(AltosMap.maptype_satellite); + if (view == roadmap) + done(AltosMap.maptype_roadmap); + if (view == terrain) + done(AltosMap.maptype_terrain); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.map_type); + + hybrid = (Button) findViewById(R.id.map_type_hybrid); + satellite = (Button) findViewById(R.id.map_type_satellite); + roadmap = (Button) findViewById(R.id.map_type_roadmap); + terrain = (Button) findViewById(R.id.map_type_terrain); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/PreloadMapActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/PreloadMapActivity.java new file mode 100644 index 00000000..e393b566 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/PreloadMapActivity.java @@ -0,0 +1,384 @@ +/* + * Copyright © 2015 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import java.io.*; +import java.text.*; + +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.widget.*; +import android.widget.AdapterView.*; +import android.location.Location; +import android.location.LocationManager; +import android.location.LocationListener; +import android.location.Criteria; + +import org.altusmetrum.altoslib_13.*; + +/** + * This Activity appears as a dialog. It lists any paired devices and + * devices detected in the area after discovery. When a device is chosen + * by the user, the MAC address of the device is sent back to the parent + * Activity in the result Intent. + */ +public class PreloadMapActivity extends Activity implements AltosLaunchSiteListener, AltosMapLoaderListener, LocationListener { + + private ArrayAdapter known_sites_adapter; + +/* + private CheckBox hybrid; + private CheckBox satellite; + private CheckBox roadmap; + private CheckBox terrain; +*/ + + private Spinner known_sites_spinner; + private Spinner min_zoom; + private Spinner max_zoom; + private TextView radius_label; + private Spinner radius; + + private EditText latitude; + private EditText longitude; + + private ProgressBar progress; + + private AltosMapLoader loader; + + long loader_notify_time; + + /* AltosMapLoaderListener interfaces */ + public void loader_start(final int max) { + loader_notify_time = System.currentTimeMillis(); + + this.runOnUiThread(new Runnable() { + public void run() { + progress.setMax(max); + progress.setProgress(0); + } + }); + } + + public void loader_notify(final int cur, final int max, final String name) { + long now = System.currentTimeMillis(); + + if (now - loader_notify_time < 100) + return; + + loader_notify_time = now; + + this.runOnUiThread(new Runnable() { + public void run() { + progress.setProgress(cur); + } + }); + } + + public void loader_done(int max) { + loader = null; + this.runOnUiThread(new Runnable() { + public void run() { + progress.setProgress(0); + finish(); + } + }); + } + + public void debug(String format, Object ... arguments) { + AltosDebug.debug(format, arguments); + } + + /* AltosLaunchSiteListener interface */ + + public void notify_launch_sites(final List sites) { + this.runOnUiThread(new Runnable() { + public void run() { + for (AltosLaunchSite site : sites) + known_sites_adapter.add(site); + } + }); + } + + /* LocationProvider interface */ + + AltosLaunchSite current_location_site; + + public void onLocationChanged(Location location) { + AltosDebug.debug("location changed"); + if (current_location_site == null) { + AltosLaunchSite selected_item = (AltosLaunchSite) known_sites_spinner.getSelectedItem(); + + current_location_site = new AltosLaunchSite("Current Location", location.getLatitude(), location.getLongitude()); + known_sites_adapter.insert(current_location_site, 0); + + if (selected_item != null) + known_sites_spinner.setSelection(known_sites_adapter.getPosition(selected_item)); + else { + latitude.setText(new StringBuffer(String.format("%12.6f", current_location_site.latitude))); + longitude.setText(new StringBuffer(String.format("%12.6f", current_location_site.longitude))); + } + } else { + current_location_site.latitude = location.getLatitude(); + current_location_site.longitude = location.getLongitude(); + } + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + public void onProviderEnabled(String provider) { + } + + public void onProviderDisabled(String provider) { + } + + private double text(EditText view) throws ParseException { + return AltosParse.parse_double_locale(view.getEditableText().toString()); + } + + private double latitude() throws ParseException { + return text(latitude); + } + + private double longitude() throws ParseException { + return text(longitude); + } + + private int value(Spinner spinner) { + return (Integer) spinner.getSelectedItem(); + } + + private int min_z() { + return value(min_zoom); + } + + private int max_z() { + return value(max_zoom); + } + + private double value_distance(Spinner spinner) { + return (Double) spinner.getSelectedItem(); + } + + private double radius() { + double r = value_distance(radius); + if (AltosPreferences.imperial_units()) + r = AltosConvert.miles_to_meters(r); + else + r = r * 1000; + return r; + } + +/* + private int bit(CheckBox box, int value) { + if (box.isChecked()) + return 1 << value; + return 0; + } +*/ + + private int types() { +/* + return (bit(hybrid, AltosMap.maptype_hybrid) | + bit(satellite, AltosMap.maptype_satellite) | + bit(roadmap, AltosMap.maptype_roadmap) | + bit(terrain, AltosMap.maptype_terrain)); +*/ + return 1 << AltosMap.maptype_hybrid; + } + + private void load() { + if (loader != null) + return; + + try { + double lat = latitude(); + double lon = longitude(); + int min = min_z(); + int max = max_z(); + double r = radius(); + int t = types(); + + AltosDebug.debug("PreloadMap load %f %f %d %d %f %d\n", + lat, lon, min, max, r, t); + loader = new AltosMapLoader(this, lat, lon, min, max, r, t, AltosMapOffline.scale); + } catch (ParseException e) { + AltosDebug.debug("PreloadMap load raised exception %s", e.toString()); + } + } + + private void add_numbers(Spinner spinner, int min, int max, int def) { + + ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); + + int spinner_def = 0; + int pos = 0; + + for (int i = min; i <= max; i++) { + adapter.add(new Integer(i)); + if (i == def) + spinner_def = pos; + pos++; + } + + spinner.setAdapter(adapter); + spinner.setSelection(spinner_def); + } + + + private void add_distance(Spinner spinner, double[] distances_km, double def_km, double[] distances_mi, double def_mi) { + + ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); + + int spinner_def = 0; + int pos = 0; + + double[] distances; + double def; + if (AltosPreferences.imperial_units()) { + distances = distances_mi; + def = def_mi; + } else { + distances = distances_km; + def = def_km; + } + + for (int i = 0; i < distances.length; i++) { + adapter.add(distances[i]); + if (distances[i] == def) + spinner_def = pos; + pos++; + } + + spinner.setAdapter(adapter); + spinner.setSelection(spinner_def); + } + + + + class SiteListListener implements OnItemSelectedListener { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + AltosLaunchSite site = (AltosLaunchSite) parent.getItemAtPosition(pos); + latitude.setText(new StringBuffer(String.format("%12.6f", site.latitude))); + longitude.setText(new StringBuffer(String.format("%12.6f", site.longitude))); + } + public void onNothingSelected(AdapterView parent) { + } + + public SiteListListener() { + } + } + + double[] radius_mi = { 1, 2, 5, 10, 20 }; + double radius_def_mi = 2; + double[] radius_km = { 1, 2, 5, 10, 20, 30 }; + double radius_def_km = 2; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.map_preload); + + // Set result CANCELED incase the user backs out + setResult(Activity.RESULT_CANCELED); + + // Initialize the button to perform device discovery + Button loadButton = (Button) findViewById(R.id.preload_load); + loadButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + load(); + } + }); + + latitude = (EditText) findViewById(R.id.preload_latitude); + longitude = (EditText) findViewById(R.id.preload_longitude); + +/* + hybrid = (CheckBox) findViewById(R.id.preload_hybrid); + satellite = (CheckBox) findViewById(R.id.preload_satellite); + roadmap = (CheckBox) findViewById(R.id.preload_roadmap); + terrain = (CheckBox) findViewById(R.id.preload_terrain); + + hybrid.setChecked(true); +*/ + + min_zoom = (Spinner) findViewById(R.id.preload_min_zoom); + add_numbers(min_zoom, + AltosMap.min_zoom - AltosMap.default_zoom, + AltosMap.max_zoom - AltosMap.default_zoom, -2); + max_zoom = (Spinner) findViewById(R.id.preload_max_zoom); + add_numbers(max_zoom, + AltosMap.min_zoom - AltosMap.default_zoom, + AltosMap.max_zoom - AltosMap.default_zoom, 2); + radius_label = (TextView) findViewById(R.id.preload_radius_label); + radius = (Spinner) findViewById(R.id.preload_radius); + if (AltosPreferences.imperial_units()) + radius_label.setText("Radius (miles)"); + else + radius_label.setText("Radius (km)"); + add_distance(radius, radius_km, radius_def_km, radius_mi, radius_def_mi); + + progress = (ProgressBar) findViewById(R.id.preload_progress); + + // Initialize array adapters. One for already paired devices and + // one for newly discovered devices + known_sites_spinner = (Spinner) findViewById(R.id.preload_site_list); + + known_sites_adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); + + known_sites_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + known_sites_spinner.setAdapter(known_sites_adapter); + known_sites_spinner.setOnItemSelectedListener(new SiteListListener()); + + // Listen for GPS and Network position updates + LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); + + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, this); + + new AltosLaunchSites(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (loader != null) + loader.abort(); + + // Stop listening for location updates + ((LocationManager) getSystemService(Context.LOCATION_SERVICE)).removeUpdates(this); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/SetupActivity.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/SetupActivity.java new file mode 100644 index 00000000..bad0b821 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/SetupActivity.java @@ -0,0 +1,346 @@ +/* + * Copyright © 2016 Keith Packard + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.lang.ref.WeakReference; +import java.util.*; +import org.altusmetrum.AltosDroid.R; + +import android.app.Activity; +import android.bluetooth.*; +import android.content.*; +import android.os.*; +import android.view.*; +import android.view.View.*; +import android.widget.*; +import android.widget.AdapterView.*; + +import org.altusmetrum.altoslib_13.*; + +public class SetupActivity extends Activity { + private Spinner select_rate; + private Spinner set_units; + private Spinner map_type; + private Spinner map_source; + private Button manage_frequencies; + private Button preload_maps; + private Button done; + + private boolean is_bound; + private Messenger service = null; + + public final static String EXTRA_SETUP_CHANGES = "setup_changes"; + + private ServiceConnection connection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder binder) { + service = new Messenger(binder); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been unexpectedly disconnected - process crashed. + service = null; + } + }; + + void doBindService() { + bindService(new Intent(this, TelemetryService.class), connection, Context.BIND_AUTO_CREATE); + is_bound = true; + } + + void doUnbindService() { + if (is_bound) { + // If we have received the service, and hence registered with it, then now is the time to unregister. + unbindService(connection); + is_bound = false; + } + } + + static final String[] rates = { + "38400", + "9600", + "2400", + }; + + static final String[] map_types = { + "Hybrid", + "Satellite", + "Roadmap", + "Terrain" + }; + + static final int[] map_type_values = { + AltosMap.maptype_hybrid, + AltosMap.maptype_satellite, + AltosMap.maptype_roadmap, + AltosMap.maptype_terrain, + }; + + static final String[] map_sources = { + "Online", + "Offline" + }; + + private int set_telemetry_rate; + private int set_map_source; + private int set_map_type; + private boolean set_imperial_units; + + private int changes = 0; + + private void add_change(int change) { + changes |= change; + } + + private void done() { + Intent intent = new Intent(); + if ((changes & AltosDroid.SETUP_BAUD) != 0) + AltosPreferences.set_telemetry_rate(1, set_telemetry_rate); + if ((changes & AltosDroid.SETUP_UNITS) != 0) + AltosPreferences.set_imperial_units(set_imperial_units); + if ((changes & AltosDroid.SETUP_MAP_SOURCE) != 0) + AltosDroidPreferences.set_map_source(set_map_source); + if ((changes & AltosDroid.SETUP_MAP_TYPE) != 0) + AltosPreferences.set_map_type(set_map_type); + intent.putExtra(EXTRA_SETUP_CHANGES, changes); + setResult(Activity.RESULT_OK, intent); + finish(); + } + + private void add_strings(Spinner spinner, String[] strings, int def) { + ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item); + + for (int i = 0; i < strings.length; i++) + adapter.add(strings[i]); + + spinner.setAdapter(adapter); + if (def >= 0) + spinner.setSelection(def); + } + + private int default_rate_pos() { + int default_rate = AltosPreferences.telemetry_rate(1); + + for (int pos = 0; pos < rates.length; pos++) { + if (string_to_rate(rates[pos]) == default_rate) + return pos; + } + return -1; + } + + private void setBaud(int baud) { + try { + service.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud)); + set_telemetry_rate = baud; + add_change(AltosDroid.SETUP_BAUD); + } catch (RemoteException e) { + } + } + + private int string_to_rate(String baud) { + int rate = AltosLib.ao_telemetry_rate_38400; + try { + int value = Integer.parseInt(baud); + switch (value) { + case 2400: + rate = AltosLib.ao_telemetry_rate_2400; + break; + case 9600: + rate = AltosLib.ao_telemetry_rate_9600; + break; + case 38400: + rate = AltosLib.ao_telemetry_rate_38400; + break; + } + } catch (NumberFormatException e) { + } + return rate; + } + + private void setBaud(String baud) { + setBaud(string_to_rate(baud)); + } + + private void select_rate(int pos) { + setBaud(rates[pos]); + } + + static final String[] units = { + "Metric", + "Imperial" + }; + + private int default_units_pos() { + boolean imperial = AltosPreferences.imperial_units(); + + if (imperial) + return 1; + return 0; + } + + private void set_units(int pos) { + switch (pos) { + default: + set_imperial_units = false; + break; + case 1: + set_imperial_units = true; + break; + } + add_change(AltosDroid.SETUP_UNITS); + } + + private int default_map_type_pos() { + int default_map_type = AltosPreferences.map_type(); + + for (int pos = 0; pos < map_types.length; pos++) + if (map_type_values[pos] == default_map_type) + return pos; + return 0; + } + + private void select_map_type(int pos) { + set_map_type = map_type_values[pos]; + add_change(AltosDroid.SETUP_MAP_TYPE); + } + + private int default_map_source_pos() { + int default_source = AltosDroidPreferences.map_source(); + + switch (default_source) { + case AltosDroidPreferences.MAP_SOURCE_OFFLINE: + return 1; + default: + return 0; + } + } + + private void select_map_source(int pos) { + switch (pos) { + default: + set_map_source = AltosDroidPreferences.MAP_SOURCE_ONLINE; + break; + case 1: + set_map_source = AltosDroidPreferences.MAP_SOURCE_OFFLINE; + break; + } + add_change(AltosDroid.SETUP_MAP_SOURCE); + } + + private void manage_frequencies(){ + Intent intent = new Intent(this, ManageFrequenciesActivity.class); + startActivity(intent); + } + + private void preload_maps(){ + Intent intent = new Intent(this, PreloadMapActivity.class); + startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + AltosDebug.init(this); + AltosDebug.debug("+++ ON CREATE +++"); + + // Initialise preferences + AltosDroidPreferences.init(this); + + // Setup the window + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.setup); + + select_rate = (Spinner) findViewById(R.id.select_rate); + add_strings(select_rate, rates, default_rate_pos()); + select_rate.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + select_rate(pos); + } + public void onNothingSelected(AdapterView parent) { + } + }); + + set_units = (Spinner) findViewById(R.id.set_units); + add_strings(set_units, units, default_units_pos()); + set_units.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + set_units(pos); + } + public void onNothingSelected(AdapterView parent) { + } + }); + + map_type = (Spinner) findViewById(R.id.map_type); + add_strings(map_type, map_types, default_map_type_pos()); + map_type.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + select_map_type(pos); + } + public void onNothingSelected(AdapterView parent) { + } + }); + + map_source = (Spinner) findViewById(R.id.map_source); + add_strings(map_source, map_sources, default_map_source_pos()); + map_source.setOnItemSelectedListener(new OnItemSelectedListener() { + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + select_map_source(pos); + } + public void onNothingSelected(AdapterView parent) { + } + }); + + + manage_frequencies = (Button) findViewById(R.id.manage_frequencies); + manage_frequencies.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + manage_frequencies(); + } + }); + + preload_maps = (Button) findViewById(R.id.preload_maps); + preload_maps.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + preload_maps(); + } + }); + + done = (Button) findViewById(R.id.done); + done.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + done(); + } + }); + + // Set result for when the user backs out + setResult(Activity.RESULT_CANCELED); + } + + @Override + protected void onStart() { + super.onStart(); + doBindService(); + } + + @Override + protected void onStop() { + super.onStop(); + doUnbindService(); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabFlight.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabFlight.java new file mode 100644 index 00000000..8997d967 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabFlight.java @@ -0,0 +1,128 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.*; +import android.widget.*; +import android.location.Location; + +public class TabFlight extends AltosDroidTab { + private TextView speed_view; + private TextView height_view; + private TextView max_speed_view; + private TextView max_height_view; + private TextView elevation_view; + private TextView range_view; + private TextView bearing_view; + private TextView compass_view; + private TextView distance_view; + private TextView latitude_view; + private TextView longitude_view; + private View apogee_view; + private TextView apogee_voltage_view; + private TextView apogee_voltage_label; + private GoNoGoLights apogee_lights; + private View main_view; + private TextView main_voltage_view; + private TextView main_voltage_label; + private GoNoGoLights main_lights; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_flight, container, false); + + speed_view = (TextView) v.findViewById(R.id.speed_value); + height_view = (TextView) v.findViewById(R.id.height_value); + max_speed_view = (TextView) v.findViewById(R.id.max_speed_value); + max_height_view= (TextView) v.findViewById(R.id.max_height_value); + elevation_view = (TextView) v.findViewById(R.id.elevation_value); + range_view = (TextView) v.findViewById(R.id.range_value); + bearing_view = (TextView) v.findViewById(R.id.bearing_value); + compass_view = (TextView) v.findViewById(R.id.compass_value); + distance_view = (TextView) v.findViewById(R.id.distance_value); + latitude_view = (TextView) v.findViewById(R.id.lat_value); + longitude_view = (TextView) v.findViewById(R.id.lon_value); + + apogee_view = v.findViewById(R.id.apogee_view); + apogee_voltage_view = (TextView) v.findViewById(R.id.apogee_voltage_value); + apogee_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.apogee_redled), + (ImageView) v.findViewById(R.id.apogee_greenled), + getResources()); + apogee_voltage_label = (TextView) v.findViewById(R.id.apogee_voltage_label); + + main_view = v.findViewById(R.id.main_view); + main_voltage_view = (TextView) v.findViewById(R.id.main_voltage_value); + main_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.main_redled), + (ImageView) v.findViewById(R.id.main_greenled), + getResources()); + main_voltage_label = (TextView) v.findViewById(R.id.main_voltage_label); + + return v; + } + + public String tab_name() { return AltosDroid.tab_flight_name; } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + if (state != null) { + set_value(speed_view, AltosConvert.speed, 6, state.speed()); + set_value(height_view, AltosConvert.height, 6, state.height()); + set_value(max_speed_view, AltosConvert.speed, 6, state.max_speed()); + set_value(max_height_view, AltosConvert.height, 6, state.max_height()); + if (from_receiver != null) { + elevation_view.setText(AltosDroid.number("%3.0f°", from_receiver.elevation)); + set_value(range_view, AltosConvert.distance, 6, from_receiver.range); + bearing_view.setText(AltosDroid.number("%3.0f°", from_receiver.bearing)); + compass_view.setText(from_receiver.bearing_words(AltosGreatCircle.BEARING_LONG)); + set_value(distance_view, AltosConvert.distance, 6, from_receiver.distance); + } else { + elevation_view.setText(""); + range_view.setText(""); + bearing_view.setText(""); + compass_view.setText(""); + distance_view.setText(""); + } + if (state.gps != null) { + latitude_view.setText(AltosDroid.pos(state.gps.lat, "N", "S")); + longitude_view.setText(AltosDroid.pos(state.gps.lon, "E", "W")); + } + + if (state.apogee_voltage == AltosLib.MISSING) { + apogee_view.setVisibility(View.GONE); + } else { + apogee_voltage_view.setText(AltosDroid.number("%4.2f V", state.apogee_voltage)); + apogee_lights.set(state.apogee_voltage > 3.2, state.apogee_voltage == AltosLib.MISSING); + apogee_view.setVisibility(View.VISIBLE); + } + + if (state.main_voltage == AltosLib.MISSING) { + main_view.setVisibility(View.GONE); + } else { + main_voltage_view.setText(AltosDroid.number("%4.2f V", state.main_voltage)); + main_lights.set(state.main_voltage > 3.2, state.main_voltage == AltosLib.MISSING); + main_view.setVisibility(View.VISIBLE); + } + } + } + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabMap.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabMap.java new file mode 100644 index 00000000..a2f997f1 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabMap.java @@ -0,0 +1,173 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import java.io.*; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.graphics.*; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.view.*; +import android.widget.*; +import android.location.Location; +import android.content.*; + +public class TabMap extends AltosDroidTab implements AltosDroidMapSourceListener { + + AltosLatLon here; + + private TextView mDistanceView; + private TextView mBearingLabel; + private TextView mBearingView; + private TextView mTargetLatitudeView; + private TextView mTargetLongitudeView; + private TextView mReceiverLatitudeView; + private TextView mReceiverLongitudeView; + private AltosMapOffline map_offline; + private AltosMapOnline map_online; + private View view; + private int map_source; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + view = inflater.inflate(R.layout.tab_map, container, false); + int map_source = AltosDroidPreferences.map_source(); + + mDistanceView = (TextView)view.findViewById(R.id.distance_value); + mBearingLabel = (TextView)view.findViewById(R.id.bearing_label); + mBearingView = (TextView)view.findViewById(R.id.bearing_value); + mTargetLatitudeView = (TextView)view.findViewById(R.id.target_lat_value); + mTargetLongitudeView = (TextView)view.findViewById(R.id.target_lon_value); + mReceiverLatitudeView = (TextView)view.findViewById(R.id.receiver_lat_value); + mReceiverLongitudeView = (TextView)view.findViewById(R.id.receiver_lon_value); + map_offline = (AltosMapOffline)view.findViewById(R.id.map_offline); + map_offline.onCreateView(altos_droid); + map_online = new AltosMapOnline(view.getContext()); + map_online.onCreateView(altos_droid); + map_source_changed(AltosDroidPreferences.map_source()); + AltosDroidPreferences.register_map_source_listener(this); + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + if (map_online != null) + getChildFragmentManager().beginTransaction().add(R.id.map_online, map_online.mMapFragment).commit(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + map_offline.onDestroyView(); + map_online.onDestroyView(); + AltosDroidPreferences.unregister_map_source_listener(this); + } + + public String tab_name() { return AltosDroid.tab_map_name; } + + private void center(double lat, double lon, double accuracy) { + if (map_offline != null) + map_offline.center(lat, lon, accuracy); + if (map_online != null) + map_online.center(lat, lon, accuracy); + } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + if (from_receiver != null) { + String direction = AltosDroid.direction(from_receiver, receiver); + if (direction != null) { + mBearingLabel.setText("Direction"); + mBearingView.setText(direction); + } else { + mBearingLabel.setText("Bearing"); + mBearingView.setText(String.format("%3.0f°", from_receiver.bearing)); + } + set_value(mDistanceView, AltosConvert.distance, 6, from_receiver.distance); + } else { + mBearingLabel.setText("Bearing"); + mBearingView.setText(""); + set_value(mDistanceView, AltosConvert.distance, 6, AltosLib.MISSING); + } + + if (state != null) { + if (state.gps != null) { + mTargetLatitudeView.setText(AltosDroid.pos(state.gps.lat, "N", "S")); + mTargetLongitudeView.setText(AltosDroid.pos(state.gps.lon, "E", "W")); + } + } + + if (receiver != null) { + double accuracy; + + here = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude()); + if (receiver.hasAccuracy()) + accuracy = receiver.getAccuracy(); + else + accuracy = 1000; + mReceiverLatitudeView.setText(AltosDroid.pos(here.lat, "N", "S")); + mReceiverLongitudeView.setText(AltosDroid.pos(here.lon, "E", "W")); + center (here.lat, here.lon, accuracy); + } + if (map_source == AltosDroidPreferences.MAP_SOURCE_OFFLINE) { + if (map_offline != null) + map_offline.show(telem_state, state, from_receiver, receiver); + } else { + if (map_online != null) + map_online.show(telem_state, state, from_receiver, receiver); + } + } + + public void map_source_changed(int map_source) { + this.map_source = map_source; + if (map_source == AltosDroidPreferences.MAP_SOURCE_OFFLINE) { + if (map_online != null) + map_online.set_visible(false); + if (map_offline != null) { + map_offline.set_visible(true); + map_offline.show(last_telem_state, last_state, last_from_receiver, last_receiver); + } + } else { + if (map_offline != null) + map_offline.set_visible(false); + if (map_online != null) { + map_online.set_visible(true); + map_online.show(last_telem_state, last_state, last_from_receiver, last_receiver); + } + } + } + + public TabMap() { + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabPad.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabPad.java new file mode 100644 index 00000000..f317ae91 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabPad.java @@ -0,0 +1,239 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.*; +import android.widget.*; +import android.location.Location; + +public class TabPad extends AltosDroidTab { + private TextView battery_voltage_view; + private GoNoGoLights battery_lights; + + private TableRow receiver_row; + private TextView receiver_voltage_view; + private TextView receiver_voltage_label; + private GoNoGoLights receiver_voltage_lights; + + private TableRow apogee_row; + private TextView apogee_voltage_view; + private TextView apogee_voltage_label; + private GoNoGoLights apogee_lights; + + private TableRow main_row; + private TextView main_voltage_view; + private TextView main_voltage_label; + private GoNoGoLights main_lights; + + private TextView data_logging_view; + private GoNoGoLights data_logging_lights; + + private TextView gps_locked_view; + private GoNoGoLights gps_locked_lights; + + private TextView gps_ready_view; + private GoNoGoLights gps_ready_lights; + + private TextView receiver_latitude_view; + private TextView receiver_longitude_view; + private TextView receiver_altitude_view; + + private TableRow[] ignite_row = new TableRow[4]; + private TextView[] ignite_voltage_view = new TextView[4]; + private TextView[] ignite_voltage_label = new TextView[4]; + private GoNoGoLights[] ignite_lights = new GoNoGoLights[4]; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_pad, container, false); + battery_voltage_view = (TextView) v.findViewById(R.id.battery_voltage_value); + battery_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.battery_redled), + (ImageView) v.findViewById(R.id.battery_greenled), + getResources()); + + receiver_row = (TableRow) v.findViewById(R.id.receiver_row); + receiver_voltage_view = (TextView) v.findViewById(R.id.receiver_voltage_value); + receiver_voltage_label = (TextView) v.findViewById(R.id.receiver_voltage_label); + receiver_voltage_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.receiver_redled), + (ImageView) v.findViewById(R.id.receiver_greenled), + getResources()); + + apogee_row = (TableRow) v.findViewById(R.id.apogee_row); + apogee_voltage_view = (TextView) v.findViewById(R.id.apogee_voltage_value); + apogee_voltage_label = (TextView) v.findViewById(R.id.apogee_voltage_label); + apogee_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.apogee_redled), + (ImageView) v.findViewById(R.id.apogee_greenled), + getResources()); + + main_row = (TableRow) v.findViewById(R.id.main_row); + main_voltage_view = (TextView) v.findViewById(R.id.main_voltage_value); + main_voltage_label = (TextView) v.findViewById(R.id.main_voltage_label); + main_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.main_redled), + (ImageView) v.findViewById(R.id.main_greenled), + getResources()); + + data_logging_view = (TextView) v.findViewById(R.id.logging_value); + data_logging_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.logging_redled), + (ImageView) v.findViewById(R.id.logging_greenled), + getResources()); + + gps_locked_view = (TextView) v.findViewById(R.id.gps_locked_value); + gps_locked_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.gps_locked_redled), + (ImageView) v.findViewById(R.id.gps_locked_greenled), + getResources()); + + gps_ready_view = (TextView) v.findViewById(R.id.gps_ready_value); + gps_ready_lights = new GoNoGoLights((ImageView) v.findViewById(R.id.gps_ready_redled), + (ImageView) v.findViewById(R.id.gps_ready_greenled), + getResources()); + + for (int i = 0; i < 4; i++) { + int row_id, view_id, label_id, lights_id; + int red_id, green_id; + switch (i) { + case 0: + default: + row_id = R.id.ignite_a_row; + view_id = R.id.ignite_a_voltage_value; + label_id = R.id.ignite_a_voltage_label; + red_id = R.id.ignite_a_redled; + green_id = R.id.ignite_a_greenled; + break; + case 1: + row_id = R.id.ignite_b_row; + view_id = R.id.ignite_b_voltage_value; + label_id = R.id.ignite_b_voltage_label; + red_id = R.id.ignite_b_redled; + green_id = R.id.ignite_b_greenled; + break; + case 2: + row_id = R.id.ignite_c_row; + view_id = R.id.ignite_c_voltage_value; + label_id = R.id.ignite_c_voltage_label; + red_id = R.id.ignite_c_redled; + green_id = R.id.ignite_c_greenled; + break; + case 3: + row_id = R.id.ignite_d_row; + view_id = R.id.ignite_d_voltage_value; + label_id = R.id.ignite_d_voltage_label; + red_id = R.id.ignite_d_redled; + green_id = R.id.ignite_d_greenled; + break; + } + ignite_row[i] = (TableRow) v.findViewById(row_id); + ignite_voltage_view[i] = (TextView) v.findViewById(view_id); + ignite_voltage_label[i] = (TextView) v.findViewById(label_id); + ignite_lights[i] = new GoNoGoLights((ImageView) v.findViewById(red_id), + (ImageView) v.findViewById(green_id), + getResources()); + } + + receiver_latitude_view = (TextView) v.findViewById(R.id.receiver_lat_value); + receiver_longitude_view = (TextView) v.findViewById(R.id.receiver_lon_value); + receiver_altitude_view = (TextView) v.findViewById(R.id.receiver_alt_value); + return v; + } + + public String tab_name() { return AltosDroid.tab_pad_name; } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + if (state != null) { + battery_voltage_view.setText(AltosDroid.number(" %4.2f V", state.battery_voltage)); + battery_lights.set(state.battery_voltage >= AltosLib.ao_battery_good, state.battery_voltage == AltosLib.MISSING); + if (state.apogee_voltage == AltosLib.MISSING) { + apogee_row.setVisibility(View.GONE); + } else { + apogee_voltage_view.setText(AltosDroid.number(" %4.2f V", state.apogee_voltage)); + apogee_row.setVisibility(View.VISIBLE); + } + apogee_lights.set(state.apogee_voltage >= AltosLib.ao_igniter_good, state.apogee_voltage == AltosLib.MISSING); + if (state.main_voltage == AltosLib.MISSING) { + main_row.setVisibility(View.GONE); + } else { + main_voltage_view.setText(AltosDroid.number(" %4.2f V", state.main_voltage)); + main_row.setVisibility(View.VISIBLE); + } + main_lights.set(state.main_voltage >= AltosLib.ao_igniter_good, state.main_voltage == AltosLib.MISSING); + + int num_igniter = state.igniter_voltage == null ? 0 : state.igniter_voltage.length; + + for (int i = 0; i < 4; i++) { + double voltage = i >= num_igniter ? AltosLib.MISSING : state.igniter_voltage[i]; + if (voltage == AltosLib.MISSING) { + ignite_row[i].setVisibility(View.GONE); + } else { + ignite_voltage_view[i].setText(AltosDroid.number(" %4.2f V", voltage)); + ignite_row[i].setVisibility(View.VISIBLE); + } + ignite_lights[i].set(voltage >= AltosLib.ao_igniter_good, voltage == AltosLib.MISSING); + } + + if (state.cal_data().flight != 0) { + if (state.state() <= AltosLib.ao_flight_pad) + data_logging_view.setText("Ready to record"); + else if (state.state() < AltosLib.ao_flight_landed) + data_logging_view.setText("Recording data"); + else + data_logging_view.setText("Recorded data"); + } else { + data_logging_view.setText("Storage full"); + } + data_logging_lights.set(state.cal_data().flight != 0, state.cal_data().flight == AltosLib.MISSING); + + if (state.gps != null) { + int soln = state.gps.nsat; + int nsat = state.gps.cc_gps_sat != null ? state.gps.cc_gps_sat.length : 0; + gps_locked_view.setText(String.format("%d in soln, %d in view", soln, nsat)); + gps_locked_lights.set(state.gps.locked && state.gps.nsat >= 4, false); + if (state.gps_ready) + gps_ready_view.setText("Ready"); + else + gps_ready_view.setText(AltosDroid.integer("Waiting %d", state.gps_waiting)); + } else + gps_locked_lights.set(false, true); + gps_ready_lights.set(state.gps_ready, state.gps == null); + } + + if (telem_state != null) { + if (telem_state.receiver_battery == AltosLib.MISSING) { + receiver_row.setVisibility(View.GONE); + } else { + receiver_voltage_view.setText(AltosDroid.number(" %4.2f V", telem_state.receiver_battery)); + receiver_row.setVisibility(View.VISIBLE); + } + receiver_voltage_lights.set(telem_state.receiver_battery >= AltosLib.ao_battery_good, telem_state.receiver_battery == AltosLib.MISSING); + } + + if (receiver != null) { + double altitude = AltosLib.MISSING; + if (receiver.hasAltitude()) + altitude = receiver.getAltitude(); + receiver_latitude_view.setText(AltosDroid.pos(receiver.getLatitude(), "N", "S")); + receiver_longitude_view.setText(AltosDroid.pos(receiver.getLongitude(), "E", "W")); + set_value(receiver_altitude_view, AltosConvert.height, 1, altitude); + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabRecover.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabRecover.java new file mode 100644 index 00000000..3df48385 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabRecover.java @@ -0,0 +1,90 @@ +/* + * Copyright © 2013 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import org.altusmetrum.altoslib_13.*; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.location.Location; + +public class TabRecover extends AltosDroidTab { + private TextView mBearingView; + private TextView mDirectionView; + private TextView mDistanceView; + private TextView mTargetLatitudeView; + private TextView mTargetLongitudeView; + private TextView mReceiverLatitudeView; + private TextView mReceiverLongitudeView; + private TextView mMaxHeightView; + private TextView mMaxSpeedView; + private TextView mMaxAccelView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.tab_recover, container, false); + + mBearingView = (TextView) v.findViewById(R.id.bearing_value); + mDirectionView = (TextView) v.findViewById(R.id.direction_value); + mDistanceView = (TextView) v.findViewById(R.id.distance_value); + mTargetLatitudeView = (TextView) v.findViewById(R.id.target_lat_value); + mTargetLongitudeView = (TextView) v.findViewById(R.id.target_lon_value); + mReceiverLatitudeView = (TextView) v.findViewById(R.id.receiver_lat_value); + mReceiverLongitudeView = (TextView) v.findViewById(R.id.receiver_lon_value); + mMaxHeightView = (TextView) v.findViewById(R.id.max_height_value); + mMaxSpeedView = (TextView) v.findViewById(R.id.max_speed_value); + mMaxAccelView = (TextView) v.findViewById(R.id.max_accel_value); + + return v; + } + + public String tab_name() { return AltosDroid.tab_recover_name; } + + public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) { + if (from_receiver != null) { + mBearingView.setText(String.format("%3.0f°", from_receiver.bearing)); + set_value(mDistanceView, AltosConvert.distance, 6, from_receiver.distance); + String direction = AltosDroid.direction(from_receiver, receiver); + if (direction == null) + mDirectionView.setText(""); + else + mDirectionView.setText(direction); + } + if (state != null && state.gps != null) { + mTargetLatitudeView.setText(AltosDroid.pos(state.gps.lat, "N", "S")); + mTargetLongitudeView.setText(AltosDroid.pos(state.gps.lon, "E", "W")); + } + + if (receiver != null) { + mReceiverLatitudeView.setText(AltosDroid.pos(receiver.getLatitude(), "N", "S")); + mReceiverLongitudeView.setText(AltosDroid.pos(receiver.getLongitude(), "E", "W")); + } + + if (state != null) { + set_value(mMaxHeightView, AltosConvert.height, 6, state.max_height()); + set_value(mMaxAccelView, AltosConvert.accel, 6, state.max_acceleration()); + set_value(mMaxSpeedView, AltosConvert.speed, 6, state.max_speed()); + } + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabsAdapter.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabsAdapter.java new file mode 100644 index 00000000..b34a25b6 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TabsAdapter.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.ArrayList; + +import android.content.Context; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TabHost; +import android.widget.TabWidget; + +/** + * This is a helper class that implements the management of tabs and all + * details of connecting a ViewPager with associated TabHost. It relies on a + * trick. Normally a tab host has a simple API for supplying a View or + * Intent that each tab will show. This is not sufficient for switching + * between pages. So instead we make the content part of the tab host + * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy + * view to show as the tab content. It listens to changes in tabs, and takes + * care of switch to the correct paged in the ViewPager whenever the selected + * tab changes. + */ +public class TabsAdapter extends FragmentPagerAdapter + implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener { + private final Context mContext; + private final TabHost mTabHost; + private final ViewPager mViewPager; + private final ArrayList mTabs = new ArrayList(); + private int position; + + static class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager) { + super(activity.getSupportFragmentManager()); + mContext = activity; + mTabHost = tabHost; + mViewPager = pager; + mTabHost.setOnTabChangedListener(this); + mViewPager.setAdapter(this); + mViewPager.setOnPageChangeListener(this); + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + mTabs.add(info); + mTabHost.addTab(tabSpec); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mTabs.size(); + } + + @Override + public Fragment getItem(int position) { + TabInfo info = mTabs.get(position); + AltosDebug.debug("TabsAdapter.getItem(%d)", position); + info.fragment = Fragment.instantiate(mContext, info.clss.getName(), info.args); + return info.fragment; + } + + public Fragment currentItem() { + TabInfo info = mTabs.get(position); + return info.fragment; + } + + public void onTabChanged(String tabId) { + AltosDroidTab prev_frag = (AltosDroidTab) mTabs.get(position).fragment; + + position = mTabHost.getCurrentTab(); + + AltosDroidTab cur_frag = (AltosDroidTab) mTabs.get(position).fragment; + + if (prev_frag != cur_frag) { + if (prev_frag != null) { + prev_frag.set_visible(false); + } + } + if (cur_frag != null) { + cur_frag.set_visible(true); + } + AltosDebug.debug("TabsAdapter.onTabChanged(%s) = %d", tabId, position); + mViewPager.setCurrentItem(position); + } + + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + public void onPageSelected(int position) { + // Unfortunately when TabHost changes the current tab, it kindly + // also takes care of putting focus on it when not in touch mode. + // The jerk. + // This hack tries to prevent this from pulling focus out of our + // ViewPager. + TabWidget widget = mTabHost.getTabWidget(); + int oldFocusability = widget.getDescendantFocusability(); + widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); + mTabHost.setCurrentTab(position); + widget.setDescendantFocusability(oldFocusability); + } + + public void onPageScrollStateChanged(int state) { + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryLogger.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryLogger.java new file mode 100644 index 00000000..49ba5476 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryLogger.java @@ -0,0 +1,69 @@ +package org.altusmetrum.AltosDroid; + +import org.altusmetrum.altoslib_13.*; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Environment; + +public class TelemetryLogger { + private Context context = null; + private AltosLink link = null; + private AltosLog logger = null; + + private BroadcastReceiver mExternalStorageReceiver; + + public TelemetryLogger(Context in_context, AltosLink in_link) { + context = in_context; + link = in_link; + + startWatchingExternalStorage(); + } + + public void stop() { + stopWatchingExternalStorage(); + close(); + } + + private void close() { + if (logger != null) { + AltosDebug.debug("Shutting down Telemetry Logging"); + logger.close(); + logger = null; + } + } + + void handleExternalStorageState() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + if (logger == null) { + AltosDebug.debug("Starting up Telemetry Logging"); + logger = new AltosLog(link); + } + } else { + AltosDebug.debug("External Storage not present - stopping"); + close(); + } + } + + void startWatchingExternalStorage() { + mExternalStorageReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleExternalStorageState(); + } + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_REMOVED); + context.registerReceiver(mExternalStorageReceiver, filter); + handleExternalStorageState(); + } + + void stopWatchingExternalStorage() { + context.unregisterReceiver(mExternalStorageReceiver); + } + +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryReader.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryReader.java new file mode 100644 index 00000000..5cfa647b --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryReader.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2011 Keith Packard + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + + +package org.altusmetrum.AltosDroid; + +import java.text.*; +import java.io.*; +import java.util.*; +import java.util.concurrent.*; +import android.os.Handler; + +import org.altusmetrum.altoslib_13.*; + + +public class TelemetryReader extends Thread { + + int crc_errors; + + Handler handler; + + AltosLink link; + + LinkedBlockingQueue telemQueue; + + public AltosTelemetry read() throws ParseException, AltosCRCException, InterruptedException, IOException { + AltosLine l = telemQueue.take(); + if (l.line == null) + throw new IOException("IO error"); + AltosTelemetry telem = AltosTelemetryLegacy.parse(l.line); + return telem; + } + + public void close() { + link.remove_monitor(telemQueue); + link = null; + telemQueue.clear(); + telemQueue = null; + } + + public void run() { + try { + AltosDebug.debug("starting loop"); + while (telemQueue != null) { + try { + AltosTelemetry telem = read(); + telem.set_frequency(link.frequency); + handler.obtainMessage(TelemetryService.MSG_TELEMETRY, telem).sendToTarget(); + } catch (ParseException pp) { + AltosDebug.error("Parse error: %d \"%s\"", pp.getErrorOffset(), pp.getMessage()); + } catch (AltosCRCException ce) { + ++crc_errors; + handler.obtainMessage(TelemetryService.MSG_CRC_ERROR, new Integer(crc_errors)).sendToTarget(); + } + } + } catch (InterruptedException ee) { + } catch (IOException ie) { + AltosDebug.error("IO exception in telemetry reader"); + handler.obtainMessage(TelemetryService.MSG_DISCONNECTED, link).sendToTarget(); + } finally { + close(); + } + } + + public TelemetryReader (AltosLink in_link, Handler in_handler) { + AltosDebug.debug("connected TelemetryReader create started"); + link = in_link; + handler = in_handler; + + telemQueue = new LinkedBlockingQueue(); + link.add_monitor(telemQueue); + link.set_telemetry(AltosLib.ao_telemetry_standard); + + AltosDebug.debug("connected TelemetryReader created"); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryService.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryService.java new file mode 100644 index 00000000..22a2bbd7 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryService.java @@ -0,0 +1,716 @@ +/* + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.lang.ref.WeakReference; +import java.util.concurrent.TimeoutException; +import java.util.*; + +import android.app.Notification; +//import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothAdapter; +import android.hardware.usb.*; +import android.content.Intent; +import android.content.Context; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.os.Looper; +import android.widget.Toast; +import android.location.Criteria; + +import org.altusmetrum.altoslib_13.*; + +public class TelemetryService extends Service implements AltosIdleMonitorListener { + + static final int MSG_REGISTER_CLIENT = 1; + static final int MSG_UNREGISTER_CLIENT = 2; + static final int MSG_CONNECT = 3; + static final int MSG_OPEN_USB = 4; + static final int MSG_CONNECTED = 5; + static final int MSG_CONNECT_FAILED = 6; + static final int MSG_DISCONNECTED = 7; + static final int MSG_TELEMETRY = 8; + static final int MSG_SETFREQUENCY = 9; + static final int MSG_CRC_ERROR = 10; + static final int MSG_SETBAUD = 11; + static final int MSG_DISCONNECT = 12; + static final int MSG_DELETE_SERIAL = 13; + static final int MSG_BLUETOOTH_ENABLED = 14; + static final int MSG_MONITOR_IDLE_START= 15; + static final int MSG_MONITOR_IDLE_STOP = 16; + static final int MSG_REBOOT = 17; + static final int MSG_IGNITER_QUERY = 18; + static final int MSG_IGNITER_FIRE = 19; + + // Unique Identification Number for the Notification. + // We use it on Notification start, and to cancel it. + private int NOTIFICATION = R.string.telemetry_service_label; + //private NotificationManager mNM; + + ArrayList clients = new ArrayList(); // Keeps track of all current registered clients. + final Handler handler = new IncomingHandler(this); + final Messenger messenger = new Messenger(handler); // Target we publish for clients to send messages to IncomingHandler. + + // Name of the connected device + DeviceAddress address; + private AltosDroidLink altos_link = null; + private TelemetryReader telemetry_reader = null; + private TelemetryLogger telemetry_logger = null; + + // Local Bluetooth adapter + private BluetoothAdapter bluetooth_adapter = null; + + // Last data seen; send to UI when it starts + private TelemetryState telemetry_state; + + // Idle monitor if active + AltosIdleMonitor idle_monitor = null; + + // Igniter bits + AltosIgnite ignite = null; + boolean ignite_running; + + // Handler of incoming messages from clients. + static class IncomingHandler extends Handler { + private final WeakReference service; + IncomingHandler(TelemetryService s) { service = new WeakReference(s); } + + @Override + public void handleMessage(Message msg) { + DeviceAddress address; + + TelemetryService s = service.get(); + AltosDroidLink bt = null; + if (s == null) + return; + + switch (msg.what) { + + /* Messages from application */ + case MSG_REGISTER_CLIENT: + s.add_client(msg.replyTo); + break; + case MSG_UNREGISTER_CLIENT: + s.remove_client(msg.replyTo); + break; + case MSG_CONNECT: + AltosDebug.debug("Connect command received"); + address = (DeviceAddress) msg.obj; + AltosDroidPreferences.set_active_device(address); + s.start_altos_bluetooth(address, false); + break; + case MSG_OPEN_USB: + AltosDebug.debug("Open USB command received"); + UsbDevice device = (UsbDevice) msg.obj; + s.start_usb(device); + break; + case MSG_DISCONNECT: + AltosDebug.debug("Disconnect command received"); + s.address = null; + if (!(Boolean) msg.obj) + AltosDroidPreferences.set_active_device(null); + s.disconnect(true); + break; + case MSG_DELETE_SERIAL: + AltosDebug.debug("Delete Serial command received"); + s.delete_serial((Integer) msg.obj); + break; + case MSG_SETFREQUENCY: + AltosDebug.debug("MSG_SETFREQUENCY"); + s.telemetry_state.frequency = (Double) msg.obj; + if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) { + try { + s.altos_link.set_radio_frequency(s.telemetry_state.frequency); + s.altos_link.save_frequency(); + } catch (InterruptedException e) { + } catch (TimeoutException e) { + } + } + s.send_to_clients(); + break; + case MSG_SETBAUD: + AltosDebug.debug("MSG_SETBAUD"); + s.telemetry_state.telemetry_rate = (Integer) msg.obj; + if (s.telemetry_state.connect == TelemetryState.CONNECT_CONNECTED) { + s.altos_link.set_telemetry_rate(s.telemetry_state.telemetry_rate); + s.altos_link.save_telemetry_rate(); + } + s.send_to_clients(); + break; + + /* + *Messages from AltosBluetooth + */ + case MSG_CONNECTED: + AltosDebug.debug("MSG_CONNECTED"); + bt = (AltosDroidLink) msg.obj; + + if (bt != s.altos_link) { + AltosDebug.debug("Stale message"); + break; + } + AltosDebug.debug("Connected to device"); + try { + s.connected(); + } catch (InterruptedException ie) { + } + break; + case MSG_CONNECT_FAILED: + AltosDebug.debug("MSG_CONNECT_FAILED"); + bt = (AltosDroidLink) msg.obj; + + if (bt != s.altos_link) { + AltosDebug.debug("Stale message"); + break; + } + if (s.address != null) { + AltosDebug.debug("Connection failed... retrying"); + s.start_altos_bluetooth(s.address, true); + } else { + s.disconnect(true); + } + break; + case MSG_DISCONNECTED: + + /* This can be sent by either AltosDroidLink or TelemetryReader */ + AltosDebug.debug("MSG_DISCONNECTED"); + bt = (AltosDroidLink) msg.obj; + + if (bt != s.altos_link) { + AltosDebug.debug("Stale message"); + break; + } + if (s.address != null) { + AltosDebug.debug("Connection lost... retrying"); + s.start_altos_bluetooth(s.address, true); + } else { + s.disconnect(true); + } + break; + + /* + * Messages from TelemetryReader + */ + case MSG_TELEMETRY: + s.telemetry((AltosTelemetry) msg.obj); + break; + case MSG_CRC_ERROR: + // forward crc error messages + s.telemetry_state.crc_errors = (Integer) msg.obj; + s.send_to_clients(); + break; + case MSG_BLUETOOTH_ENABLED: + AltosDebug.debug("TelemetryService notes that BT is now enabled"); + address = AltosDroidPreferences.active_device(); + if (address != null && !address.address.startsWith("USB")) + s.start_altos_bluetooth(address, false); + break; + case MSG_MONITOR_IDLE_START: + AltosDebug.debug("start monitor idle"); + s.start_idle_monitor(); + break; + case MSG_MONITOR_IDLE_STOP: + AltosDebug.debug("stop monitor idle"); + s.stop_idle_monitor(); + break; + case MSG_REBOOT: + AltosDebug.debug("reboot"); + s.reboot_remote(); + break; + case MSG_IGNITER_QUERY: + AltosDebug.debug("igniter query"); + s.igniter_query(msg.replyTo); + break; + case MSG_IGNITER_FIRE: + AltosDebug.debug("igniter fire"); + s.igniter_fire((String) msg.obj); + break; + default: + super.handleMessage(msg); + } + } + } + + /* Handle telemetry packet + */ + private void telemetry(AltosTelemetry telem) { + AltosState state; + + if (telemetry_state.states.containsKey(telem.serial())) + state = telemetry_state.states.get(telem.serial()); + else + state = new AltosState(new AltosCalData()); + telem.provide_data(state); + telemetry_state.states.put(telem.serial(), state); + telemetry_state.quiet = false; + if (state != null) { + AltosPreferences.set_state(state,telem.serial()); + } + send_to_clients(); + } + + /* Construct the message to deliver to clients + */ + private Message message() { + if (telemetry_state == null) + AltosDebug.debug("telemetry_state null!"); + if (telemetry_state.states == null) + AltosDebug.debug("telemetry_state.states null!"); + return Message.obtain(null, AltosDroid.MSG_STATE, telemetry_state); + } + + /* A new friend has connected + */ + private void add_client(Messenger client) { + + clients.add(client); + AltosDebug.debug("Client bound to service"); + + /* On connect, send the current state to the new client + */ + send_to_client(client); + send_idle_mode_to_client(client); + + /* If we've got an address from a previous session, then + * go ahead and try to reconnect to the device + */ + if (address != null && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) { + AltosDebug.debug("Reconnecting now..."); + start_altos_bluetooth(address, false); + } + } + + /* A client has disconnected, clean up + */ + private void remove_client(Messenger client) { + clients.remove(client); + AltosDebug.debug("Client unbound from service"); + + /* When the list of clients is empty, stop the service if + * we have no current telemetry source + */ + + if (clients.isEmpty() && telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED) { + AltosDebug.debug("No clients, no connection. Stopping\n"); + stopSelf(); + } + } + + private void send_to_client(Messenger client) { + Message m = message(); + try { + client.send(m); + } catch (RemoteException e) { + AltosDebug.error("Client %s disappeared", client.toString()); + remove_client(client); + } + } + + private void send_to_clients() { + for (Messenger client : clients) + send_to_client(client); + } + + private void send_idle_mode_to_client(Messenger client) { + Message m = Message.obtain(null, AltosDroid.MSG_IDLE_MODE, idle_monitor != null); + try { + client.send(m); + } catch (RemoteException e) { + AltosDebug.error("Client %s disappeared", client.toString()); + remove_client(client); + } + } + + private void send_idle_mode_to_clients() { + for (Messenger client : clients) + send_idle_mode_to_client(client); + } + + private void telemetry_start() { + if (telemetry_reader == null && idle_monitor == null && !ignite_running) { + telemetry_reader = new TelemetryReader(altos_link, handler); + telemetry_reader.start(); + } + } + + private void telemetry_stop() { + if (telemetry_reader != null) { + AltosDebug.debug("disconnect(): stopping TelemetryReader"); + telemetry_reader.interrupt(); + try { + telemetry_reader.join(); + } catch (InterruptedException e) { + } + telemetry_reader = null; + } + } + + private void disconnect(boolean notify) { + AltosDebug.debug("disconnect(): begin"); + + telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED; + telemetry_state.address = null; + + if (idle_monitor != null) + stop_idle_monitor(); + + if (altos_link != null) + altos_link.closing(); + + stop_receiver_voltage_timer(); + + telemetry_stop(); + if (telemetry_logger != null) { + AltosDebug.debug("disconnect(): stopping TelemetryLogger"); + telemetry_logger.stop(); + telemetry_logger = null; + } + if (altos_link != null) { + AltosDebug.debug("disconnect(): stopping AltosDroidLink"); + altos_link.close(); + altos_link = null; + ignite = null; + } + telemetry_state.config = null; + if (notify) { + AltosDebug.debug("disconnect(): send message to clients"); + send_to_clients(); + if (clients.isEmpty()) { + AltosDebug.debug("disconnect(): no clients, terminating"); + stopSelf(); + } + } + } + + private void start_usb(UsbDevice device) { + AltosUsb d = new AltosUsb(this, device, handler); + + if (d != null) { + disconnect(false); + altos_link = d; + try { + connected(); + } catch (InterruptedException ie) { + } + } + } + + private void delete_serial(int serial) { + telemetry_state.states.remove((Integer) serial); + AltosPreferences.remove_state(serial); + send_to_clients(); + } + + private void start_altos_bluetooth(DeviceAddress address, boolean pause) { + if (bluetooth_adapter == null || !bluetooth_adapter.isEnabled()) + return; + + disconnect(false); + + // Get the BluetoothDevice object + BluetoothDevice device = bluetooth_adapter.getRemoteDevice(address.address); + + this.address = address; + AltosDebug.debug("start_altos_bluetooth(): Connecting to %s (%s)", device.getName(), device.getAddress()); + altos_link = new AltosBluetooth(device, handler, pause); + telemetry_state.connect = TelemetryState.CONNECT_CONNECTING; + telemetry_state.address = address; + send_to_clients(); + } + + private void start_idle_monitor() { + if (altos_link != null && idle_monitor == null) { + telemetry_stop(); + idle_monitor = new AltosIdleMonitor(this, altos_link, true, false); + idle_monitor.set_callsign(AltosPreferences.callsign()); + idle_monitor.start(); + send_idle_mode_to_clients(); + } + } + + private void stop_idle_monitor() { + if (idle_monitor != null) { + try { + idle_monitor.abort(); + } catch (InterruptedException ie) { + } + idle_monitor = null; + telemetry_start(); + send_idle_mode_to_clients(); + } + } + + private void reboot_remote() { + if (altos_link != null) { + stop_idle_monitor(); + try { + altos_link.start_remote(); + altos_link.printf("r eboot\n"); + altos_link.flush_output(); + } catch (TimeoutException te) { + } catch (InterruptedException ie) { + } finally { + try { + altos_link.stop_remote(); + } catch (InterruptedException ie) { + } + } + } + } + + private void ensure_ignite() { + if (ignite == null) + ignite = new AltosIgnite(altos_link, true, false); + } + + private synchronized void igniter_query(Messenger client) { + ensure_ignite(); + HashMap status_map = null; + ignite_running = true; + try { + stop_idle_monitor(); + try { + status_map = ignite.status(); + } catch (InterruptedException ie) { + AltosDebug.debug("ignite.status interrupted"); + } catch (TimeoutException te) { + AltosDebug.debug("ignite.status timeout"); + } + } finally { + ignite_running = false; + } + Message m = Message.obtain(null, AltosDroid.MSG_IGNITER_STATUS, status_map); + try { + client.send(m); + } catch (RemoteException e) { + } + } + + private synchronized void igniter_fire(String igniter) { + ensure_ignite(); + ignite_running = true; + stop_idle_monitor(); + try { + ignite.fire(igniter); + } catch (InterruptedException ie) { + } finally { + ignite_running = false; + } + } + + // Timer for receiver battery voltage monitoring + Timer receiver_voltage_timer; + + private void update_receiver_voltage() { + if (altos_link != null && idle_monitor == null && !ignite_running) { + try { + double voltage = altos_link.monitor_battery(); + telemetry_state.receiver_battery = voltage; + send_to_clients(); + } catch (InterruptedException ie) { + } + } + } + + private void stop_receiver_voltage_timer() { + if (receiver_voltage_timer != null) { + receiver_voltage_timer.cancel(); + receiver_voltage_timer.purge(); + receiver_voltage_timer = null; + } + } + + private void start_receiver_voltage_timer() { + if (receiver_voltage_timer == null && altos_link.has_monitor_battery()) { + receiver_voltage_timer = new Timer(); + receiver_voltage_timer.scheduleAtFixedRate(new TimerTask() { public void run() {update_receiver_voltage();}}, 1000L, 10000L); + } + } + + private void connected() throws InterruptedException { + AltosDebug.debug("connected top"); + AltosDebug.check_ui("connected\n"); + try { + if (altos_link == null) + throw new InterruptedException("no bluetooth"); + telemetry_state.config = altos_link.config_data(); + altos_link.set_radio_frequency(telemetry_state.frequency); + altos_link.set_telemetry_rate(telemetry_state.telemetry_rate); + } catch (TimeoutException e) { + // If this timed out, then we really want to retry it, but + // probably safer to just retry the connection from scratch. + AltosDebug.debug("connected timeout"); + if (address != null) { + AltosDebug.debug("connected timeout, retrying"); + start_altos_bluetooth(address, true); + } else { + handler.obtainMessage(MSG_CONNECT_FAILED).sendToTarget(); + disconnect(true); + } + return; + } + + AltosDebug.debug("connected bluetooth configured"); + telemetry_state.connect = TelemetryState.CONNECT_CONNECTED; + telemetry_state.address = address; + + telemetry_start(); + + AltosDebug.debug("connected TelemetryReader started"); + + telemetry_logger = new TelemetryLogger(this, altos_link); + + start_receiver_voltage_timer(); + + AltosDebug.debug("Notify UI of connection"); + + send_to_clients(); + } + + + @Override + public void onCreate() { + + AltosDebug.init(this); + + // Initialise preferences + AltosDroidPreferences.init(this); + + // Get local Bluetooth adapter + bluetooth_adapter = BluetoothAdapter.getDefaultAdapter(); + + telemetry_state = new TelemetryState(); + + // Create a reference to the NotificationManager so that we can update our notifcation text later + //mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + + telemetry_state.connect = TelemetryState.CONNECT_DISCONNECTED; + telemetry_state.address = null; + + /* Pull the saved state information out of the preferences database + */ + ArrayList serials = AltosPreferences.list_states(); + + telemetry_state.latest_serial = AltosPreferences.latest_state(); + + telemetry_state.quiet = true; + + AltosDebug.debug("latest serial %d\n", telemetry_state.latest_serial); + + for (int serial : serials) { + AltosState saved_state = AltosPreferences.state(serial); + if (saved_state != null) { + if (telemetry_state.latest_serial == 0) + telemetry_state.latest_serial = serial; + + AltosDebug.debug("recovered old state serial %d flight %d", + serial, + saved_state.cal_data().flight); + if (saved_state.gps != null) + AltosDebug.debug("\tposition %f,%f", + saved_state.gps.lat, + saved_state.gps.lon); + telemetry_state.states.put(serial, saved_state); + } else { + AltosDebug.debug("Failed to recover state for %d", serial); + AltosPreferences.remove_state(serial); + } + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + AltosDebug.debug("Received start id %d: %s", startId, intent); + + CharSequence text = getText(R.string.telemetry_service_started); + + // Create notification to be displayed while the service runs + Notification notification = new Notification(R.drawable.am_status_c, text, 0); + + // The PendingIntent to launch our activity if the user selects this notification + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + new Intent(this, AltosDroid.class), 0); + + // Set the info for the views that show in the notification panel. + notification.setLatestEventInfo(this, getText(R.string.telemetry_service_label), text, contentIntent); + + // Set the notification to be in the "Ongoing" section. + notification.flags |= Notification.FLAG_ONGOING_EVENT; + + // Move us into the foreground. + startForeground(NOTIFICATION, notification); + + /* Start bluetooth if we don't have a connection already */ + if (intent != null && + (telemetry_state.connect == TelemetryState.CONNECT_NONE || + telemetry_state.connect == TelemetryState.CONNECT_DISCONNECTED)) + { + String action = intent.getAction(); + + if (action.equals(AltosDroid.ACTION_BLUETOOTH)) { + DeviceAddress address = AltosDroidPreferences.active_device(); + if (address != null && !address.address.startsWith("USB")) + start_altos_bluetooth(address, false); + } + } + + // We want this service to continue running until it is explicitly + // stopped, so return sticky. + return START_STICKY; + } + + @Override + public void onDestroy() { + + // Stop the bluetooth Comms threads + disconnect(true); + + // Demote us from the foreground, and cancel the persistent notification. + stopForeground(true); + + // Tell the user we stopped. + Toast.makeText(this, R.string.telemetry_service_stopped, Toast.LENGTH_SHORT).show(); + } + + @Override + public IBinder onBind(Intent intent) { + return messenger.getBinder(); + } + + /* AltosIdleMonitorListener */ + public void update(AltosState state, AltosListenerState listener_state) { + telemetry_state.states.put(state.cal_data().serial, state); + telemetry_state.receiver_battery = listener_state.battery; + send_to_clients(); + } + + public void failed() { + } + + public void error(String reason) { + stop_idle_monitor(); + } +} diff --git a/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryState.java b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryState.java new file mode 100644 index 00000000..74d5cd22 --- /dev/null +++ b/altosdroid/app/src/main/java/org/altusmetrum/AltosDroid/TelemetryState.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2012 Mike Beattie + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ + +package org.altusmetrum.AltosDroid; + +import java.util.*; +import org.altusmetrum.altoslib_13.*; +import android.location.Location; + +public class TelemetryState { + public static final int CONNECT_NONE = 0; + public static final int CONNECT_DISCONNECTED = 1; + public static final int CONNECT_CONNECTING = 2; + public static final int CONNECT_CONNECTED = 3; + + int connect; + DeviceAddress address; + AltosConfigData config; + int crc_errors; + double receiver_battery; + double frequency; + int telemetry_rate; + + boolean quiet; + + HashMap states; + + int latest_serial; + + public TelemetryState() { + connect = CONNECT_NONE; + config = null; + states = new HashMap(); + crc_errors = 0; + receiver_battery = AltosLib.MISSING; + frequency = AltosPreferences.frequency(0); + telemetry_rate = AltosPreferences.telemetry_rate(0); + } +} diff --git a/altosdroid/app/src/main/res/drawable-hdpi/am_status_c.png b/altosdroid/app/src/main/res/drawable-hdpi/am_status_c.png new file mode 100644 index 00000000..d4393217 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-hdpi/am_status_c.png differ diff --git a/altosdroid/app/src/main/res/drawable-hdpi/am_status_g.png b/altosdroid/app/src/main/res/drawable-hdpi/am_status_g.png new file mode 100644 index 00000000..03f9dd7d Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-hdpi/am_status_g.png differ diff --git a/altosdroid/app/src/main/res/drawable-hdpi/app_icon.png b/altosdroid/app/src/main/res/drawable-hdpi/app_icon.png new file mode 100644 index 00000000..da2f08a8 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-hdpi/app_icon.png differ diff --git a/altosdroid/app/src/main/res/drawable-hdpi/ic_maps_indicator_current_position.png b/altosdroid/app/src/main/res/drawable-hdpi/ic_maps_indicator_current_position.png new file mode 100644 index 00000000..bc9160df Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-hdpi/ic_maps_indicator_current_position.png differ diff --git a/altosdroid/app/src/main/res/drawable-mdpi/am_status_c.png b/altosdroid/app/src/main/res/drawable-mdpi/am_status_c.png new file mode 100644 index 00000000..30a8d29a Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-mdpi/am_status_c.png differ diff --git a/altosdroid/app/src/main/res/drawable-mdpi/am_status_g.png b/altosdroid/app/src/main/res/drawable-mdpi/am_status_g.png new file mode 100644 index 00000000..07f7f073 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-mdpi/am_status_g.png differ diff --git a/altosdroid/app/src/main/res/drawable-mdpi/ic_maps_indicator_current_position.png b/altosdroid/app/src/main/res/drawable-mdpi/ic_maps_indicator_current_position.png new file mode 100644 index 00000000..4e427d89 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable-mdpi/ic_maps_indicator_current_position.png differ diff --git a/altosdroid/app/src/main/res/drawable/app_icon.png b/altosdroid/app/src/main/res/drawable/app_icon.png new file mode 100644 index 00000000..b0a3c9c0 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable/app_icon.png differ diff --git a/altosdroid/app/src/main/res/drawable/pad.png b/altosdroid/app/src/main/res/drawable/pad.png new file mode 100644 index 00000000..b2e65c8a Binary files /dev/null and b/altosdroid/app/src/main/res/drawable/pad.png differ diff --git a/altosdroid/app/src/main/res/drawable/rocket.png b/altosdroid/app/src/main/res/drawable/rocket.png new file mode 100644 index 00000000..7e62f6c4 Binary files /dev/null and b/altosdroid/app/src/main/res/drawable/rocket.png differ diff --git a/altosdroid/app/src/main/res/layout/altosdroid.xml b/altosdroid/app/src/main/res/layout/altosdroid.xml new file mode 100644 index 00000000..df870672 --- /dev/null +++ b/altosdroid/app/src/main/res/layout/altosdroid.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/altosdroid/app/src/main/res/layout/custom_title.xml b/altosdroid/app/src/main/res/layout/custom_title.xml new file mode 100644 index 00000000..57eb6b4a --- /dev/null +++ b/altosdroid/app/src/main/res/layout/custom_title.xml @@ -0,0 +1,39 @@ + + + + + + \ No newline at end of file diff --git a/altosdroid/app/src/main/res/layout/device_list.xml b/altosdroid/app/src/main/res/layout/device_list.xml new file mode 100644 index 00000000..0c103e9d --- /dev/null +++ b/altosdroid/app/src/main/res/layout/device_list.xml @@ -0,0 +1,63 @@ + + + +