Loading app/src/main/AndroidManifest.xml +13 −6 Original line number Diff line number Diff line Loading @@ -18,10 +18,13 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- For web sync retries --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- For exporting tracks --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Needed if app targets API >= 21 --> <uses-feature android:name="android.hardware.location.gps" /> <uses-feature android:name="android.hardware.location.network" /> <!-- Todo: attach camera image to position --> <!-- <uses-feature android:name="android.hardware.camera" android:required="false" /> --> Loading @@ -32,24 +35,28 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="net.fabiszewski.ulogger.MainActivity"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="net.fabiszewski.ulogger.SettingsActivity" /> <activity android:name=".SettingsActivity" /> <service android:name="net.fabiszewski.ulogger.LoggerService" android:name=".LoggerService" android:enabled="true" android:exported="false" /> <service android:name="net.fabiszewski.ulogger.WebSyncService" android:name=".WebSyncService" android:exported="false" /> <service android:name=".GpxExportService" android:exported="false" /> <receiver android:name="net.fabiszewski.ulogger.BootCompletedReceiver" android:name=".BootCompletedReceiver" android:enabled="true" android:exported="true"> <intent-filter> Loading app/src/main/java/net/fabiszewski/ulogger/DbAccess.java +41 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ package net.fabiszewski.ulogger; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.util.Log; Loading Loading @@ -90,6 +91,18 @@ class DbAccess { db.insert(DbContract.Positions.TABLE_NAME, null, values); } /** * Get result set containing all positions. * * @return Result set */ Cursor getPositions() { return db.query(DbContract.Positions.TABLE_NAME, new String[] {"*"}, null, null, null, null, DbContract.Positions._ID); } /** * Get result set containing positions marked as not synchronized. * Loading Loading @@ -157,6 +170,15 @@ class DbAccess { new String[] { String.valueOf(id) }); } /** * Get number of all positions in track * * @return Count */ int countPositions() { return (int) DatabaseUtils.queryNumEntries(db, DbContract.Positions.TABLE_NAME); } /** * Get number of not synchronized items. * Loading Loading @@ -186,6 +208,25 @@ class DbAccess { return (countUnsynced() > 0); } /** * Get first saved location time. * * @return UTC timestamp in seconds */ long getFirstTimestamp() { Cursor query = db.query(DbContract.Positions.TABLE_NAME, new String[] {DbContract.Positions.COLUMN_TIME}, null, null, null, null, DbContract.Positions._ID + " ASC", "1"); long timestamp = 0; if (query.moveToFirst()) { timestamp = query.getInt(0); } query.close(); return timestamp; } /** * Get last saved location time. * Loading app/src/main/java/net/fabiszewski/ulogger/GpxExportService.java 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (c) 2017 Bartek Fabiszewski * http://www.fabiszewski.net * * This file is part of μlogger-android. * Licensed under GPL, either version 3, or any later. * See <http://www.gnu.org/licenses/> */ package net.fabiszewski.ulogger; import android.Manifest; import android.app.IntentService; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.os.Environment; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.text.format.DateFormat; import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringWriter; /** * Export track to GPX format */ public class GpxExportService extends IntentService { private static final String TAG = GpxExportService.class.getSimpleName(); public static final String BROADCAST_WRITE_PERMISSION_DENIED = "net.fabiszewski.ulogger.broadcast.write_permission_denied"; public static final String BROADCAST_EXPORT_FAILED = "net.fabiszewski.ulogger.broadcast.write_failed"; public static final String BROADCAST_EXPORT_DONE = "net.fabiszewski.ulogger.broadcast.write_ok"; private static final String ns_gpx = "http://www.topografix.com/GPX/1/1"; private static final String ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"; private static final String schemaLocation = ns_gpx + " http://www.topografix.com/GPX/1/1/gpx.xsd"; private static final String ULOGGER_DIR = "ulogger_tracks"; private static final String GPX_EXTENSION = ".gpx"; public GpxExportService() { super("GpxExportService"); } private DbAccess db; @Override public void onCreate() { super.onCreate(); if (Logger.DEBUG) { Log.d(TAG, "[gpx export create]"); } db = DbAccess.getInstance(); db.open(this); } /** * Cleanup */ @Override public void onDestroy() { if (Logger.DEBUG) { Log.d(TAG, "[gpx export stop]"); } if (db != null) { db.close(); } super.onDestroy(); } /** * Handle intent * * @param intent Intent */ @Override protected void onHandleIntent(Intent intent) { if (!hasWritePermission()) { // no permission to write if (Logger.DEBUG) { Log.d(TAG, "[export gpx no permission]"); } sendBroadcast(BROADCAST_WRITE_PERMISSION_DENIED, null); return; } if (!isExternalStorageWritable()) { // no access to external storage if (Logger.DEBUG) { Log.d(TAG, "[export gpx not writable]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, getString(R.string.e_external_not_writable)); return; } try { String trackName = db.getTrackName(); if (trackName == null) { trackName = getString(R.string.unknown_track); } File dir = getDir(); if (dir == null) { if (Logger.DEBUG) { Log.d(TAG, "[export gpx failed to create output folder]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, getString(R.string.e_output_dir)); return; } File file = getFile(dir, trackName); int i = 0; while (file.exists()) { file = getFile(dir, trackName + "_" + (++i)); } FileOutputStream fileOutputStream = new FileOutputStream(file); XmlSerializer serializer = Xml.newSerializer(); StringWriter writer = new StringWriter(); serializer.setOutput(writer); // header serializer.startDocument("UTF-8", true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.setPrefix("xsi", ns_xsi); serializer.startTag("", "gpx"); serializer.attribute(null, "xmlns", ns_gpx); serializer.attribute(ns_xsi, "schemaLocation", schemaLocation); serializer.attribute(null, "version", "1.1"); String creator = getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME; serializer.attribute(null, "creator", creator); // metadata long trackTimestamp = db.getFirstTimestamp(); String trackTime = DateFormat.format("yyyy-MM-ddThh:mm:ss", trackTimestamp * 1000).toString(); serializer.startTag(null, "metadata"); writeTag(serializer, "name", trackName); writeTag(serializer, "time", trackTime); serializer.endTag(null, "metadata"); // track serializer.startTag(null, "trk"); writeTag(serializer, "name", trackName); writePositions(serializer); serializer.endTag(null, "trk"); serializer.endTag("", "gpx"); serializer.endDocument(); serializer.flush(); String dataWrite = writer.toString(); fileOutputStream.write(dataWrite.getBytes()); fileOutputStream.close(); if (Logger.DEBUG) { Log.d(TAG, "[export gpx file written to " + file.getPath()); } sendBroadcast(BROADCAST_EXPORT_DONE, null); } catch (IOException|IllegalArgumentException|IllegalStateException e) { if (Logger.DEBUG) { Log.d(TAG, "[export gpx exception: " + e + "]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, e.getMessage()); } } /** * Write <trkseg> tag * * @param serializer XmlSerializer * @throws IOException IO exception * @throws IllegalArgumentException Xml illegal argument * @throws IllegalStateException Xml illegal state */ private void writePositions(@NonNull XmlSerializer serializer) throws IOException, IllegalArgumentException, IllegalStateException { Cursor cursor = db.getPositions(); serializer.startTag(null, "trkseg"); while (cursor.moveToNext()) { serializer.startTag(null, "trkpt"); serializer.attribute(null, "lat", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_LATITUDE))); serializer.attribute(null, "lon", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_LONGITUDE))); if (!cursor.isNull(cursor.getColumnIndex(DbContract.Positions.COLUMN_ALTITUDE))) { writeTag(serializer, "ele", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_ALTITUDE))); } long timestamp = cursor.getLong(cursor.getColumnIndex(DbContract.Positions.COLUMN_TIME)); String time = DateFormat.format("yyyy-MM-ddThh:mm:ss", timestamp * 1000).toString(); writeTag(serializer, "time", time); writeTag(serializer, "name", cursor.getString(cursor.getColumnIndex(DbContract.Positions._ID))); serializer.endTag(null, "trkpt"); } cursor.close(); serializer.endTag(null, "trkseg"); } /** * Write tag * * @param serializer XmlSerializer * @param name Tag name * @param text Tag text * @throws IOException IO exception * @throws IllegalArgumentException Xml illegal argument * @throws IllegalStateException Xml illegal state */ private void writeTag(@NonNull XmlSerializer serializer, @NonNull String name, @NonNull String text) throws IOException, IllegalArgumentException, IllegalStateException { serializer.startTag(null, name); serializer.text(text); serializer.endTag(null, name); } /** * Has user granted write permission? * * @return True if permission granted, false otherwise */ private boolean hasWritePermission() { return (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); } /** * Is there external storage we can write to? * * @return True if writable, false otherwise */ private boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state); } /** * Set up directory in Downloads folder * * @return File instance or null in case of failure */ private File getDir() { File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), ULOGGER_DIR); if (!dir.exists() && !dir.mkdirs()) { dir = null; } return dir; } /** * Set up file instance with given name in given folder * * @param dir Folder * @param trackName File name * @return File instance */ private File getFile(@NonNull File dir, @NonNull String trackName) { String fileName = trackName.replaceAll("[?:\"'*|/\\\\<>]", "_") + GPX_EXTENSION; return new File(dir, fileName); } /** * Send broadcast message * @param broadcast Broadcast intent * @param message Optional extra message */ private void sendBroadcast(String broadcast, String message) { Intent intent = new Intent(broadcast); if (message != null) { intent.putExtra("message", message); } sendBroadcast(intent); } } app/src/main/java/net/fabiszewski/ulogger/MainActivity.java +40 −4 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ public class MainActivity extends AppCompatActivity { private final static int LED_YELLOW = 3; private final static int PERMISSION_LOCATION = 1; private final static int PERMISSION_WRITE = 2; private final static int RESULT_PREFS_UPDATED = 1; private String pref_units; Loading Loading @@ -182,6 +183,9 @@ public class MainActivity extends AppCompatActivity { case R.id.menu_about: showAbout(); return true; case R.id.menu_export: startExport(); return true; default: return super.onOptionsItemSelected(item); Loading @@ -199,16 +203,23 @@ public class MainActivity extends AppCompatActivity { */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { // onPause closed db db.open(this); switch (requestCode) { case PERMISSION_LOCATION: if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // onPause closed db db.open(this); startLogger(); db.close(); } break; case PERMISSION_WRITE: if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { startExport(); } break; } db.close(); } /** Loading Loading @@ -279,6 +290,19 @@ public class MainActivity extends AppCompatActivity { stopService(intent); } /** * Start export service */ private void startExport() { if (db.countPositions() > 0) { Intent exportIntent = new Intent(MainActivity.this, GpxExportService.class); startService(exportIntent); showToast(getString(R.string.export_started)); } else { showToast(getString(R.string.nothing_to_export)); } } /** * Called when the user clicks the New track button * @param view View Loading Loading @@ -650,6 +674,9 @@ public class MainActivity extends AppCompatActivity { filter.addAction(LoggerService.BROADCAST_LOCATION_GPS_ENABLED); filter.addAction(LoggerService.BROADCAST_LOCATION_NETWORK_ENABLED); filter.addAction(LoggerService.BROADCAST_LOCATION_PERMISSION_DENIED); filter.addAction(GpxExportService.BROADCAST_WRITE_PERMISSION_DENIED); filter.addAction(GpxExportService.BROADCAST_EXPORT_FAILED); filter.addAction(GpxExportService.BROADCAST_EXPORT_DONE); filter.addAction(WebSyncService.BROADCAST_SYNC_DONE); filter.addAction(WebSyncService.BROADCAST_SYNC_FAILED); registerReceiver(mBroadcastReceiver, filter); Loading @@ -661,9 +688,7 @@ public class MainActivity extends AppCompatActivity { private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Logger.DEBUG) { Log.d(TAG, "[broadcast received " + intent + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[broadcast received " + intent + "]"); } if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_UPDATED)) { updateLocationLabel(LoggerService.lastUpdateRealtime()); setLocLed(LED_GREEN); Loading Loading @@ -715,10 +740,21 @@ public class MainActivity extends AppCompatActivity { showToast(getString(R.string.using_network), Toast.LENGTH_LONG); } else if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_GPS_ENABLED)) { showToast(getString(R.string.using_gps), Toast.LENGTH_LONG); } else if (intent.getAction().equals(GpxExportService.BROADCAST_EXPORT_DONE)) { showToast(getString(R.string.export_done), Toast.LENGTH_LONG); } else if (intent.getAction().equals(GpxExportService.BROADCAST_EXPORT_FAILED)) { String message = getString(R.string.export_failed); if (intent.hasExtra("message")) { message += "\n" + intent.getStringExtra("message"); } showToast(message, Toast.LENGTH_LONG); } else if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_PERMISSION_DENIED)) { showToast(getString(R.string.location_permission_denied), Toast.LENGTH_LONG); setLocLed(LED_RED); ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_LOCATION); } else if (intent.getAction().equals(GpxExportService.BROADCAST_WRITE_PERMISSION_DENIED)) { showToast(getString(R.string.write_permission_denied), Toast.LENGTH_LONG); ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_WRITE); } } }; Loading app/src/main/res/drawable/ic_file_download_white_24dp.xml 0 → 100644 +21 −0 Original line number Diff line number Diff line <!-- ~ Copyright (c) 2017 Bartek Fabiszewski ~ http://www.fabiszewski.net ~ ~ This file is part of μlogger-android. ~ Licensed under GPL, either version 3, or any later. ~ See <http://www.gnu.org/licenses/> ~ Material Design icon by Google. ~ Released under Apache License 2.0. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FFFFFFFF" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> </vector> Loading
app/src/main/AndroidManifest.xml +13 −6 Original line number Diff line number Diff line Loading @@ -18,10 +18,13 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- For web sync retries --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <!-- For exporting tracks --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Needed if app targets API >= 21 --> <uses-feature android:name="android.hardware.location.gps" /> <uses-feature android:name="android.hardware.location.network" /> <!-- Todo: attach camera image to position --> <!-- <uses-feature android:name="android.hardware.camera" android:required="false" /> --> Loading @@ -32,24 +35,28 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="net.fabiszewski.ulogger.MainActivity"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="net.fabiszewski.ulogger.SettingsActivity" /> <activity android:name=".SettingsActivity" /> <service android:name="net.fabiszewski.ulogger.LoggerService" android:name=".LoggerService" android:enabled="true" android:exported="false" /> <service android:name="net.fabiszewski.ulogger.WebSyncService" android:name=".WebSyncService" android:exported="false" /> <service android:name=".GpxExportService" android:exported="false" /> <receiver android:name="net.fabiszewski.ulogger.BootCompletedReceiver" android:name=".BootCompletedReceiver" android:enabled="true" android:exported="true"> <intent-filter> Loading
app/src/main/java/net/fabiszewski/ulogger/DbAccess.java +41 −0 Original line number Diff line number Diff line Loading @@ -12,6 +12,7 @@ package net.fabiszewski.ulogger; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.location.Location; import android.util.Log; Loading Loading @@ -90,6 +91,18 @@ class DbAccess { db.insert(DbContract.Positions.TABLE_NAME, null, values); } /** * Get result set containing all positions. * * @return Result set */ Cursor getPositions() { return db.query(DbContract.Positions.TABLE_NAME, new String[] {"*"}, null, null, null, null, DbContract.Positions._ID); } /** * Get result set containing positions marked as not synchronized. * Loading Loading @@ -157,6 +170,15 @@ class DbAccess { new String[] { String.valueOf(id) }); } /** * Get number of all positions in track * * @return Count */ int countPositions() { return (int) DatabaseUtils.queryNumEntries(db, DbContract.Positions.TABLE_NAME); } /** * Get number of not synchronized items. * Loading Loading @@ -186,6 +208,25 @@ class DbAccess { return (countUnsynced() > 0); } /** * Get first saved location time. * * @return UTC timestamp in seconds */ long getFirstTimestamp() { Cursor query = db.query(DbContract.Positions.TABLE_NAME, new String[] {DbContract.Positions.COLUMN_TIME}, null, null, null, null, DbContract.Positions._ID + " ASC", "1"); long timestamp = 0; if (query.moveToFirst()) { timestamp = query.getInt(0); } query.close(); return timestamp; } /** * Get last saved location time. * Loading
app/src/main/java/net/fabiszewski/ulogger/GpxExportService.java 0 → 100644 +267 −0 Original line number Diff line number Diff line /* * Copyright (c) 2017 Bartek Fabiszewski * http://www.fabiszewski.net * * This file is part of μlogger-android. * Licensed under GPL, either version 3, or any later. * See <http://www.gnu.org/licenses/> */ package net.fabiszewski.ulogger; import android.Manifest; import android.app.IntentService; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.os.Environment; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.text.format.DateFormat; import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.StringWriter; /** * Export track to GPX format */ public class GpxExportService extends IntentService { private static final String TAG = GpxExportService.class.getSimpleName(); public static final String BROADCAST_WRITE_PERMISSION_DENIED = "net.fabiszewski.ulogger.broadcast.write_permission_denied"; public static final String BROADCAST_EXPORT_FAILED = "net.fabiszewski.ulogger.broadcast.write_failed"; public static final String BROADCAST_EXPORT_DONE = "net.fabiszewski.ulogger.broadcast.write_ok"; private static final String ns_gpx = "http://www.topografix.com/GPX/1/1"; private static final String ns_xsi = "http://www.w3.org/2001/XMLSchema-instance"; private static final String schemaLocation = ns_gpx + " http://www.topografix.com/GPX/1/1/gpx.xsd"; private static final String ULOGGER_DIR = "ulogger_tracks"; private static final String GPX_EXTENSION = ".gpx"; public GpxExportService() { super("GpxExportService"); } private DbAccess db; @Override public void onCreate() { super.onCreate(); if (Logger.DEBUG) { Log.d(TAG, "[gpx export create]"); } db = DbAccess.getInstance(); db.open(this); } /** * Cleanup */ @Override public void onDestroy() { if (Logger.DEBUG) { Log.d(TAG, "[gpx export stop]"); } if (db != null) { db.close(); } super.onDestroy(); } /** * Handle intent * * @param intent Intent */ @Override protected void onHandleIntent(Intent intent) { if (!hasWritePermission()) { // no permission to write if (Logger.DEBUG) { Log.d(TAG, "[export gpx no permission]"); } sendBroadcast(BROADCAST_WRITE_PERMISSION_DENIED, null); return; } if (!isExternalStorageWritable()) { // no access to external storage if (Logger.DEBUG) { Log.d(TAG, "[export gpx not writable]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, getString(R.string.e_external_not_writable)); return; } try { String trackName = db.getTrackName(); if (trackName == null) { trackName = getString(R.string.unknown_track); } File dir = getDir(); if (dir == null) { if (Logger.DEBUG) { Log.d(TAG, "[export gpx failed to create output folder]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, getString(R.string.e_output_dir)); return; } File file = getFile(dir, trackName); int i = 0; while (file.exists()) { file = getFile(dir, trackName + "_" + (++i)); } FileOutputStream fileOutputStream = new FileOutputStream(file); XmlSerializer serializer = Xml.newSerializer(); StringWriter writer = new StringWriter(); serializer.setOutput(writer); // header serializer.startDocument("UTF-8", true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.setPrefix("xsi", ns_xsi); serializer.startTag("", "gpx"); serializer.attribute(null, "xmlns", ns_gpx); serializer.attribute(ns_xsi, "schemaLocation", schemaLocation); serializer.attribute(null, "version", "1.1"); String creator = getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME; serializer.attribute(null, "creator", creator); // metadata long trackTimestamp = db.getFirstTimestamp(); String trackTime = DateFormat.format("yyyy-MM-ddThh:mm:ss", trackTimestamp * 1000).toString(); serializer.startTag(null, "metadata"); writeTag(serializer, "name", trackName); writeTag(serializer, "time", trackTime); serializer.endTag(null, "metadata"); // track serializer.startTag(null, "trk"); writeTag(serializer, "name", trackName); writePositions(serializer); serializer.endTag(null, "trk"); serializer.endTag("", "gpx"); serializer.endDocument(); serializer.flush(); String dataWrite = writer.toString(); fileOutputStream.write(dataWrite.getBytes()); fileOutputStream.close(); if (Logger.DEBUG) { Log.d(TAG, "[export gpx file written to " + file.getPath()); } sendBroadcast(BROADCAST_EXPORT_DONE, null); } catch (IOException|IllegalArgumentException|IllegalStateException e) { if (Logger.DEBUG) { Log.d(TAG, "[export gpx exception: " + e + "]"); } sendBroadcast(BROADCAST_EXPORT_FAILED, e.getMessage()); } } /** * Write <trkseg> tag * * @param serializer XmlSerializer * @throws IOException IO exception * @throws IllegalArgumentException Xml illegal argument * @throws IllegalStateException Xml illegal state */ private void writePositions(@NonNull XmlSerializer serializer) throws IOException, IllegalArgumentException, IllegalStateException { Cursor cursor = db.getPositions(); serializer.startTag(null, "trkseg"); while (cursor.moveToNext()) { serializer.startTag(null, "trkpt"); serializer.attribute(null, "lat", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_LATITUDE))); serializer.attribute(null, "lon", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_LONGITUDE))); if (!cursor.isNull(cursor.getColumnIndex(DbContract.Positions.COLUMN_ALTITUDE))) { writeTag(serializer, "ele", cursor.getString(cursor.getColumnIndex(DbContract.Positions.COLUMN_ALTITUDE))); } long timestamp = cursor.getLong(cursor.getColumnIndex(DbContract.Positions.COLUMN_TIME)); String time = DateFormat.format("yyyy-MM-ddThh:mm:ss", timestamp * 1000).toString(); writeTag(serializer, "time", time); writeTag(serializer, "name", cursor.getString(cursor.getColumnIndex(DbContract.Positions._ID))); serializer.endTag(null, "trkpt"); } cursor.close(); serializer.endTag(null, "trkseg"); } /** * Write tag * * @param serializer XmlSerializer * @param name Tag name * @param text Tag text * @throws IOException IO exception * @throws IllegalArgumentException Xml illegal argument * @throws IllegalStateException Xml illegal state */ private void writeTag(@NonNull XmlSerializer serializer, @NonNull String name, @NonNull String text) throws IOException, IllegalArgumentException, IllegalStateException { serializer.startTag(null, name); serializer.text(text); serializer.endTag(null, name); } /** * Has user granted write permission? * * @return True if permission granted, false otherwise */ private boolean hasWritePermission() { return (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); } /** * Is there external storage we can write to? * * @return True if writable, false otherwise */ private boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state); } /** * Set up directory in Downloads folder * * @return File instance or null in case of failure */ private File getDir() { File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), ULOGGER_DIR); if (!dir.exists() && !dir.mkdirs()) { dir = null; } return dir; } /** * Set up file instance with given name in given folder * * @param dir Folder * @param trackName File name * @return File instance */ private File getFile(@NonNull File dir, @NonNull String trackName) { String fileName = trackName.replaceAll("[?:\"'*|/\\\\<>]", "_") + GPX_EXTENSION; return new File(dir, fileName); } /** * Send broadcast message * @param broadcast Broadcast intent * @param message Optional extra message */ private void sendBroadcast(String broadcast, String message) { Intent intent = new Intent(broadcast); if (message != null) { intent.putExtra("message", message); } sendBroadcast(intent); } }
app/src/main/java/net/fabiszewski/ulogger/MainActivity.java +40 −4 Original line number Diff line number Diff line Loading @@ -64,6 +64,7 @@ public class MainActivity extends AppCompatActivity { private final static int LED_YELLOW = 3; private final static int PERMISSION_LOCATION = 1; private final static int PERMISSION_WRITE = 2; private final static int RESULT_PREFS_UPDATED = 1; private String pref_units; Loading Loading @@ -182,6 +183,9 @@ public class MainActivity extends AppCompatActivity { case R.id.menu_about: showAbout(); return true; case R.id.menu_export: startExport(); return true; default: return super.onOptionsItemSelected(item); Loading @@ -199,16 +203,23 @@ public class MainActivity extends AppCompatActivity { */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { // onPause closed db db.open(this); switch (requestCode) { case PERMISSION_LOCATION: if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // onPause closed db db.open(this); startLogger(); db.close(); } break; case PERMISSION_WRITE: if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { startExport(); } break; } db.close(); } /** Loading Loading @@ -279,6 +290,19 @@ public class MainActivity extends AppCompatActivity { stopService(intent); } /** * Start export service */ private void startExport() { if (db.countPositions() > 0) { Intent exportIntent = new Intent(MainActivity.this, GpxExportService.class); startService(exportIntent); showToast(getString(R.string.export_started)); } else { showToast(getString(R.string.nothing_to_export)); } } /** * Called when the user clicks the New track button * @param view View Loading Loading @@ -650,6 +674,9 @@ public class MainActivity extends AppCompatActivity { filter.addAction(LoggerService.BROADCAST_LOCATION_GPS_ENABLED); filter.addAction(LoggerService.BROADCAST_LOCATION_NETWORK_ENABLED); filter.addAction(LoggerService.BROADCAST_LOCATION_PERMISSION_DENIED); filter.addAction(GpxExportService.BROADCAST_WRITE_PERMISSION_DENIED); filter.addAction(GpxExportService.BROADCAST_EXPORT_FAILED); filter.addAction(GpxExportService.BROADCAST_EXPORT_DONE); filter.addAction(WebSyncService.BROADCAST_SYNC_DONE); filter.addAction(WebSyncService.BROADCAST_SYNC_FAILED); registerReceiver(mBroadcastReceiver, filter); Loading @@ -661,9 +688,7 @@ public class MainActivity extends AppCompatActivity { private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Logger.DEBUG) { Log.d(TAG, "[broadcast received " + intent + "]"); } if (Logger.DEBUG) { Log.d(TAG, "[broadcast received " + intent + "]"); } if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_UPDATED)) { updateLocationLabel(LoggerService.lastUpdateRealtime()); setLocLed(LED_GREEN); Loading Loading @@ -715,10 +740,21 @@ public class MainActivity extends AppCompatActivity { showToast(getString(R.string.using_network), Toast.LENGTH_LONG); } else if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_GPS_ENABLED)) { showToast(getString(R.string.using_gps), Toast.LENGTH_LONG); } else if (intent.getAction().equals(GpxExportService.BROADCAST_EXPORT_DONE)) { showToast(getString(R.string.export_done), Toast.LENGTH_LONG); } else if (intent.getAction().equals(GpxExportService.BROADCAST_EXPORT_FAILED)) { String message = getString(R.string.export_failed); if (intent.hasExtra("message")) { message += "\n" + intent.getStringExtra("message"); } showToast(message, Toast.LENGTH_LONG); } else if (intent.getAction().equals(LoggerService.BROADCAST_LOCATION_PERMISSION_DENIED)) { showToast(getString(R.string.location_permission_denied), Toast.LENGTH_LONG); setLocLed(LED_RED); ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_LOCATION); } else if (intent.getAction().equals(GpxExportService.BROADCAST_WRITE_PERMISSION_DENIED)) { showToast(getString(R.string.write_permission_denied), Toast.LENGTH_LONG); ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_WRITE); } } }; Loading
app/src/main/res/drawable/ic_file_download_white_24dp.xml 0 → 100644 +21 −0 Original line number Diff line number Diff line <!-- ~ Copyright (c) 2017 Bartek Fabiszewski ~ http://www.fabiszewski.net ~ ~ This file is part of μlogger-android. ~ Licensed under GPL, either version 3, or any later. ~ See <http://www.gnu.org/licenses/> ~ Material Design icon by Google. ~ Released under Apache License 2.0. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FFFFFFFF" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> </vector>