/******************************************************************************* * 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.gaggle; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import android.content.Intent; import android.database.Cursor; import android.location.Address; import android.location.Geocoder; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.text.format.DateFormat; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.MenuItem; import android.view.View; import android.webkit.WebView; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.geeksville.android.DBListActivity; import com.geeksville.info.FlightSummary; import com.geeksville.location.CSVWriter; import com.geeksville.location.GPXWriter; import com.geeksville.location.IGCWriter; import com.geeksville.location.KMLWriter; import com.geeksville.location.LeonardoUpload; import com.geeksville.location.LocationList; import com.geeksville.location.LocationListWriter; import com.geeksville.location.LocationLogDbAdapter; import com.geeksville.location.LocationUtils; import com.geeksville.location.PositionWriter; import com.geeksville.location.SummaryWriter; import com.geeksville.view.AsyncProgressDialog; /** * Browse previous flights * * @author kevinh * */ public class ListFlightsActivity extends DBListActivity { /** * Debugging tag */ private static final String TAG = "ListFlightsActivity"; private LocationLogDbAdapter db; private java.text.DateFormat datefmt; private java.text.DateFormat timefmt; private Geocoder coder; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { coder = new Geocoder(this); datefmt = DateFormat.getDateFormat(this); timefmt = DateFormat.getTimeFormat(this); // Fill our table db = new LocationLogDbAdapter(this); setContentView(R.layout.list_main); super.onCreate(savedInstanceState); WebView view = (WebView) findViewById(android.R.id.empty); // view.getSettings().setJavaScriptEnabled(true); view.loadUrl(getResources().getString(R.string.no_flights_url)); } /** * Handle our context menu * * @see android.app.Activity#onContextItemSelected(android.view.MenuItem) */ @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.logged_upload_menu: // FIXME - show a progress dialog leonardoUpload(itemToRowId(item)); return true; case R.id.send_igc: emailFlight(itemToRowId(item), "igc"); return true; case R.id.send_kml: emailFlight(itemToRowId(item), "kml"); return true; case R.id.send_gpx: emailFlight(itemToRowId(item), "gpx"); return true; case R.id.send_csv: emailFlight(itemToRowId(item), "csv"); return true; // case R.id.view_kml: // externalViewFlight(itemToRowId(item), "kml"); // return true; case R.id.view_summary: viewFlightSummary(itemToRowId(item)); return true; default: break; } return super.onContextItemSelected(item); } /** * Another flight might have been added, so refresh our cursor * * @see android.app.Activity#onResume() */ @Override protected void onResume() { super.onResume(); myCursor.requery(); } /** * * @see android.app.Activity#onCreateContextMenu(android.view.ContextMenu, * android.view.View, android.view.ContextMenu.ContextMenuInfo) */ @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); getMenuInflater().inflate(R.menu.logged_flight_context, menu); } @Override protected Cursor createCursor() { return db.fetchAllFlights(); } /** * Get a long human readable description for a time * * @param timeMsec * @return */ private String timeToString(long timeMsec) { Date startdate = new Date(timeMsec); String title = datefmt.format(startdate) + " " + timefmt.format(startdate); return title; } /** * Use google to find a human style name for a lat/long (i.e. town name) * * @author kevinh * */ private class FindGeocodeTask extends AsyncTask<Long, Void, String> { TextView dest; /** * Constructor * * @param dest * the view to update when we get a response */ public FindGeocodeTask(TextView dest) { this.dest = dest; } @Override protected String doInBackground(Long... params) { try { synchronized (coder) { // only one lookup at a time long id = params[0]; Cursor locs = db.fetchLocations(id); if (locs.getCount() < 1) return null; // No lat longs avail double latitude = locs.getDouble(locs .getColumnIndex(LocationLogDbAdapter.KEY_LATITUDE)); double longitude = locs.getDouble(locs .getColumnIndex(LocationLogDbAdapter.KEY_LONGITUDE)); List<Address> addrs = coder.getFromLocation(latitude, longitude, 1); if (addrs.size() < 1) return null; // Failed to find on google Address addr = addrs.get(0); StringBuilder builder = new StringBuilder(); int maxLine = Math.min(addr.getMaxAddressLineIndex(), 1); for (int line = 0; line <= maxLine; line++) builder.append(addr.getAddressLine(line) + " "); String result = builder.toString(); // Store this as the new description for the flight db.updateFlight(id, null, result, null, null); return result; } } catch (Exception ex) { // FIXME - log failures return null; } } protected void onPostExecute(String result) { if (result != null) dest.setText(result); } } @Override protected BaseAdapter createListAdapter() { // Create an array to specify the fields we want to display in the // list String[] from = new String[] { LocationLogDbAdapter.KEY_DESCRIPTION, LocationLogDbAdapter.KEY_FLT_STARTTIME, LocationLogDbAdapter.KEY_FLT_ENDTIME }; // and an array of the fields we want to bind those fields to int[] to = new int[] { R.id.flightTitle, R.id.date, R.id.duration }; // Now create a simple cursor adapter and set it to display SimpleCursorAdapter a = new SimpleCursorAdapter(this, R.layout.flights_row, myCursor, from, to); final int idcol = myCursor.getColumnIndex(LocationLogDbAdapter.KEY_ROWID); final int desccol = myCursor .getColumnIndex(LocationLogDbAdapter.KEY_DESCRIPTION); final int startcol = myCursor .getColumnIndex(LocationLogDbAdapter.KEY_FLT_STARTTIME); final int endcol = myCursor .getColumnIndex(LocationLogDbAdapter.KEY_FLT_ENDTIME); a.setViewBinder(new SimpleCursorAdapter.ViewBinder() { public boolean setViewValue(View _view, Cursor cursor, int columnIndex) { if (columnIndex == desccol) { TextView view = (TextView) _view; String desc = cursor.getString(desccol); boolean needGeocode = desc == null || desc.length() == 0; if (needGeocode) { // No user provided description, so we show the date of // flight instead long startTime = cursor.getLong(startcol); desc = timeToString(startTime); } view.setText(desc); if (needGeocode) { // Try to find something better next time FindGeocodeTask task = new FindGeocodeTask(view); task.execute(cursor.getLong(idcol)); } return true; } if (columnIndex == startcol) { TextView view = (TextView) _view; long startTime = cursor.getLong(startcol); view.setText(timeToString(startTime)); return true; } if (columnIndex == endcol) { TextView view = (TextView) _view; long startTime = cursor.getLong(startcol); long endTime = cursor.getLong(endcol); // If this flight is still active we won't know the end time String str = ""; if (endTime >= startTime) { int numSec = (int) ((endTime - startTime) / 1000); int numHr = numSec / (60 * 60); int numMin = (numSec / 60) % 60; str = String.format("%d:%02d", numHr, numMin); } view.setText(str); return true; } return false; } }); return a; } /** * * @see com.geeksville.android.DBListActivity#handleDeleteItem(android.view.MenuItem * ) */ @Override protected boolean handleDeleteItem(MenuItem item) { db.deleteFlight(itemToRowId(item)); return true; } /** * * @see com.geeksville.android.DBListActivity#handleViewItem(android.view.MenuItem * ) */ @Override protected void handleViewItem(MenuItem item) { viewFlightId(itemToRowId(item)); } /** * Show the flight on the map * * @param flightid */ private void viewFlightId(long flightid) { // Get all the points in that flight LocationList locs = new LocationList(); PositionWriter locsWriter = new LocationListWriter(locs); LocationUtils.dbToWriter(db, locsWriter, flightid); Intent i = FlyMapActivity.createIntentLogView(this, locs); startActivity(i); } /** * View flight summary statistics * * @param flightId */ private void viewFlightSummary(final long flightId) { FlightSummary summary = new FlightSummary(); SummaryWriter summaryWriter = new SummaryWriter(summary); LocationUtils.dbToWriter(db, summaryWriter, flightId); Intent i = new Intent(this, SummaryListActivity.class); summary.addDataToIntent(i); startActivity(i); } /** * Write a flight to an in memory stream of IGC data * * @param flightid * @return * @throws IOException */ private String flightToIGC(long flightid) throws IOException { // File cacheDir = getCacheDir(); // FileOutputStream s = this.openFileOutput(basename, // MODE_WORLD_READABLE); ByteArrayOutputStream s = new ByteArrayOutputStream(4096); // This will close the file descriptor once done writing PositionWriter writer; GagglePrefs prefs = new GagglePrefs(this); writer = new IGCWriter(s, prefs.getPilotName(), null, // FIXME - not quite // right, we should // get this from DB prefs.getWingModel(), prefs.getPilotId(), this); LocationUtils.dbToWriter(db, writer, flightid); byte[] contents = s.toByteArray(); // Super skanky to pass this (big?) stuff as a string, but I'm tired of // fighting http post return new String(contents); } /** * Write a flight to a file * * @param flightid * @param filetype * igc or kml * @return * @throws IOException */ private File flightToFile(long flightid, String filetype) throws IOException { // File cacheDir = getCacheDir(); File sdcard = Environment.getExternalStorageDirectory(); if (!sdcard.exists()) throw new IOException(getString(R.string.sd_card_not_found)); String path = getString(R.string.file_folder); File tracklog = new File(sdcard, path); if (!tracklog.exists()) tracklog.mkdir(); path += '/' + getString(R.string.tracklogs); tracklog = new File(sdcard, path); if (!tracklog.exists()) tracklog.mkdir(); String basename = getString(R.string.flight_) + flightid + "." + filetype; // FIXME, // use // a // better // filename // File fname = new File(cacheDir, basename); // FIXME - use getCacheDir - or even better a sdcard directory for // flights File fullname = new File(tracklog, basename); // FileOutputStream s = this.openFileOutput(basename, // MODE_WORLD_READABLE); FileOutputStream s = new FileOutputStream(fullname); // This will close the file descriptor once done writing PositionWriter writer; GagglePrefs prefs = new GagglePrefs(this); if (filetype.equals("igc")) writer = new IGCWriter(s, prefs.getPilotName(), null, prefs.getWingModel(), prefs.getPilotId(), this); else if (filetype.equals("csv")) writer = new CSVWriter(s, prefs.getPilotName(), null, prefs.getWingModel(), prefs.getPilotId()); else if (filetype.equals("gpx")) writer = new GPXWriter(s, prefs.getPilotName(), null, prefs.getWingModel(), prefs.getPilotId()); else writer = new KMLWriter(s, prefs.getPilotName(), null, prefs.getWingModel(), prefs.getPilotId()); LocationUtils.dbToWriter(db, writer, flightid); return fullname; } private void leonardoUpload(final long flightid) { final Account acct = new Account(this, "delayed"); final GagglePrefs gprefs = new GagglePrefs(this); if (acct.isValid()) { AsyncProgressDialog progress = new AsyncProgressDialog(this, getString(R.string.uploading), getString(R.string.please_wait)) { @Override protected void doInBackground() { String fileLoc; try { fileLoc = flightToIGC(flightid); } catch (IOException ex) { showCompletionDialog(getString(R.string.igc_stream_write_failed), ex.getLocalizedMessage()); return; } try { String basename = getString(R.string.flight_) + flightid; String toastMessage = LeonardoUpload.upload(acct.username, acct.password, acct.serverURL, gprefs.getCompetitionClass(), basename, fileLoc, acct.connectionTimeout, acct.operationTimeout); if (toastMessage != null && toastMessage.length() == 0) toastMessage = context.getString(R.string.upload_failed_bad_url); showCompletionToast(toastMessage); } catch (IOException ex) { showCompletionDialog(getString(R.string.upload_failed), ex.getLocalizedMessage()); } } }; progress.execute(); } else { Toast.makeText(this, R.string.please_set_your_leonardo_account_information, Toast.LENGTH_LONG).show(); startActivity(new Intent(this, MyPreferences.class)); } } private class AsyncFileWriter extends AsyncProgressDialog { protected File fileLoc = null; long flightid; String filetype; public AsyncFileWriter(final long flightid, final String filetype) { super(ListFlightsActivity.this, getString(R.string.writing_file), getString(R.string.please_wait)); this.flightid = flightid; this.filetype = filetype; } @Override protected void doInBackground() { try { fileLoc = flightToFile(flightid, filetype); } catch (IOException ex) { showCompletionDialog(getString(R.string.file_write_failed), ex.getLocalizedMessage()); } } } /** * email a flight * * @param flightid * @param filetype * kml, igc, csv or gpx */ private void emailFlight(final long flightid, final String filetype) { final String filetypeUpper = filetype.toUpperCase(); AsyncProgressDialog progress = new AsyncFileWriter(flightid, filetype) { /* * (non-Javadoc) * * @see com.geeksville.view.AsyncProgressDialog#onPostExecute (java.lang * .Void) */ @Override protected void onPostExecute(Void unused) { super.onPostExecute(unused); if (fileLoc != null) { Uri fileuri = Uri.fromFile(fileLoc); // Intent sendIntent = new // Intent(Intent.ACTION_SEND_MULTIPLE); Intent sendIntent = new Intent(Intent.ACTION_SEND); // Mime type of the attachment (or) u can use // sendIntent.setType("*/*") if (filetype.equals("igc")) sendIntent.setType("application/x-igc"); else if (filetype.equals("csv")) sendIntent.setType("text/csv; header"); else if (filetype.equals("gpx")) sendIntent.setType("application/gpx+xml"); else // FIXME, support kmz sendIntent.setType("application/vnd.google-earth.kml+xml"); // sendIntent.setType("*/*"); // Subject for the message or Email sendIntent.putExtra(Intent.EXTRA_SUBJECT, "My Flight"); // Full Path to the attachment // ArrayList<Uri> files = new ArrayList<Uri>(); // files.add(fileuri); // sendIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, // files); sendIntent.putExtra(Intent.EXTRA_STREAM, fileuri); // Use a chooser to decide whether email or mms startActivity(Intent.createChooser(sendIntent, "Send flight")); // Keep stats on # of emails sent Map<String, String> map = new HashMap<String, String>(); map.put("Time", (new Date()).toGMTString()); FlurryAgent.onEvent("EmailFlight", map); } } }; progress.execute(); } /** * view a flight in google earth - FIXME, doesn't yet work - need to check * their manifest * * @param flightid * @param filetype * kml or igc */ private void externalViewFlight(final long flightid, final String filetype) { final String filetypeUpper = filetype.toUpperCase(); AsyncProgressDialog progress = new AsyncFileWriter(flightid, filetype) { /* * (non-Javadoc) * * @see com.geeksville.view.AsyncProgressDialog#onPostExecute (java.lang * .Void) */ @Override protected void onPostExecute(Void unused) { super.onPostExecute(unused); if (fileLoc != null) { Uri fileuri = Uri.fromFile(fileLoc); // Intent sendIntent = new // Intent(Intent.ACTION_SEND_MULTIPLE); Intent sendIntent = new Intent(Intent.ACTION_VIEW, fileuri); // Mime type of the attachment (or) u can use // sendIntent.setType("*/*") if (filetype.equals("igc")) sendIntent.setType("application/x-igc"); else // FIXME, support kmz sendIntent.setType("application/vnd.google-earth.kml+xml"); startActivity(Intent.createChooser(sendIntent, getString(R.string.view_flight))); // Keep stats on # of emails sent Map<String, String> map = new HashMap<String, String>(); map.put("Time", (new Date()).toGMTString()); FlurryAgent.onEvent("GEarthView", map); } } }; progress.execute(); } /** * Handle clicks on an individual flight log */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Find the flight ID the user selected long flightid = rowToRowId(position); viewFlightId(flightid); } }