Сравнить коммиты

...
104 Коммитов
Автор SHA1 Сообщение Дата
Anton Tananaev 88cc37e96e Update hidden icon 2023-02-10 16:07:44 -08:00
Anton Tananaev b80fde0b6d Update icons and colors 2023-02-10 16:02:44 -08:00
Anton Tananaev f734d0256f Update project files 2023-02-10 15:31:45 -08:00
Anton Tananaev 1dcc26b47b Fix dependencies 2023-01-25 07:36:55 -08:00
Anton Tananaev 5580f0b6bf Update project files 2023-01-25 07:28:33 -08:00
Anton Tananaev ddf1f0fbe0 Match shortcut intents 2023-01-09 09:31:19 -08:00
Anton Tananaev 235e58d3e2 Fix translation issues 2022-10-21 08:50:43 -07:00
Anton Tananaev 9cdb10db14 Update localization 2022-10-21 08:49:00 -07:00
Anton Tananaev 745a8523ef Update project 2022-10-21 08:17:05 -07:00
Anton Tananaev a2b2c215da Handle GPS rollover (fix #414) 2022-10-21 07:38:43 -07:00
Anton Tananaev 6d1c7b47e1 Fix Google flavor 2022-08-15 18:12:27 -07:00
Anton Tananaev 10bc96caf9 Fix typo 2022-08-14 17:38:34 -07:00
Anton Tananaev 0873aa7302 Add charging status 2022-08-14 17:28:44 -07:00
Anton Tananaev 37d3584f77 Handle deprecation 2022-08-14 17:02:22 -07:00
Anton Tananaev 404886f80e Update project files 2022-08-14 16:57:14 -07:00
Anton Tananaev a68b489681 Merge pull request #434 from aguerraVikua/master
Upgrade dependencies
2022-07-28 15:38:56 -07:00
Antonio Guerra 04e5b01cc7 ⬆️Upgrade dependencies 2022-07-19 21:31:48 -04:00
Anton Tananaev 5f059645a1 Update project 2022-05-29 10:50:11 -07:00
Anton Tananaev 8b985526dd Update version number 2022-01-30 10:18:36 -08:00
Anton Tananaev 5e8ea134e0 Attempt at fixing crashes 2022-01-30 10:18:09 -08:00
Anton Tananaev ec85edd56f Update version number 2021-11-04 19:21:19 -07:00
Anton Tananaev c5ab10919e Update dependencies 2021-11-04 19:20:08 -07:00
Anton Tananaev 3e8ba9a603 Fix Android 11 location permissions 2021-11-04 19:18:09 -07:00
Anton Tananaev 826d638ea3 Update version number 2021-10-11 23:26:51 -07:00
Anton Tananaev 1b687bd075 Handle security exception 2021-10-11 23:26:28 -07:00
Anton Tananaev 42d5584d8d Fix hidden app 2021-10-09 17:59:58 -07:00
Anton Tananaev ff88682e40 Update app version 2021-10-09 17:41:18 -07:00
Anton Tananaev 307e692459 Integrate DKMA info 2021-10-09 17:40:27 -07:00
Anton Tananaev b7ec644ecf Improve app autostart 2021-10-09 17:40:27 -07:00
Anton Tananaev 842baa2aec Merge pull request #427 from scfarley/master
Accept user CA's for connections via HTTPS proxy
2021-09-12 18:25:32 -07:00
Sean Farley 26bc92cf8e Accept user CA's for connections via HTTPS proxy
Permit Traccar client to communicate with the Traccar server via an
HTTPS proxy that uses a certificate generated from a user (non-system)
CA.
2021-09-12 21:02:20 -04:00
Anton Tananaev 6dadf5e7c3 Update project files 2021-08-29 22:13:52 -07:00
Anton Tananaev 39e9c171c4 Handle battery optimization exception 2021-08-29 18:17:31 -07:00
Anton Tananaev bcb5985cef Alarm foreground priority 2021-08-29 15:55:52 -07:00
Anton Tananaev eb868056bf Change alarm flag 2021-07-31 07:37:47 -10:00
Anton Tananaev 94e094e648 Update app version 2021-07-24 11:24:26 -07:00
Anton Tananaev 25c0434e4c Fix database management 2021-07-24 11:23:58 -07:00
Anton Tananaev 3afcfa17d0 Update version number 2021-07-20 23:12:26 -07:00
Anton Tananaev 02263d5fd9 Fix crash 2021-07-20 23:12:04 -07:00
Anton Tananaev a884b2f3d7 Reorder dependencies 2021-07-19 20:26:42 -07:00
Anton Tananaev 6910790fa7 Specify location services version 2021-07-19 20:17:53 -07:00
Anton Tananaev 6cab1a0b36 Update Firebase 2021-07-19 20:14:15 -07:00
Anton Tananaev edd4f670fe Remove unused 2021-07-19 20:03:06 -07:00
Anton Tananaev 298d7becb5 Update localization 2021-07-17 22:02:57 -07:00
Anton Tananaev 9a2e3f9126 Update app version 2021-07-17 21:57:48 -07:00
Anton Tananaev d3f7ee0c8b Convert project to Kotlin 2021-07-17 19:14:15 -07:00
Anton Tananaev 8ae486dbc8 Rename .java to .kt 2021-07-17 19:14:15 -07:00
Anton Tananaev 02295f5fcf Convert project to Kotlin 2021-07-17 19:11:29 -07:00
Anton Tananaev 958eb77c51 Rename .java to .kt 2021-07-17 19:11:29 -07:00
Anton Tananaev 0a3a8381a5 Add Kotlin support 2021-07-17 16:56:49 -07:00
Anton Tananaev 6009dba342 Update project 2021-07-17 16:51:10 -07:00
Anton Tananaev 59085f668c Remove disclosure 2020-12-16 20:50:40 -08:00
Anton Tananaev 1cb5326e07 Update app version 2020-12-14 21:21:04 -08:00
Anton Tananaev 28befc6180 Remove background location 2020-12-14 21:20:41 -08:00
Anton Tananaev e83ef80fa3 Add target SDK 2020-12-13 22:24:53 -08:00
Anton Tananaev b26a1bd133 Update app version 2020-12-13 22:21:24 -08:00
Anton Tananaev 0bbee716a6 Add background location disclosure 2020-12-13 22:00:44 -08:00
Anton Tananaev 52e3b839ae Update dependencies 2020-12-13 21:45:35 -08:00
Anton Tananaev cfe04eb0a6 Update tools 2020-12-13 21:38:17 -08:00
Anton Tananaev c243912c55 Update app version 2020-10-08 22:33:09 -07:00
Anton Tananaev 914a157afc Implement rating flow 2020-10-08 22:32:43 -07:00
Anton Tananaev 454ce6be21 Disable jetifier 2020-09-28 21:36:25 -07:00
Anton Tananaev e8a1e015d4 Enable multidex 2020-09-28 21:21:16 -07:00
Anton Tananaev ceb15c1378 Update version number 2020-09-28 21:07:42 -07:00
Anton Tananaev d25c745013 Update crashlytics 2020-09-28 21:07:17 -07:00
Anton Tananaev c24df5b246 Update notification priority 2020-09-20 11:16:41 -07:00
Anton Tananaev 283d0467b4 Add option to disable wakelock 2020-09-20 11:08:10 -07:00
Anton Tananaev 83bf1daa84 Use different method 2020-09-20 11:01:03 -07:00
Anton Tananaev 96d61c4c66 Update libraries 2020-09-20 10:50:21 -07:00
Anton Tananaev 3978a3d8d6 Update path 2020-09-06 15:05:41 -07:00
Anton Tananaev 78018c3038 Update templates 2020-07-05 11:05:41 -07:00
Anton Tananaev 19b7c4da21 Minor project file update 2020-06-19 22:28:02 -07:00
Anton Tananaev a3d43294ce Update project and tests 2020-06-10 21:14:19 -07:00
Anton Tananaev 330dc27f0f Update version number 2020-04-22 22:30:03 -07:00
Anton Tananaev f052be83c3 Update libraries 2020-04-22 22:29:27 -07:00
Anton Tananaev c77d4b477b Update localization files 2020-04-22 22:24:43 -07:00
Anton Tananaev eb7de9eb98 Fix numeric input type 2020-02-03 21:50:28 -08:00
Anton Tananaev 3a786b14b7 Option to disable buffering (fix #411) 2020-02-02 20:55:12 -08:00
Anton Tananaev 6bad9786ec Set default flavor 2020-01-26 22:33:40 -08:00
Anton Tananaev 259aaf2000 Update Play Store icon 2019-11-30 12:52:15 -08:00
Anton Tananaev aa00a33f5b Fix build issue 2019-11-24 21:32:05 -08:00
Anton Tananaev ab86076b9b Update version number 2019-11-24 11:01:57 -08:00
Anton Tananaev fed2a73999 Implement dark theme support 2019-11-24 11:00:32 -08:00
Anton Tananaev 42c210763c Update localization 2019-11-24 10:34:11 -08:00
Anton Tananaev faa05dea5a Update app version 2019-11-17 13:08:25 -08:00
Anton Tananaev 453edfa97e Temporary downgrade libraries 2019-11-17 12:56:15 -08:00
Anton Tananaev 4c6c79b6a2 Remove about screen 2019-11-11 23:51:04 -08:00
Anton Tananaev 93456bf7fb Target Android 10 2019-09-26 00:04:21 -07:00
Anton Tananaev a2ff7fcb38 Update version number 2019-05-04 14:25:00 -07:00
Anton Tananaev 7015f75edb Update project files 2019-05-04 14:23:37 -07:00
Anton Tananaev 636ceaf027 Keep wake lock for service 2019-05-04 14:13:12 -07:00
Anton Tananaev 350b800184 Update app version 2019-01-26 20:36:35 -08:00
Anton Tananaev 7ecba2cffd Update translations (fix #393) 2019-01-26 20:33:59 -08:00
Anton Tananaev 5d8f0dfb83 Update app version 2019-01-06 14:49:43 -08:00
Anton Tananaev f42ecf9f09 Use fused location provider 2019-01-06 14:48:47 -08:00
Anton Tananaev 556856ab5c Update copyright notice 2019-01-06 14:23:45 -08:00
Anton Tananaev f9f37ead08 Split position provider implementation 2019-01-06 14:22:19 -08:00
Anton Tananaev b3ee99140e Fix unit tests 2019-01-06 12:12:51 -08:00
Anton Tananaev 85f1e09308 Fix compilation issue 2019-01-06 12:03:38 -08:00
Anton Tananaev 20fd20f425 Migrate to AndroidX libraries 2019-01-06 11:56:36 -08:00
Anton Tananaev 1ae7b27683 Use app compat classes 2019-01-06 11:44:08 -08:00
Anton Tananaev f5b12b9e74 Update project files 2019-01-06 11:41:08 -08:00
Anton Tananaev d2f653034d Update app version 2018-11-06 20:58:48 +13:00
Anton Tananaev ab5aec9721 Improvements in permissions handling 2018-11-06 20:57:17 +13:00
180 изменённых файлов: 3455 добавлений и 2577 удалений
+1
Просмотреть файл
@@ -0,0 +1 @@
custom: https://www.traccar.org/donate/
-7
Просмотреть файл
@@ -1,7 +0,0 @@
<!--
Do you want to ask a question? Traccar official forum is the best place for getting support:
https://www.traccar.org/forums/
-->
+38
Просмотреть файл
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
Просмотреть файл
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+45 -24
Просмотреть файл
@@ -1,29 +1,35 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android { android {
compileSdkVersion 28 compileSdkVersion 33
buildToolsVersion '28.0.2'
defaultConfig { defaultConfig {
applicationId "org.traccar.client" applicationId 'org.traccar.client'
buildConfigField "boolean", "HIDDEN_APP", "false" buildConfigField 'boolean', 'HIDDEN_APP', 'false'
minSdkVersion 15 minSdkVersion 16
targetSdkVersion 28 targetSdkVersion 33
versionCode 52 versionCode 81
versionName '5.15' versionName '7.0'
multiDexEnabled true
}
namespace 'org.traccar.client'
packagingOptions {
resources {
excludes += ['META-INF/DEPENDENCIES']
}
} }
lintOptions { flavorDimensions 'default'
checkReleaseBuilds false
}
flavorDimensions "default"
productFlavors { productFlavors {
regular regular {
isDefault = true
}
google google
hidden { hidden {
applicationId "org.traccar.client.hidden" applicationId 'org.traccar.client.hidden'
buildConfigField "boolean", "HIDDEN_APP", "true" buildConfigField 'boolean', 'HIDDEN_APP', 'true'
} }
} }
@@ -32,22 +38,37 @@ android {
includeAndroidResources = true includeAndroidResources = true
} }
} }
lint {
checkReleaseBuilds false
}
} }
dependencies { dependencies {
implementation 'com.android.support:design:28.0.0-rc02' implementation 'com.google.android.material:material:1.6.1' // wait for preference-ktx update
testImplementation 'junit:junit:4.12' implementation 'androidx.multidex:multidex:2.0.1'
testImplementation 'org.robolectric:robolectric:3.8' implementation 'androidx.preference:preference-ktx:1.2.0'
googleImplementation 'com.google.firebase:firebase-core:16.0.3' implementation 'androidx.core:core-ktx:1.9.0'
googleImplementation 'com.crashlytics.sdk.android:crashlytics:2.9.5' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.github.judemanutd:autostarter:1.1.0'
implementation('dev.doubledot.doki:library:0.0.1@aar') {
transitive = true
}
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.7.3'
googleImplementation platform('com.google.firebase:firebase-bom:28.2.1')
googleImplementation 'com.google.firebase:firebase-core'
googleImplementation 'com.google.firebase:firebase-analytics'
googleImplementation 'com.google.firebase:firebase-crashlytics'
googleImplementation 'com.google.android.gms:play-services-location:21.0.1'
googleImplementation 'com.google.android.play:core:1.10.3'
} }
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Google")) { if (getGradle().getStartParameter().getTaskRequests().toString().contains('Google')) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric' apply plugin: 'com.google.firebase.crashlytics'
task copyJson(type: Copy) { task copyJson(type: Copy) {
from '../../traccar-shared/google-services' from '../../environment/google-services'
into '.' into '.'
include 'traccar-client-android.json' include 'traccar-client-android.json'
rename('traccar-client-android.json', 'google-services.json') rename('traccar-client-android.json', 'google-services.json')
+2 -1
Просмотреть файл
@@ -5,7 +5,8 @@
<application <application
android:name="org.traccar.client.GoogleMainApplication" android:name="org.traccar.client.GoogleMainApplication"
tools:replace="android:name"> tools:replace="android:name"
tools:ignore="GoogleAppIndexingWarning">
</application> </application>
+61
Просмотреть файл
@@ -0,0 +1,61 @@
/*
* Copyright 2017 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.app.Activity
import android.content.IntentFilter
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.android.play.core.tasks.Task
import com.google.firebase.analytics.FirebaseAnalytics
class GoogleMainApplication : MainApplication() {
private var firebaseAnalytics: FirebaseAnalytics? = null
override fun onCreate() {
super.onCreate()
firebaseAnalytics = FirebaseAnalytics.getInstance(this)
val filter = IntentFilter()
filter.addAction(TrackingService.ACTION_STARTED)
filter.addAction(TrackingService.ACTION_STOPPED)
registerReceiver(ServiceReceiver(), filter)
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
override fun handleRatingFlow(activity: Activity) {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val ratingShown = preferences.getBoolean(KEY_RATING_SHOWN, false)
val totalDuration = preferences.getLong(ServiceReceiver.KEY_DURATION, 0)
if (!ratingShown && totalDuration > RATING_THRESHOLD) {
val reviewManager = ReviewManagerFactory.create(activity)
reviewManager.requestReviewFlow().addOnCompleteListener { infoTask: Task<ReviewInfo?> ->
if (infoTask.isSuccessful) {
val flow = reviewManager.launchReviewFlow(activity, infoTask.result)
flow.addOnCompleteListener { preferences.edit().putBoolean(KEY_RATING_SHOWN, true).apply() }
}
}
}
}
companion object {
private const val KEY_RATING_SHOWN = "ratingShown"
private const val RATING_THRESHOLD = -24 * 60 * 60 * 1000L
}
}
+66
Просмотреть файл
@@ -0,0 +1,66 @@
/*
* Copyright 2019 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.annotation.SuppressLint
import android.content.Context
import android.os.Looper
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
class GooglePositionProvider(context: Context, listener: PositionListener) : PositionProvider(context, listener) {
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
@Suppress("DEPRECATION", "MissingPermission")
override fun startUpdates() {
val locationRequest = LocationRequest()
locationRequest.priority = getPriority(preferences.getString(MainFragment.KEY_ACCURACY,"medium"))
locationRequest.interval = if (distance > 0 || angle > 0) MINIMUM_INTERVAL else interval
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
override fun stopUpdates() {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
@SuppressLint("MissingPermission")
override fun requestSingleLocation() {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
listener.onPositionUpdate(Position(deviceId, location, getBatteryStatus(context)))
}
}
}
private val locationCallback: LocationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
for (location in locationResult.locations) {
processLocation(location)
}
}
}
private fun getPriority(accuracy: String?): Int {
return when (accuracy) {
"high" -> LocationRequest.PRIORITY_HIGH_ACCURACY
"low" -> LocationRequest.PRIORITY_LOW_POWER
else -> LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
}
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright 2017 Anton Tananaev (anton.tananaev@gmail.com) * Copyright 2019 - 2021 Anton Tananaev (anton@traccar.org)
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -13,20 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.traccar.client; package org.traccar.client
import android.os.Bundle; import android.content.Context
import android.support.annotation.Nullable; import org.traccar.client.PositionProvider.PositionListener
import android.support.v7.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity { object PositionProviderFactory {
@Override fun create(context: Context, listener: PositionListener): PositionProvider {
protected void onCreate(@Nullable Bundle savedInstanceState) { return GooglePositionProvider(context, listener)
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
getFragmentManager().beginTransaction().replace(android.R.id.content, new MainFragment()).commit();
} }
} }
}
+44
Просмотреть файл
@@ -0,0 +1,44 @@
/*
* Copyright 2020 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
class ServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (TrackingService.ACTION_STARTED == intent.action) {
startTime = System.currentTimeMillis()
} else if (startTime > 0) {
updateTime(context, System.currentTimeMillis() - startTime)
startTime = 0
}
}
private fun updateTime(context: Context, duration: Long) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val totalDuration = preferences.getLong(KEY_DURATION, 0)
preferences.edit().putLong(KEY_DURATION, totalDuration + duration).apply()
}
companion object {
const val KEY_DURATION = "serviceTime"
private var startTime: Long = 0
}
}
+5 -2
Просмотреть файл
@@ -9,7 +9,8 @@
tools:replace="android:label"> tools:replace="android:label">
<activity <activity
android:name="org.traccar.client.MainActivity"> android:name="org.traccar.client.MainActivity"
android:exported="true">
<intent-filter android:label="@string/app_name"> <intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -18,7 +19,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name="org.traccar.client.DialLaunchReceiver" > <receiver
android:name="org.traccar.client.DialLaunchReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" /> <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter> </intent-filter>
+26
Просмотреть файл
@@ -0,0 +1,26 @@
/*
* Copyright 2019 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.Context
import org.traccar.client.PositionProvider.PositionListener
object PositionProviderFactory {
fun create(context: Context, listener: PositionListener): PositionProvider {
return AndroidPositionProvider(context, listener)
}
}
+15
Просмотреть файл
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="5.04"
android:translateY="5.04">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</group>
</vector>
+3 -2
Просмотреть файл
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/> <background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
-5
Просмотреть файл
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.3 KiB

После

Ширина:  |  Высота:  |  Размер: 3.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.1 KiB

После

Ширина:  |  Высота:  |  Размер: 2.2 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.2 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.7 KiB

После

Ширина:  |  Высота:  |  Размер: 5.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 7.6 KiB

После

Ширина:  |  Высота:  |  Размер: 8.2 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.7 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 11 KiB

После

Ширина:  |  Высота:  |  Размер: 12 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.4 KiB

+29 -9
Просмотреть файл
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest <manifest
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
package="org.traccar.client"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -12,13 +13,20 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="oppo.permission.OPPO_COMPONENT_SAFE" />
<uses-permission android:name="com.huawei.permission.external_app_settings.USE_COMPONENT" />
<uses-feature android:name="android.hardware.location.network" />
<uses-feature android:name="android.hardware.location.gps" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:name=".MainApplication"> android:name=".MainApplication"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<meta-data <meta-data
android:name="google_analytics_adid_collection_enabled" android:name="google_analytics_adid_collection_enabled"
@@ -26,10 +34,12 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity-alias <activity-alias
android:name=".Launcher" android:name=".Launcher"
android:exported="true"
android:targetActivity=".MainActivity"> android:targetActivity=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -40,24 +50,34 @@
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity-alias> </activity-alias>
<activity android:name=".StatusActivity"/> <activity
android:name=".StatusActivity"
<activity android:name=".AboutActivity"/> android:exported="false" />
<activity <activity
android:name=".ShortcutActivity" android:name=".ShortcutActivity"
android:exported="true"
android:label="@string/menu_shortcuts"> android:label="@string/menu_shortcuts">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.VIEW" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" /> <action android:name="android.intent.action.CREATE_SHORTCUT" />
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".TrackingService" /> <service
android:name=".TrackingService"
android:exported="false"
android:foregroundServiceType="location" />
<service android:name=".TrackingService$HideNotificationService" /> <service
android:name=".TrackingService$HideNotificationService"
android:exported="false" />
<receiver android:name=".AutostartReceiver"> <receiver
android:name=".AutostartReceiver"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 21 KiB

Двоичные данные
Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 48 KiB

-39
Просмотреть файл
@@ -1,39 +0,0 @@
/*
* Copyright 2012 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
public class AboutActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.about);
TextView title = findViewById(R.id.title);
try {
title.setText(title.getText() + " " + getPackageManager().getPackageInfo(this.getPackageName(), 0).versionName);
} catch (NameNotFoundException e) {
Log.w(AboutActivity.class.getSimpleName(), e);
}
}
}
+83
Просмотреть файл
@@ -0,0 +1,83 @@
/*
* Copyright 2019 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Looper
class AndroidPositionProvider(context: Context, listener: PositionListener) : PositionProvider(context, listener), LocationListener {
private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val provider = getProvider(preferences.getString(MainFragment.KEY_ACCURACY, "medium"))
@SuppressLint("MissingPermission")
override fun startUpdates() {
try {
locationManager.requestLocationUpdates(
provider, if (distance > 0 || angle > 0) MINIMUM_INTERVAL else interval, 0f, this)
} catch (e: RuntimeException) {
listener.onPositionError(e)
}
}
override fun stopUpdates() {
locationManager.removeUpdates(this)
}
@Suppress("DEPRECATION", "MissingPermission")
override fun requestSingleLocation() {
try {
val location = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
if (location != null) {
listener.onPositionUpdate(Position(deviceId, location, getBatteryStatus(context)))
} else {
locationManager.requestSingleUpdate(provider, object : LocationListener {
override fun onLocationChanged(location: Location) {
listener.onPositionUpdate(Position(deviceId, location, getBatteryStatus(context)))
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}, Looper.myLooper())
}
} catch (e: RuntimeException) {
listener.onPositionError(e)
}
}
override fun onLocationChanged(location: Location) {
processLocation(location)
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
private fun getProvider(accuracy: String?): String {
return when (accuracy) {
"high" -> LocationManager.GPS_PROVIDER
"low" -> LocationManager.PASSIVE_PROVIDER
else -> LocationManager.NETWORK_PROVIDER
}
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright 2013 - 2017 Anton Tananaev (anton.tananaev@gmail.com) * Copyright 2013 - 2021 Anton Tananaev (anton@traccar.org)
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -13,20 +13,19 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.traccar.client; package org.traccar.client
import android.content.Context; import android.content.Context
import android.content.Intent; import android.content.Intent
import android.content.SharedPreferences; import androidx.preference.PreferenceManager
import android.preference.PreferenceManager;
public class AutostartReceiver extends WakefulBroadcastReceiver { class AutostartReceiver : WakefulBroadcastReceiver() {
@Override @Suppress("UnsafeProtectedBroadcastReceiver")
public void onReceive(Context context, Intent intent) { override fun onReceive(context: Context, intent: Intent) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
if (sharedPreferences.getBoolean(MainFragment.KEY_STATUS, false)) { if (sharedPreferences.getBoolean(MainFragment.KEY_STATUS, false)) {
startWakefulForegroundService(context, new Intent(context, TrackingService.class)); startWakefulForegroundService(context, Intent(context, TrackingService::class.java))
} }
} }
+102
Просмотреть файл
@@ -0,0 +1,102 @@
/*
* Copyright 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.preference.PreferenceManager
import com.judemanutd.autostarter.AutoStartPermissionHelper
class BatteryOptimizationHelper {
private fun showDialog(context: Context, onSuccess: () -> Unit) {
val builder = AlertDialog.Builder(context)
builder.setMessage(context.getString(R.string.request_exception))
builder.setPositiveButton(android.R.string.ok) { _, _ -> onSuccess() }
builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
}
private fun requestVendorException(context: Context) {
val vendorIntentList = listOf(
Intent().setComponent(ComponentName("com.miui.securitycenter", "com.miui.permcenter.autostart.AutoStartManagementActivity")),
Intent().setComponent(ComponentName("com.letv.android.letvsafe", "com.letv.android.letvsafe.AutobootManageActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity")),
Intent().setComponent(ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity")),
Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.coloros.safecenter", "com.coloros.safecenter.startupapp.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity")),
Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")),
Intent().setComponent(ComponentName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")),
Intent().setComponent(ComponentName("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")),
Intent().setComponent(ComponentName("com.samsung.android.lool", "com.samsung.android.sm.ui.battery.BatteryActivity")),
Intent().setComponent(ComponentName("com.htc.pitroad", "com.htc.pitroad.landingpage.activity.LandingPageActivity")),
Intent().setComponent(ComponentName("com.asus.mobilemanager", "com.asus.mobilemanager.MainActivity")),
)
for (vendorIntent in vendorIntentList) {
if (context.packageManager.resolveActivity(vendorIntent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
try {
context.startActivity(vendorIntent)
return
} catch (e: Exception) {
continue
}
}
}
}
fun requestException(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
if (!sharedPreferences.getBoolean(KEY_EXCEPTION_REQUESTED, false)) {
sharedPreferences.edit().putBoolean(KEY_EXCEPTION_REQUESTED, true).apply()
val powerManager = context.getSystemService(PowerManager::class.java)
if (!powerManager.isIgnoringBatteryOptimizations(context.packageName)) {
showDialog(context) {
try {
context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
} catch (e: ActivityNotFoundException) {
requestVendorException(context)
}
}
return true
}
} else if (!sharedPreferences.getBoolean(KEY_AUTOSTART_REQUESTED, false)) {
sharedPreferences.edit().putBoolean(KEY_AUTOSTART_REQUESTED, true).apply()
try {
if (AutoStartPermissionHelper.getInstance().getAutoStartPermission(context)) {
return true
}
} catch (e: SecurityException) {
}
}
}
return false
}
companion object {
private const val KEY_EXCEPTION_REQUESTED = "exceptionRequested"
private const val KEY_AUTOSTART_REQUESTED = "autostartRequested"
}
}
+21
Просмотреть файл
@@ -0,0 +1,21 @@
/*
* Copyright 2022 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
data class BatteryStatus(
val level: Double = 0.0,
val charging: Boolean = false,
)
-180
Просмотреть файл
@@ -1,180 +0,0 @@
/*
* Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import java.util.Date;
public class DatabaseHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 3;
public static final String DATABASE_NAME = "traccar.db";
public interface DatabaseHandler<T> {
void onComplete(boolean success, T result);
}
private static abstract class DatabaseAsyncTask<T> extends AsyncTask<Void, Void, T> {
private DatabaseHandler<T> handler;
private RuntimeException error;
public DatabaseAsyncTask(DatabaseHandler<T> handler) {
this.handler = handler;
}
@Override
protected T doInBackground(Void... params) {
try {
return executeMethod();
} catch (RuntimeException error) {
this.error = error;
return null;
}
}
protected abstract T executeMethod();
@Override
protected void onPostExecute(T result) {
handler.onComplete(error == null, result);
}
}
private SQLiteDatabase db;
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
db = getWritableDatabase();
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE position (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"deviceId TEXT," +
"time INTEGER," +
"latitude REAL," +
"longitude REAL," +
"altitude REAL," +
"speed REAL," +
"course REAL," +
"accuracy REAL," +
"battery REAL," +
"mock INTEGER)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS position;");
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS position;");
onCreate(db);
}
public void insertPosition(Position position) {
ContentValues values = new ContentValues();
values.put("deviceId", position.getDeviceId());
values.put("time", position.getTime().getTime());
values.put("latitude", position.getLatitude());
values.put("longitude", position.getLongitude());
values.put("altitude", position.getAltitude());
values.put("speed", position.getSpeed());
values.put("course", position.getCourse());
values.put("accuracy", position.getAccuracy());
values.put("battery", position.getBattery());
values.put("mock", position.getMock() ? 1 : 0);
db.insertOrThrow("position", null, values);
}
public void insertPositionAsync(final Position position, DatabaseHandler<Void> handler) {
new DatabaseAsyncTask<Void>(handler) {
@Override
protected Void executeMethod() {
insertPosition(position);
return null;
}
}.execute();
}
public Position selectPosition() {
Position position = new Position();
Cursor cursor = db.rawQuery("SELECT * FROM position ORDER BY id LIMIT 1", null);
try {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
position.setId(cursor.getLong(cursor.getColumnIndex("id")));
position.setDeviceId(cursor.getString(cursor.getColumnIndex("deviceId")));
position.setTime(new Date(cursor.getLong(cursor.getColumnIndex("time"))));
position.setLatitude(cursor.getDouble(cursor.getColumnIndex("latitude")));
position.setLongitude(cursor.getDouble(cursor.getColumnIndex("longitude")));
position.setAltitude(cursor.getDouble(cursor.getColumnIndex("altitude")));
position.setSpeed(cursor.getDouble(cursor.getColumnIndex("speed")));
position.setCourse(cursor.getDouble(cursor.getColumnIndex("course")));
position.setAccuracy(cursor.getDouble(cursor.getColumnIndex("accuracy")));
position.setBattery(cursor.getDouble(cursor.getColumnIndex("battery")));
position.setMock(cursor.getInt(cursor.getColumnIndex("mock")) > 0);
} else {
return null;
}
} finally {
cursor.close();
}
return position;
}
public void selectPositionAsync(DatabaseHandler<Position> handler) {
new DatabaseAsyncTask<Position>(handler) {
@Override
protected Position executeMethod() {
return selectPosition();
}
}.execute();
}
public void deletePosition(long id) {
if (db.delete("position", "id = ?", new String[] { String.valueOf(id) }) != 1) {
throw new SQLException();
}
}
public void deletePositionAsync(final long id, DatabaseHandler<Void> handler) {
new DatabaseAsyncTask<Void>(handler) {
@Override
protected Void executeMethod() {
deletePosition(id);
return null;
}
}.execute();
}
}
+159
Просмотреть файл
@@ -0,0 +1,159 @@
/*
* Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION", "StaticFieldLeak")
package org.traccar.client
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.database.SQLException
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.AsyncTask
import java.sql.Date
class DatabaseHelper(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
interface DatabaseHandler<T> {
fun onComplete(success: Boolean, result: T)
}
private abstract class DatabaseAsyncTask<T>(val handler: DatabaseHandler<T?>) : AsyncTask<Unit, Unit, T?>() {
private var error: RuntimeException? = null
override fun doInBackground(vararg params: Unit): T? {
return try {
executeMethod()
} catch (error: RuntimeException) {
this.error = error
null
}
}
protected abstract fun executeMethod(): T
override fun onPostExecute(result: T?) {
handler.onComplete(error == null, result)
}
}
private val db: SQLiteDatabase = writableDatabase
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(
"CREATE TABLE position (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"deviceId TEXT," +
"time INTEGER," +
"latitude REAL," +
"longitude REAL," +
"altitude REAL," +
"speed REAL," +
"course REAL," +
"accuracy REAL," +
"battery REAL," +
"charging INTEGER," +
"mock INTEGER)"
)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS position;")
onCreate(db)
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS position;")
onCreate(db)
}
fun insertPosition(position: Position) {
val values = ContentValues()
values.put("deviceId", position.deviceId)
values.put("time", position.time.time)
values.put("latitude", position.latitude)
values.put("longitude", position.longitude)
values.put("altitude", position.altitude)
values.put("speed", position.speed)
values.put("course", position.course)
values.put("accuracy", position.accuracy)
values.put("battery", position.battery)
values.put("charging", if (position.charging) 1 else 0)
values.put("mock", if (position.mock) 1 else 0)
db.insertOrThrow("position", null, values)
}
fun insertPositionAsync(position: Position, handler: DatabaseHandler<Unit?>) {
object : DatabaseAsyncTask<Unit>(handler) {
override fun executeMethod() {
insertPosition(position)
}
}.execute()
}
@SuppressLint("Range")
fun selectPosition(): Position? {
db.rawQuery("SELECT * FROM position ORDER BY id LIMIT 1", null).use { cursor ->
if (cursor.count > 0) {
cursor.moveToFirst()
return Position(
id = cursor.getLong(cursor.getColumnIndex("id")),
deviceId = cursor.getString(cursor.getColumnIndex("deviceId")),
time = Date(cursor.getLong(cursor.getColumnIndex("time"))),
latitude = cursor.getDouble(cursor.getColumnIndex("latitude")),
longitude = cursor.getDouble(cursor.getColumnIndex("longitude")),
altitude = cursor.getDouble(cursor.getColumnIndex("altitude")),
speed = cursor.getDouble(cursor.getColumnIndex("speed")),
course = cursor.getDouble(cursor.getColumnIndex("course")),
accuracy = cursor.getDouble(cursor.getColumnIndex("accuracy")),
battery = cursor.getDouble(cursor.getColumnIndex("battery")),
charging = cursor.getInt(cursor.getColumnIndex("charging")) > 0,
mock = cursor.getInt(cursor.getColumnIndex("mock")) > 0,
)
}
}
return null
}
fun selectPositionAsync(handler: DatabaseHandler<Position?>) {
object : DatabaseAsyncTask<Position?>(handler) {
override fun executeMethod(): Position? {
return selectPosition()
}
}.execute()
}
fun deletePosition(id: Long) {
if (db.delete("position", "id = ?", arrayOf(id.toString())) != 1) {
throw SQLException()
}
}
fun deletePositionAsync(id: Long, handler: DatabaseHandler<Unit?>) {
object : DatabaseAsyncTask<Unit>(handler) {
override fun executeMethod() {
deletePosition(id)
}
}.execute()
}
companion object {
const val DATABASE_VERSION = 4
const val DATABASE_NAME = "traccar.db"
}
}
-37
Просмотреть файл
@@ -1,37 +0,0 @@
/*
* Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class DialLaunchReceiver extends BroadcastReceiver {
private static final String LAUNCHER_NUMBER = "8722227"; // TRACCAR
@Override
public void onReceive(Context context, Intent intent) {
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
if (phoneNumber.equals(LAUNCHER_NUMBER)) {
setResultData(null);
Intent appIntent = new Intent(context, MainActivity.class);
appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(appIntent);
}
}
}
+38
Просмотреть файл
@@ -0,0 +1,38 @@
/*
* Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class DialLaunchReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER)
if (phoneNumber == LAUNCHER_NUMBER) {
resultData = null
val appIntent = Intent(context, MainActivity::class.java)
appIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(appIntent)
}
}
companion object {
private const val LAUNCHER_NUMBER = "8722227" // TRACCAR
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright 2017 Anton Tananaev (anton.tananaev@gmail.com) * Copyright 2017 - 2021 Anton Tananaev (anton@traccar.org)
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -13,18 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.traccar.client; package org.traccar.client
import com.google.firebase.analytics.FirebaseAnalytics; import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
public class GoogleMainApplication extends MainApplication { class MainActivity : AppCompatActivity() {
private FirebaseAnalytics firebaseAnalytics; override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@Override setContentView(R.layout.main)
public void onCreate() {
super.onCreate();
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
} }
} }
-73
Просмотреть файл
@@ -1,73 +0,0 @@
/*
* Copyright 2016 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
public class MainApplication extends Application {
public static final String PRIMARY_CHANNEL = "default";
@Override
public void onCreate() {
super.onCreate();
System.setProperty("http.keepAliveDuration", String.valueOf(30 * 60 * 1000));
migrateLegacyPreferences(PreferenceManager.getDefaultSharedPreferences(this));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerChannel();
}
}
@TargetApi(Build.VERSION_CODES.O)
private void registerChannel() {
NotificationChannel channel = new NotificationChannel(
PRIMARY_CHANNEL, getString(R.string.channel_default), NotificationManager.IMPORTANCE_MIN);
channel.setLightColor(Color.GREEN);
channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
}
private void migrateLegacyPreferences(SharedPreferences preferences) {
String port = preferences.getString("port", null);
if (port != null) {
String host = preferences.getString("address", getString(R.string.settings_url_default_value));
String scheme = preferences.getBoolean("secure", false) ? "https" : "http";
Uri.Builder builder = new Uri.Builder();
builder.scheme(scheme).encodedAuthority(host + ":" + port).build();
SharedPreferences.Editor editor = preferences.edit();
editor.putString(MainFragment.KEY_URL, builder.toString());
editor.remove("port");
editor.remove("address");
editor.remove("secure");
editor.apply();
}
}
}
+53
Просмотреть файл
@@ -0,0 +1,53 @@
/*
* Copyright 2016 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import androidx.multidex.MultiDexApplication
import android.annotation.TargetApi
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Notification
import android.graphics.Color
import android.os.Build
import android.app.Activity
open class MainApplication : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
System.setProperty("http.keepAliveDuration", (30 * 60 * 1000).toString())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
registerChannel()
}
}
@TargetApi(Build.VERSION_CODES.O)
private fun registerChannel() {
val channel = NotificationChannel(
PRIMARY_CHANNEL, getString(R.string.channel_default), NotificationManager.IMPORTANCE_LOW
)
channel.lightColor = Color.GREEN
channel.lockscreenVisibility = Notification.VISIBILITY_SECRET
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)
}
open fun handleRatingFlow(activity: Activity) {}
companion object {
const val PRIMARY_CHANNEL = "default"
}
}
-278
Просмотреть файл
@@ -1,278 +0,0 @@
/*
* Copyright 2012 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.TwoStatePreference;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.webkit.URLUtil;
import android.widget.Toast;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class MainFragment extends PreferenceFragment implements OnSharedPreferenceChangeListener {
private static final String TAG = MainFragment.class.getSimpleName();
private static final int ALARM_MANAGER_INTERVAL = 15000;
public static final String KEY_DEVICE = "id";
public static final String KEY_URL = "url";
public static final String KEY_INTERVAL = "interval";
public static final String KEY_DISTANCE = "distance";
public static final String KEY_ANGLE = "angle";
public static final String KEY_ACCURACY = "accuracy";
public static final String KEY_STATUS = "status";
private static final int PERMISSIONS_REQUEST_LOCATION = 2;
private SharedPreferences sharedPreferences;
private AlarmManager alarmManager;
private PendingIntent alarmIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.HIDDEN_APP) {
removeLauncherIcon();
}
setHasOptionsMenu(true);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
addPreferencesFromResource(R.xml.preferences);
initPreferences();
findPreference(KEY_DEVICE).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
return newValue != null && !newValue.equals("");
}
});
findPreference(KEY_URL).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
return (newValue != null) && validateServerURL(newValue.toString());
}
});
findPreference(KEY_INTERVAL).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue != null) {
try {
int value = Integer.parseInt((String) newValue);
return value > 0;
} catch (NumberFormatException e) {
Log.w(TAG, e);
}
}
return false;
}
});
Preference.OnPreferenceChangeListener numberValidationListener = new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue != null) {
try {
int value = Integer.parseInt((String) newValue);
return value >= 0;
} catch (NumberFormatException e) {
Log.w(TAG, e);
}
}
return false;
}
};
findPreference(KEY_DISTANCE).setOnPreferenceChangeListener(numberValidationListener);
findPreference(KEY_ANGLE).setOnPreferenceChangeListener(numberValidationListener);
alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
alarmIntent = PendingIntent.getBroadcast(getActivity(), 0, new Intent(getActivity(), AutostartReceiver.class), 0);
if (sharedPreferences.getBoolean(KEY_STATUS, false)) {
startTrackingService(true, false);
}
}
private void removeLauncherIcon() {
String className = MainActivity.class.getCanonicalName().replace(".MainActivity", ".Launcher");
ComponentName componentName = new ComponentName(getActivity().getPackageName(), className);
PackageManager packageManager = getActivity().getPackageManager();
if (packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
packageManager.setComponentEnabledSetting(
componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.hidden_alert));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
}
@Override
public void onResume() {
super.onResume();
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
super.onPause();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
private void setPreferencesEnabled(boolean enabled) {
findPreference(KEY_DEVICE).setEnabled(enabled);
findPreference(KEY_URL).setEnabled(enabled);
findPreference(KEY_INTERVAL).setEnabled(enabled);
findPreference(KEY_DISTANCE).setEnabled(enabled);
findPreference(KEY_ANGLE).setEnabled(enabled);
findPreference(KEY_ACCURACY).setEnabled(enabled);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(KEY_STATUS)) {
if (sharedPreferences.getBoolean(KEY_STATUS, false)) {
startTrackingService(true, false);
} else {
stopTrackingService();
}
} else if (key.equals(KEY_DEVICE)) {
findPreference(KEY_DEVICE).setSummary(sharedPreferences.getString(KEY_DEVICE, null));
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.status) {
startActivity(new Intent(getActivity(), StatusActivity.class));
return true;
} else if (item.getItemId() == R.id.about) {
startActivity(new Intent(getActivity(), AboutActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
private void initPreferences() {
PreferenceManager.setDefaultValues(getActivity(), R.xml.preferences, false);
if (!sharedPreferences.contains(KEY_DEVICE)) {
String id = String.valueOf(new Random().nextInt(900000) + 100000);
sharedPreferences.edit().putString(KEY_DEVICE, id).apply();
((EditTextPreference) findPreference(KEY_DEVICE)).setText(id);
}
findPreference(KEY_DEVICE).setSummary(sharedPreferences.getString(KEY_DEVICE, null));
}
private void startTrackingService(boolean checkPermission, boolean permission) {
if (checkPermission) {
Set<String> missingPermissions = new HashSet<>();
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
missingPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
}
if (missingPermissions.isEmpty()) {
permission = true;
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]),
PERMISSIONS_REQUEST_LOCATION);
}
return;
}
}
if (permission) {
setPreferencesEnabled(false);
ContextCompat.startForegroundService(getActivity(), new Intent(getActivity(), TrackingService.class));
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
ALARM_MANAGER_INTERVAL, ALARM_MANAGER_INTERVAL, alarmIntent);
} else {
sharedPreferences.edit().putBoolean(KEY_STATUS, false).apply();
TwoStatePreference preference = (TwoStatePreference) findPreference(KEY_STATUS);
preference.setChecked(false);
}
}
private void stopTrackingService() {
alarmManager.cancel(alarmIntent);
getActivity().stopService(new Intent(getActivity(), TrackingService.class));
setPreferencesEnabled(true);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSIONS_REQUEST_LOCATION) {
boolean granted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
granted = false;
break;
}
}
startTrackingService(false, granted);
}
}
private boolean validateServerURL(String userUrl) {
int port = Uri.parse(userUrl).getPort();
if (URLUtil.isValidUrl(userUrl) && (port == -1 || (port > 0 && port <= 65535))
&& (URLUtil.isHttpUrl(userUrl) || URLUtil.isHttpsUrl(userUrl))) {
return true;
}
Toast.makeText(getActivity(), R.string.error_msg_invalid_url, Toast.LENGTH_LONG).show();
return false;
}
}
+330
Просмотреть файл
@@ -0,0 +1,330 @@
/*
* Copyright 2012 - 2022 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.webkit.URLUtil
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.preference.EditTextPreference
import androidx.preference.EditTextPreferenceDialogFragmentCompat
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.TwoStatePreference
import dev.doubledot.doki.ui.DokiActivity
import java.util.*
import kotlin.collections.HashSet
class MainFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeListener {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var alarmManager: AlarmManager
private lateinit var alarmIntent: PendingIntent
private var requestingPermissions: Boolean = false
@SuppressLint("UnspecifiedImmutableFlag")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
if (BuildConfig.HIDDEN_APP && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
removeLauncherIcon()
}
setHasOptionsMenu(true)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
setPreferencesFromResource(R.xml.preferences, rootKey)
initPreferences()
findPreference<Preference>(KEY_DEVICE)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
newValue != null && newValue != ""
}
findPreference<Preference>(KEY_URL)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
newValue != null && validateServerURL(newValue.toString())
}
findPreference<Preference>(KEY_INTERVAL)?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
try {
newValue != null && (newValue as String).toInt() > 0
} catch (e: NumberFormatException) {
Log.w(TAG, e)
false
}
}
val numberValidationListener = Preference.OnPreferenceChangeListener { _, newValue ->
try {
newValue != null && (newValue as String).toInt() >= 0
} catch (e: NumberFormatException) {
Log.w(TAG, e)
false
}
}
findPreference<Preference>(KEY_DISTANCE)?.onPreferenceChangeListener = numberValidationListener
findPreference<Preference>(KEY_ANGLE)?.onPreferenceChangeListener = numberValidationListener
alarmManager = requireActivity().getSystemService(Context.ALARM_SERVICE) as AlarmManager
val originalIntent = Intent(activity, AutostartReceiver::class.java)
originalIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
alarmIntent = PendingIntent.getBroadcast(activity, 0, originalIntent, flags)
if (sharedPreferences.getBoolean(KEY_STATUS, false)) {
startTrackingService(checkPermission = true, initialPermission = false)
}
}
class NumericEditTextPreferenceDialogFragment : EditTextPreferenceDialogFragmentCompat() {
override fun onBindDialogView(view: View) {
val editText = view.findViewById<EditText>(android.R.id.edit)
editText.inputType = InputType.TYPE_CLASS_NUMBER
super.onBindDialogView(view)
}
companion object {
fun newInstance(key: String?): NumericEditTextPreferenceDialogFragment {
val fragment = NumericEditTextPreferenceDialogFragment()
val bundle = Bundle()
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}
@Suppress("DEPRECATION")
override fun onDisplayPreferenceDialog(preference: Preference) {
if (listOf(KEY_INTERVAL, KEY_DISTANCE, KEY_ANGLE).contains(preference.key)) {
val f: EditTextPreferenceDialogFragmentCompat =
NumericEditTextPreferenceDialogFragment.newInstance(preference.key)
f.setTargetFragment(this, 0)
f.show(requireFragmentManager(), "androidx.preference.PreferenceFragment.DIALOG")
} else {
super.onDisplayPreferenceDialog(preference)
}
}
private fun removeLauncherIcon() {
val className = MainActivity::class.java.canonicalName!!.replace(".MainActivity", ".Launcher")
val componentName = ComponentName(requireActivity().packageName, className)
val packageManager = requireActivity().packageManager
if (packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
val builder = AlertDialog.Builder(requireActivity())
builder.setIcon(android.R.drawable.ic_dialog_alert)
builder.setMessage(getString(R.string.hidden_alert))
builder.setPositiveButton(android.R.string.ok, null)
builder.show()
}
}
override fun onStart() {
super.onStart()
if (requestingPermissions) {
requestingPermissions = BatteryOptimizationHelper().requestException(requireContext())
}
}
override fun onResume() {
super.onResume()
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
private fun setPreferencesEnabled(enabled: Boolean) {
findPreference<Preference>(KEY_DEVICE)?.isEnabled = enabled
findPreference<Preference>(KEY_URL)?.isEnabled = enabled
findPreference<Preference>(KEY_INTERVAL)?.isEnabled = enabled
findPreference<Preference>(KEY_DISTANCE)?.isEnabled = enabled
findPreference<Preference>(KEY_ANGLE)?.isEnabled = enabled
findPreference<Preference>(KEY_ACCURACY)?.isEnabled = enabled
findPreference<Preference>(KEY_BUFFER)?.isEnabled = enabled
findPreference<Preference>(KEY_WAKELOCK)?.isEnabled = enabled
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (key == KEY_STATUS) {
if (sharedPreferences.getBoolean(KEY_STATUS, false)) {
startTrackingService(checkPermission = true, initialPermission = false)
} else {
stopTrackingService()
}
(requireActivity().application as MainApplication).handleRatingFlow(requireActivity())
} else if (key == KEY_DEVICE) {
findPreference<Preference>(KEY_DEVICE)?.summary = sharedPreferences.getString(KEY_DEVICE, null)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.status) {
startActivity(Intent(activity, StatusActivity::class.java))
return true
} else if (item.itemId == R.id.info) {
DokiActivity.start(requireContext())
return true
}
return super.onOptionsItemSelected(item)
}
private fun initPreferences() {
PreferenceManager.setDefaultValues(requireActivity(), R.xml.preferences, false)
if (!sharedPreferences.contains(KEY_DEVICE)) {
val id = (Random().nextInt(900000) + 100000).toString()
sharedPreferences.edit().putString(KEY_DEVICE, id).apply()
findPreference<EditTextPreference>(KEY_DEVICE)?.text = id
}
findPreference<Preference>(KEY_DEVICE)?.summary = sharedPreferences.getString(KEY_DEVICE, null)
}
private fun showBackgroundLocationDialog(context: Context, onSuccess: () -> Unit) {
val builder = AlertDialog.Builder(context)
val option = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.packageManager.backgroundPermissionOptionLabel
} else {
context.getString(R.string.request_background_option)
}
builder.setMessage(context.getString(R.string.request_background, option))
builder.setPositiveButton(android.R.string.ok) { _, _ -> onSuccess() }
builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
}
private fun startTrackingService(checkPermission: Boolean, initialPermission: Boolean) {
var permission = initialPermission
if (checkPermission) {
val requiredPermissions: MutableSet<String> = HashSet()
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
}
permission = requiredPermissions.isEmpty()
if (!permission) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(requiredPermissions.toTypedArray(), PERMISSIONS_REQUEST_LOCATION)
}
return
}
}
if (permission) {
setPreferencesEnabled(false)
ContextCompat.startForegroundService(requireContext(), Intent(activity, TrackingService::class.java))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
alarmManager.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
ALARM_MANAGER_INTERVAL.toLong(), ALARM_MANAGER_INTERVAL.toLong(), alarmIntent
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
requestingPermissions = true
showBackgroundLocationDialog(requireContext()) {
requestPermissions(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION), PERMISSIONS_REQUEST_BACKGROUND_LOCATION)
}
} else {
requestingPermissions = BatteryOptimizationHelper().requestException(requireContext())
}
} else {
sharedPreferences.edit().putBoolean(KEY_STATUS, false).apply()
val preference = findPreference<TwoStatePreference>(KEY_STATUS)
preference?.isChecked = false
}
}
private fun stopTrackingService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
alarmManager.cancel(alarmIntent)
}
requireActivity().stopService(Intent(activity, TrackingService::class.java))
setPreferencesEnabled(true)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (requestCode == PERMISSIONS_REQUEST_LOCATION) {
var granted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
granted = false
break
}
}
startTrackingService(false, granted)
}
}
private fun validateServerURL(userUrl: String): Boolean {
val port = Uri.parse(userUrl).port
if (
URLUtil.isValidUrl(userUrl) &&
(port == -1 || port in 1..65535) &&
(URLUtil.isHttpUrl(userUrl) || URLUtil.isHttpsUrl(userUrl))
) {
return true
}
Toast.makeText(activity, R.string.error_msg_invalid_url, Toast.LENGTH_LONG).show()
return false
}
companion object {
private val TAG = MainFragment::class.java.simpleName
private const val ALARM_MANAGER_INTERVAL = 15000
const val KEY_DEVICE = "id"
const val KEY_URL = "url"
const val KEY_INTERVAL = "interval"
const val KEY_DISTANCE = "distance"
const val KEY_ANGLE = "angle"
const val KEY_ACCURACY = "accuracy"
const val KEY_STATUS = "status"
const val KEY_BUFFER = "buffer"
const val KEY_WAKELOCK = "wakelock"
private const val PERMISSIONS_REQUEST_LOCATION = 2
private const val PERMISSIONS_REQUEST_BACKGROUND_LOCATION = 3
}
}
-68
Просмотреть файл
@@ -1,68 +0,0 @@
/*
* Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;
public class NetworkManager extends BroadcastReceiver {
private static final String TAG = NetworkManager.class.getSimpleName();
private Context context;
private NetworkHandler handler;
private ConnectivityManager connectivityManager;
public NetworkManager(Context context, NetworkHandler handler) {
this.context = context;
this.handler = handler;
connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
public interface NetworkHandler {
void onNetworkUpdate(boolean isOnline);
}
public boolean isOnline() {
NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
}
public void start() {
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(this, filter);
}
public void stop() {
context.unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION) && handler != null) {
boolean isOnline = isOnline();
Log.i(TAG, "network " + (isOnline ? "on" : "off"));
handler.onNetworkUpdate(isOnline);
}
}
}
+62
Просмотреть файл
@@ -0,0 +1,62 @@
/*
* Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package org.traccar.client
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.util.Log
class NetworkManager(private val context: Context, private val handler: NetworkHandler?) : BroadcastReceiver() {
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
interface NetworkHandler {
fun onNetworkUpdate(isOnline: Boolean)
}
val isOnline: Boolean
get() {
val activeNetwork = connectivityManager.activeNetworkInfo
return activeNetwork != null && activeNetwork.isConnectedOrConnecting
}
fun start() {
val filter = IntentFilter()
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(this, filter)
}
fun stop() {
context.unregisterReceiver(this)
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION && handler != null) {
val isOnline = isOnline
Log.i(TAG, "network " + if (isOnline) "on" else "off")
handler.onNetworkUpdate(isOnline)
}
}
companion object {
private val TAG = NetworkManager::class.java.simpleName
}
}
-156
Просмотреть файл
@@ -1,156 +0,0 @@
/*
* Copyright 2015 - 2018 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.location.Location;
import android.location.LocationManager;
import android.os.Build;
import java.util.Date;
public class Position {
public Position() {
}
public Position(String deviceId, Location location, double battery) {
this.deviceId = deviceId;
time = new Date(location.getTime());
latitude = location.getLatitude();
longitude = location.getLongitude();
altitude = location.getAltitude();
speed = location.getSpeed() * 1.943844; // speed in knots
course = location.getBearing();
if (location.getProvider() != null && !location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
accuracy = location.getAccuracy();
}
this.battery = battery;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
this.mock = location.isFromMockProvider();
}
}
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
private String deviceId;
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
private Date time;
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
private double latitude;
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
private double longitude;
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
private double altitude;
public double getAltitude() {
return altitude;
}
public void setAltitude(double altitude) {
this.altitude = altitude;
}
private double speed;
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
private double course;
public double getCourse() {
return course;
}
public void setCourse(double course) {
this.course = course;
}
private double accuracy;
public double getAccuracy() {
return accuracy;
}
public void setAccuracy(double accuracy) {
this.accuracy = accuracy;
}
private double battery;
public double getBattery() {
return battery;
}
public void setBattery(double battery) {
this.battery = battery;
}
private boolean mock;
public boolean getMock() {
return mock;
}
public void setMock(boolean mock) {
this.mock = mock;
}
}
+69
Просмотреть файл
@@ -0,0 +1,69 @@
/*
* Copyright 2015 - 2022 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.location.Location
import android.location.LocationManager
import android.os.Build
import java.util.*
data class Position(
val id: Long = 0,
val deviceId: String,
val time: Date,
val latitude: Double = 0.0,
val longitude: Double = 0.0,
val altitude: Double = 0.0,
val speed: Double = 0.0,
val course: Double = 0.0,
val accuracy: Double = 0.0,
val battery: Double = 0.0,
val charging: Boolean = false,
val mock: Boolean = false,
) {
constructor(deviceId: String, location: Location, battery: BatteryStatus) : this(
deviceId = deviceId,
time = Date(location.time.correctRollover()),
latitude = location.latitude,
longitude = location.longitude,
altitude = location.altitude,
speed = location.speed * 1.943844, // speed in knots
course = location.bearing.toDouble(),
accuracy = if (location.provider != null && location.provider != LocationManager.GPS_PROVIDER) {
location.accuracy.toDouble()
} else {
0.0
},
battery = battery.level,
charging = battery.charging,
mock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
location.isMock
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
@Suppress("DEPRECATION")
location.isFromMockProvider
} else {
false
},
)
}
private const val rolloverDate = 1554508800000L // April 6, 2019
private const val rolloverOffset = 619315200000L // 1024 weeks
private fun Long.correctRollover(): Long {
return if (this < rolloverDate) this + rolloverOffset else this
}
-131
Просмотреть файл
@@ -1,131 +0,0 @@
/*
* Copyright 2013 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.BatteryManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.widget.Toast;
public class PositionProvider implements LocationListener {
private static final String TAG = PositionProvider.class.getSimpleName();
private static final int MINIMUM_INTERVAL = 1000;
public interface PositionListener {
void onPositionUpdate(Position position);
}
private final PositionListener listener;
private final Context context;
private SharedPreferences preferences;
private LocationManager locationManager;
private String deviceId;
private long interval;
private double distance;
private double angle;
private Location lastLocation;
public PositionProvider(Context context, PositionListener listener) {
this.context = context;
this.listener = listener;
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
preferences = PreferenceManager.getDefaultSharedPreferences(context);
deviceId = preferences.getString(MainFragment.KEY_DEVICE, "undefined");
interval = Long.parseLong(preferences.getString(MainFragment.KEY_INTERVAL, "600")) * 1000;
distance = Integer.parseInt(preferences.getString(MainFragment.KEY_DISTANCE, "0"));
angle = Integer.parseInt(preferences.getString(MainFragment.KEY_ANGLE, "0"));
}
@SuppressLint("MissingPermission")
public void startUpdates() {
try {
locationManager.requestLocationUpdates(
getProvider(preferences.getString(MainFragment.KEY_ACCURACY, "medium")),
distance > 0 || angle > 0 ? MINIMUM_INTERVAL : interval, 0, this);
} catch (RuntimeException e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
public static String getProvider(String accuracy) {
switch (accuracy) {
case "high":
return LocationManager.GPS_PROVIDER;
case "low":
return LocationManager.PASSIVE_PROVIDER;
default:
return LocationManager.NETWORK_PROVIDER;
}
}
@Override
public void onLocationChanged(Location location) {
if (location != null && (lastLocation == null
|| location.getTime() - lastLocation.getTime() >= interval
|| distance > 0 && location.distanceTo(lastLocation) >= distance
|| angle > 0 && Math.abs(location.getBearing() - lastLocation.getBearing()) >= angle)) {
Log.i(TAG, "location new");
lastLocation = location;
listener.onPositionUpdate(new Position(deviceId, location, getBatteryLevel(context)));
} else {
Log.i(TAG, location != null ? "location ignored" : "location nil");
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
public void stopUpdates() {
locationManager.removeUpdates(this);
}
public static double getBatteryLevel(Context context) {
Intent batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
if (batteryIntent != null) {
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 1);
return (level * 100.0) / scale;
}
return 0;
}
}
+83
Просмотреть файл
@@ -0,0 +1,83 @@
/*
* Copyright 2013 - 2022 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.location.Location
import android.os.BatteryManager
import androidx.preference.PreferenceManager
import android.util.Log
import kotlin.math.abs
abstract class PositionProvider(
protected val context: Context,
protected val listener: PositionListener,
) {
interface PositionListener {
fun onPositionUpdate(position: Position)
fun onPositionError(error: Throwable)
}
protected var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
protected var deviceId = preferences.getString(MainFragment.KEY_DEVICE, "undefined")!!
protected var interval = preferences.getString(MainFragment.KEY_INTERVAL, "600")!!.toLong() * 1000
protected var distance: Double = preferences.getString(MainFragment.KEY_DISTANCE, "0")!!.toInt().toDouble()
protected var angle: Double = preferences.getString(MainFragment.KEY_ANGLE, "0")!!.toInt().toDouble()
private var lastLocation: Location? = null
abstract fun startUpdates()
abstract fun stopUpdates()
abstract fun requestSingleLocation()
protected fun processLocation(location: Location?) {
val lastLocation = this.lastLocation
if (location != null &&
(lastLocation == null || location.time - lastLocation!!.time >= interval || distance > 0
&& location.distanceTo(lastLocation) >= distance || angle > 0
&& abs(location.bearing - lastLocation!!.bearing) >= angle)
) {
Log.i(TAG, "location new")
this.lastLocation = location
listener.onPositionUpdate(Position(deviceId, location, getBatteryStatus(context)))
} else {
Log.i(TAG, if (location != null) "location ignored" else "location nil")
}
}
protected fun getBatteryStatus(context: Context): BatteryStatus {
val batteryIntent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
if (batteryIntent != null) {
val level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
val scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 1)
val status = batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
return BatteryStatus(
level = level * 100.0 / scale,
charging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL,
)
}
return BatteryStatus()
}
companion object {
private val TAG = PositionProvider::class.java.simpleName
const val MINIMUM_INTERVAL: Long = 1000
}
}
-49
Просмотреть файл
@@ -1,49 +0,0 @@
/*
* Copyright 2012 - 2016 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.net.Uri;
public class ProtocolFormatter {
public static String formatRequest(String url, Position position) {
return formatRequest(url, position, null);
}
public static String formatRequest(String url, Position position, String alarm) {
Uri serverUrl = Uri.parse(url);
Uri.Builder builder = serverUrl.buildUpon()
.appendQueryParameter("id", position.getDeviceId())
.appendQueryParameter("timestamp", String.valueOf(position.getTime().getTime() / 1000))
.appendQueryParameter("lat", String.valueOf(position.getLatitude()))
.appendQueryParameter("lon", String.valueOf(position.getLongitude()))
.appendQueryParameter("speed", String.valueOf(position.getSpeed()))
.appendQueryParameter("bearing", String.valueOf(position.getCourse()))
.appendQueryParameter("altitude", String.valueOf(position.getAltitude()))
.appendQueryParameter("accuracy", String.valueOf(position.getAccuracy()))
.appendQueryParameter("batt", String.valueOf(position.getBattery()));
if (position.getMock()) {
builder.appendQueryParameter("mock", String.valueOf(position.getMock()));
}
if (alarm != null) {
builder.appendQueryParameter("alarm", alarm);
}
return builder.build().toString();
}
}
+45
Просмотреть файл
@@ -0,0 +1,45 @@
/*
* Copyright 2012 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.net.Uri
object ProtocolFormatter {
fun formatRequest(url: String, position: Position, alarm: String? = null): String {
val serverUrl = Uri.parse(url)
val builder = serverUrl.buildUpon()
.appendQueryParameter("id", position.deviceId)
.appendQueryParameter("timestamp", (position.time.time / 1000).toString())
.appendQueryParameter("lat", position.latitude.toString())
.appendQueryParameter("lon", position.longitude.toString())
.appendQueryParameter("speed", position.speed.toString())
.appendQueryParameter("bearing", position.course.toString())
.appendQueryParameter("altitude", position.altitude.toString())
.appendQueryParameter("accuracy", position.accuracy.toString())
.appendQueryParameter("batt", position.battery.toString())
if (position.charging) {
builder.appendQueryParameter("charge", position.charging.toString())
}
if (position.mock) {
builder.appendQueryParameter("mock", position.mock.toString())
}
if (alarm != null) {
builder.appendQueryParameter("alarm", alarm)
}
return builder.build().toString()
}
}
-83
Просмотреть файл
@@ -1,83 +0,0 @@
/*
* Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class RequestManager {
private static final int TIMEOUT = 15 * 1000;
public interface RequestHandler {
void onComplete(boolean success);
}
private static class RequestAsyncTask extends AsyncTask<String, Void, Boolean> {
private RequestHandler handler;
public RequestAsyncTask(RequestHandler handler) {
this.handler = handler;
}
@Override
protected Boolean doInBackground(String... request) {
return sendRequest(request[0]);
}
@Override
protected void onPostExecute(Boolean result) {
handler.onComplete(result);
}
}
public static boolean sendRequest(String request) {
InputStream inputStream = null;
try {
URL url = new URL(request);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(TIMEOUT);
connection.setConnectTimeout(TIMEOUT);
connection.setRequestMethod("POST");
connection.connect();
inputStream = connection.getInputStream();
while (inputStream.read() != -1);
return true;
} catch (IOException error) {
return false;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException secondError) {
Log.w(RequestManager.class.getSimpleName(), secondError);
}
}
}
public static void sendRequestAsync(String request, RequestHandler handler) {
RequestAsyncTask task = new RequestAsyncTask(handler);
task.execute(request);
}
}
+71
Просмотреть файл
@@ -0,0 +1,71 @@
/*
* Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("DEPRECATION")
package org.traccar.client
import android.os.AsyncTask
import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
object RequestManager {
private const val TIMEOUT = 15 * 1000
fun sendRequest(request: String?): Boolean {
var inputStream: InputStream? = null
return try {
val url = URL(request)
val connection = url.openConnection() as HttpURLConnection
connection.readTimeout = TIMEOUT
connection.connectTimeout = TIMEOUT
connection.requestMethod = "POST"
connection.connect()
inputStream = connection.inputStream
while (inputStream.read() != -1) {}
true
} catch (error: IOException) {
false
} finally {
try {
inputStream?.close()
} catch (secondError: IOException) {
Log.w(RequestManager::class.java.simpleName, secondError)
}
}
}
fun sendRequestAsync(request: String, handler: RequestHandler) {
RequestAsyncTask(handler).execute(request)
}
interface RequestHandler {
fun onComplete(success: Boolean)
}
private class RequestAsyncTask(private val handler: RequestHandler) : AsyncTask<String, Unit, Boolean>() {
override fun doInBackground(vararg request: String): Boolean {
return sendRequest(request[0])
}
override fun onPostExecute(result: Boolean) {
handler.onComplete(result)
}
}
}
-201
Просмотреть файл
@@ -1,201 +0,0 @@
/*
* Copyright 2016 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.pm.ShortcutInfoCompat;
import android.support.v4.content.pm.ShortcutManagerCompat;
import android.support.v4.graphics.drawable.IconCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
public class ShortcutActivity extends AppCompatActivity {
public static final String EXTRA_ACTION = "action";
public static final String ACTION_START = "start";
public static final String ACTION_STOP = "stop";
public static final String ACTION_SOS = "sos";
private static final String ALARM_SOS = "sos";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!executeAction(getIntent())) {
setContentView(R.layout.list);
final String[] items = new String[] {
getString(R.string.shortcut_start),
getString(R.string.shortcut_stop),
getString(R.string.shortcut_sos)
};
ListView listView = findViewById(android.R.id.list);
listView.setAdapter(new ArrayAdapter<>(
this, android.R.layout.simple_list_item_1, items));
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position) {
case 0:
setShortcutResult(items[position], R.mipmap.ic_start, ACTION_START);
break;
case 1:
setShortcutResult(items[position], R.mipmap.ic_stop, ACTION_STOP);
break;
case 2:
setShortcutResult(items[position], R.mipmap.ic_sos, ACTION_SOS);
break;
}
finish();
}
});
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
executeAction(intent);
}
private void setShortcutResult(String label, @DrawableRes int iconResId, String action) {
Intent intent = new Intent(Intent.ACTION_DEFAULT, null, this, ShortcutActivity.class);
intent.putExtra(EXTRA_ACTION, action);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, action)
.setShortLabel(label)
.setIcon(IconCompat.createWithResource(this, iconResId))
.setIntent(intent)
.build();
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut));
}
@SuppressWarnings("MissingPermission")
private void sendAlarm() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
String provider = PositionProvider.getProvider(
preferences.getString(MainFragment.KEY_ACCURACY, "medium"));
try {
Location location = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
if (location != null) {
sendAlarmLocation(location);
} else {
locationManager.requestSingleUpdate(provider, new LocationListener() {
@Override
public void onLocationChanged(Location location) {
sendAlarmLocation(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
}, Looper.myLooper());
}
} catch (RuntimeException e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
}
}
private void sendAlarmLocation(Location location) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
Position position = new Position(
preferences.getString(MainFragment.KEY_DEVICE, null),
location, PositionProvider.getBatteryLevel(this));
String request = ProtocolFormatter.formatRequest(
preferences.getString(MainFragment.KEY_URL, null), position, ALARM_SOS);
RequestManager.sendRequestAsync(request, new RequestManager.RequestHandler() {
@Override
public void onComplete(boolean success) {
if (success) {
Toast.makeText(ShortcutActivity.this, R.string.status_send_success, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ShortcutActivity.this, R.string.status_send_fail, Toast.LENGTH_SHORT).show();
}
}
});
}
private boolean executeAction(Intent intent) {
String action;
if (intent.hasExtra("shortcutAction")) {
action = intent.getBooleanExtra("shortcutAction", false)
? ACTION_START : ACTION_STOP;
} else {
action = intent.getStringExtra(EXTRA_ACTION);
}
if (action != null) {
switch (action) {
case ACTION_START:
PreferenceManager.getDefaultSharedPreferences(this)
.edit().putBoolean(MainFragment.KEY_STATUS, true).apply();
ContextCompat.startForegroundService(this, new Intent(this, TrackingService.class));
Toast.makeText(this, R.string.status_service_create, Toast.LENGTH_SHORT).show();
break;
case ACTION_STOP:
PreferenceManager.getDefaultSharedPreferences(this)
.edit().putBoolean(MainFragment.KEY_STATUS, false).apply();
stopService(new Intent(this, TrackingService.class));
Toast.makeText(this, R.string.status_service_destroy, Toast.LENGTH_SHORT).show();
break;
case ACTION_SOS:
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
sendAlarm();
} else {
Toast.makeText(this, R.string.status_send_fail, Toast.LENGTH_SHORT).show();
}
break;
}
finish();
}
return action != null;
}
}
+138
Просмотреть файл
@@ -0,0 +1,138 @@
/*
* Copyright 2016 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.AdapterView.OnItemClickListener
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.preference.PreferenceManager
import org.traccar.client.PositionProvider.PositionListener
import org.traccar.client.ProtocolFormatter.formatRequest
import org.traccar.client.RequestManager.RequestHandler
import org.traccar.client.RequestManager.sendRequestAsync
class ShortcutActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!executeAction(intent)) {
setContentView(R.layout.list)
val items = arrayOf(
getString(R.string.shortcut_start),
getString(R.string.shortcut_stop),
getString(R.string.shortcut_sos)
)
val listView = findViewById<ListView>(android.R.id.list)
listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
listView.onItemClickListener = OnItemClickListener { _, _, position, _ ->
when (position) {
0 -> setShortcutResult(items[position], R.mipmap.ic_start, ACTION_START)
1 -> setShortcutResult(items[position], R.mipmap.ic_stop, ACTION_STOP)
2 -> setShortcutResult(items[position], R.mipmap.ic_sos, ACTION_SOS)
}
finish()
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
executeAction(intent)
}
private fun setShortcutResult(label: String, @DrawableRes iconResId: Int, action: String) {
val intent = Intent(Intent.ACTION_DEFAULT, null, this, ShortcutActivity::class.java)
intent.putExtra(EXTRA_ACTION, action)
val shortcut = ShortcutInfoCompat.Builder(this, action)
.setShortLabel(label)
.setIcon(IconCompat.createWithResource(this, iconResId))
.setIntent(intent)
.build()
setResult(RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut))
}
private fun sendAlarm() {
PositionProviderFactory.create(this, object : PositionListener {
override fun onPositionUpdate(position: Position) {
val preferences = PreferenceManager.getDefaultSharedPreferences(this@ShortcutActivity)
val request = formatRequest(preferences.getString(MainFragment.KEY_URL, null)!!, position, ALARM_SOS)
sendRequestAsync(request, object : RequestHandler {
override fun onComplete(success: Boolean) {
if (success) {
Toast.makeText(this@ShortcutActivity, R.string.status_send_success, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@ShortcutActivity, R.string.status_send_fail, Toast.LENGTH_SHORT).show()
}
}
})
}
override fun onPositionError(error: Throwable) {
Toast.makeText(this@ShortcutActivity, error.message, Toast.LENGTH_LONG).show()
}
}).requestSingleLocation()
}
private fun executeAction(intent: Intent): Boolean {
val action: String? = if (intent.hasExtra("shortcutAction")) {
if (intent.getBooleanExtra("shortcutAction", false)) ACTION_START else ACTION_STOP
} else {
intent.getStringExtra(EXTRA_ACTION)
}
if (action != null) {
when (action) {
ACTION_START -> {
PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean(MainFragment.KEY_STATUS, true).apply()
ContextCompat.startForegroundService(this, Intent(this, TrackingService::class.java))
Toast.makeText(this, R.string.status_service_create, Toast.LENGTH_SHORT).show()
}
ACTION_STOP -> {
PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean(MainFragment.KEY_STATUS, false).apply()
stopService(Intent(this, TrackingService::class.java))
Toast.makeText(this, R.string.status_service_destroy, Toast.LENGTH_SHORT).show()
}
ACTION_SOS -> {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
sendAlarm()
} else {
Toast.makeText(this, R.string.status_send_fail, Toast.LENGTH_SHORT).show()
}
}
}
finish()
}
return action != null
}
companion object {
const val EXTRA_ACTION = "action"
const val ACTION_START = "start"
const val ACTION_STOP = "stop"
const val ACTION_SOS = "sos"
private const val ALARM_SOS = "sos"
}
}
-94
Просмотреть файл
@@ -1,94 +0,0 @@
/*
* Copyright 2012 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ArrayAdapter;
import android.widget.ListView;
public class StatusActivity extends AppCompatActivity {
private static final int LIMIT = 20;
private static final LinkedList<String> messages = new LinkedList<>();
private static final Set<ArrayAdapter<String>> adapters = new HashSet<>();
private static void notifyAdapters() {
for (ArrayAdapter<String> adapter : adapters) {
adapter.notifyDataSetChanged();
}
}
public static void addMessage(String message) {
DateFormat format = DateFormat.getTimeInstance(DateFormat.MEDIUM);
message = format.format(new Date()) + " - " + message;
messages.add(message);
while (messages.size() > LIMIT) {
messages.removeFirst();
}
notifyAdapters();
}
public static void clearMessages() {
messages.clear();
notifyAdapters();
}
private ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list);
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, android.R.id.text1, messages);
ListView listView = findViewById(android.R.id.list);
listView.setAdapter(adapter);
adapters.add(adapter);
}
@Override
protected void onDestroy() {
adapters.remove(adapter);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.status, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.clear) {
clearMessages();
return true;
}
return super.onOptionsItemSelected(item);
}
}
+86
Просмотреть файл
@@ -0,0 +1,86 @@
/*
* Copyright 2012 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import androidx.appcompat.app.AppCompatActivity
import android.widget.ArrayAdapter
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ListView
import java.text.DateFormat
import java.util.*
class StatusActivity : AppCompatActivity() {
private var adapter: ArrayAdapter<String>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.list)
adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1, messages)
val listView = findViewById<ListView>(android.R.id.list)
listView.adapter = adapter
adapter?.let { adapters.add(it) }
}
override fun onDestroy() {
adapters.remove(adapter)
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.status, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.clear) {
clearMessages()
return true
}
return super.onOptionsItemSelected(item)
}
companion object {
private const val LIMIT = 20
private val messages = LinkedList<String>()
private val adapters: MutableSet<ArrayAdapter<String>> = HashSet()
private fun notifyAdapters() {
for (adapter in adapters) {
adapter.notifyDataSetChanged()
}
}
fun addMessage(originalMessage: String) {
var message = originalMessage
val format = DateFormat.getTimeInstance(DateFormat.MEDIUM)
message = format.format(Date()) + " - " + message
messages.add(message)
while (messages.size > LIMIT) {
messages.removeFirst()
}
notifyAdapters()
}
fun clearMessages() {
messages.clear()
notifyAdapters()
}
}
}
-217
Просмотреть файл
@@ -1,217 +0,0 @@
/*
* Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.util.Log;
public class TrackingController implements PositionProvider.PositionListener, NetworkManager.NetworkHandler {
private static final String TAG = TrackingController.class.getSimpleName();
private static final int RETRY_DELAY = 30 * 1000;
private static final int WAKE_LOCK_TIMEOUT = 120 * 1000;
private boolean isOnline;
private boolean isWaiting;
private Context context;
private Handler handler;
private SharedPreferences preferences;
private String url;
private PositionProvider positionProvider;
private DatabaseHelper databaseHelper;
private NetworkManager networkManager;
private PowerManager.WakeLock wakeLock;
private void lock() {
wakeLock.acquire(WAKE_LOCK_TIMEOUT);
}
private void unlock() {
if (wakeLock.isHeld()) {
wakeLock.release();
}
}
public TrackingController(Context context) {
this.context = context;
handler = new Handler();
preferences = PreferenceManager.getDefaultSharedPreferences(context);
positionProvider = new PositionProvider(context, this);
databaseHelper = new DatabaseHelper(context);
networkManager = new NetworkManager(context, this);
isOnline = networkManager.isOnline();
url = preferences.getString(MainFragment.KEY_URL, context.getString(R.string.settings_url_default_value));
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName());
}
public void start() {
if (isOnline) {
read();
}
try {
positionProvider.startUpdates();
} catch (SecurityException e) {
Log.w(TAG, e);
}
networkManager.start();
}
public void stop() {
networkManager.stop();
try {
positionProvider.stopUpdates();
} catch (SecurityException e) {
Log.w(TAG, e);
}
handler.removeCallbacksAndMessages(null);
}
@Override
public void onPositionUpdate(Position position) {
StatusActivity.addMessage(context.getString(R.string.status_location_update));
if (position != null) {
write(position);
}
}
@Override
public void onNetworkUpdate(boolean isOnline) {
int message = isOnline ? R.string.status_network_online : R.string.status_network_offline;
StatusActivity.addMessage(context.getString(message));
if (!this.isOnline && isOnline) {
read();
}
this.isOnline = isOnline;
}
//
// State transition examples:
//
// write -> read -> send -> delete -> read
//
// read -> send -> retry -> read -> send
//
private void log(String action, Position position) {
if (position != null) {
action += " (" +
"id:" + position.getId() +
" time:" + position.getTime().getTime() / 1000 +
" lat:" + position.getLatitude() +
" lon:" + position.getLongitude() + ")";
}
Log.d(TAG, action);
}
private void write(Position position) {
log("write", position);
lock();
databaseHelper.insertPositionAsync(position, new DatabaseHelper.DatabaseHandler<Void>() {
@Override
public void onComplete(boolean success, Void result) {
if (success) {
if (isOnline && isWaiting) {
read();
isWaiting = false;
}
}
unlock();
}
});
}
private void read() {
log("read", null);
lock();
databaseHelper.selectPositionAsync(new DatabaseHelper.DatabaseHandler<Position>() {
@Override
public void onComplete(boolean success, Position result) {
if (success) {
if (result != null) {
if (result.getDeviceId().equals(preferences.getString(MainFragment.KEY_DEVICE, null))) {
send(result);
} else {
delete(result);
}
} else {
isWaiting = true;
}
} else {
retry();
}
unlock();
}
});
}
private void delete(Position position) {
log("delete", position);
lock();
databaseHelper.deletePositionAsync(position.getId(), new DatabaseHelper.DatabaseHandler<Void>() {
@Override
public void onComplete(boolean success, Void result) {
if (success) {
read();
} else {
retry();
}
unlock();
}
});
}
private void send(final Position position) {
log("send", position);
lock();
String request = ProtocolFormatter.formatRequest(url, position);
RequestManager.sendRequestAsync(request, new RequestManager.RequestHandler() {
@Override
public void onComplete(boolean success) {
if (success) {
delete(position);
} else {
StatusActivity.addMessage(context.getString(R.string.status_send_fail));
retry();
}
unlock();
}
});
}
private void retry() {
log("retry", null);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (isOnline) {
read();
}
}
}, RETRY_DELAY);
}
}
+186
Просмотреть файл
@@ -0,0 +1,186 @@
/*
* Copyright 2015 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.Context
import org.traccar.client.ProtocolFormatter.formatRequest
import org.traccar.client.RequestManager.sendRequestAsync
import org.traccar.client.PositionProvider.PositionListener
import org.traccar.client.NetworkManager.NetworkHandler
import android.os.Handler
import android.os.Looper
import androidx.preference.PreferenceManager
import android.util.Log
import org.traccar.client.DatabaseHelper.DatabaseHandler
import org.traccar.client.RequestManager.RequestHandler
class TrackingController(private val context: Context) : PositionListener, NetworkHandler {
private val handler = Handler(Looper.getMainLooper())
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val positionProvider = PositionProviderFactory.create(context, this)
private val databaseHelper = DatabaseHelper(context)
private val networkManager = NetworkManager(context, this)
private val url: String = preferences.getString(MainFragment.KEY_URL, context.getString(R.string.settings_url_default_value))!!
private val buffer: Boolean = preferences.getBoolean(MainFragment.KEY_BUFFER, true)
private var isOnline = networkManager.isOnline
private var isWaiting = false
fun start() {
if (isOnline) {
read()
}
try {
positionProvider.startUpdates()
} catch (e: SecurityException) {
Log.w(TAG, e)
}
networkManager.start()
}
fun stop() {
networkManager.stop()
try {
positionProvider.stopUpdates()
} catch (e: SecurityException) {
Log.w(TAG, e)
}
handler.removeCallbacksAndMessages(null)
}
override fun onPositionUpdate(position: Position) {
StatusActivity.addMessage(context.getString(R.string.status_location_update))
if (buffer) {
write(position)
} else {
send(position)
}
}
override fun onPositionError(error: Throwable) {}
override fun onNetworkUpdate(isOnline: Boolean) {
val message = if (isOnline) R.string.status_network_online else R.string.status_network_offline
StatusActivity.addMessage(context.getString(message))
if (!this.isOnline && isOnline) {
read()
}
this.isOnline = isOnline
}
//
// State transition examples:
//
// write -> read -> send -> delete -> read
//
// read -> send -> retry -> read -> send
//
private fun log(action: String, position: Position?) {
var formattedAction: String = action
if (position != null) {
formattedAction +=
" (id:" + position.id +
" time:" + position.time.time / 1000 +
" lat:" + position.latitude +
" lon:" + position.longitude + ")"
}
Log.d(TAG, formattedAction)
}
private fun write(position: Position) {
log("write", position)
databaseHelper.insertPositionAsync(position, object : DatabaseHandler<Unit?> {
override fun onComplete(success: Boolean, result: Unit?) {
if (success) {
if (isOnline && isWaiting) {
read()
isWaiting = false
}
}
}
})
}
private fun read() {
log("read", null)
databaseHelper.selectPositionAsync(object : DatabaseHandler<Position?> {
override fun onComplete(success: Boolean, result: Position?) {
if (success) {
if (result != null) {
if (result.deviceId == preferences.getString(MainFragment.KEY_DEVICE, null)) {
send(result)
} else {
delete(result)
}
} else {
isWaiting = true
}
} else {
retry()
}
}
})
}
private fun delete(position: Position) {
log("delete", position)
databaseHelper.deletePositionAsync(position.id, object : DatabaseHandler<Unit?> {
override fun onComplete(success: Boolean, result: Unit?) {
if (success) {
read()
} else {
retry()
}
}
})
}
private fun send(position: Position) {
log("send", position)
val request = formatRequest(url, position)
sendRequestAsync(request, object : RequestHandler {
override fun onComplete(success: Boolean) {
if (success) {
if (buffer) {
delete(position)
}
} else {
StatusActivity.addMessage(context.getString(R.string.status_send_fail))
if (buffer) {
retry()
}
}
}
})
}
private fun retry() {
log("retry", null)
handler.postDelayed({
if (isOnline) {
read()
}
}, RETRY_DELAY.toLong())
}
companion object {
private val TAG = TrackingController::class.java.simpleName
private const val RETRY_DELAY = 30 * 1000
}
}
-120
Просмотреть файл
@@ -1,120 +0,0 @@
/*
* Copyright 2012 - 2017 Anton Tananaev (anton.tananaev@gmail.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
public class TrackingService extends Service {
private static final String TAG = TrackingService.class.getSimpleName();
private static final int NOTIFICATION_ID = 1;
private TrackingController trackingController;
private static Notification createNotification(Context context) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, MainApplication.PRIMARY_CHANNEL)
.setSmallIcon(R.drawable.ic_stat_notify)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(Notification.CATEGORY_SERVICE);
Intent intent;
if (!BuildConfig.HIDDEN_APP) {
intent = new Intent(context, MainActivity.class);
builder
.setContentTitle(context.getString(R.string.settings_status_on_summary))
.setTicker(context.getString(R.string.settings_status_on_summary))
.setColor(ContextCompat.getColor(context, R.color.primary_dark));
} else {
intent = new Intent(android.provider.Settings.ACTION_SETTINGS);
}
builder.setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
return builder.build();
}
public static class HideNotificationService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
startForeground(NOTIFICATION_ID, createNotification(this));
stopForeground(true);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
stopSelfResult(startId);
return START_NOT_STICKY;
}
}
@Override
public void onCreate() {
Log.i(TAG, "service create");
StatusActivity.addMessage(getString(R.string.status_service_create));
startForeground(NOTIFICATION_ID, createNotification(this));
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
trackingController = new TrackingController(this);
trackingController.start();
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(this, new Intent(this, HideNotificationService.class));
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@TargetApi(Build.VERSION_CODES.ECLAIR)
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
AutostartReceiver.completeWakefulIntent(intent);
}
return START_STICKY;
}
@Override
public void onDestroy() {
Log.i(TAG, "service destroy");
StatusActivity.addMessage(getString(R.string.status_service_destroy));
stopForeground(true);
if (trackingController != null) {
trackingController.stop();
}
}
}
+133
Просмотреть файл
@@ -0,0 +1,133 @@
/*
* Copyright 2012 - 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.PowerManager.WakeLock
import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
class TrackingService : Service() {
private var wakeLock: WakeLock? = null
private var trackingController: TrackingController? = null
class HideNotificationService : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
startForeground(NOTIFICATION_ID, createNotification(this))
stopForeground(true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
stopSelfResult(startId)
return START_NOT_STICKY
}
}
@SuppressLint("WakelockTimeout")
override fun onCreate() {
startForeground(NOTIFICATION_ID, createNotification(this))
Log.i(TAG, "service create")
sendBroadcast(Intent(ACTION_STARTED))
StatusActivity.addMessage(getString(R.string.status_service_create))
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(MainFragment.KEY_WAKELOCK, true)) {
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, javaClass.name)
wakeLock?.acquire()
}
trackingController = TrackingController(this)
trackingController?.start()
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(this, Intent(this, HideNotificationService::class.java))
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
@TargetApi(Build.VERSION_CODES.ECLAIR)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
WakefulBroadcastReceiver.completeWakefulIntent(intent)
return START_STICKY
}
override fun onDestroy() {
stopForeground(true)
Log.i(TAG, "service destroy")
sendBroadcast(Intent(ACTION_STOPPED))
StatusActivity.addMessage(getString(R.string.status_service_destroy))
if (wakeLock?.isHeld == true) {
wakeLock?.release()
}
trackingController?.stop()
}
companion object {
const val ACTION_STARTED = "org.traccar.action.SERVICE_STARTED"
const val ACTION_STOPPED = "org.traccar.action.SERVICE_STOPPED"
private val TAG = TrackingService::class.java.simpleName
private const val NOTIFICATION_ID = 1
@SuppressLint("UnspecifiedImmutableFlag")
private fun createNotification(context: Context): Notification {
val builder = NotificationCompat.Builder(context, MainApplication.PRIMARY_CHANNEL)
.setSmallIcon(R.drawable.ic_stat_notify)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
val intent: Intent
if (!BuildConfig.HIDDEN_APP) {
intent = Intent(context, MainActivity::class.java)
builder
.setContentTitle(context.getString(R.string.settings_status_on_summary))
.setTicker(context.getString(R.string.settings_status_on_summary))
.color = ContextCompat.getColor(context, R.color.primary_dark)
} else {
intent = Intent(Settings.ACTION_SETTINGS)
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
builder.setContentIntent(PendingIntent.getActivity(context, 0, intent, flags))
return builder.build()
}
}
}
-62
Просмотреть файл
@@ -1,62 +0,0 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.support.v4.content.ContextCompat;
import android.util.SparseArray;
public abstract class WakefulBroadcastReceiver extends BroadcastReceiver {
private static final String EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid";
private static final SparseArray<PowerManager.WakeLock> mActiveWakeLocks = new SparseArray<>();
private static int mNextId = 1;
public static void startWakefulForegroundService(Context context, Intent intent) {
synchronized (mActiveWakeLocks) {
int id = mNextId;
mNextId++;
if (mNextId <= 0) {
mNextId = 1;
}
intent.putExtra(EXTRA_WAKE_LOCK_ID, id);
ContextCompat.startForegroundService(context, intent);
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
WakefulBroadcastReceiver.class.getSimpleName());
wl.setReferenceCounted(false);
wl.acquire(60*1000);
mActiveWakeLocks.put(id, wl);
}
}
public static boolean completeWakefulIntent(Intent intent) {
final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0);
if (id == 0) {
return false;
}
synchronized (mActiveWakeLocks) {
PowerManager.WakeLock wl = mActiveWakeLocks.get(id);
if (wl != null) {
wl.release();
mActiveWakeLocks.remove(id);
return true;
}
return true;
}
}
}
+70
Просмотреть файл
@@ -0,0 +1,70 @@
/*
* Copyright 2021 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.traccar.client
import android.content.BroadcastReceiver
import android.content.Context
import android.util.SparseArray
import android.os.PowerManager.WakeLock
import android.content.Intent
import androidx.core.content.ContextCompat
import android.os.PowerManager
abstract class WakefulBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid"
private val activeWakeLocks = SparseArray<WakeLock>()
private var nextId = 1
fun startWakefulForegroundService(context: Context, intent: Intent) {
synchronized(activeWakeLocks) {
val id = nextId
nextId += 1
if (nextId <= 0) {
nextId = 1
}
intent.putExtra(EXTRA_WAKE_LOCK_ID, id)
ContextCompat.startForegroundService(context, intent)
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
WakefulBroadcastReceiver::class.java.simpleName
)
wakeLock.setReferenceCounted(false)
wakeLock.acquire((60 * 1000).toLong())
activeWakeLocks.put(id, wakeLock)
}
}
fun completeWakefulIntent(intent: Intent?): Boolean {
val id = intent?.getIntExtra(EXTRA_WAKE_LOCK_ID, 0) ?: 0
if (id == 0) {
return false
}
synchronized(activeWakeLocks) {
val wakeLock = activeWakeLocks[id]
if (wakeLock != null) {
wakeLock.release()
activeWakeLocks.remove(id)
return true
}
return true
}
}
}
}
+15
Просмотреть файл
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="64"
android:viewportHeight="64">
<group android:scaleX="0.71"
android:scaleY="0.71"
android:translateX="9.28"
android:translateY="9.28">
<path
android:pathData="m32.459,10.617c-3.783,-0.085 -7.636,0.834 -11.152,2.865 -10.228,5.906 -13.731,18.983 -7.826,29.211 5.906,10.228 18.983,13.731 29.211,7.826 10.228,-5.906 13.733,-18.983 7.828,-29.211 -3.875,-6.712 -10.839,-10.529 -18.061,-10.691zM40.778,19.38c1.737,1.331 3.242,2.997 4.404,5.01 1.162,2.013 1.854,4.15 2.137,6.319l-3.804,0.547c-0.223,-1.679 -0.73,-3.405 -1.629,-4.964 -0.9,-1.559 -2.139,-2.86 -3.482,-3.892zM36.188,25.461c0.948,0.734 1.766,1.632 2.402,2.734 0.636,1.102 1.003,2.258 1.165,3.446l-3.803,0.549c-0.101,-0.699 -0.284,-1.445 -0.658,-2.093 -0.374,-0.648 -0.928,-1.179 -1.483,-1.615l2.377,-3.019zM21.601,29.215 L25.407,35.806 29.213,42.398a7.611,7.611 0,0 1,-7.612 0,7.611 7.611,0 0,1 -3.806,-6.592 7.611,7.611 0,0 1,3.806 -6.591zM30.306,31.049a1.903,1.903 0,0 1,1.693 0.951,1.903 1.903,0 0,1 -0.697,2.599 1.903,1.903 0,0 1,-2.599 -0.695,1.903 1.903,0 0,1 0.697,-2.601 1.903,1.903 0,0 1,0.906 -0.254z"
android:strokeWidth=".84379"
android:fillColor="#2e7d32"/>
</group>
</vector>
+15
Просмотреть файл
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#2E7D32">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="5.04"
android:translateY="5.04">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</group>
</vector>
+15
Просмотреть файл
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#2E7D32">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="5.04"
android:translateY="5.04">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM9.5,16.5v-9l7,4.5L9.5,16.5z"/>
</group>
</vector>
+15
Просмотреть файл
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#2E7D32">
<group android:scaleX="0.58"
android:scaleY="0.58"
android:translateX="5.04"
android:translateY="5.04">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM11,16H9V8h2V16zM15,16h-2V8h2V16z"/>
</group>
</vector>
-37
Просмотреть файл
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_toRightOf="@+id/logo"
android:layout_toEndOf="@+id/logo"
android:padding="@dimen/spacing_normal"
android:orientation="vertical" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_normal"
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_normal"
android:text="@string/about_description" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/spacing_normal"
android:text="@string/about_license" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/about_web"
android:autoLink="web" />
</LinearLayout>
+13
Просмотреть файл
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.traccar.client.MainFragment" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+3 -3
Просмотреть файл
@@ -9,8 +9,8 @@
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/about" android:id="@+id/info"
android:title="@string/menu_about" android:title="@string/menu_info"
app:showAsAction="never" /> app:showAsAction="ifRoom" />
</menu> </menu>
+3 -2
Просмотреть файл
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/primary"/> <background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
-5
Просмотреть файл
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/primary"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
+3 -2
Просмотреть файл
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/> <background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_sos_foreground"/> <foreground android:drawable="@drawable/ic_sos_foreground"/>
<monochrome android:drawable="@drawable/ic_sos_foreground"/>
</adaptive-icon> </adaptive-icon>
-5
Просмотреть файл
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@mipmap/ic_sos_foreground"/>
</adaptive-icon>
+3 -2
Просмотреть файл
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/> <background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_start_foreground"/> <foreground android:drawable="@drawable/ic_start_foreground"/>
<monochrome android:drawable="@drawable/ic_start_foreground"/>
</adaptive-icon> </adaptive-icon>
-5
Просмотреть файл
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@mipmap/ic_start_foreground"/>
</adaptive-icon>
+3 -2
Просмотреть файл
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/> <background android:drawable="@android:color/white"/>
<foreground android:drawable="@mipmap/ic_stop_foreground"/> <foreground android:drawable="@drawable/ic_stop_foreground"/>
<monochrome android:drawable="@drawable/ic_stop_foreground"/>
</adaptive-icon> </adaptive-icon>
-5
Просмотреть файл
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/white"/>
<foreground android:drawable="@mipmap/ic_stop_foreground"/>
</adaptive-icon>
Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.2 KiB

После

Ширина:  |  Высота:  |  Размер: 4.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.1 KiB

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.2 KiB

После

Ширина:  |  Высота:  |  Размер: 3.7 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.7 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.1 KiB

После

Ширина:  |  Высота:  |  Размер: 3.6 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.5 KiB

После

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.7 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.9 KiB

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 930 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.0 KiB

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.0 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.0 KiB

После

Ширина:  |  Высота:  |  Размер: 1.9 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 943 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 5.9 KiB

После

Ширина:  |  Высота:  |  Размер: 5.7 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 3.8 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.5 KiB

После

Ширина:  |  Высота:  |  Размер: 5.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.6 KiB

После

Ширина:  |  Высота:  |  Размер: 5.3 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 2.4 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 4.5 KiB

После

Ширина:  |  Высота:  |  Размер: 5.1 KiB

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше