├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── ca │ │ └── klostermann │ │ └── philip │ │ └── location_tracker │ │ ├── LocationPost.java │ │ ├── LocationReceiver.java │ │ ├── LogMessage.java │ │ ├── LoginActivity.java │ │ ├── LoginTaskListener.java │ │ ├── MainActivity.java │ │ ├── Prefs.java │ │ ├── SignupTaskListener.java │ │ ├── TrackerService.java │ │ ├── UserLoginTask.java │ │ └── UserSignupTask.java │ ├── res │ ├── layout │ │ ├── activity_login.xml │ │ └── main.xml │ ├── menu │ │ └── main.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── service_icon.png │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── service_icon.png │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── service_icon.png │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── service_icon.png │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── service_icon.png │ └── values │ │ ├── arrays.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── strings_activity_login.xml │ │ └── styles.xml │ └── service_icon-web.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 2 | 3 | *.iml 4 | 5 | ## Directory-based project format: 6 | .idea/ 7 | # if you remove the above rule, at least ignore the following: 8 | 9 | # User-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | 14 | # Sensitive or high-churn files: 15 | # .idea/dataSources.ids 16 | # .idea/dataSources.xml 17 | # .idea/sqlDataSources.xml 18 | # .idea/dynamic.xml 19 | # .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | # .idea/gradle.xml 23 | # .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | # .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.ipr 30 | *.iws 31 | 32 | ## Plugin-specific files: 33 | 34 | # IntelliJ 35 | /out/ 36 | 37 | # mpeltonen/sbt-idea plugin 38 | .idea_modules/ 39 | 40 | # JIRA plugin 41 | atlassian-ide-plugin.xml 42 | 43 | # Crashlytics plugin (for Android Studio and IntelliJ) 44 | com_crashlytics_export_strings.xml 45 | crashlytics.properties 46 | crashlytics-build.properties 47 | 48 | # Built application files 49 | *.apk 50 | *.ap_ 51 | 52 | # Files for the Dalvik VM 53 | *.dex 54 | 55 | # Java class files 56 | *.class 57 | 58 | # Generated files 59 | bin/ 60 | gen/ 61 | 62 | # Gradle files 63 | .gradle/ 64 | build/ 65 | 66 | # Local configuration file (sdk path, etc) 67 | local.properties 68 | 69 | # Proguard folder generated by Eclipse 70 | proguard/ 71 | 72 | # Log Files 73 | *.log 74 | 75 | # Android Studio Navigation editor temp files 76 | .navigation/ 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 joshua stein 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. The name of the author may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Android Location Tracker 2 | An Android location tracker forked from [jcs/triptracker](https://github.com/jcs/triptracker) and modified to use [Firebase](https://www.firebase.com/) as a backend. This fork also uses Google Play Services' fused location API to track the location using GPS, wifi and cell networks. 3 | 4 | The formidable original work: Copyright (c) 2012 joshua stein 5 | 6 | ### Location Tracker 7 | The App sends location updates at a specified time interval to a given Firebase URL. Basic Email/Password authentication is used to allow for multiple users. 8 | 9 | **This is still work-in-progress.** 10 | 11 | 12 | ![screenshot](https://raw.githubusercontent.com/philbot9/android-location-tracker/master/screenshot.png) 13 | 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "ca.klostermann.philip.location_tracker" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | packagingOptions { 21 | exclude 'META-INF/LICENSE' 22 | exclude 'META-INF/LICENSE-FIREBASE.txt' 23 | exclude 'META-INF/NOTICE' 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(include: ['*.jar'], dir: 'libs') 29 | compile 'com.android.support:appcompat-v7:22.2.1' 30 | compile 'com.google.android.gms:play-services:7.5.0' 31 | compile 'com.firebase:firebase-client-android:2.3.1' 32 | } 33 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/phil/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philbot9/android-location-tracker/aa0ff5807d1d96c83bef0436626fde3ec4bd7c1b/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/LocationPost.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.location.Location; 4 | 5 | import java.util.Calendar; 6 | import java.util.GregorianCalendar; 7 | 8 | public class LocationPost { 9 | private long time; 10 | private double latitude; 11 | private double longitude; 12 | private float speed; 13 | private double altitude; 14 | private float accuracy; 15 | 16 | public LocationPost() {} 17 | 18 | public LocationPost(Location location) { 19 | this.time = location.getTime(); 20 | this.latitude = location.getLatitude(); 21 | this.longitude = location.getLongitude(); 22 | this.speed = location.getSpeed(); 23 | this.altitude = location.getAltitude(); 24 | this.accuracy = location.getAccuracy(); 25 | } 26 | 27 | public static long getDateKey(long time) { 28 | //Use timestamp of today's date at midnight as key 29 | GregorianCalendar d = new GregorianCalendar(); 30 | d.setTimeInMillis(time); 31 | d.set(Calendar.HOUR, 0); 32 | d.set(Calendar.HOUR_OF_DAY, 0); 33 | d.set(Calendar.MINUTE, 0); 34 | d.set(Calendar.SECOND, 0); 35 | d.set(Calendar.MILLISECOND, 0); 36 | return d.getTimeInMillis(); 37 | } 38 | 39 | public long getTime() { 40 | return time; 41 | } 42 | 43 | public void setTime(long time) { 44 | this.time = time; 45 | } 46 | 47 | public double getLatitude() { 48 | return latitude; 49 | } 50 | 51 | public void setLatitude(double latitude) { 52 | this.latitude = latitude; 53 | } 54 | 55 | public double getLongitude() { 56 | return longitude; 57 | } 58 | 59 | public void setLongitude(double longitude) { 60 | this.longitude = longitude; 61 | } 62 | 63 | public float getSpeed() { 64 | return speed; 65 | } 66 | 67 | public void setSpeed(float speed) { 68 | this.speed = speed; 69 | } 70 | 71 | public double getAltitude() { 72 | return altitude; 73 | } 74 | 75 | public void setAltitude(double altitude) { 76 | this.altitude = altitude; 77 | } 78 | 79 | public float getAccuracy() { 80 | return accuracy; 81 | } 82 | 83 | public void setAccuracy(float accuracy) { 84 | this.accuracy = accuracy; 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | return "LocationPost{" + 90 | "time=" + time + 91 | ", latitude=" + latitude + 92 | ", longitude=" + longitude + 93 | ", speed=" + speed + 94 | ", altitude=" + altitude + 95 | ", accuracy=" + accuracy + 96 | '}'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/LocationReceiver.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.location.Location; 7 | 8 | import com.google.android.gms.location.FusedLocationProviderApi; 9 | 10 | public class LocationReceiver extends BroadcastReceiver{ 11 | @Override 12 | public void onReceive(Context context, Intent intent) { 13 | Location location = (Location) intent.getExtras().get( 14 | FusedLocationProviderApi.KEY_LOCATION_CHANGED); 15 | 16 | if(TrackerService.isRunning()) { 17 | TrackerService.service.sendLocation(location); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/LogMessage.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import java.util.Date; 4 | 5 | public class LogMessage { 6 | public final Date date; 7 | public final String message; 8 | 9 | public LogMessage(Date date, String message) { 10 | this.date = date; 11 | this.message = message; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.annotation.TargetApi; 6 | 7 | import android.app.Activity; 8 | import android.app.AlertDialog; 9 | import android.content.DialogInterface; 10 | import android.content.Intent; 11 | 12 | import android.os.Build; 13 | import android.os.Bundle; 14 | import android.text.TextUtils; 15 | import android.util.Log; 16 | import android.view.View; 17 | import android.view.View.OnClickListener; 18 | import android.view.inputmethod.InputMethodManager; 19 | import android.widget.AutoCompleteTextView; 20 | import android.widget.Button; 21 | import android.widget.EditText; 22 | import android.widget.Toast; 23 | 24 | import com.firebase.client.AuthData; 25 | import com.firebase.client.FirebaseError; 26 | 27 | import java.util.Map; 28 | 29 | public class LoginActivity extends Activity implements 30 | LoginTaskListener, SignupTaskListener { 31 | 32 | private static final String TAG = "LocationTracker/Login"; 33 | 34 | private AutoCompleteTextView mEmailView; 35 | private EditText mPasswordView; 36 | private View mProgressView; 37 | private View mLoginFormView; 38 | 39 | @Override 40 | protected void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.activity_login); 43 | setupActionBar(); 44 | 45 | // Set up the login form. 46 | mEmailView = (AutoCompleteTextView) findViewById(R.id.email); 47 | 48 | mPasswordView = (EditText) findViewById(R.id.password); 49 | 50 | Button mEmailSignInButton = (Button) findViewById(R.id.email_sign_in_button); 51 | mEmailSignInButton.setOnClickListener(new OnClickListener() { 52 | @Override 53 | public void onClick(View view) { 54 | InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); 55 | imm.hideSoftInputFromWindow(mPasswordView.getWindowToken(), 0); 56 | imm.hideSoftInputFromWindow(mEmailView.getWindowToken(), 0); 57 | attemptLogin(); 58 | } 59 | }); 60 | 61 | Button mEmailSignUpButton = (Button) findViewById(R.id.email_sign_up_button); 62 | mEmailSignUpButton.setOnClickListener(new OnClickListener() { 63 | @Override 64 | public void onClick(View view) { 65 | InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); 66 | imm.hideSoftInputFromWindow(mPasswordView.getWindowToken(), 0); 67 | imm.hideSoftInputFromWindow(mEmailView.getWindowToken(), 0); 68 | attemptSignup(); 69 | } 70 | }); 71 | 72 | mLoginFormView = findViewById(R.id.login_form); 73 | mProgressView = findViewById(R.id.login_progress); 74 | 75 | //Load credentials from Prefs (if any) 76 | String storedEmail = Prefs.getUserEmail(this); 77 | if(storedEmail != null) { 78 | mEmailView.setText(storedEmail); 79 | } 80 | String storedPassword = Prefs.getUserPassword(this); 81 | if(storedEmail != null) { 82 | mPasswordView.setText(storedPassword); 83 | } 84 | } 85 | 86 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 87 | private void setupActionBar() { 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 89 | // Show the Up button in the action bar. 90 | if(getActionBar() != null) { 91 | getActionBar().setDisplayHomeAsUpEnabled(true); 92 | } 93 | } 94 | } 95 | 96 | 97 | public void attemptLogin() { 98 | if(!validateForm()) { 99 | return; 100 | } 101 | 102 | // Store values at the time of the login attempt. 103 | String email = mEmailView.getText().toString(); 104 | String password = mPasswordView.getText().toString(); 105 | 106 | // Show a progress spinner, and kick off a background task to 107 | // perform the user login attempt. 108 | showProgress(true); 109 | UserLoginTask loginTask = new UserLoginTask( 110 | this, Prefs.getEndpoint(this), email, password); 111 | loginTask.execute(); 112 | } 113 | 114 | @Override 115 | public void onLoginSuccess(AuthData authData) { 116 | showProgress(false); 117 | Intent returnIntent = new Intent(); 118 | returnIntent.putExtra("uid", authData.getUid()); 119 | returnIntent.putExtra("email", mEmailView.getText().toString()); 120 | returnIntent.putExtra("password", mPasswordView.getText().toString()); 121 | setResult(RESULT_OK, returnIntent); 122 | finish(); 123 | } 124 | 125 | @Override 126 | public void onLoginFailure(FirebaseError firebaseError) { 127 | if(firebaseError == null) { 128 | firebaseError = new FirebaseError( 129 | FirebaseError.OPERATION_FAILED, "Firebase Auth completely failed."); 130 | } 131 | Log.d(TAG, "authentication failed: " + firebaseError.getMessage()); 132 | showProgress(false); 133 | handleFirebaseError(firebaseError); 134 | } 135 | 136 | 137 | public void attemptSignup() { 138 | if(!validateForm()) { 139 | return; 140 | } 141 | 142 | // Store values at the time of the signup attempt. 143 | String email = mEmailView.getText().toString(); 144 | String password = mPasswordView.getText().toString(); 145 | 146 | showProgress(true); 147 | UserSignupTask signupTask = new UserSignupTask( 148 | this, Prefs.getEndpoint(this), email, password); 149 | signupTask.execute(); 150 | } 151 | 152 | @Override 153 | public void onSignupSuccess(Map result) { 154 | Log.d(TAG, "Successfully created user account with uid: " + result.get("uid")); 155 | Toast.makeText(LoginActivity.this, "New Account created.", Toast.LENGTH_LONG).show(); 156 | attemptLogin(); 157 | } 158 | 159 | @Override 160 | public void onSignupFailure(FirebaseError firebaseError) { 161 | if(firebaseError == null) { 162 | firebaseError = new FirebaseError( 163 | FirebaseError.OPERATION_FAILED, "Firebase Signup completely failed."); 164 | } 165 | Log.d(TAG, "Creating new Account failed: " + firebaseError.toString()); 166 | showProgress(false); 167 | handleFirebaseError(firebaseError); 168 | } 169 | 170 | 171 | public boolean validateForm() { 172 | // Reset errors. 173 | mEmailView.setError(null); 174 | mPasswordView.setError(null); 175 | 176 | // Store values at the time of the login attempt. 177 | String email = mEmailView.getText().toString(); 178 | String password = mPasswordView.getText().toString(); 179 | 180 | boolean valid = true; 181 | View focusView = null; 182 | 183 | // Check for a valid password, if the user entered one. 184 | if (!TextUtils.isEmpty(password) && !isPasswordValid(password)) { 185 | mPasswordView.setError(getString(R.string.error_invalid_password)); 186 | focusView = mPasswordView; 187 | valid = false; 188 | } 189 | 190 | // Check for a valid email address. 191 | if (TextUtils.isEmpty(email)) { 192 | mEmailView.setError(getString(R.string.error_field_required)); 193 | focusView = mEmailView; 194 | valid = false; 195 | } else if (!isEmailValid(email)) { 196 | mEmailView.setError(getString(R.string.error_invalid_email)); 197 | focusView = mEmailView; 198 | valid = false; 199 | } 200 | if (!valid) { 201 | focusView.requestFocus(); 202 | } 203 | 204 | return valid; 205 | } 206 | 207 | private boolean isEmailValid(String email) { 208 | return email.contains("@"); 209 | } 210 | 211 | private boolean isPasswordValid(String password) { 212 | return password.length() > 4; 213 | } 214 | 215 | private void handleFirebaseError(FirebaseError error) { 216 | View focusView = null; 217 | 218 | switch (error.getCode()) { 219 | case FirebaseError.EMAIL_TAKEN: 220 | mEmailView.setError(getString(R.string.error_email_taken)); 221 | focusView = mEmailView; 222 | break; 223 | case FirebaseError.INVALID_EMAIL: 224 | case FirebaseError.USER_DOES_NOT_EXIST: 225 | mEmailView.setError(getString(R.string.error_invalid_email)); 226 | focusView = mEmailView; 227 | break; 228 | case FirebaseError.INVALID_PASSWORD: 229 | mPasswordView.setError(getString(R.string.error_incorrect_password)); 230 | focusView = mPasswordView; 231 | break; 232 | default: 233 | AlertDialog alertDialog = new AlertDialog.Builder(this).create(); 234 | alertDialog.setTitle("Authentication Error"); 235 | alertDialog.setMessage(error.getMessage()); 236 | alertDialog.setIcon(android.R.drawable.ic_dialog_alert); 237 | alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", 238 | new DialogInterface.OnClickListener() { 239 | public void onClick(DialogInterface dialog, int which) { 240 | dialog.dismiss(); 241 | } 242 | }); 243 | alertDialog.show(); 244 | } 245 | 246 | if (focusView != null) { 247 | focusView.requestFocus(); 248 | } 249 | } 250 | 251 | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) 252 | private void showProgress(final boolean show) { 253 | // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow 254 | // for very easy animations. If available, use these APIs to fade-in 255 | // the progress spinner. 256 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 257 | int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); 258 | 259 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 260 | mLoginFormView.animate().setDuration(shortAnimTime).alpha( 261 | show ? 0 : 1).setListener(new AnimatorListenerAdapter() { 262 | @Override 263 | public void onAnimationEnd(Animator animation) { 264 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 265 | } 266 | }); 267 | 268 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 269 | mProgressView.animate().setDuration(shortAnimTime).alpha( 270 | show ? 1 : 0).setListener(new AnimatorListenerAdapter() { 271 | @Override 272 | public void onAnimationEnd(Animator animation) { 273 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 274 | } 275 | }); 276 | } else { 277 | // The ViewPropertyAnimator APIs are not available, so simply show 278 | // and hide the relevant UI components. 279 | mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); 280 | mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); 281 | } 282 | } 283 | } 284 | 285 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/LoginTaskListener.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import com.firebase.client.AuthData; 4 | import com.firebase.client.FirebaseError; 5 | 6 | public interface LoginTaskListener { 7 | void onLoginSuccess(AuthData authData); 8 | void onLoginFailure(FirebaseError firebaseError); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/MainActivity.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.os.Bundle; 9 | import android.os.IBinder; 10 | import android.os.Handler; 11 | import android.os.Message; 12 | import android.os.Messenger; 13 | import android.os.RemoteException; 14 | import android.util.Log; 15 | import android.view.Menu; 16 | import android.view.MenuInflater; 17 | import android.view.MenuItem; 18 | import android.view.View; 19 | import android.view.View.OnClickListener; 20 | import android.view.View.OnFocusChangeListener; 21 | import android.view.inputmethod.InputMethodManager; 22 | import android.widget.AdapterView; 23 | import android.widget.ArrayAdapter; 24 | import android.widget.CheckBox; 25 | import android.widget.EditText; 26 | import android.widget.Spinner; 27 | import android.widget.ScrollView; 28 | import android.widget.TextView; 29 | 30 | import com.firebase.client.Firebase; 31 | 32 | import java.net.URL; 33 | import java.text.SimpleDateFormat; 34 | import java.util.ArrayList; 35 | import java.util.Date; 36 | import java.util.Locale; 37 | 38 | public class MainActivity extends Activity { 39 | private static final String TAG = "TripTracker/Main"; 40 | static final int LOGIN_REQUEST = 1; 41 | 42 | private CheckBox enabler; 43 | 44 | Messenger mService = null; 45 | boolean mIsBound; 46 | final Messenger mMessenger = new Messenger(new IncomingHandler()); 47 | 48 | @Override 49 | public void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | 52 | setContentView(R.layout.main); 53 | 54 | Firebase.setAndroidContext(this); 55 | 56 | /* load saved endpoint */ 57 | final EditText endpoint = (EditText)findViewById(R.id.main_endpoint); 58 | String savedendpoint = Prefs.getEndpoint(this); 59 | if (savedendpoint != null) 60 | endpoint.setText(savedendpoint); 61 | 62 | /* save endpoint when textbox loses focus if the url is valid */ 63 | endpoint.setOnFocusChangeListener(new OnFocusChangeListener() { 64 | @Override 65 | public void onFocusChange(View v, boolean hasFocus) { 66 | if (hasFocus) 67 | return; 68 | 69 | String e = endpoint.getText().toString().trim(); 70 | 71 | if(!e.matches("(?i)^https?:\\/\\/.+")) { 72 | e = "https://" + e; 73 | endpoint.setText(e); 74 | } 75 | 76 | try { 77 | URL u = new URL(e); 78 | if(!u.getHost().matches("(?i)^.+?\\.firebaseio.com$")) { 79 | throw new Exception("Not a Firebase URL"); 80 | } 81 | 82 | e = u.toURI().toString(); 83 | 84 | Prefs.putEndpoint(MainActivity.this, e); 85 | Prefs.putUserId(MainActivity.this, null); 86 | } 87 | catch (Exception ex) { 88 | endpoint.setError("Invalid Firebase URL"); 89 | Prefs.putEndpoint(MainActivity.this, null); 90 | return; 91 | } 92 | 93 | /* hide the keyboard */ 94 | InputMethodManager imm = (InputMethodManager) 95 | getSystemService(Activity.INPUT_METHOD_SERVICE); 96 | imm.hideSoftInputFromWindow(endpoint.getWindowToken(), 0); 97 | } 98 | }); 99 | 100 | /* add minute values to update frequency drop down */ 101 | final Spinner updatefreq = (Spinner)findViewById(R.id.main_updatefreq); 102 | ArrayAdapter adapter = ArrayAdapter.createFromResource( 103 | this, R.array.main_updatefreq_entries, 104 | android.R.layout.simple_spinner_item); 105 | adapter.setDropDownViewResource( 106 | android.R.layout.simple_spinner_dropdown_item); 107 | updatefreq.setAdapter(adapter); 108 | 109 | /* load saved frequency */ 110 | String savedfreq = Prefs.getUpdateFreq(this); 111 | if (savedfreq != null) 112 | updatefreq.setSelection(adapter.getPosition(savedfreq)); 113 | 114 | /* store frequency when we change it */ 115 | updatefreq.setOnItemSelectedListener( 116 | new AdapterView.OnItemSelectedListener() { 117 | public void onItemSelected(AdapterView parent, View view, 118 | int pos, long id) { 119 | Prefs.putUpdateFreq(MainActivity.this, 120 | updatefreq.getSelectedItem().toString()); 121 | 122 | /* if the service is already running, restart it */ 123 | if (Prefs.getEndpoint(MainActivity.this) == null && 124 | TrackerService.isRunning()) { 125 | doUnbindService(); 126 | stopService(new Intent(MainActivity.this, 127 | TrackerService.class)); 128 | startService(new Intent(MainActivity.this, 129 | TrackerService.class)); 130 | doBindService(); 131 | } 132 | } 133 | 134 | public void onNothingSelected(AdapterView parent) { 135 | } 136 | }); 137 | 138 | enabler = (CheckBox)findViewById(R.id.main_enabler); 139 | 140 | enabler.setOnClickListener(new OnClickListener() { 141 | @Override 142 | public void onClick(View v) { 143 | /* divert focus away from the endpoint text field so it can 144 | * validate and hide the keyboard */ 145 | findViewById(R.id.main_layout).requestFocus(); 146 | 147 | if (enabler.isChecked()) { 148 | if (Prefs.getEndpoint(MainActivity.this) == null) { 149 | enabler.setChecked(false); 150 | return; 151 | } 152 | 153 | if (Prefs.getUserId(MainActivity.this) == null) { 154 | enabler.setChecked(false); 155 | Intent authenticateIntent = new Intent(MainActivity.this, LoginActivity.class); 156 | startActivityForResult(authenticateIntent, LOGIN_REQUEST); 157 | return; 158 | } 159 | 160 | startService(new Intent(MainActivity.this, 161 | TrackerService.class)); 162 | doBindService(); 163 | } else { 164 | stopService(new Intent(MainActivity.this, 165 | TrackerService.class)); 166 | doUnbindService(); 167 | } 168 | 169 | Prefs.putEnabled(MainActivity.this, enabler.isChecked()); 170 | } 171 | }); 172 | 173 | 174 | 175 | 176 | /* if the service is already running, bind and we'll get back its 177 | * recent log ring buffer */ 178 | if (TrackerService.isRunning()) { 179 | enabler.setChecked(true); 180 | doBindService(); 181 | } 182 | else if (Prefs.getEnabled(this) && Prefs.getUserId(this) != null) { 183 | enabler.performClick(); 184 | } 185 | } 186 | 187 | @Override 188 | public boolean onCreateOptionsMenu(Menu menu) { 189 | MenuInflater menuInflater = getMenuInflater(); 190 | menuInflater.inflate(R.menu.main, menu); 191 | 192 | return super.onCreateOptionsMenu(menu); 193 | } 194 | 195 | @Override 196 | public boolean onOptionsItemSelected(MenuItem item) { 197 | switch (item.getItemId()) { 198 | case R.id.menu_exit: 199 | stopService(new Intent(MainActivity.this, TrackerService.class)); 200 | doUnbindService(); 201 | finish(); 202 | return true; 203 | 204 | case R.id.menu_signin: 205 | Intent i = new Intent(this, LoginActivity.class); 206 | startActivityForResult(i, LOGIN_REQUEST); 207 | return true; 208 | } 209 | 210 | return super.onOptionsItemSelected(item); 211 | } 212 | 213 | @Override 214 | protected void onDestroy() { 215 | super.onDestroy(); 216 | 217 | try { 218 | doUnbindService(); 219 | } 220 | catch (Throwable e) { 221 | Log.e(TAG, e.getMessage()); 222 | } 223 | } 224 | 225 | @Override 226 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 227 | if (requestCode == LOGIN_REQUEST) { 228 | if(resultCode == RESULT_OK){ 229 | Prefs.putUserId(MainActivity.this, data.getStringExtra("uid")); 230 | Prefs.putUserEmail(MainActivity.this, data.getStringExtra("email")); 231 | Prefs.putUserPassword(MainActivity.this, data.getStringExtra("password")); 232 | enabler.performClick(); 233 | } else if (resultCode == RESULT_CANCELED) { 234 | if(!TrackerService.isRunning()) { 235 | enabler.setChecked(false); 236 | } 237 | } 238 | } 239 | } 240 | 241 | public void logText(String text) { 242 | logText(text, new Date()); 243 | } 244 | 245 | public void logText(String text, Date date) { 246 | /* DateFormat.SHORT doesn't honor 24 hour time :( */ 247 | String now = (new SimpleDateFormat("HH:mm:ss", Locale.US)).format(date); 248 | 249 | TextView log = (TextView)findViewById(R.id.main_log); 250 | log.append("[" + now + "] " + text + "\n"); 251 | 252 | /* we have to scroll asynchronously because the view isn't updated from 253 | * our append() yet, so scrolling to its bottom will not actually reach 254 | * the bottom */ 255 | final ScrollView scroller = (ScrollView)findViewById( 256 | R.id.main_log_scroller); 257 | scroller.post(new Runnable() { 258 | @Override 259 | public void run() { 260 | scroller.fullScroll(View.FOCUS_DOWN); 261 | } 262 | }); 263 | } 264 | 265 | class IncomingHandler extends Handler { 266 | @Override 267 | public void handleMessage(Message msg) { 268 | switch (msg.what) { 269 | case TrackerService.MSG_LOG: 270 | logText(msg.getData().getString("log")); 271 | break; 272 | 273 | case TrackerService.MSG_LOG_RING: 274 | TextView log = (TextView)findViewById(R.id.main_log); 275 | log.setText(""); 276 | 277 | ArrayList logs = (ArrayList)msg.obj; 278 | 279 | for (int i = 0; i < logs.size(); i++) { 280 | LogMessage l = logs.get(i); 281 | logText(l.message, l.date); 282 | } 283 | 284 | break; 285 | 286 | default: 287 | super.handleMessage(msg); 288 | } 289 | } 290 | } 291 | 292 | private ServiceConnection mConnection = new ServiceConnection() { 293 | public void onServiceConnected(ComponentName className, 294 | IBinder service) { 295 | mService = new Messenger(service); 296 | try { 297 | Message msg = Message.obtain(null, 298 | TrackerService.MSG_REGISTER_CLIENT); 299 | msg.replyTo = mMessenger; 300 | mService.send(msg); 301 | } 302 | catch (RemoteException e) { 303 | logText("Error connecting to service: " + e.getMessage()); 304 | } 305 | } 306 | 307 | public void onServiceDisconnected(ComponentName className) { 308 | mService = null; 309 | logText("Disconnected from service"); 310 | } 311 | }; 312 | 313 | private void doBindService() { 314 | bindService(new Intent(this, TrackerService.class), mConnection, 315 | Context.BIND_AUTO_CREATE); 316 | mIsBound = true; 317 | } 318 | 319 | private void doUnbindService() { 320 | if (!mIsBound) { 321 | return; 322 | } 323 | if (mService != null) { 324 | try { 325 | Message msg = Message.obtain(null, 326 | TrackerService.MSG_UNREGISTER_CLIENT); 327 | msg.replyTo = mMessenger; 328 | mService.send(msg); 329 | } 330 | catch (RemoteException e) { 331 | Log.e(TAG, e.getMessage()); 332 | } 333 | } 334 | unbindService(mConnection); 335 | mIsBound = false; 336 | logText("Service stopped"); 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/Prefs.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | public final class Prefs { 7 | public static String ENDPOINT = "endpoint"; 8 | public static String ENABLED = "enabled"; 9 | public static String UPDATE_FREQ = "update_freq"; 10 | public static String USER_ID = "account_id"; 11 | public static String USER_EMAIL = "account_email"; 12 | public static String USER_PASSWORD = "account_password"; 13 | 14 | public static SharedPreferences get(final Context context) { 15 | return context.getSharedPreferences("ca.klostermann.philip.location_tracker", 16 | Context.MODE_PRIVATE); 17 | } 18 | 19 | public static String getPref(final Context context, String pref, 20 | String def) { 21 | SharedPreferences prefs = Prefs.get(context); 22 | String val = prefs.getString(pref, def); 23 | 24 | if (val == null || val.equals("") || val.equals("null")) 25 | return def; 26 | else 27 | return val; 28 | } 29 | 30 | public static void putPref(final Context context, String pref, 31 | String val) { 32 | SharedPreferences prefs = Prefs.get(context); 33 | SharedPreferences.Editor editor = prefs.edit(); 34 | 35 | editor.putString(pref, val); 36 | editor.apply(); 37 | } 38 | 39 | public static String getEndpoint(final Context context) { 40 | return Prefs.getPref(context, ENDPOINT, null); 41 | } 42 | 43 | public static String getUpdateFreq(final Context context) { 44 | return Prefs.getPref(context, UPDATE_FREQ, "30m"); 45 | } 46 | 47 | public static boolean getEnabled(final Context context) { 48 | String e = Prefs.getPref(context, ENABLED, "false"); 49 | return e.equals("true"); 50 | } 51 | 52 | public static String getUserId(final Context context) { 53 | return Prefs.getPref(context, USER_ID, null); 54 | } 55 | 56 | public static String getUserEmail(final Context context) { 57 | return Prefs.getPref(context, USER_EMAIL, null); 58 | } 59 | 60 | public static String getUserPassword(final Context context) { 61 | return Prefs.getPref(context, USER_PASSWORD, null); 62 | } 63 | 64 | 65 | public static void putUpdateFreq(final Context context, String freq) { 66 | Prefs.putPref(context, UPDATE_FREQ, freq); 67 | } 68 | 69 | public static void putEndpoint(final Context context, String endpoint) { 70 | Prefs.putPref(context, ENDPOINT, endpoint); 71 | } 72 | 73 | public static void putEnabled(final Context context, boolean enabled) { 74 | Prefs.putPref(context, ENABLED, (enabled ? "true" : "false")); 75 | } 76 | 77 | public static void putUserId(final Context context, String id) { 78 | Prefs.putPref(context, USER_ID, id); 79 | } 80 | 81 | public static void putUserEmail(final Context context, String email) { 82 | Prefs.putPref(context, USER_EMAIL, email); 83 | } 84 | 85 | public static void putUserPassword(final Context context, String password) { 86 | Prefs.putPref(context, USER_PASSWORD, password); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/SignupTaskListener.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import com.firebase.client.FirebaseError; 4 | import java.util.Map; 5 | 6 | public interface SignupTaskListener { 7 | void onSignupSuccess(Map result); 8 | void onSignupFailure(FirebaseError firebaseError); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/TrackerService.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStreamWriter; 9 | import java.text.DecimalFormat; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.regex.Pattern; 17 | import java.util.regex.Matcher; 18 | 19 | import android.app.Notification; 20 | import android.app.NotificationManager; 21 | import android.app.PendingIntent; 22 | import android.app.Service; 23 | import android.location.Location; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.os.Build; 27 | import android.os.Bundle; 28 | import android.os.Handler; 29 | import android.os.IBinder; 30 | import android.os.Message; 31 | import android.os.Messenger; 32 | import android.os.PowerManager; 33 | import android.os.RemoteException; 34 | import android.provider.Settings; 35 | import android.util.Log; 36 | 37 | import com.firebase.client.AuthData; 38 | import com.firebase.client.Firebase; 39 | import com.firebase.client.FirebaseError; 40 | import com.google.android.gms.common.ConnectionResult; 41 | import com.google.android.gms.common.GooglePlayServicesUtil; 42 | import com.google.android.gms.common.api.GoogleApiClient; 43 | import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 44 | import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; 45 | 46 | import com.google.android.gms.location.LocationRequest; 47 | import com.google.android.gms.location.LocationServices; 48 | 49 | import org.json.JSONException; 50 | import org.json.JSONObject; 51 | 52 | 53 | public class TrackerService extends Service { 54 | private static final String TAG = "LocationTracker/Service"; 55 | 56 | public static TrackerService service; 57 | 58 | private NotificationManager nm; 59 | private Notification notification; 60 | private static boolean isRunning = false; 61 | 62 | private String freqString; 63 | private int freqSeconds; 64 | private String endpoint; 65 | 66 | private static volatile PowerManager.WakeLock wakeLock; 67 | private PendingIntent mLocationIntent; 68 | 69 | private GoogleApiClient mGoogleApiClient; 70 | private LocationListener mLocationListener; 71 | private Firebase mFirebaseRef; 72 | private String mUserId; 73 | private Location mLastReportedLocation; 74 | 75 | ArrayList mLogRing = new ArrayList<>(); 76 | ArrayList mClients = new ArrayList<>(); 77 | final Messenger mMessenger = new Messenger(new IncomingHandler()); 78 | 79 | static final int MSG_REGISTER_CLIENT = 1; 80 | static final int MSG_UNREGISTER_CLIENT = 2; 81 | static final int MSG_LOG = 3; 82 | static final int MSG_LOG_RING = 4; 83 | 84 | static final String LOGFILE_NAME = "TrackerService.log"; 85 | static final int MAX_RING_SIZE = 250; 86 | 87 | @Override 88 | public IBinder onBind(Intent intent) { 89 | return mMessenger.getBinder(); 90 | } 91 | 92 | @Override 93 | public void onCreate() { 94 | super.onCreate(); 95 | 96 | // Check whether Google Play Services is installed 97 | int resp = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); 98 | if(resp != ConnectionResult.SUCCESS){ 99 | logText("Google Play Services not found. Please install to use this app."); 100 | stopSelf(); 101 | return; 102 | } 103 | 104 | TrackerService.service = this; 105 | 106 | endpoint = Prefs.getEndpoint(this); 107 | if (endpoint == null || endpoint.equals("")) { 108 | logText("Invalid endpoint, stopping service"); 109 | stopSelf(); 110 | return; 111 | } 112 | 113 | freqSeconds = 0; 114 | freqString = Prefs.getUpdateFreq(this); 115 | if (freqString != null && !freqString.equals("")) { 116 | try { 117 | Pattern p = Pattern.compile("(\\d+)(m|h|s)"); 118 | Matcher m = p.matcher(freqString); 119 | m.find(); 120 | freqSeconds = Integer.parseInt(m.group(1)); 121 | if (m.group(2).equals("h")) { 122 | freqSeconds *= (60 * 60); 123 | } else if (m.group(2).equals("m")) { 124 | freqSeconds *= 60; 125 | } 126 | } 127 | catch (Exception e) { 128 | Log.d(TAG, e.toString()); 129 | } 130 | } 131 | 132 | if (freqSeconds < 1) { 133 | logText("Invalid frequency (" + freqSeconds + "), stopping " + 134 | "service"); 135 | stopSelf(); 136 | return; 137 | } 138 | 139 | // load saved log messages from disk 140 | ArrayList logs = loadLogsFromDisk(); 141 | if(logs != null) { 142 | Log.d(TAG, "Loaded " + logs.size() + " logs from disk"); 143 | 144 | if(logs.size() > MAX_RING_SIZE) { 145 | mLogRing.addAll(logs.subList(logs.size() - MAX_RING_SIZE, logs.size() - 1)); 146 | } else { 147 | mLogRing.addAll(logs); 148 | } 149 | 150 | if(logs.size() > (2 * MAX_RING_SIZE)) { 151 | cleanLogFile(MAX_RING_SIZE); 152 | } 153 | } 154 | 155 | Firebase.setAndroidContext(this); 156 | 157 | // Authenticate user 158 | String email = Prefs.getUserEmail(this); 159 | String password = Prefs.getUserPassword(this); 160 | if(email == null || email.equals("") 161 | || password == null || password.equals("")) { 162 | logText("No email/password found, stopping service"); 163 | stopSelf(); 164 | return; 165 | } 166 | 167 | showNotification(); 168 | isRunning = true; 169 | 170 | logText("Service authenticating..."); 171 | mFirebaseRef = new Firebase(Prefs.getEndpoint(this)); 172 | mFirebaseRef.authWithPassword(email, password, new Firebase.AuthResultHandler() { 173 | @Override 174 | public void onAuthenticated(AuthData authData) { 175 | logText("Successfully authenticated"); 176 | mUserId = authData.getUid(); 177 | 178 | // set this device's info in Firebase 179 | mFirebaseRef.child("devices/" + mUserId + "/" + getDeviceId()).setValue(getDeviceInfo()); 180 | 181 | // mGoogleApiClient.connect() will callback to this 182 | mLocationListener = new LocationListener(); 183 | mGoogleApiClient = buildGoogleApiClient(); 184 | mGoogleApiClient.connect(); 185 | 186 | /* we're not registered yet, so this will just log to our ring buffer, 187 | * but as soon as the client connects we send the log buffer anyway */ 188 | logText("Service started, update frequency " + freqString); 189 | } 190 | 191 | @Override 192 | public void onAuthenticationError(FirebaseError firebaseError) { 193 | logText("Authentication failed, please check email/password, stopping service"); 194 | stopSelf(); 195 | } 196 | }); 197 | } 198 | 199 | @Override 200 | public void onDestroy() { 201 | super.onDestroy(); 202 | 203 | /* kill persistent notification */ 204 | if(nm != null) { 205 | nm.cancelAll(); 206 | } 207 | 208 | if(mGoogleApiClient != null && mLocationIntent != null) { 209 | LocationServices.FusedLocationApi.removeLocationUpdates( 210 | mGoogleApiClient, mLocationIntent); 211 | } 212 | isRunning = false; 213 | } 214 | 215 | @Override 216 | public int onStartCommand(Intent intent, int flags, int startId) { 217 | return START_STICKY; 218 | } 219 | 220 | public static boolean isRunning() { 221 | return isRunning; 222 | } 223 | 224 | private synchronized GoogleApiClient buildGoogleApiClient() { 225 | return new GoogleApiClient.Builder(this) 226 | .addConnectionCallbacks(mLocationListener) 227 | .addOnConnectionFailedListener(mLocationListener) 228 | .addApi(LocationServices.API) 229 | .build(); 230 | } 231 | 232 | private LocationRequest createLocationRequest() { 233 | return new LocationRequest() 234 | .setInterval(freqSeconds * 1000) 235 | .setFastestInterval(5000) 236 | .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); 237 | } 238 | 239 | private void showNotification() { 240 | nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); 241 | notification = new Notification(R.mipmap.service_icon, 242 | "Location Tracker Started", System.currentTimeMillis()); 243 | PendingIntent contentIntent = PendingIntent.getActivity(this, 0, 244 | new Intent(this, MainActivity.class), 0); 245 | notification.setLatestEventInfo(this, "Location Tracker", 246 | "Service started", contentIntent); 247 | notification.flags = Notification.FLAG_ONGOING_EVENT; 248 | nm.notify(1, notification); 249 | } 250 | 251 | private void updateNotification(String text) { 252 | if (nm != null) { 253 | PendingIntent contentIntent = PendingIntent.getActivity(this, 0, 254 | new Intent(this, MainActivity.class), 0); 255 | notification.setLatestEventInfo(this, "Location Tracker", text, 256 | contentIntent); 257 | notification.when = System.currentTimeMillis(); 258 | nm.notify(1, notification); 259 | } 260 | } 261 | 262 | private String getDeviceId() { 263 | return Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID); 264 | } 265 | 266 | private Map getDeviceInfo() { 267 | Map info = new HashMap<>(); 268 | info.put("deviceId", getDeviceId()); 269 | info.put("brand", Build.BRAND); 270 | info.put("device", Build.DEVICE); 271 | info.put("hardware", Build.HARDWARE); 272 | info.put("id", Build.ID); 273 | info.put("manufacturer", Build.MANUFACTURER); 274 | info.put("model", Build.MODEL); 275 | info.put("product", Build.PRODUCT); 276 | 277 | return info; 278 | } 279 | 280 | private boolean saveLogToDisk(LogMessage logMessage) { 281 | JSONObject jsonObj = new JSONObject(); 282 | try { 283 | jsonObj.put("date", String.valueOf(logMessage.date.getTime())); 284 | jsonObj.put("message", logMessage.message); 285 | } catch (JSONException e) { 286 | Log.e(TAG, "Saving Log to disk failed, cannot create JSON object: " + e); 287 | return false; 288 | } 289 | 290 | OutputStreamWriter osw; 291 | try { 292 | osw = new OutputStreamWriter(openFileOutput(LOGFILE_NAME, Context.MODE_APPEND)); 293 | osw.write(jsonObj.toString() + "\n"); 294 | osw.close(); 295 | } catch (FileNotFoundException e) { 296 | Log.e(TAG, "Saving Log to disk failed, file not found: " + e); 297 | return false; 298 | } catch (IOException e) { 299 | Log.e(TAG, "Saving Log to disk failed, IO Exception: " + e); 300 | return false; 301 | } 302 | 303 | return true; 304 | } 305 | 306 | private ArrayList loadLogsFromDisk() { 307 | ArrayList logs = new ArrayList<>(); 308 | 309 | ArrayList jsonLogMessages = new ArrayList<>(); 310 | InputStream inputStream; 311 | try { 312 | inputStream = openFileInput(LOGFILE_NAME); 313 | 314 | if ( inputStream != null ) { 315 | InputStreamReader inputStreamReader = new InputStreamReader(inputStream); 316 | BufferedReader bufferedReader = new BufferedReader(inputStreamReader); 317 | 318 | String line; 319 | while ((line = bufferedReader.readLine()) != null ) { 320 | jsonLogMessages.add(line); 321 | } 322 | inputStream.close(); 323 | } 324 | } catch (FileNotFoundException e) { 325 | Log.e(TAG, "Reading Logs from disk failed, file not found: " + e); 326 | return null; 327 | } catch (IOException e) { 328 | Log.e(TAG, "Reading Logs from disk failed, IO Exception: " + e); 329 | return null; 330 | } 331 | 332 | try { 333 | for(int i = jsonLogMessages.size() - 1; i >= 0; i--) { 334 | JSONObject jsonObject = new JSONObject(jsonLogMessages.get(i)); 335 | Date d = new Date(); 336 | d.setTime(jsonObject.getLong("date")); 337 | LogMessage lm = new LogMessage(d, jsonObject.getString("message")); 338 | logs.add(lm); 339 | } 340 | } catch (JSONException e) { 341 | Log.e(TAG, "Reading logs from disk failed, unable to parse JSON object: " + e); 342 | return null; 343 | } 344 | 345 | Collections.reverse(logs); 346 | return logs; 347 | } 348 | 349 | private boolean cleanLogFile(int numLogsToKeep) { 350 | ArrayList logs = loadLogsFromDisk(); 351 | if(logs == null || logs.size() <= 0) { 352 | Log.e(TAG, "Cleaning log failed, unable to load log file."); 353 | return false; 354 | } 355 | if(!deleteFile(LOGFILE_NAME)) { 356 | Log.e(TAG, "Cleaning log failed, unable to delete log file."); 357 | return false; 358 | } 359 | 360 | List recentLogs = logs.subList(logs.size() - numLogsToKeep - 1, logs.size() - 1); 361 | 362 | for(int i = 0; i < recentLogs.size(); i++) { 363 | if(!saveLogToDisk(recentLogs.get(i))) { 364 | Log.e(TAG, "Cleaning log failed, error trying to write to new log file"); 365 | return false; 366 | } 367 | } 368 | return true; 369 | } 370 | 371 | public void logText(String log) { 372 | LogMessage lm = new LogMessage(new Date(), log); 373 | mLogRing.add(lm); 374 | saveLogToDisk(lm); 375 | if (mLogRing.size() > MAX_RING_SIZE) 376 | mLogRing.remove(0); 377 | 378 | updateNotification(log); 379 | 380 | for (int i = mClients.size() - 1; i >= 0; i--) { 381 | try { 382 | Bundle b = new Bundle(); 383 | b.putString("log", log); 384 | Message msg = Message.obtain(null, MSG_LOG); 385 | msg.setData(b); 386 | mClients.get(i).send(msg); 387 | } 388 | catch (RemoteException e) { 389 | /* client is dead, how did this happen */ 390 | mClients.remove(i); 391 | } 392 | } 393 | } 394 | 395 | public void sendLocation(Location location) { 396 | /* Wake up */ 397 | if (wakeLock == null) { 398 | PowerManager pm = (PowerManager)this.getSystemService( 399 | Context.POWER_SERVICE); 400 | 401 | /* we don't need the screen on */ 402 | wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 403 | "locationtracker"); 404 | wakeLock.setReferenceCounted(true); 405 | } 406 | 407 | if (!wakeLock.isHeld()) { 408 | wakeLock.acquire(); 409 | } 410 | 411 | if(location == null) { 412 | return; 413 | } 414 | 415 | Log.d(TAG, "Location update received"); 416 | 417 | if(mLastReportedLocation != null) { 418 | float accuracy = Math.max(location.getAccuracy(), mLastReportedLocation.getAccuracy()); 419 | if(mLastReportedLocation.distanceTo(location) < accuracy) { 420 | Log.d(TAG, "Location has not changed enough. Not sending..."); 421 | return; 422 | } 423 | } 424 | 425 | LocationPost locationPost = new LocationPost(location); 426 | //Use timestamp of today's date at midnight as key 427 | long dateKey = LocationPost.getDateKey(locationPost.getTime()); 428 | 429 | try { 430 | mFirebaseRef.child("locations/" + mUserId + "/" + getDeviceId() + "/" + dateKey) 431 | .push() 432 | .setValue(locationPost); 433 | 434 | mLastReportedLocation = location; 435 | 436 | Log.d(TAG, "Location sent"); 437 | logText("Location " + 438 | (new DecimalFormat("#.######").format(locationPost.getLatitude())) + 439 | ", " + 440 | (new DecimalFormat("#.######").format(locationPost.getLongitude()))); 441 | 442 | 443 | } catch(Exception e) { 444 | Log.e(TAG, "Posting to Firebase failed: " + e.toString()); 445 | logText("Failed to send location data."); 446 | } 447 | } 448 | 449 | class LocationListener implements 450 | ConnectionCallbacks, 451 | OnConnectionFailedListener { 452 | @Override 453 | public void onConnected(Bundle connectionHint) { 454 | LocationRequest locationRequest = createLocationRequest(); 455 | Intent intent = new Intent(service, LocationReceiver.class); 456 | mLocationIntent = PendingIntent.getBroadcast( 457 | getApplicationContext(), 458 | 14872, 459 | intent, 460 | PendingIntent.FLAG_CANCEL_CURRENT); 461 | 462 | // Register for automatic location updates 463 | LocationServices.FusedLocationApi.requestLocationUpdates( 464 | mGoogleApiClient, locationRequest, mLocationIntent); 465 | } 466 | 467 | @Override 468 | public void onConnectionSuspended(int i) { 469 | Log.w(TAG, "Location connection suspended " + i); 470 | mGoogleApiClient.connect(); 471 | } 472 | 473 | @Override 474 | public void onConnectionFailed(ConnectionResult connectionResult) { 475 | Log.e(TAG, "Location connection failed" + connectionResult); 476 | logText("No Location found"); 477 | } 478 | } 479 | 480 | class IncomingHandler extends Handler { 481 | @Override 482 | public void handleMessage(Message msg) { 483 | switch (msg.what) { 484 | case MSG_REGISTER_CLIENT: 485 | mClients.add(msg.replyTo); 486 | 487 | /* respond with our log ring to show what we've been up to */ 488 | try { 489 | Message replyMsg = Message.obtain(null, MSG_LOG_RING); 490 | replyMsg.obj = mLogRing; 491 | msg.replyTo.send(replyMsg); 492 | } 493 | catch (RemoteException e) { 494 | Log.e(TAG, e.getMessage()); 495 | } 496 | break; 497 | case MSG_UNREGISTER_CLIENT: 498 | mClients.remove(msg.replyTo); 499 | break; 500 | 501 | default: 502 | super.handleMessage(msg); 503 | } 504 | } 505 | } 506 | 507 | } 508 | -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/UserLoginTask.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.util.Log; 4 | 5 | import com.firebase.client.AuthData; 6 | import com.firebase.client.Firebase; 7 | import com.firebase.client.FirebaseError; 8 | 9 | public class UserLoginTask { 10 | private final String TAG = "UserLoginTask"; 11 | private final LoginTaskListener mCaller; 12 | private final String mFirebaseUrl; 13 | private final String mEmail; 14 | private final String mPassword; 15 | 16 | private FirebaseError mError; 17 | private AuthData mAuthData; 18 | 19 | public UserLoginTask(LoginTaskListener caller, String firebaseURL, String email, String password) { 20 | mCaller = caller; 21 | mFirebaseUrl = firebaseURL; 22 | mEmail = email; 23 | mPassword = password; 24 | } 25 | 26 | public void execute() { 27 | try { 28 | Firebase ref = new Firebase(mFirebaseUrl); 29 | ref.authWithPassword(mEmail, mPassword, new Firebase.AuthResultHandler() { 30 | @Override 31 | public void onAuthenticated(AuthData authData) { 32 | Log.d(TAG, authData.toString()); 33 | mAuthData = authData; 34 | onComplete(true); 35 | } 36 | 37 | @Override 38 | public void onAuthenticationError(FirebaseError firebaseError) { 39 | Log.e(TAG, firebaseError.toString()); 40 | mError = firebaseError; 41 | onComplete(false); 42 | } 43 | }); 44 | } catch (Exception e) { 45 | mError = new FirebaseError( 46 | FirebaseError.OPERATION_FAILED, e.getMessage()); 47 | Log.e(TAG, e.toString()); 48 | onComplete(false); 49 | } 50 | } 51 | 52 | protected void onComplete(final Boolean success) { 53 | if (success && mAuthData != null) { 54 | mCaller.onLoginSuccess(mAuthData); 55 | } else if (!success && mError != null) { 56 | mCaller.onLoginFailure(mError); 57 | } else { 58 | mCaller.onLoginFailure(null); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/main/java/ca/klostermann/philip/location_tracker/UserSignupTask.java: -------------------------------------------------------------------------------- 1 | package ca.klostermann.philip.location_tracker; 2 | 3 | import android.util.Log; 4 | 5 | import com.firebase.client.Firebase; 6 | import com.firebase.client.FirebaseError; 7 | 8 | import java.util.Map; 9 | 10 | public class UserSignupTask { 11 | private final String TAG = "UserSignupTask"; 12 | 13 | private final SignupTaskListener mCaller; 14 | private final String mFirebaseUrl; 15 | private final String mEmail; 16 | private final String mPassword; 17 | 18 | private FirebaseError mError; 19 | private Map mResult; 20 | 21 | UserSignupTask(SignupTaskListener caller, String firebaseURL, String email, String password) { 22 | mCaller = caller; 23 | mFirebaseUrl = firebaseURL; 24 | mEmail = email; 25 | mPassword = password; 26 | } 27 | 28 | public void execute() { 29 | try { 30 | Firebase ref = new Firebase(mFirebaseUrl); 31 | ref.createUser(mEmail, mPassword, new Firebase.ValueResultHandler>() { 32 | @Override 33 | public void onSuccess(Map result) { 34 | mResult = result; 35 | onComplete(true); 36 | } 37 | 38 | @Override 39 | public void onError(FirebaseError firebaseError) { 40 | mError = firebaseError; 41 | onComplete(false); 42 | } 43 | }); 44 | 45 | } catch (Exception e) { 46 | mError = new FirebaseError( 47 | FirebaseError.OPERATION_FAILED, e.getMessage()); 48 | Log.e(TAG, e.toString()); 49 | onComplete(false); 50 | } 51 | } 52 | 53 | protected void onComplete(final Boolean success) { 54 | if (success && mResult != null) { 55 | mCaller.onSignupSuccess(mResult); 56 | } else if (!success && mError != null) { 57 | mCaller.onSignupFailure(mError); 58 | } else { 59 | mCaller.onSignupFailure(null); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 14 | 15 | 17 | 19 | 20 | 22 | 23 | 27 | 28 | 34 | 35 |