From fa29c79481b06dae6c1fe1b04ca4398a032bdf6c Mon Sep 17 00:00:00 2001 From: Frank van Diggelen Date: Tue, 27 Sep 2016 14:26:22 -0700 Subject: [PATCH 1/2] Fixed index of zero in PlotPseudorangeRates.m, this allows code to work with legacy files from Nexus 5x and Nexus 6p, where clock is always discontinuous. --- opensource/PlotPseudorangeRates.m | 7 +++++-- opensource/PlotPvt.m | 9 +++++---- opensource/ProcessGnssMeasScript.m | 2 +- opensource/ReadGnssLogger.m | 21 +++++++++++++++------ opensource/SetDataFilter.m | 13 +++++++------ 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/opensource/PlotPseudorangeRates.m b/opensource/PlotPseudorangeRates.m index c052868..85ec601 100644 --- a/opensource/PlotPseudorangeRates.m +++ b/opensource/PlotPseudorangeRates.m @@ -60,8 +60,11 @@ for i=1:M else colors(i,:) = get(h,'Color'); end - text(ti,y(iFi(end)-1),int2str(gnssMeas.Svid(i)),'Color',colors(i,:)); - meanDprM = mean(y(isfinite(y)));%store for analysing delta prr dpr + iFi = find(isfinite(y)); + if any(iFi) + text(ti,y(iFi(end)),int2str(gnssMeas.Svid(i)),'Color',colors(i,:)); + end + meanDprM = mean(y(iFi));%store for analysing delta prr dpr deltaMeanM(i) = meanPrrM - meanDprM; end end diff --git a/opensource/PlotPvt.m b/opensource/PlotPvt.m index 691b0ee..7ac7886 100644 --- a/opensource/PlotPvt.m +++ b/opensource/PlotPvt.m @@ -25,17 +25,18 @@ iFi = isfinite(gpsPvt.allLlaDegDegM(:,1));%index into finite results if ~any(iFi) return end -llaMed = median(gpsPvt.allLlaDegDegM(iFi,:));%use median position as reference +llaMed = median(gpsPvt.allLlaDegDegM(iFi,:));%median position +%print median lla so user can use it as reference position if they want: +fprintf('Median llaDegDegM = [%.7f %.7f %.2f]\n',llaMed) + if nargin < 3, llaTrueDegDegM = []; end -if nargin < 4. titleString = 'PVT solution'; end +if nargin < 4, titleString = 'PVT solution'; end bGotLlaTrue = ~isempty(llaTrueDegDegM) && any(llaTrueDegDegM); %not empty and not all zeros if bGotLlaTrue llaRef = llaTrueDegDegM; else llaRef = llaMed; - %print median lla so user can use it as reference position if they want - fprintf('Median llaDegDegM = [%.7f %.7f %.2f]\n',llaMed) end %% plot ne errors vs llaTrueDegDegM -------------------------------------------- diff --git a/opensource/ProcessGnssMeasScript.m b/opensource/ProcessGnssMeasScript.m index 1bae34a..410ac78 100644 --- a/opensource/ProcessGnssMeasScript.m +++ b/opensource/ProcessGnssMeasScript.m @@ -28,7 +28,7 @@ param.llaTrueDegDegM = [37.422578, -122.081678, -28];%Charleston Park Test Site %% Set the data filter and Read log file dataFilter = SetDataFilter; -[gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,prFileName,dataFilter,param); +[gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,prFileName,dataFilter); if isempty(gnssRaw), return, end %% Get online ephemeris from Nasa ftp, first compute UTC Time from gnssRaw: diff --git a/opensource/ReadGnssLogger.m b/opensource/ReadGnssLogger.m index d90735d..ec9f2a4 100644 --- a/opensource/ReadGnssLogger.m +++ b/opensource/ReadGnssLogger.m @@ -1,5 +1,5 @@ function [gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,fileName,dataFilter,gnssAnalysis) -%% [gnssRaw,gnssAnalysis]=ReadGnssLogger(dirName,fileName,dataFilter,gnssAnalysis); +%% [gnssRaw,gnssAnalysis]=ReadGnssLogger(dirName,fileName,[dataFilter],[gnssAnalysis]); % Read the log file created by Gnss Logger App in Android % Compatible with Android release N % @@ -7,12 +7,14 @@ function [gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,fileName,dataFilter,gns % dirName = string with directory of fileName, % e.g. '~/Documents/MATLAB/Pseudoranges/2016-03-28' % fileName = string with filename -% dataFilter = nx2 cell array of pairs of strings, +% optional inputs: +% [dataFilter], nx2 cell array of pairs of strings, % dataFilter{i,1} is a string with one of 'Raw' header values from the % GnssLogger log file e.g. 'ConstellationType' % dataFilter{i,2} is a string with a valid matlab expression, containing % the header value, e.g. 'ConstellationType==1' -% See SetDataFilter.m for full rules and examples of dataFilter. +% See SetDataFilter.m for full rules and examples of dataFilter. +% [gnssAnalysis] structure containing analysis, incl list of missing fields % % Output: % gnssRaw, all GnssClock and GnssMeasurement fields from log file, including: @@ -45,6 +47,7 @@ function [gnssRaw,gnssAnalysis] = ReadGnssLogger(dirName,fileName,dataFilter,gns % ReportMissingFields() %% Initialize outputs and inputs +gnssRaw = []; gnssAnalysis.GnssClockErrors = 'GnssClock Errors.'; gnssAnalysis.GnssMeasurementErrors = 'GnssMeasurement Errors.'; gnssAnalysis.ApiPassFail = ''; @@ -63,7 +66,8 @@ rawCsvFile = MakeCsv(dirName,fileName); %% apply dataFilter [bOk] = CheckDataFilter(dataFilter,header); if ~bOk, return, end -C = FilterData(C,dataFilter,header); +[bOk,C] = FilterData(C,dataFilter,header); +if ~bOk, return, end %% pack data into gnssRaw structure [gnssRaw,missing] = PackGnssRaw(C,header); @@ -130,7 +134,9 @@ end if isempty(strfind(sPlatform,'N')) %add || strfind(platform,'O') and so on for future platforms fprintf('\nThis version of ReadGnssLogger supports Android N\n') - error('Found "%s" in log file, expected "Platform: N"',line) + fprintf('WARNING: did not find "Platform" type in log file, expected "Platform: N"\n') + fprintf('Please Update GnssLogger\n') + sPlatform = 'N';%assume version N end v1 = [1;4;0;0]; @@ -237,9 +243,10 @@ fclose(fid); end% of function ReadRawCsv %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -function C = FilterData(C,dataFilter,header) +function [bOk,C] = FilterData(C,dataFilter,header) %% filter C based on contents of dataFilter +bOk = true; iS = ones(size(C{1})); %initialize index into rows of C for i=1:size(dataFilter,1) j=find(strcmp(header,dataFilter{i,1}));%j = index into header @@ -262,6 +269,8 @@ end if ~any(iS) %if all zeros fprintf('\nAll measurements removed. Specify dataFilter less strictly than this:, ') dataFilter(:,2) + bOk=false; + C=[]; return end diff --git a/opensource/SetDataFilter.m b/opensource/SetDataFilter.m index eb60f33..98f45f2 100644 --- a/opensource/SetDataFilter.m +++ b/opensource/SetDataFilter.m @@ -8,13 +8,9 @@ function dataFilter = SetDataFilter %Author: Frank van Diggelen %Open Source code for processing Android GNSS Measurements -%filter for fine time measurements only <=> uncertainty < 10 ms = 1e7 ns -dataFilter{1,1} = 'BiasUncertaintyNanos'; -dataFilter{1,2} = 'BiasUncertaintyNanos < 1e7'; - %filter out FullBiasNanos == 0 -dataFilter{end+1,1} = 'FullBiasNanos'; -dataFilter{end,2} = 'FullBiasNanos ~= 0'; +dataFilter{1,1} = 'FullBiasNanos'; +dataFilter{1,2} = 'FullBiasNanos ~= 0'; %you can create other filters in the same way ... %for example, suppose you want to remove Svid 23: @@ -26,6 +22,11 @@ dataFilter{end,2} = 'FullBiasNanos ~= 0'; % NOTE: you *cannot* use 'any(Svid)==[2,5,10,17]' because Svid refers to a % vector variable and you must compare it to a scalar. +%filter for fine time measurements only <=> uncertainty < 10 ms = 1e7 ns +%For Nexus 5x and 6p this field is not filled, so comment out these next 2 lines +% dataFilter{end+1,1} = 'BiasUncertaintyNanos'; +% dataFilter{end,2} = 'BiasUncertaintyNanos < 1e7'; + %keep only Svid 2 % dataFilter{end+1,1} = 'Svid'; % dataFilter{end,2} = 'Svid==2'; From 0c7a9037e2e988591e1329f6f3b7e24e6289b2e8 Mon Sep 17 00:00:00 2001 From: Mohammed Khider Date: Fri, 30 Sep 2016 17:54:10 -0700 Subject: [PATCH 2/2] Adds the GNSSLoger Android Studio project. --- GNSSLogger/.gitignore | 9 + GNSSLogger/GNSSLogger.iml | 19 + GNSSLogger/app/.gitignore | 1 + GNSSLogger/app/app.iml | 148 +++++++ GNSSLogger/app/build.gradle | 31 ++ GNSSLogger/app/proguard-rules.pro | 17 + GNSSLogger/app/src/main/AndroidManifest.xml | 23 + .../location/gps/gnsslogger/FileLogger.java | 401 ++++++++++++++++++ .../gps/gnsslogger/GnssContainer.java | 289 +++++++++++++ .../location/gps/gnsslogger/GnssListener.java | 58 +++ .../location/gps/gnsslogger/HelpDialog.java | 83 ++++ .../gps/gnsslogger/LoggerFragment.java | 163 +++++++ .../location/gps/gnsslogger/MainActivity.java | 172 ++++++++ .../gps/gnsslogger/SettingsFragment.java | 219 ++++++++++ .../location/gps/gnsslogger/UiLogger.java | 220 ++++++++++ .../app/src/main/res/drawable/ic_launcher.png | Bin 0 -> 2162 bytes .../app/src/main/res/layout/activity_main.xml | 19 + .../app/src/main/res/layout/fragment_log.xml | 67 +++ .../app/src/main/res/layout/fragment_main.xml | 155 +++++++ GNSSLogger/app/src/main/res/layout/help.xml | 13 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2162 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../app/src/main/res/raw/help_contents.txt | 20 + .../app/src/main/res/values-w820dp/dimens.xml | 6 + GNSSLogger/app/src/main/res/values/colors.xml | 6 + GNSSLogger/app/src/main/res/values/dimens.xml | 6 + .../app/src/main/res/values/strings.xml | 19 + GNSSLogger/app/src/main/res/values/styles.xml | 5 + GNSSLogger/build.gradle | 23 + GNSSLogger/gradle.properties | 17 + GNSSLogger/gradlew | 160 +++++++ GNSSLogger/gradlew.bat | 90 ++++ GNSSLogger/local.properties | 10 + GNSSLogger/settings.gradle | 1 + opensource/README.md | 20 +- 38 files changed, 2486 insertions(+), 4 deletions(-) create mode 100644 GNSSLogger/.gitignore create mode 100644 GNSSLogger/GNSSLogger.iml create mode 100644 GNSSLogger/app/.gitignore create mode 100644 GNSSLogger/app/app.iml create mode 100644 GNSSLogger/app/build.gradle create mode 100644 GNSSLogger/app/proguard-rules.pro create mode 100644 GNSSLogger/app/src/main/AndroidManifest.xml create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/FileLogger.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssContainer.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssListener.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/HelpDialog.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/LoggerFragment.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/MainActivity.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/SettingsFragment.java create mode 100644 GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/UiLogger.java create mode 100644 GNSSLogger/app/src/main/res/drawable/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/layout/activity_main.xml create mode 100644 GNSSLogger/app/src/main/res/layout/fragment_log.xml create mode 100644 GNSSLogger/app/src/main/res/layout/fragment_main.xml create mode 100644 GNSSLogger/app/src/main/res/layout/help.xml create mode 100644 GNSSLogger/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 GNSSLogger/app/src/main/res/raw/help_contents.txt create mode 100644 GNSSLogger/app/src/main/res/values-w820dp/dimens.xml create mode 100644 GNSSLogger/app/src/main/res/values/colors.xml create mode 100644 GNSSLogger/app/src/main/res/values/dimens.xml create mode 100644 GNSSLogger/app/src/main/res/values/strings.xml create mode 100644 GNSSLogger/app/src/main/res/values/styles.xml create mode 100644 GNSSLogger/build.gradle create mode 100644 GNSSLogger/gradle.properties create mode 100644 GNSSLogger/gradlew create mode 100644 GNSSLogger/gradlew.bat create mode 100644 GNSSLogger/local.properties create mode 100644 GNSSLogger/settings.gradle diff --git a/GNSSLogger/.gitignore b/GNSSLogger/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/GNSSLogger/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/GNSSLogger/GNSSLogger.iml b/GNSSLogger/GNSSLogger.iml new file mode 100644 index 0000000..0cd6d47 --- /dev/null +++ b/GNSSLogger/GNSSLogger.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GNSSLogger/app/.gitignore b/GNSSLogger/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/GNSSLogger/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/GNSSLogger/app/app.iml b/GNSSLogger/app/app.iml new file mode 100644 index 0000000..af54888 --- /dev/null +++ b/GNSSLogger/app/app.iml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GNSSLogger/app/build.gradle b/GNSSLogger/app/build.gradle new file mode 100644 index 0000000..0c14983 --- /dev/null +++ b/GNSSLogger/app/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 24 + buildToolsVersion "23.0.1" + defaultConfig { + applicationId "com.google.android.apps.location.gps.gnsslogger" + minSdkVersion 24 + targetSdkVersion 24 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:24.2.1' + testCompile 'junit:junit:4.12' + compile 'com.android.support:design:24.2.1' + compile 'com.android.support:support-v13:24.2.1' +} diff --git a/GNSSLogger/app/proguard-rules.pro b/GNSSLogger/app/proguard-rules.pro new file mode 100644 index 0000000..83d4526 --- /dev/null +++ b/GNSSLogger/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /media/build/master/prebuilts/fullsdk/linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/GNSSLogger/app/src/main/AndroidManifest.xml b/GNSSLogger/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a6441be --- /dev/null +++ b/GNSSLogger/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/FileLogger.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/FileLogger.java new file mode 100644 index 0000000..eb4e138 --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/FileLogger.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.content.Context; +import android.content.Intent; +import android.location.GnssClock; +import android.location.GnssMeasurement; +import android.location.GnssMeasurementsEvent; +import android.location.GnssNavigationMessage; +import android.location.GnssStatus; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.SystemClock; +import android.util.Log; +import android.widget.Toast; +import com.google.android.apps.location.gps.gnsslogger.LoggerFragment.UIFragmentComponent; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileFilter; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +/** + * A GNSS logger to store information to a file. + */ +public class FileLogger implements GnssListener { + + private static final String TAG = "FileLogger"; + private static final String FILE_PREFIX = "pseudoranges"; + private static final String ERROR_WRITING_FILE = "Problem writing to file."; + private static final String COMMENT_START = "# "; + private static final char RECORD_DELIMITER = ','; + private static final String VERSION_TAG = "Version: "; + private static final String FILE_VERSION = "1.4.0.0, Platform: N"; + + private static final int MAX_FILES_STORED = 100; + private static final int MINIMUM_USABLE_FILE_SIZE_BYTES = 1000; + + private final Context mContext; + + private final Object mFileLock = new Object(); + private BufferedWriter mFileWriter; + private File mFile; + + private UIFragmentComponent mUiComponent; + + public synchronized UIFragmentComponent getUiComponent() { + return mUiComponent; + } + + public synchronized void setUiComponent(UIFragmentComponent value) { + mUiComponent = value; + } + + public FileLogger(Context context) { + this.mContext = context; + } + + /** + * Start a new file logging process. + */ + public void startNewLog() { + synchronized (mFileLock) { + File baseDirectory; + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + baseDirectory = new File(Environment.getExternalStorageDirectory(), FILE_PREFIX); + baseDirectory.mkdirs(); + } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { + logError("Cannot write to external storage."); + return; + } else { + logError("Cannot read external storage."); + return; + } + + SimpleDateFormat formatter = new SimpleDateFormat("yyy_MM_dd_HH_mm_ss"); + Date now = new Date(); + String fileName = String.format("%s_log_%s.txt", FILE_PREFIX, formatter.format(now)); + File currentFile = new File(baseDirectory, fileName); + String currentFilePath = currentFile.getAbsolutePath(); + BufferedWriter currentFileWriter; + try { + currentFileWriter = new BufferedWriter(new FileWriter(currentFile)); + } catch (IOException e) { + logException("Could not open file: " + currentFilePath, e); + return; + } + + // initialize the contents of the file + try { + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.write("Header Description:"); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.write(VERSION_TAG); + currentFileWriter.write(FILE_VERSION); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.write( + "Raw,ElapsedRealtimeMillis,TimeNanos,LeapSecond,TimeUncertaintyNanos,FullBiasNanos," + + "BiasNanos,BiasUncertaintyNanos,DriftNanosPerSecond,DriftUncertaintyNanosPerSecond," + + "HardwareClockDiscontinuityCount, Svid,TimeOffsetNanos,State,ReceivedSvTimeNanos," + + "ReceivedSvTimeUncertaintyNanos,Cn0DbHz,PseudorangeRateMetersPerSecond," + + "PseudorangeRateUncertaintyMetersPerSecond," + + "AccumulatedDeltaRangeState,AccumulatedDeltaRangeMeters," + + "AccumulatedDeltaRangeUncertaintyMeters,CarrierFrequencyHz,CarrierCycles," + + "CarrierPhase,CarrierPhaseUncertainty,MultipathIndicator,SnrInDb," + + "ConstellationType"); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.write( + "Fix,Provider,Latitude,Longitude,Altitude,Speed,Accuracy,(UTC)TimeInMs"); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.write("Nav,Svid,Type,Status,MessageId,Sub-messageId,Data(Bytes)"); + currentFileWriter.newLine(); + currentFileWriter.write(COMMENT_START); + currentFileWriter.newLine(); + } catch (IOException e) { + logException("Count not initialize file: " + currentFilePath, e); + return; + } + + if (mFileWriter != null) { + try { + mFileWriter.close(); + } catch (IOException e) { + logException("Unable to close all file streams.", e); + return; + } + } + + mFile = currentFile; + mFileWriter = currentFileWriter; + Toast.makeText(mContext, "File opened: " + currentFilePath, Toast.LENGTH_SHORT).show(); + + // To make sure that files do not fill up the external storage: + // - Remove all empty files + FileFilter filter = new FileToDeleteFilter(mFile); + for (File existingFile : baseDirectory.listFiles(filter)) { + existingFile.delete(); + } + // - Trim the number of files with data + File[] existingFiles = baseDirectory.listFiles(); + int filesToDeleteCount = existingFiles.length - MAX_FILES_STORED; + if (filesToDeleteCount > 0) { + Arrays.sort(existingFiles); + for (int i = 0; i < filesToDeleteCount; ++i) { + existingFiles[i].delete(); + } + } + } + } + + /** + * Send the current log via email or other options selected from a pop menu shown to the user. A + * new log is started when calling this function. + */ + public void send() { + if (mFile == null) { + return; + } + + Intent emailIntent = new Intent(Intent.ACTION_SEND); + emailIntent.setType("*/*"); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, "SensorLog"); + emailIntent.putExtra(Intent.EXTRA_TEXT, ""); + // attach the file + emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(mFile)); + getUiComponent().startActivity(Intent.createChooser(emailIntent, "Send log..")); + if (mFileWriter != null) { + try { + mFileWriter.close(); + mFileWriter = null; + } catch (IOException e) { + logException("Unable to close all file streams.", e); + return; + } + } + } + + @Override + public void onProviderEnabled(String provider) {} + + @Override + public void onProviderDisabled(String provider) {} + + @Override + public void onLocationChanged(Location location) { + if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) { + synchronized (mFileLock) { + if (mFileWriter == null) { + return; + } + String locationStream = + String.format( + Locale.US, + "Fix,%s,%f,%f,%f,%f,%f,%d", + location.getProvider(), + location.getLatitude(), + location.getLongitude(), + location.getAltitude(), + location.getSpeed(), + location.getAccuracy(), + location.getTime()); + try { + mFileWriter.write(locationStream); + mFileWriter.newLine(); + } catch (IOException e) { + logException(ERROR_WRITING_FILE, e); + } + } + } + } + + @Override + public void onLocationStatusChanged(String provider, int status, Bundle extras) {} + + @Override + public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) { + synchronized (mFileLock) { + if (mFileWriter == null) { + return; + } + GnssClock gnssClock = event.getClock(); + for (GnssMeasurement measurement : event.getMeasurements()) { + try { + writeGnssMeasurementToFile(gnssClock, measurement); + } catch (IOException e) { + logException(ERROR_WRITING_FILE, e); + } + } + } + } + + @Override + public void onGnssMeasurementsStatusChanged(int status) {} + + @Override + public void onGnssNavigationMessageReceived(GnssNavigationMessage navigationMessage) { + synchronized (mFileLock) { + if (mFileWriter == null) { + return; + } + StringBuilder builder = new StringBuilder("Nav"); + builder.append(RECORD_DELIMITER); + builder.append(navigationMessage.getSvid()); + builder.append(RECORD_DELIMITER); + builder.append(navigationMessage.getType()); + builder.append(RECORD_DELIMITER); + + int status = navigationMessage.getStatus(); + builder.append(status); + builder.append(RECORD_DELIMITER); + builder.append(navigationMessage.getMessageId()); + builder.append(RECORD_DELIMITER); + builder.append(navigationMessage.getSubmessageId()); + byte[] data = navigationMessage.getData(); + for (byte word : data) { + builder.append(RECORD_DELIMITER); + builder.append(word); + } + try { + mFileWriter.write(builder.toString()); + mFileWriter.newLine(); + } catch (IOException e) { + logException(ERROR_WRITING_FILE, e); + } + } + } + + @Override + public void onGnssNavigationMessageStatusChanged(int status) {} + + @Override + public void onGnssStatusChanged(GnssStatus gnssStatus) {} + + @Override + public void onNmeaReceived(long timestamp, String s) {} + + @Override + public void onListenerRegistration(String listener, boolean result) {} + + private void writeGnssMeasurementToFile(GnssClock clock, GnssMeasurement measurement) + throws IOException { + String clockStream = + String.format( + "Raw,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s", + SystemClock.elapsedRealtime(), + clock.getTimeNanos(), + clock.hasLeapSecond() ? clock.getLeapSecond() : "", + clock.hasTimeUncertaintyNanos() ? clock.getTimeUncertaintyNanos() : "", + clock.getFullBiasNanos(), + clock.hasBiasNanos() ? clock.getBiasNanos() : "", + clock.hasBiasUncertaintyNanos() ? clock.getBiasUncertaintyNanos() : "", + clock.hasDriftNanosPerSecond() ? clock.getDriftNanosPerSecond() : "", + clock.hasDriftUncertaintyNanosPerSecond() + ? clock.getDriftUncertaintyNanosPerSecond() + : "", + clock.getHardwareClockDiscontinuityCount() + ","); + mFileWriter.write(clockStream); + + String measurementStream = + String.format( + "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s", + measurement.getSvid(), + measurement.getTimeOffsetNanos(), + measurement.getState(), + measurement.getReceivedSvTimeNanos(), + measurement.getReceivedSvTimeUncertaintyNanos(), + measurement.getCn0DbHz(), + measurement.getPseudorangeRateMetersPerSecond(), + measurement.getPseudorangeRateUncertaintyMetersPerSecond(), + measurement.getAccumulatedDeltaRangeState(), + measurement.getAccumulatedDeltaRangeMeters(), + measurement.getAccumulatedDeltaRangeUncertaintyMeters(), + measurement.hasCarrierFrequencyHz() ? measurement.getCarrierFrequencyHz() : "", + measurement.hasCarrierCycles() ? measurement.getCarrierCycles() : "", + measurement.hasCarrierPhase() ? measurement.getCarrierPhase() : "", + measurement.hasCarrierPhaseUncertainty() + ? measurement.getCarrierPhaseUncertainty() + : "", + measurement.getMultipathIndicator(), + measurement.hasSnrInDb() ? measurement.getSnrInDb() : "", + measurement.getConstellationType()); + mFileWriter.write(measurementStream); + mFileWriter.newLine(); + } + + private void logException(String errorMessage, Exception e) { + Log.e(GnssContainer.TAG + TAG, errorMessage, e); + Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show(); + } + + private void logError(String errorMessage) { + Log.e(GnssContainer.TAG + TAG, errorMessage); + Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show(); + } + + /** + * Implements a {@link FileFilter} to delete files that are not in the + * {@link FileToDeleteFilter#mRetainedFiles}. + */ + private static class FileToDeleteFilter implements FileFilter { + private final List mRetainedFiles; + + public FileToDeleteFilter(File... retainedFiles) { + this.mRetainedFiles = Arrays.asList(retainedFiles); + } + + /** + * Returns {@code true} to delete the file, and {@code false} to keep the file. + * + *

Files are deleted if they are not in the {@link FileToDeleteFilter#mRetainedFiles} list. + */ + @Override + public boolean accept(File pathname) { + if (pathname == null || !pathname.exists()) { + return false; + } + if (mRetainedFiles.contains(pathname)) { + return false; + } + return pathname.length() < MINIMUM_USABLE_FILE_SIZE_BYTES; + } + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssContainer.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssContainer.java new file mode 100644 index 0000000..df0d53a --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssContainer.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.content.Context; +import android.location.GnssMeasurementsEvent; +import android.location.GnssNavigationMessage; +import android.location.GnssStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.OnNmeaMessageListener; +import android.os.Bundle; +import android.os.SystemClock; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A container for GPS related API calls, it binds the {@link LocationManager} with {@link UiLogger} + */ +public class GnssContainer { + + public static final String TAG = "GnssLogger"; + + private static final long LOCATION_RATE_GPS_MS = TimeUnit.SECONDS.toMillis(1L); + private static final long LOCATION_RATE_NETWORK_MS = TimeUnit.SECONDS.toMillis(60L); + + private boolean mLogLocations = true; + private boolean mLogNavigationMessages = true; + private boolean mLogMeasurements = true; + private boolean mLogStatuses = true; + private boolean mLogNmeas = true; + + private final List mLoggers; + + private final LocationManager mLocationManager; + private final LocationListener mLocationListener = + new LocationListener() { + + @Override + public void onProviderEnabled(String provider) { + if (mLogLocations) { + for (GnssListener logger : mLoggers) { + logger.onProviderEnabled(provider); + } + } + } + + @Override + public void onProviderDisabled(String provider) { + if (mLogLocations) { + for (GnssListener logger : mLoggers) { + logger.onProviderDisabled(provider); + } + } + } + + @Override + public void onLocationChanged(Location location) { + if (mLogLocations) { + for (GnssListener logger : mLoggers) { + logger.onLocationChanged(location); + } + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (mLogLocations) { + for (GnssListener logger : mLoggers) { + logger.onLocationStatusChanged(provider, status, extras); + } + } + } + }; + + private final GnssMeasurementsEvent.Callback gnssMeasurementsEventListener = + new GnssMeasurementsEvent.Callback() { + @Override + public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) { + if (mLogMeasurements) { + for (GnssListener logger : mLoggers) { + logger.onGnssMeasurementsReceived(event); + } + } + } + + @Override + public void onStatusChanged(int status) { + if (mLogMeasurements) { + for (GnssListener logger : mLoggers) { + logger.onGnssMeasurementsStatusChanged(status); + } + } + } + }; + + private final GnssNavigationMessage.Callback gnssNavigationMessageListener = + new GnssNavigationMessage.Callback() { + @Override + public void onGnssNavigationMessageReceived(GnssNavigationMessage event) { + if (mLogNavigationMessages) { + for (GnssListener logger : mLoggers) { + logger.onGnssNavigationMessageReceived(event); + } + } + } + + @Override + public void onStatusChanged(int status) { + if (mLogNavigationMessages) { + for (GnssListener logger : mLoggers) { + logger.onGnssNavigationMessageStatusChanged(status); + } + } + } + }; + + private final GnssStatus.Callback gnssStatusListener = + new GnssStatus.Callback() { + @Override + public void onStarted() {} + + @Override + public void onStopped() {} + + @Override + public void onSatelliteStatusChanged(GnssStatus status) { + for (GnssListener logger : mLoggers) { + logger.onGnssStatusChanged(status); + } + } + }; + + private final OnNmeaMessageListener nmeaListener = + new OnNmeaMessageListener() { + @Override + public void onNmeaMessage(String s, long l) { + if (mLogNmeas) { + for (GnssListener logger : mLoggers) { + logger.onNmeaReceived(l, s); + } + } + } + }; + + public GnssContainer(Context context, GnssListener... loggers) { + this.mLoggers = Arrays.asList(loggers); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } + + public LocationManager getLocationManager() { + return mLocationManager; + } + + public void setLogLocations(boolean value) { + mLogLocations = value; + } + + public boolean canLogLocations() { + return mLogLocations; + } + + public void setLogNavigationMessages(boolean value) { + mLogNavigationMessages = value; + } + + public boolean canLogNavigationMessages() { + return mLogNavigationMessages; + } + + public void setLogMeasurements(boolean value) { + mLogMeasurements = value; + } + + public boolean canLogMeasurements() { + return mLogMeasurements; + } + + public void setLogStatuses(boolean value) { + mLogStatuses = value; + } + + public boolean canLogStatuses() { + return mLogStatuses; + } + + public void setLogNmeas(boolean value) { + mLogNmeas = value; + } + + public boolean canLogNmeas() { + return mLogNmeas; + } + + public void registerLocation() { + boolean isGpsProviderEnabled = mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + if (isGpsProviderEnabled) { + mLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + LOCATION_RATE_NETWORK_MS, + 0.0f /* minDistance */, + mLocationListener); + mLocationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + LOCATION_RATE_GPS_MS, + 0.0f /* minDistance */, + mLocationListener); + } + logRegistration("LocationUpdates", isGpsProviderEnabled); + } + + public void unregisterLocation() { + mLocationManager.removeUpdates(mLocationListener); + } + + public void registerMeasurements() { + logRegistration( + "GnssMeasurements", + mLocationManager.registerGnssMeasurementsCallback(gnssMeasurementsEventListener)); + } + + public void unregisterMeasurements() { + mLocationManager.unregisterGnssMeasurementsCallback(gnssMeasurementsEventListener); + } + + public void registerNavigation() { + logRegistration( + "GpsNavigationMessage", + mLocationManager.registerGnssNavigationMessageCallback(gnssNavigationMessageListener)); + } + + public void unregisterNavigation() { + mLocationManager.unregisterGnssNavigationMessageCallback(gnssNavigationMessageListener); + } + + public void registerGnssStatus() { + logRegistration("GnssStatus", mLocationManager.registerGnssStatusCallback(gnssStatusListener)); + } + + public void unregisterGpsStatus() { + mLocationManager.unregisterGnssStatusCallback(gnssStatusListener); + } + + public void registerNmea() { + logRegistration("Nmea", mLocationManager.addNmeaListener(nmeaListener)); + } + + public void unregisterNmea() { + mLocationManager.removeNmeaListener(nmeaListener); + } + + public void registerAll() { + registerLocation(); + registerMeasurements(); + registerNavigation(); + registerGnssStatus(); + registerNmea(); + } + + public void unregisterAll() { + unregisterLocation(); + unregisterMeasurements(); + unregisterNavigation(); + unregisterGpsStatus(); + unregisterNmea(); + } + + private void logRegistration(String listener, boolean result) { + for (GnssListener logger : mLoggers) { + logger.onListenerRegistration(listener, result); + } + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssListener.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssListener.java new file mode 100644 index 0000000..9065c8f --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/GnssListener.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.location.GnssMeasurementsEvent; +import android.location.GnssNavigationMessage; +import android.location.GnssStatus; +import android.location.Location; +import android.location.LocationListener; +import android.location.OnNmeaMessageListener; +import android.os.Bundle; + +/** A class representing an interface for logging GPS information. */ +public interface GnssListener { + + /** @see LocationListener#onProviderEnabled(String) */ + void onProviderEnabled(String provider); + /** @see LocationListener#onProviderDisabled(String) */ + void onProviderDisabled(String provider); + /** @see LocationListener#onLocationChanged(Location) */ + void onLocationChanged(Location location); + /** @see LocationListener#onStatusChanged(String, int, Bundle) */ + void onLocationStatusChanged(String provider, int status, Bundle extras); + /** + * @see android.location.GnssMeasurementsEvent.Callback# + * onGnssMeasurementsReceived(GnssMeasurementsEvent) + */ + void onGnssMeasurementsReceived(GnssMeasurementsEvent event); + /** @see GnssMeasurementsEvent.Callback#onStatusChanged(int) */ + void onGnssMeasurementsStatusChanged(int status); + /** + * @see GnssNavigationMessage.Callback# + * onGnssNavigationMessageReceived(GnssNavigationMessage) + */ + void onGnssNavigationMessageReceived(GnssNavigationMessage event); + /** @see GnssNavigationMessage.Callback#onStatusChanged(int) */ + void onGnssNavigationMessageStatusChanged(int status); + /** @see GnssStatus.Callback#onSatelliteStatusChanged(GnssStatus) */ + void onGnssStatusChanged(GnssStatus gnssStatus); + /** Called when the listener is registered to listen to GNSS events */ + void onListenerRegistration(String listener, boolean result); + /** @see OnNmeaMessageListener#onNmeaMessage(String, long) */ + void onNmeaReceived(long l, String s); +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/HelpDialog.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/HelpDialog.java new file mode 100644 index 0000000..4938a5c --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/HelpDialog.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.MailTo; +import android.net.Uri; +import android.os.Bundle; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +public class HelpDialog extends Dialog { + + private static Context mContext = null; + + public HelpDialog(Context context) { + super(context); + mContext = context; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + setContentView(R.layout.help); + WebView help = (WebView)findViewById(R.id.helpView); + help.setWebViewClient(new WebViewClient(){ + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if(url.startsWith("mailto:")){ + MailTo mt = MailTo.parse(url); + Intent emailIntent = new Intent(Intent.ACTION_SEND); + emailIntent.setType("*/*"); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, "GNSSLogger Feedback"); + emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { mt.getTo()}); + emailIntent.putExtra(Intent.EXTRA_TEXT, ""); + mContext.startActivity(Intent.createChooser(emailIntent, "Send Feedback...")); + return true; + } + else{ + view.loadUrl(url); + } + return true; + } + }); + + String helpText = readRawTextFile(R.raw.help_contents); + help.loadData(helpText, "text/html; charset=utf-8", "utf-8"); + } + + private String readRawTextFile(int id) { + InputStream inputStream = mContext.getResources().openRawResource(id); + InputStreamReader in = new InputStreamReader(inputStream); + BufferedReader buf = new BufferedReader(in); + String line; + StringBuilder text = new StringBuilder(); + try { + while (( line = buf.readLine()) != null) + text.append(line); + } catch (IOException e) { + return null; + } + return text.toString(); + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/LoggerFragment.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/LoggerFragment.java new file mode 100644 index 0000000..e0445a3 --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/LoggerFragment.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +/** The UI fragment that hosts a logging view. */ +public class LoggerFragment extends Fragment { + + private TextView mLogView; + private ScrollView mScrollView; + private FileLogger mFileLogger; + private UiLogger mUiLogger; + + private final UIFragmentComponent mUiComponent = new UIFragmentComponent(); + + public void setUILogger(UiLogger value) { + mUiLogger = value; + } + + public void setFileLogger(FileLogger value) { + mFileLogger = value; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View newView = inflater.inflate(R.layout.fragment_log, container, false /* attachToRoot */); + mLogView = (TextView) newView.findViewById(R.id.log_view); + mScrollView = (ScrollView) newView.findViewById(R.id.log_scroll); + + UiLogger currentUiLogger = mUiLogger; + if (currentUiLogger != null) { + currentUiLogger.setUiFragmentComponent(mUiComponent); + } + FileLogger currentFileLogger = mFileLogger; + if (currentFileLogger != null) { + currentFileLogger.setUiComponent(mUiComponent); + } + + Button start = (Button) newView.findViewById(R.id.start_log); + start.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + mScrollView.fullScroll(View.FOCUS_UP); + } + }); + + Button end = (Button) newView.findViewById(R.id.end_log); + end.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + mScrollView.fullScroll(View.FOCUS_DOWN); + } + }); + + Button clear = (Button) newView.findViewById(R.id.clear_log); + clear.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + mLogView.setText(""); + } + }); + + final Button startLog = (Button) newView.findViewById(R.id.start_logs); + final Button sendFile = (Button) newView.findViewById(R.id.send_file); + + startLog.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + startLog.setEnabled(false); + sendFile.setEnabled(true); + Toast.makeText(getContext(), "Starting log...", Toast.LENGTH_LONG).show(); + mFileLogger.startNewLog(); + } + }); + + sendFile.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + startLog.setEnabled(true); + sendFile.setEnabled(false); + Toast.makeText(getContext(), "Sending file...", Toast.LENGTH_LONG).show(); + mFileLogger.send(); + } + }); + + return newView; + } + + /** + * A facade for UI and Activity related operations that are required for {@link GnssListener}s. + */ + public class UIFragmentComponent { + + private static final int MAX_LENGTH = 12000; + private static final int LOWER_THRESHOLD = (int) (MAX_LENGTH * 0.5); + + public synchronized void logTextFragment(final String tag, final String text, int color) { + final SpannableStringBuilder builder = new SpannableStringBuilder(); + builder.append(tag).append(" | ").append(text).append("\n"); + builder.setSpan( + new ForegroundColorSpan(color), + 0 /* start */, + builder.length(), + SpannableStringBuilder.SPAN_INCLUSIVE_EXCLUSIVE); + + Activity activity = getActivity(); + if (activity == null) { + return; + } + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + mLogView.append(builder); + Editable editable = mLogView.getEditableText(); + int length = editable.length(); + if (length > MAX_LENGTH) { + editable.delete(0, length - LOWER_THRESHOLD); + } + } + }); + } + + public void startActivity(Intent intent) { + getActivity().startActivity(intent); + } + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/MainActivity.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/MainActivity.java new file mode 100644 index 0000000..737d5a7 --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/MainActivity.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.Manifest; +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.design.widget.TabLayout.TabLayoutOnPageChangeListener; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v13.app.FragmentStatePagerAdapter; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; + +import java.util.Locale; + +/** The activity for the application. */ +public class MainActivity extends AppCompatActivity { + + private static final int LOCATION_REQUEST_ID = 1; + private static final String[] REQUIRED_PERMISSIONS = { + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.WRITE_EXTERNAL_STORAGE + }; + private static final int NUMBER_OF_FRAGMENTS = 2; + private static final int FRAGMENT_INDEX_SETTING = 0; + private static final int FRAGMENT_INDEX_LOGGER = 1; + + private GnssContainer mGnssContainer; + private UiLogger mUiLogger; + private FileLogger mFileLogger; + private Fragment[] mFragments; + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + requestPermissionAndSetupFragments(this); + } + + /** + * A {@link FragmentPagerAdapter} that returns a fragment corresponding to one of the + * sections/tabs/pages. + */ + public class ViewPagerAdapter extends FragmentStatePagerAdapter { + + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + switch (position) { + case FRAGMENT_INDEX_SETTING: + return mFragments[FRAGMENT_INDEX_SETTING]; + case FRAGMENT_INDEX_LOGGER: + return mFragments[FRAGMENT_INDEX_LOGGER]; + default: + throw new IllegalArgumentException("Invalid section: " + position); + } + } + + @Override + public int getCount() { + // Show total pages. + return 2; + } + + @Override + public CharSequence getPageTitle(int position) { + Locale locale = Locale.getDefault(); + switch (position) { + case 0: + return getString(R.string.title_settings).toUpperCase(locale); + case 1: + return getString(R.string.title_log).toUpperCase(locale); + default: + return super.getPageTitle(position); + } + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String permissions[], int[] grantResults) { + if (requestCode == LOCATION_REQUEST_ID) { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + setupFragments(); + } + } + } + + private void setupFragments() { + mUiLogger = new UiLogger(); + mFileLogger = new FileLogger(getApplicationContext()); + mGnssContainer = new GnssContainer(getApplicationContext(), mUiLogger, mFileLogger); + mFragments = new Fragment[NUMBER_OF_FRAGMENTS]; + SettingsFragment settingsFragment = new SettingsFragment(); + settingsFragment.setGpsContainer(mGnssContainer); + mFragments[FRAGMENT_INDEX_SETTING] = settingsFragment; + + LoggerFragment loggerFragment = new LoggerFragment(); + loggerFragment.setUILogger(mUiLogger); + loggerFragment.setFileLogger(mFileLogger); + mFragments[FRAGMENT_INDEX_LOGGER] = loggerFragment; + + + // The viewpager that will host the section contents. + ViewPager viewPager = (ViewPager) findViewById(R.id.pager); + viewPager.setOffscreenPageLimit(2); + ViewPagerAdapter adapter = new ViewPagerAdapter(getFragmentManager()); + viewPager.setAdapter(adapter); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout); + tabLayout.setTabsFromPagerAdapter(adapter); + + // Set a listener via setOnTabSelectedListener(OnTabSelectedListener) to be notified when any + // tab's selection state has been changed. + tabLayout.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager)); + + // Use a TabLayout.TabLayoutOnPageChangeListener to forward the scroll and selection changes to + // this layout + viewPager.addOnPageChangeListener(new TabLayoutOnPageChangeListener(tabLayout)); + } + + private boolean hasPermissions(Activity activity) { + if (Build.VERSION.SDK_INT < VERSION_CODES.M) { + // Permissions granted at install time. + return true; + } + for (String p : REQUIRED_PERMISSIONS) { + if (ContextCompat.checkSelfPermission(activity, p) != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; + } + + private void requestPermissionAndSetupFragments(final Activity activity) { + if (hasPermissions(activity)) { + setupFragments(); + } else { + ActivityCompat.requestPermissions(activity, REQUIRED_PERMISSIONS, LOCATION_REQUEST_ID); + } + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/SettingsFragment.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/SettingsFragment.java new file mode 100644 index 0000000..33f002e --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/SettingsFragment.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.app.Fragment; +import android.location.LocationManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; +import com.google.android.apps.location.gps.gnsslogger.GnssContainer; +import java.lang.reflect.InvocationTargetException; +import android.widget.Button; + +/** + * The UI fragment showing a set of configurable settings for the client to request GPS data. + */ +public class SettingsFragment extends Fragment { + + public static final String TAG = ":SettingsFragment"; + private GnssContainer mGpsContainer; + private HelpDialog helpDialog; + + public void setGpsContainer(GnssContainer value) { + mGpsContainer = value; + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false /* attachToRoot */); + + final Switch registerLocation = (Switch) view.findViewById(R.id.register_location); + final TextView registerLocationLabel = + (TextView) view.findViewById(R.id.register_location_label); + //set the switch to OFF + registerLocation.setChecked(false); + registerLocationLabel.setText("Switch is OFF"); + registerLocation.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (isChecked) { + mGpsContainer.registerLocation(); + registerLocationLabel.setText("Switch is ON"); + } else { + mGpsContainer.unregisterLocation(); + registerLocationLabel.setText("Switch is OFF"); + } + } + }); + + final Switch registerMeasurements = (Switch) view.findViewById(R.id.register_measurements); + final TextView registerMeasurementsLabel = + (TextView) view.findViewById(R.id.register_measurement_label); + //set the switch to OFF + registerMeasurements.setChecked(false); + registerMeasurementsLabel.setText("Switch is OFF"); + registerMeasurements.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (isChecked) { + mGpsContainer.registerMeasurements(); + registerMeasurementsLabel.setText("Switch is ON"); + } else { + mGpsContainer.unregisterMeasurements(); + registerMeasurementsLabel.setText("Switch is OFF"); + } + } + }); + + final Switch registerNavigation = (Switch) view.findViewById(R.id.register_navigation); + final TextView registerNavigationLabel = + (TextView) view.findViewById(R.id.register_navigation_label); + //set the switch to OFF + registerNavigation.setChecked(false); + registerNavigationLabel.setText("Switch is OFF"); + registerNavigation.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (isChecked) { + mGpsContainer.registerNavigation(); + registerNavigationLabel.setText("Switch is ON"); + } else { + mGpsContainer.unregisterNavigation(); + registerNavigationLabel.setText("Switch is OFF"); + } + } + }); + + final Switch registerGpsStatus = (Switch) view.findViewById(R.id.register_status); + final TextView registerGpsStatusLabel = + (TextView) view.findViewById(R.id.register_status_label); + //set the switch to OFF + registerGpsStatus.setChecked(false); + registerGpsStatusLabel.setText("Switch is OFF"); + registerGpsStatus.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (isChecked) { + mGpsContainer.registerGnssStatus(); + registerGpsStatusLabel.setText("Switch is ON"); + } else { + mGpsContainer.unregisterGpsStatus(); + registerGpsStatusLabel.setText("Switch is OFF"); + } + } + }); + + final Switch registerNmea = (Switch) view.findViewById(R.id.register_nmea); + final TextView registerNmeaLabel = (TextView) view.findViewById(R.id.register_nmea_label); + //set the switch to OFF + registerNmea.setChecked(false); + registerNmeaLabel.setText("Switch is OFF"); + registerNmea.setOnCheckedChangeListener( + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + if (isChecked) { + mGpsContainer.registerNmea(); + registerNmeaLabel.setText("Switch is ON"); + } else { + mGpsContainer.unregisterNmea(); + registerNmeaLabel.setText("Switch is OFF"); + } + } + }); + + Button help = (Button) view.findViewById(R.id.help); + helpDialog = new HelpDialog(getContext()); + helpDialog.setTitle("Help contents"); + helpDialog.create(); + + help.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + helpDialog.show(); + } + }); + + Button exit = (Button) view.findViewById(R.id.exit); + exit.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + getActivity().finishAffinity(); + } + }); + + TextView swInfo = (TextView) view.findViewById(R.id.sw_info); + + java.lang.reflect.Method method; + LocationManager locationManager = mGpsContainer.getLocationManager(); + try { + method = locationManager.getClass().getMethod("getGnssYearOfHardware"); + int hwYear = (int) method.invoke(locationManager); + if (hwYear == 0) { + swInfo.append("HW Year: " + "2015 or older \n"); + } else { + swInfo.append("HW Year: " + hwYear + "\n"); + } + + } catch (NoSuchMethodException e) { + logException("No such method exception: ", e); + return null; + } catch (IllegalAccessException e) { + logException("Illegal Access exception: ", e); + return null; + } catch (InvocationTargetException e) { + logException("Invocation Target Exception: ", e); + return null; + } + + String platfromVersionString = Build.VERSION.RELEASE; + swInfo.append("Platform: " + platfromVersionString + "\n"); + int apiLivelInt = Build.VERSION.SDK_INT; + swInfo.append("Api Level: " + apiLivelInt); + + return view; + } + + private void logException(String errorMessage, Exception e) { + Log.e(GnssContainer.TAG + TAG, errorMessage, e); + Toast.makeText(getContext(), errorMessage, Toast.LENGTH_LONG).show(); + } +} diff --git a/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/UiLogger.java b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/UiLogger.java new file mode 100644 index 0000000..3fddf42 --- /dev/null +++ b/GNSSLogger/app/src/main/java/com/google/android/apps/location/gps/gnsslogger/UiLogger.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2016 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 com.google.android.apps.location.gps.gnsslogger; + +import android.graphics.Color; +import android.location.GnssMeasurementsEvent; +import android.location.GnssNavigationMessage; +import android.location.GnssStatus; +import android.location.Location; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; +import com.google.android.apps.location.gps.gnsslogger.LoggerFragment.UIFragmentComponent; +import java.util.concurrent.TimeUnit; + +/** + * A class representing a UI logger for the application. Its responsibility is show information in + * the UI. + */ +public class UiLogger implements GnssListener { + + private static final long EARTH_RADIUS_METERS = 6371000; + private static final int USED_COLOR = Color.rgb(0x4a, 0x5f, 0x70); + + public UiLogger() {} + + private UIFragmentComponent mUiFragmentComponent; + + public synchronized UIFragmentComponent getUiFragmentComponent() { + return mUiFragmentComponent; + } + + public synchronized void setUiFragmentComponent(UIFragmentComponent value) { + mUiFragmentComponent = value; + } + + @Override + public void onProviderEnabled(String provider) { + logLocationEvent("onProviderEnabled: " + provider); + } + + @Override + public void onProviderDisabled(String provider) { + logLocationEvent("onProviderDisabled: " + provider); + } + + @Override + public void onLocationChanged(Location location) { + logLocationEvent("onLocationChanged: " + location); + } + + @Override + public void onLocationStatusChanged(String provider, int status, Bundle extras) { + String message = + String.format( + "onStatusChanged: provider=%s, status=%s, extras=%s", + provider, locationStatusToString(status), extras); + logLocationEvent(message); + } + + @Override + public void onGnssMeasurementsReceived(GnssMeasurementsEvent event) { + logMeasurementEvent("onGnsssMeasurementsReceived: " + event); + } + + @Override + public void onGnssMeasurementsStatusChanged(int status) { + logMeasurementEvent("onStatusChanged: " + gnssMeasurementsStatusToString(status)); + } + + @Override + public void onGnssNavigationMessageReceived(GnssNavigationMessage event) { + logNavigationMessageEvent("onGnssNavigationMessageReceived: " + event); + } + + @Override + public void onGnssNavigationMessageStatusChanged(int status) { + logNavigationMessageEvent("onStatusChanged: " + getGnssNavigationMessageStatus(status)); + } + + @Override + public void onGnssStatusChanged(GnssStatus gnssStatus) { + logStatusEvent("onGnssStatusChanged: " + gnssStatusToString(gnssStatus)); + } + + @Override + public void onNmeaReceived(long timestamp, String s) { + logNmeaEvent(String.format("onNmeaReceived: timestamp=%d, %s", timestamp, s)); + } + + @Override + public void onListenerRegistration(String listener, boolean result) { + logEvent("Registration", String.format("add%sListener: %b", listener, result), USED_COLOR); + } + + private void logMeasurementEvent(String event) { + logEvent("Measurement", event, USED_COLOR); + } + + private void logNavigationMessageEvent(String event) { + logEvent("NavigationMsg", event, USED_COLOR); + } + + private void logStatusEvent(String event) { + logEvent("Status", event, USED_COLOR); + } + + private void logNmeaEvent(String event) { + logEvent("Nmea", event, USED_COLOR); + } + + private void logEvent(String tag, String message, int color) { + String composedTag = GnssContainer.TAG + tag; + Log.d(composedTag, message); + logText(tag, message, color); + } + + private void logText(String tag, String text, int color) { + UIFragmentComponent component = getUiFragmentComponent(); + if (component != null) { + component.logTextFragment(tag, text, color); + } + } + + private String locationStatusToString(int status) { + switch (status) { + case LocationProvider.AVAILABLE: + return "AVAILABLE"; + case LocationProvider.OUT_OF_SERVICE: + return "OUT_OF_SERVICE"; + case LocationProvider.TEMPORARILY_UNAVAILABLE: + return "TEMPORARILY_UNAVAILABLE"; + default: + return ""; + } + } + + private String gnssMeasurementsStatusToString(int status) { + switch (status) { + case GnssMeasurementsEvent.Callback.STATUS_NOT_SUPPORTED: + return "NOT_SUPPORTED"; + case GnssMeasurementsEvent.Callback.STATUS_READY: + return "READY"; + case GnssMeasurementsEvent.Callback.STATUS_LOCATION_DISABLED: + return "GNSS_LOCATION_DISABLED"; + default: + return ""; + } + } + + private String getGnssNavigationMessageStatus(int status) { + switch (status) { + case GnssNavigationMessage.STATUS_UNKNOWN: + return "Status Unknown"; + case GnssNavigationMessage.STATUS_PARITY_PASSED: + return "READY"; + case GnssNavigationMessage.STATUS_PARITY_REBUILT: + return "Status Parity Rebuilt"; + default: + return ""; + } + } + + private String gnssStatusToString(GnssStatus gnssStatus) { + + StringBuilder builder = new StringBuilder("SATELLITE_STATUS | [Satellites:\n"); + for (int i = 0; i < gnssStatus.getSatelliteCount(); i++) { + builder + .append("Constellation = ") + .append(getConstellationName(gnssStatus.getConstellationType(i))) + .append(", "); + builder.append("Svid = ").append(gnssStatus.getSvid(i)).append(", "); + builder.append("Cn0DbHz = ").append(gnssStatus.getCn0DbHz(i)).append(", "); + builder.append("Elevation = ").append(gnssStatus.getElevationDegrees(i)).append(", "); + builder.append("Azimuth = ").append(gnssStatus.getAzimuthDegrees(i)).append(", "); + builder.append("hasEphemeris = ").append(gnssStatus.hasEphemerisData(i)).append(", "); + builder.append("hasAlmanac = ").append(gnssStatus.hasAlmanacData(i)).append(", "); + builder.append("usedInFix = ").append(gnssStatus.usedInFix(i)).append("\n"); + } + builder.append("]"); + return builder.toString(); + } + + private void logLocationEvent(String event) { + logEvent("Location", event, USED_COLOR); + } + + private String getConstellationName(int id) { + switch (id) { + case 1: + return "GPS"; + case 2: + return "SBAS"; + case 3: + return "GLONASS"; + case 4: + return "QZSS"; + case 5: + return "BEIDOU"; + case 6: + return "GALILEO"; + default: + return "UNKNOWN"; + } + } +} diff --git a/GNSSLogger/app/src/main/res/drawable/ic_launcher.png b/GNSSLogger/app/src/main/res/drawable/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a1019b2c56530547c88f9f83b3e70d74f0b21b11 GIT binary patch literal 2162 zcmV-&2#xoNP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<8AY({UO#lFTB>(_`g8%^e z{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ=)Ja4^RCwC#oZU~BWf;I8f)`LU1w_i% z!lBjtIH|1zB8zEj>+I*2HmgZ(1hu3!=Rq-8tTI|A(>Rc*R)0ZTLLX{lni}8+n|_J;XDh%;8actZ;J97JI8FoQvk71xuhHk| zxLtawVcyIG6!8&PJUc%vm)S+(%mFOqW3Kylep;^6totRR1E}P;z|K$087krh$fuRb z(A-axCdH@Z#RagAGod;^EvH!<2f%Y&)w(f34|}L%1FJ|eumJswzlly#np0wPlmM?PO%Jn+Wv&msBrVF{F-3J$0M9GjAEkkXo}-+5 zX<}4?uqg_FXSgLb_Z?-v1HKh>N_F0(Dr|r?T$32O!8WXbQY(EhS6LM%Kmn(ufk0(o z_bn`*k_ICKQ|EIy0Q)5d26-eD_bn_|^M}MjTUY>9+!mCdV|i%qTUacYAWTvj20%W) z3#u=L`1J4m3RfgCQf_d79fI0%o(SFjsdqszk894h!}g0|1cEMd659>EOhDbZ}F8Jl&w3Qis_O zGiIB0fjs{JHG&iGYt1KUU_={l{>?k&Xmzv*bE^FV^a?9dw+u=o#T1!zvqYn%kiTr> zkNE_kOxXXMHHO!6&UNZ?mem?f9|-3`(jUMMVf!r6to%PcWnY6VRq84gw$r!#0d(8c ze5*8Bz<}ojvY!H_z5}+ghkO8#D@5y?l?Ffbl>@gZ^=-9{93kNy;7K9PSgbI&Op%y4 z7a7ZyLt%tqL9=G62z$ziHBIfI48QeNR_P0wk>O@D6ME+^@y_20J3 zVzS7h_`kNx0qF}HYun}20yq&# z9QGG+uljX%e1h`-y{Voqp`svbY3*dY`$I48&##3XAc7~#{vEs0H8xtU_zZ1+W-In07*qoM6N<$g6aj>E&u=k literal 0 HcmV?d00001 diff --git a/GNSSLogger/app/src/main/res/layout/activity_main.xml b/GNSSLogger/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b95955c --- /dev/null +++ b/GNSSLogger/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/GNSSLogger/app/src/main/res/layout/fragment_log.xml b/GNSSLogger/app/src/main/res/layout/fragment_log.xml new file mode 100644 index 0000000..16b47b4 --- /dev/null +++ b/GNSSLogger/app/src/main/res/layout/fragment_log.xml @@ -0,0 +1,67 @@ + + + + + +