├── .gitignore ├── LICENSE ├── README.md ├── justfile ├── quickstart-java ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── hypertrack │ │ │ └── quickstart │ │ │ ├── MainActivity.java │ │ │ ├── PermissionsApi.java │ │ │ └── PermissionsFlow.java │ │ └── res │ │ ├── drawable │ │ └── ic_green.xml │ │ ├── layout │ │ └── activity_main.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── quickstart-kotlin ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── hypertrack │ │ │ └── quickstart │ │ │ └── MainActivity.kt │ │ └── res │ │ ├── drawable │ │ └── ic_green.xml │ │ ├── layout │ │ └── activity_main.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── scripts └── update_file.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HyperTrack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Quickstart for HyperTrack SDK 2 | 3 | [![License](https://img.shields.io/github/license/hypertrack/quickstart-android?color=orange)](./LICENSE) 4 | [![Android SDK](https://img.shields.io/badge/Android%20SDK-7.4.0-brightgreen.svg)](https://github.com/hypertrack/sdk-android) 5 | 6 | [HyperTrack](https://www.hypertrack.com/) lets you add live location tracking to your mobile app. Live location is made available along with ongoing activity, tracking controls and tracking outage with reasons. 7 | 8 | This repo contains examples of Android app for Java and Kotlin that has everything you need to get started. 9 | 10 | For more information please check this [Guide](https://www.hypertrack.com/docs/install-sdk-android). 11 | 12 | ## How to get started? 13 | 14 | ### Create HyperTrack Account 15 | 16 | [Sign up](https://dashboard.hypertrack.com/signup) for HyperTrack and get your publishable key from the [Setup page](https://dashboard.hypertrack.com/setup). 17 | 18 | ### Set up the environment 19 | 20 | You need to install [Android Studio](https://developer.android.com/studio) 21 | 22 | ### Clone Quickstart app 23 | 24 | ### Set up the publishable key 25 | 26 | Follow the [instructions on setting up publishable key](https://hypertrack.com/docs/install-sdk-android#set-the-publishable-key) in our docs 27 | 28 | ### Set up silent push notifications 29 | 30 | Follow the [instructions on setting up silent push notifications](https://hypertrack.com/docs/install-sdk-android/#set-up-silent-push-notifications) in our docs. 31 | 32 | HyperTrack SDK needs Firebase Cloud Messaging to manage on-device tracking as well as enable using HyperTrack cloud APIs from your server to control the tracking. 33 | 34 | ### Run the app 35 | 36 | ### Grant permissions 37 | 38 | [Grant required permissions to the app](https://hypertrack.com/docs/install-sdk-android#grant-the-permissions-to-the-app) 39 | 40 | ### Start tracking 41 | 42 | Press `Start tracking` button. 43 | 44 | To see the device on a map, open the [HyperTrack dashboard](https://dashboard.hypertrack.com/). 45 | 46 | The app will create a driver with driver handle `test_driver_quickstart_android_` (platform here is `java` or `kotlin`) 47 | 48 | ## Support 49 | 50 | Join our [Slack community](https://join.slack.com/t/hypertracksupport/shared_invite/enQtNDA0MDYxMzY1MDMxLTdmNDQ1ZDA1MTQxOTU2NTgwZTNiMzUyZDk0OThlMmJkNmE0ZGI2NGY2ZGRhYjY0Yzc0NTJlZWY2ZmE5ZTA2NjI) for instant responses. You can also email us at help@hypertrack.com 51 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | alias ogp := open-github-prs 2 | alias us := update-sdk 3 | 4 | SDK_NAME := "HyperTrack SDK Android" 5 | REPOSITORY_NAME := "quickstart-android" 6 | 7 | open-github-prs: 8 | open "https://github.com/hypertrack/{{REPOSITORY_NAME}}/pulls" 9 | 10 | update-sdk android_version: 11 | git checkout -b update-sdk-{{android_version}} 12 | just _update-sdk-android-version-file {{android_version}} 13 | git add . 14 | git commit -m "Update {{SDK_NAME}} to {{android_version}}" 15 | just open-github-prs 16 | 17 | _update-sdk-android-version-file android_version: 18 | ./scripts/update_file.sh quickstart-java/app/build.gradle "def hyperTrackVersion = \'.*\'" "def hyperTrackVersion = \'{{android_version}}\'" 19 | ./scripts/update_file.sh quickstart-kotlin/app/build.gradle "def hyperTrackVersion = \'.*\'" "def hyperTrackVersion = \'{{android_version}}\'" 20 | -------------------------------------------------------------------------------- /quickstart-java/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle 3 | *.iml 4 | .idea/ 5 | local.properties 6 | 7 | -------------------------------------------------------------------------------- /quickstart-java/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle 3 | *.iml 4 | google-services.json 5 | 6 | -------------------------------------------------------------------------------- /quickstart-java/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | def appId = "com.hypertrack.quickstart.android.java" 6 | 7 | android { 8 | namespace appId 9 | compileSdk 34 10 | defaultConfig { 11 | applicationId appId 12 | minSdk 19 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: 'libs', include: ['*.jar']) 28 | implementation 'androidx.appcompat:appcompat:1.6.1' 29 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 30 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 31 | 32 | def hyperTrackVersion = '7.11.0' 33 | implementation "com.hypertrack:sdk-android:$hyperTrackVersion" 34 | implementation "com.hypertrack:push-service-firebase:$hyperTrackVersion" 35 | implementation "com.hypertrack:location-services-google:$hyperTrackVersion" 36 | } 37 | -------------------------------------------------------------------------------- /quickstart-java/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 28 | 31 | 34 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/java/com/hypertrack/quickstart/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hypertrack.quickstart; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.AlertDialog; 5 | import android.content.ClipData; 6 | import android.content.ClipboardManager; 7 | import android.content.Context; 8 | import android.os.Bundle; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.TextView; 12 | 13 | import androidx.annotation.Nullable; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import com.hypertrack.quickstart.android.java.R; 17 | import com.hypertrack.sdk.android.HyperTrack; 18 | import com.hypertrack.sdk.android.Json; 19 | import com.hypertrack.sdk.android.Result; 20 | 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | import java.util.Random; 24 | import java.util.Set; 25 | 26 | /** @noinspection FieldCanBeLocal*/ 27 | public class MainActivity extends AppCompatActivity { 28 | 29 | private static final String TAG = "MainActivity"; 30 | 31 | private HyperTrack.Cancellable errorsSubscription; 32 | private HyperTrack.Cancellable isAvailableSubscription; 33 | private HyperTrack.Cancellable isTrackingSubscription; 34 | private HyperTrack.Cancellable locationSubscription; 35 | private HyperTrack.Cancellable locateSubscription; 36 | private HyperTrack.Cancellable ordersSubscription; 37 | 38 | @SuppressLint("SetTextI18n") 39 | @Override 40 | protected void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.activity_main); 43 | 44 | TextView tvDeviceId = findViewById(R.id.tvDeviceId); 45 | TextView tvErrors = findViewById(R.id.tvErrors); 46 | TextView tvIsAvailable = findViewById(R.id.tvIsAvailable); 47 | TextView tvIsTracking = findViewById(R.id.tvIsTracking); 48 | TextView tvLocation = findViewById(R.id.tvLocation); 49 | TextView tvOrders = findViewById(R.id.tvOrders); 50 | 51 | View btnAddGeotag = findViewById(R.id.btnAddGeotag); 52 | View btnAddGeotagWithExpectedLocation = findViewById(R.id.btnAddGeotagWithExpectedLocation); 53 | View btnGetErrors = findViewById(R.id.btnGetErrors); 54 | View btnGetIsAvailable = findViewById(R.id.btnGetIsAvailable); 55 | View btnGetIsTracking = findViewById(R.id.btnGetIsTracking); 56 | View btnGetLocation = findViewById(R.id.btnGetLocation); 57 | View btnGetMetadata = findViewById(R.id.btnGetMetadata); 58 | View btnGetName = findViewById(R.id.btnGetName); 59 | View btnGetOrders = findViewById(R.id.btnGetOrders); 60 | View btnLocate = findViewById(R.id.btnLocate); 61 | View btnSetAvailable = findViewById(R.id.btnSetAvailable); 62 | View btnSetUnavailable = findViewById(R.id.btnSetUnavailable); 63 | View btnStartPermissionsFlow = findViewById(R.id.btnStartPermissionsFlow); 64 | View btnStartTracking = findViewById(R.id.btnStartTracking); 65 | View btnStopTracking = findViewById(R.id.btnStopTracking); 66 | 67 | // Get Device ID 68 | String deviceId = HyperTrack.getDeviceID(); 69 | Log.d(TAG, "Device ID: " + deviceId); 70 | tvDeviceId.setText(deviceId); 71 | 72 | // `worker_handle` is used to link the device and the driver. 73 | // You can use any unique user identifier here. 74 | // The recommended way is to set it on app login in set it to null on logout 75 | // (to remove the link between the device and the worker) 76 | HyperTrack.setWorkerHandle("test_worker_quickstart_android_java"); 77 | 78 | // Set Name 79 | String name = "Quickstart Android Java"; 80 | HyperTrack.setName(name); 81 | Log.d(TAG, "Name set to: " + name); 82 | 83 | // Set Metadata 84 | Map metadata = new HashMap<>(); 85 | // `driver_handle` is used to link the device and the driver. 86 | // You can use any unique user identifier here. 87 | // The recommended way is to set it on app login in set it to null on logout 88 | // (to remove the link between the device and the driver) 89 | metadata.put("driver_handle", "test_driver_quickstart_android_java"); 90 | // You can also add any custom data to the metadata. 91 | metadata.put("source", name); 92 | metadata.put("employee_id", new Random().nextInt(10000)); 93 | 94 | // We convert metadata to custom Json object to make sure that the data is Json compatible 95 | // If it can't be converted to Json, the result will be null 96 | @Nullable Json.Object metadataJson = Json.fromMap(metadata); 97 | // don't forget to check it and add proper error handling 98 | if (metadataJson == null) { 99 | throw new RuntimeException("Metadata can't be converted to Json"); 100 | } 101 | 102 | HyperTrack.setMetadata(metadataJson); 103 | Log.d(TAG, "Metadata set to: " + metadata.toString()); 104 | 105 | tvDeviceId.setOnClickListener(v -> { 106 | // copy device id to clipboard 107 | ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 108 | ClipData clip = ClipData.newPlainText("Device ID", deviceId); 109 | clipboard.setPrimaryClip(clip); 110 | }); 111 | 112 | btnStartTracking.setOnClickListener(v -> { 113 | HyperTrack.setTracking(true); 114 | }); 115 | 116 | btnStopTracking.setOnClickListener(v -> { 117 | HyperTrack.setTracking(false); 118 | }); 119 | 120 | btnSetAvailable.setOnClickListener(v -> { 121 | HyperTrack.setAvailable(true); 122 | }); 123 | 124 | btnSetUnavailable.setOnClickListener(v -> { 125 | HyperTrack.setAvailable(false); 126 | }); 127 | 128 | btnAddGeotag.setOnClickListener(v -> { 129 | // Create geotag payload 130 | Map geotagPayload = new HashMap<>(); 131 | /* 132 | geotagPayload is an arbitrary object. 133 | You can put there any JSON-serializable data. 134 | It will be displayed in the HyperTrack dashboard and 135 | available in the webhook events. 136 | */ 137 | geotagPayload.put("payload", "Quickstart AndroidJava"); 138 | geotagPayload.put("value", new Random().nextDouble()); 139 | 140 | // We convert geotagPayload to custom Json object to make sure that the data is Json compatible 141 | // If it can't be converted to Json, the result will be null 142 | @Nullable Json.Object geotagPayloadJson = Json.fromMap(geotagPayload); 143 | // don't forget to check it and add proper error handling 144 | if (geotagPayloadJson == null) { 145 | throw new RuntimeException("Geotag payload can't be converted to Json"); 146 | } 147 | 148 | // Add geotag 149 | Result geotagResult = 150 | HyperTrack.addGeotag(geotagPayloadJson); 151 | showDialog("Geotag result", getLocationResultText(geotagResult)); 152 | }); 153 | 154 | btnAddGeotagWithExpectedLocation.setOnClickListener(v -> { 155 | // Create geotag payload 156 | Map geotagPayload = new HashMap<>(); 157 | /* 158 | geotagPayload is an arbitrary object. 159 | You can put there any JSON-serializable data. 160 | It will be displayed in the HyperTrack dashboard and 161 | available in the webhook events. 162 | */ 163 | geotagPayload.put("payload", "Quickstart AndroidJava"); 164 | geotagPayload.put("value", new Random().nextDouble()); 165 | 166 | // We convert geotagPayload to custom Json object to make sure that the data is Json compatible 167 | // If it can't be converted to Json, the result will be null 168 | @Nullable Json.Object geotagPayloadJson = Json.fromMap(geotagPayload); 169 | // don't forget to check it and add proper error handling 170 | if (geotagPayloadJson == null) { 171 | throw new RuntimeException("Geotag payload can't be converted to Json"); 172 | } 173 | 174 | // Create expected location 175 | HyperTrack.Location expectedLocation = new HyperTrack.Location( 176 | 37.775, 177 | -122.4183 178 | ); 179 | 180 | // Add geotag 181 | Result geotagResult = 182 | HyperTrack.addGeotag(geotagPayloadJson, expectedLocation); 183 | showDialog("Geotag with expected location result", getLocationWithDeviationResultText(geotagResult)); 184 | }); 185 | 186 | btnLocate.setOnClickListener(v -> { 187 | // you can use subscription object to unsubscribe from the locate result 188 | // with locateSubscription.cancel() 189 | locateSubscription = HyperTrack.locate(locationResult -> { 190 | showDialog("Locate result", getLocateResultText(locationResult)); 191 | return null; 192 | }); 193 | }); 194 | 195 | btnGetErrors.setOnClickListener(v -> { 196 | showDialog("Errors", getErrorsText(HyperTrack.getErrors())); 197 | }); 198 | 199 | btnGetIsAvailable.setOnClickListener(v -> { 200 | showDialog("Get Is available", "isAvailable: " + HyperTrack.isAvailable()); 201 | }); 202 | 203 | btnGetIsTracking.setOnClickListener(v -> { 204 | showDialog("Get Is tracking", "isTracking: " + HyperTrack.isTracking()); 205 | }); 206 | 207 | btnGetLocation.setOnClickListener(v -> { 208 | showDialog("Get location", getLocationResultText(HyperTrack.getLocation())); 209 | }); 210 | 211 | btnGetMetadata.setOnClickListener(v -> { 212 | showDialog("Get metadata", "metadata: " + HyperTrack.getMetadata()); 213 | }); 214 | 215 | btnGetName.setOnClickListener(v -> { 216 | showDialog("Get name", "name: " + HyperTrack.getName()); 217 | }); 218 | 219 | btnGetOrders.setOnClickListener(v -> { 220 | showDialog("Get orders", getOrdersText(HyperTrack.getOrders())); 221 | }); 222 | 223 | btnStartPermissionsFlow.setOnClickListener(view -> { 224 | PermissionsFlow.startPermissionsFlow(this); 225 | }); 226 | 227 | errorsSubscription = HyperTrack.subscribeToErrors(errors -> { 228 | tvErrors.setText(getErrorsText(errors)); 229 | return null; 230 | }); 231 | 232 | isAvailableSubscription = HyperTrack.subscribeToIsAvailable(isAvailable -> { 233 | tvIsAvailable.setText("Is available: " + isAvailable.toString()); 234 | return null; 235 | }); 236 | 237 | isTrackingSubscription = HyperTrack.subscribeToIsTracking(isTracking -> { 238 | tvIsTracking.setText("Is tracking: " + isTracking.toString()); 239 | return null; 240 | }); 241 | 242 | locationSubscription = HyperTrack.subscribeToLocation(locationResult -> { 243 | tvLocation.setText(getLocationResultText(locationResult)); 244 | return null; 245 | }); 246 | 247 | ordersSubscription = HyperTrack.subscribeToOrders(orders -> { 248 | tvOrders.setText(getOrdersText(orders)); 249 | return null; 250 | }); 251 | } 252 | 253 | private String getErrorsText(Set errors) { 254 | StringBuilder builder = new StringBuilder(); 255 | builder.append("[\n"); 256 | 257 | for (HyperTrack.Error error : errors) { 258 | String errorName; 259 | 260 | if (error instanceof HyperTrack.Error.BlockedFromRunning) { 261 | errorName = "BlockedFromRunning"; 262 | } else if (error instanceof HyperTrack.Error.InvalidPublishableKey) { 263 | errorName = "InvalidPublishableKey"; 264 | } else if (error instanceof HyperTrack.Error.Location.Mocked) { 265 | errorName = "Location.Mocked"; 266 | } else if (error instanceof HyperTrack.Error.Location.ServicesDisabled) { 267 | errorName = "Location.ServicesDisabled"; 268 | } else if (error instanceof HyperTrack.Error.Location.ServicesUnavailable) { 269 | errorName = "Location.ServicesUnavailable"; 270 | } else if (error instanceof HyperTrack.Error.Location.SignalLost) { 271 | errorName = "Location.SignalLost"; 272 | } else if (error instanceof HyperTrack.Error.NoExemptionFromBackgroundStartRestrictions) { 273 | errorName = "NoExemptionFromBackgroundStartRestrictions"; 274 | } else if (error instanceof HyperTrack.Error.Permissions.Location.Denied) { 275 | errorName = "Permissions.Location.Denied"; 276 | } else if (error instanceof HyperTrack.Error.Permissions.Location.InsufficientForBackground) { 277 | errorName = "Permissions.Location.InsufficientForBackground"; 278 | } else if (error instanceof HyperTrack.Error.Permissions.Location.ReducedAccuracy) { 279 | errorName = "Permissions.Location.ReducedAccuracy"; 280 | } else if (error instanceof HyperTrack.Error.Permissions.Notifications.Denied) { 281 | errorName = "Permissions.Notifications.Denied"; 282 | } else { 283 | throw new RuntimeException("Unknown error type " + error.getClass().getName()); 284 | } 285 | 286 | builder.append("\t").append(errorName); 287 | builder.append(",\n"); 288 | } 289 | 290 | builder.append("]"); 291 | return builder.toString(); 292 | } 293 | 294 | private String getLocationResultText(Result locationResult) { 295 | if (locationResult instanceof Result.Success) { 296 | HyperTrack.Location location = ( 297 | (Result.Success) locationResult 298 | ).getSuccess(); 299 | return location.toString(); 300 | } else { 301 | HyperTrack.LocationError error = ( 302 | (Result.Failure) locationResult 303 | ).getFailure(); 304 | return getLocationErrorText(error); 305 | } 306 | } 307 | 308 | private String getLocationErrorText(HyperTrack.LocationError locationError) { 309 | if(locationError instanceof HyperTrack.LocationError.Errors) { 310 | HyperTrack.LocationError.Errors errors = (HyperTrack.LocationError.Errors) locationError; 311 | return getErrorsText(errors.getErrors()); 312 | } else if(locationError instanceof HyperTrack.LocationError.NotRunning) { 313 | return "Not running"; 314 | } else if(locationError instanceof HyperTrack.LocationError.Starting) { 315 | return "Starting"; 316 | } else { 317 | throw new RuntimeException("Unknown error type " + locationError.getClass().getName()); 318 | } 319 | } 320 | 321 | private String getLocationWithDeviationResultText(Result locationResult) { 322 | if (locationResult instanceof Result.Success) { 323 | HyperTrack.LocationWithDeviation location = ( 324 | (Result.Success) locationResult 325 | ).getSuccess(); 326 | return location.toString(); 327 | } else { 328 | HyperTrack.LocationError error = ( 329 | (Result.Failure) locationResult 330 | ).getFailure(); 331 | if(error instanceof HyperTrack.LocationError.Errors) { 332 | HyperTrack.LocationError.Errors errors = (HyperTrack.LocationError.Errors) error; 333 | return getErrorsText(errors.getErrors()); 334 | } else if(error instanceof HyperTrack.LocationError.NotRunning) { 335 | return "Not running"; 336 | } else if(error instanceof HyperTrack.LocationError.Starting) { 337 | return "Starting"; 338 | } else { 339 | throw new RuntimeException("Unknown error type " + error.getClass().getName()); 340 | } 341 | } 342 | } 343 | 344 | private String getLocateResultText(Result> locationResult) { 345 | if (locationResult instanceof Result.Success) { 346 | HyperTrack.Location location = ( 347 | (Result.Success>) locationResult 348 | ).getSuccess(); 349 | return location.toString(); 350 | } else { 351 | Set errors = ( 352 | (Result.Failure>) locationResult 353 | ).getFailure(); 354 | return getErrorsText(errors); 355 | } 356 | } 357 | 358 | private String getOrdersText(Map orders) { 359 | StringBuilder builder = new StringBuilder(); 360 | builder.append("[\n"); 361 | 362 | for (HyperTrack.Order order : orders.values()) { 363 | Result isInsideGeofence = order.isInsideGeofence(); 364 | String isInsideGeofenceText; 365 | 366 | if (isInsideGeofence instanceof Result.Success) { 367 | isInsideGeofenceText = ((Result.Success) isInsideGeofence).getSuccess().toString(); 368 | } else { 369 | isInsideGeofenceText = getLocationErrorText(((Result.Failure) isInsideGeofence).getFailure()); 370 | } 371 | 372 | builder.append("\t").append(order.getOrderHandle()).append(" > isInsideGeofence: ").append(isInsideGeofenceText); 373 | builder.append(",\n"); 374 | } 375 | 376 | builder.append("]"); 377 | return builder.toString(); 378 | } 379 | 380 | @Override 381 | protected void onDestroy() { 382 | if(errorsSubscription != null) { 383 | errorsSubscription.cancel(); 384 | } 385 | if(isAvailableSubscription != null) { 386 | isAvailableSubscription.cancel(); 387 | } 388 | if(isTrackingSubscription != null) { 389 | isTrackingSubscription.cancel(); 390 | } 391 | if(locationSubscription != null) { 392 | locationSubscription.cancel(); 393 | } 394 | if(locateSubscription != null) { 395 | locateSubscription.cancel(); 396 | } 397 | if(ordersSubscription != null) { 398 | ordersSubscription.cancel(); 399 | } 400 | super.onDestroy(); 401 | } 402 | 403 | private void showDialog( 404 | String title, 405 | String text 406 | ) { 407 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 408 | builder.setTitle(title); 409 | builder.setMessage(text); 410 | builder.setPositiveButton("Close", null); 411 | AlertDialog dialog = builder.create(); 412 | dialog.show(); 413 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { 414 | dialog.dismiss(); 415 | }); 416 | } 417 | 418 | } 419 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/java/com/hypertrack/quickstart/PermissionsApi.java: -------------------------------------------------------------------------------- 1 | package com.hypertrack.quickstart; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.location.LocationManager; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.provider.Settings; 9 | 10 | public class PermissionsApi { 11 | 12 | private static final int REQUEST_CODE = 4214146; 13 | 14 | static boolean isBackgroundLocationPermissionGranted(Activity activity) { 15 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 16 | return activity.checkSelfPermission( 17 | android.Manifest.permission.ACCESS_BACKGROUND_LOCATION 18 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED; 19 | } else { 20 | return true; 21 | } 22 | } 23 | 24 | static boolean isLocationPermissionGranted(Activity activity) { 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 26 | return activity.checkSelfPermission( 27 | android.Manifest.permission.ACCESS_FINE_LOCATION 28 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED; 29 | } else { 30 | return true; 31 | } 32 | } 33 | 34 | static boolean isLocationServicesEnabled(Activity activity) { 35 | return ((LocationManager) 36 | activity.getSystemService(android.content.Context.LOCATION_SERVICE) 37 | ) 38 | .isProviderEnabled(android.location.LocationManager.GPS_PROVIDER); 39 | } 40 | 41 | static boolean isNotificationsPermissionGranted(Activity activity) { 42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 43 | return activity.checkSelfPermission( 44 | android.Manifest.permission.POST_NOTIFICATIONS 45 | ) == android.content.pm.PackageManager.PERMISSION_GRANTED; 46 | } else { 47 | return true; 48 | } 49 | } 50 | 51 | static void openAppSettings(Activity activity) { 52 | activity.startActivity( 53 | new Intent( 54 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 55 | Uri.parse("package:" + activity.getPackageName()) 56 | ) 57 | ); 58 | } 59 | 60 | static void openLocationServicesSettings(Activity activity) { 61 | activity.startActivity( 62 | new Intent( 63 | Settings.ACTION_LOCATION_SOURCE_SETTINGS 64 | ) 65 | ); 66 | } 67 | 68 | static void requestLocationPermission(Activity activity) { 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 70 | activity.requestPermissions( 71 | new String[] { 72 | android.Manifest.permission.ACCESS_FINE_LOCATION, 73 | android.Manifest.permission.ACCESS_COARSE_LOCATION 74 | }, 75 | REQUEST_CODE 76 | ); 77 | } 78 | } 79 | 80 | static void requestBackgroundLocationPermission(Activity activity) { 81 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 82 | activity.requestPermissions( 83 | new String[] { 84 | android.Manifest.permission.ACCESS_BACKGROUND_LOCATION 85 | }, 86 | REQUEST_CODE 87 | ); 88 | } 89 | } 90 | 91 | static void requestNotificationsPermission(Activity activity) { 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 93 | activity.requestPermissions( 94 | new String[] { 95 | android.Manifest.permission.POST_NOTIFICATIONS 96 | }, 97 | REQUEST_CODE 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/java/com/hypertrack/quickstart/PermissionsFlow.java: -------------------------------------------------------------------------------- 1 | package com.hypertrack.quickstart; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.DialogInterface; 6 | 7 | import kotlin.Unit; 8 | import kotlin.jvm.functions.Function1; 9 | 10 | public class PermissionsFlow { 11 | 12 | static void startPermissionsFlow(Activity activity) { 13 | if (!PermissionsApi.isLocationPermissionGranted(activity)) { 14 | requestForegroundLocation(activity); 15 | } else { 16 | onForegroundLocationGranted(activity); 17 | } 18 | } 19 | 20 | private static void showDialog( 21 | Activity activity, 22 | String title, 23 | String text, 24 | Function1 onProceedClick, 25 | Function1 onGoToSettingsClick 26 | ) { 27 | AlertDialog.Builder builder = new AlertDialog.Builder(activity); 28 | builder.setTitle(title); 29 | builder.setMessage(text); 30 | builder.setPositiveButton("Proceed", null); 31 | builder.setNegativeButton("Go To Settings", null); 32 | AlertDialog dialog = builder.create(); 33 | dialog.show(); 34 | dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { 35 | onProceedClick.invoke(dialog); 36 | }); 37 | dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> { 38 | onGoToSettingsClick.invoke(dialog); 39 | }); 40 | } 41 | 42 | private static void requestForegroundLocation(Activity activity) { 43 | showDialog( 44 | activity, 45 | "Please grant Location permission", 46 | "This app collects activity and location data \n to manage your work on the move \n even when the app is closed or not in use\n\n" + 47 | "We use this data to:\n" + 48 | "- Show your movement history\n" + 49 | "- Optimize routes\n" + 50 | "- Mark places you visit\n" + 51 | "- Make expense claims easier\n" + 52 | "- Compute daily stats \n\t\t(like total driven distance)", 53 | dialog -> { 54 | if (!PermissionsApi.isLocationPermissionGranted(activity)) { 55 | PermissionsApi.requestLocationPermission(activity); 56 | } else { 57 | dialog.dismiss(); 58 | onForegroundLocationGranted(activity); 59 | } 60 | return null; 61 | }, 62 | dialog -> { 63 | PermissionsApi.openAppSettings(activity); 64 | return null; 65 | } 66 | ); 67 | } 68 | 69 | private static void onForegroundLocationGranted(Activity activity) { 70 | if (!PermissionsApi.isBackgroundLocationPermissionGranted(activity)) { 71 | requestBackgroundLocation(activity); 72 | } else { 73 | onBackgroundLocationGranted(activity); 74 | } 75 | } 76 | 77 | private static void requestBackgroundLocation(Activity activity) { 78 | showDialog( 79 | activity, 80 | "Allow All the Time Location Access", 81 | "The permission is required to automatically start tracking location based on your work schedule (without you having to open app). The app will always show a notification when tracking is active.", 82 | dialog -> { 83 | if (!PermissionsApi.isBackgroundLocationPermissionGranted(activity)) { 84 | PermissionsApi.requestBackgroundLocationPermission(activity); 85 | } else { 86 | dialog.dismiss(); 87 | onBackgroundLocationGranted(activity); 88 | } 89 | return null; 90 | }, 91 | dialog -> { 92 | PermissionsApi.openAppSettings(activity); 93 | return null; 94 | } 95 | ); 96 | } 97 | 98 | private static void onBackgroundLocationGranted(Activity activity) { 99 | if (!PermissionsApi.isNotificationsPermissionGranted(activity)) { 100 | requestNotifications(activity); 101 | } else { 102 | onNotificationsGranted(activity); 103 | } 104 | } 105 | 106 | private static void requestNotifications(Activity activity) { 107 | showDialog( 108 | activity, 109 | "Allow Notifications permission", 110 | "The permission is required by Android OS to launch the service to track your location while the app is in the background \n\n" + 111 | "Also the app will notify you about tracking status and any tracking issues.", 112 | dialog -> { 113 | if (!PermissionsApi.isNotificationsPermissionGranted(activity)) { 114 | PermissionsApi.requestNotificationsPermission(activity); 115 | } else { 116 | dialog.dismiss(); 117 | onNotificationsGranted(activity); 118 | } 119 | return null; 120 | }, 121 | dialog -> { 122 | PermissionsApi.openAppSettings(activity); 123 | return null; 124 | } 125 | ); 126 | } 127 | 128 | private static void onNotificationsGranted(Activity activity) { 129 | if (!PermissionsApi.isLocationServicesEnabled(activity)) { 130 | requestLocationServices(activity); 131 | } else { 132 | onLocationServicesEnabled(activity); 133 | } 134 | } 135 | 136 | private static void requestLocationServices(Activity activity) { 137 | showDialog( 138 | activity, 139 | "Please enable Location Services", 140 | "This app collects activity and location data \n to manage your work on the move \n even when the app is closed or not in use\n\n" + 141 | "We use this data to:\n" + 142 | "- Show your movement history\n" + 143 | "- Optimize routes\n" + 144 | "- Mark places you visit\n" + 145 | "- Make expense claims easier\n" + 146 | "- Compute daily stats \n\t\t(like total driven distance)", 147 | dialog -> { 148 | if (!PermissionsApi.isLocationServicesEnabled(activity)) { 149 | PermissionsApi.openLocationServicesSettings(activity); 150 | } else { 151 | dialog.dismiss(); 152 | onLocationServicesEnabled(activity); 153 | } 154 | return null; 155 | }, 156 | dialog -> { 157 | PermissionsApi.openLocationServicesSettings(activity); 158 | return null; 159 | } 160 | ); 161 | } 162 | 163 | private static void onLocationServicesEnabled(Activity activity) { 164 | AlertDialog.Builder builder = new AlertDialog.Builder(activity); 165 | builder.setTitle("All permissions granted"); 166 | builder.setMessage("You have granted all the permissions required for the app to work properly"); 167 | builder.setPositiveButton("OK", null); 168 | builder.show(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/res/drawable/ic_green.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /quickstart-java/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 19 | 20 | 25 | 26 | 33 | 34 | 39 | 40 | 47 | 48 | 53 | 54 | 61 | 62 | 67 | 68 |