/******************************************************************************* * Gaggle is Copyright 2010 by Geeksville Industries LLC, a California limited liability corporation. * * Gaggle is distributed under a dual license. We've chosen this approach because within Gaggle we've used a number * of components that Geeksville Industries LLC might reuse for commercial products. Gaggle can be distributed under * either of the two licenses listed below. * * 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. * * Commercial Distribution License * If you would like to distribute Gaggle (or portions thereof) under a license other than * the "GNU General Public License, version 2", contact Geeksville Industries. Geeksville Industries reserves * the right to release Gaggle source code under a commercial license of its choice. * * GNU Public License, version 2 * All other distribution of Gaggle must conform to the terms of the GNU Public License, version 2. The full * text of this license is included in the Gaggle source, see assets/manual/gpl-2.0.txt. ******************************************************************************/ package com.geeksville.location; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; import java.util.TimeZone; import com.geeksville.gaggle.R; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.util.Base64; import android.util.Log; import com.geeksville.io.LineEndingStream; /** * * @author kevinh * * Writes IGC files (FAI gliding competition GPS data) Sample file * follows: * * * // name convention: 2009-12-25-XXX-SERN-YY.IGC // where XXX is the * mfgr code. I'll pick GEK, YY is flight num for that day 01, etc... // * if not paying fee I should use XXX * * // Contents // CR/LF at end of each line */ public class IGCWriter implements PositionWriter { private PrintStream out; private Signature sig; private boolean didProlog = false; private final String versionString; private String pilotName; private String flightDesc; private String gliderType; private String pilotId; private boolean hasJRecord = false; private Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); public class SignatureOutputStream extends OutputStream { private OutputStream target; private Signature sig; /** * creates a new SignatureOutputStream which writes to * a target OutputStream and updates the Signature object. */ public SignatureOutputStream(OutputStream target, Signature sig) { this.target = target; this.sig = sig; } @Override public void write(int b) throws IOException { write(new byte[] { (byte) b }); } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int offset, int len) throws IOException { target.write(b, offset, len); try { for (int i = 0; i < len; i++) if (b[offset+i] != '\r' && b[offset+i] != '\n') sig.update(b, offset + i, 1); } catch (SignatureException ex) { throw new IOException(ex); } } @Override public void flush() throws IOException { target.flush(); } @Override public void close() throws IOException { target.close(); } } private PrivateKey getPrivateKey(Context context) throws NoSuchAlgorithmException, InvalidKeySpecException { final String private_key = context.getString(R.string.igc_private_key); KeyFactory fac = KeyFactory.getInstance("RSA"); EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(Base64.decode(private_key, Base64.DEFAULT)); return fac.generatePrivate(privKeySpec); } public IGCWriter(OutputStream dest, String pilotName, String flightDesc, String gliderType, String pilotId, Context context) throws IOException { String tmp_version; try { tmp_version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; } catch (NameNotFoundException e1) { tmp_version = "UNKNOWN"; } versionString = tmp_version; try { sig = Signature.getInstance("SHA1withRSA"); PrivateKey pk = getPrivateKey(context); sig.initSign(pk); OutputStream dOut = new PrintStream(new LineEndingStream(new SignatureOutputStream(dest, sig))); out = new PrintStream(dOut); } catch (NoSuchAlgorithmException e) { Log.e("IGCWriter", "No such algo"); out = new PrintStream(new LineEndingStream(dest)); } catch (InvalidKeyException e) { Log.e("IGCWriter", "Invalid key"); out = new PrintStream(new LineEndingStream(dest)); } catch (InvalidKeySpecException e) { Log.e("IGCWriter", "Invalid key spec"); out = new PrintStream(new LineEndingStream(dest)); } this.gliderType = gliderType; this.pilotId = pilotId; this.pilotName = pilotName; this.flightDesc = flightDesc; } /** * We close the output stream in the epilog */ @Override public void emitEpilog() { // sect 3.2, G=security record try { final byte[] signature = sig.sign(); final String sigStr = Base64.encodeToString(signature, Base64.DEFAULT).replaceAll("[\\r\\n]", ""); for (int i=0; i < sigStr.length() / 75 ; i++){ out.println("G" + sigStr.substring(i*75, i*75+75)); } if (sigStr.length() % 75 > 0){ out.println("G" + sigStr.substring(((int)(sigStr.length() / 75))*75)); } } catch (SignatureException e) { Log.e("IGCWriter", "Error when signing...", e); out.println("GGaggleFailedToSign"); } out.close(); } /** * Return a degress in IGC format * * @param degIn * @return */ private static String degreeStr(double degIn, boolean isLatitude) { boolean isPos = degIn >= 0; char dirLetter = isLatitude ? (isPos ? 'N' : 'S') : (isPos ? 'E' : 'W'); degIn = Math.abs(degIn); double minutes = 60 * (degIn - Math.floor(degIn)); degIn = Math.floor(degIn); int minwhole = (int) minutes; int minfract = (int) ((minutes - minwhole) * 1000); // DDMMmmmN(or S) latitude // DDDMMmmmE(or W) longitude String s = String.format(Locale.US, (isLatitude ? "%02d" : "%03d") + "%02d%03d%c", (int) degIn, minwhole, minfract, dirLetter); return s; } /** * * @param time * UTC time of this fix, in milliseconds since January 1, 1970. * @param latitude * @param longitude * * sect 4.1, B=fix plus extension data mentioned in I */ @Override public void emitPosition(long time, double latitude, double longitude, float altitude, int bearing, float groundSpeed, float[] accel, float vspd) { // B // HHMMSS - time UTC // DDMMmmmN(or S) latitude // DDDMMmmmE(or W) longitude // A (3d valid) or V (2d only) // PPPPP pressure altitude (00697 in this case) // GGGGG alt above WGS ellipsode (00705 in this case) // GSP is 000 here (ground speed in km/hr) // B1851353728534N12151678WA0069700705000 // Get time in UTC cal.setTimeInMillis(time); boolean is3D = !Double.isNaN(altitude); // Spit out our prolog if need be if (!didProlog) { emitProlog(cal); didProlog = true; } int hours = cal.get(Calendar.HOUR_OF_DAY); out.format(Locale.US, "B%02d%02d%02d%s%s%c%05d%05d%03d", hours, cal .get(Calendar.MINUTE), cal.get(Calendar.SECOND), degreeStr(latitude, true), degreeStr(longitude, false), is3D ? 'A' : 'V', (int) (is3D ? altitude : 0), // FIXME convert // altitudes // correctly (int) (is3D ? altitude : 0), // FIXME convert alts (int) groundSpeed); out.println(); // Don't store vertical speed info until I can find an example data // file. if (!Float.isNaN(vspd) && false) { if (!hasJRecord) { // less frequent extension - vario data out.println("J010812VAR"); hasJRecord = true; } out.format(Locale.US, "K%02d%02d%02d%03d", hours, cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), (int) vspd * 10); out.println(); } } /** * Do the heavy lifting necessary to spit out a file header */ private void emitProlog(Calendar cal) { out.println("AXGG"+versionString); // AFLY06122 - sect 3.1, A=mfgr info, // mfgr=FLY, serial num=06122 // sect 3.3.1, H=file header String dstr = String.format(Locale.US, "HFDTE%02d%02d%02d", cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.MONTH) + 1, (cal.get(Calendar.YEAR) - 1900) % 100); // date out.println(dstr); // date out.println("HFFXA100"); // accuracy in meters - required out.println("HFPLTPILOT:" + pilotName); // pilot (required) out.println("HFGTYGLIDERTYPE:" + gliderType); // glider type (required) out.println("HFGIDGLIDERID:" + pilotId); // glider ID required out.println("HFDTM100GPSDATUM:WGS84"); // datum required - must be wgs84 out.println("HFGPSGPS:" + android.os.Build.MODEL); // info on gps // manufacturer out.println("HFRFWFIRMWAREVERSION:" + versionString); // sw version of app out.println("HFRHWHARDWAREVERSION:" + versionString); // hw version out.println("HFFTYFRTYPE:Geeksville,Gaggle"); // required: manufacturer // (me) and model num // sect 3.4, I=fix extension list out.println("I013638GSP"); // one extension, starts at byte 36, ends at // 38, extension type is ground speed (was TAS) } /** * Add standard IGC prologue * */ @Override public void emitProlog() { } }