Telegram - introduced live location sharing with chats
|
@ -113,6 +113,7 @@ afterEvaluate {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':OsmAnd-java', configuration: 'android')
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
@ -121,6 +122,7 @@ dependencies {
|
|||
implementation 'com.android.support:support-annotations:27.1.1'
|
||||
implementation 'commons-logging:commons-logging-api:1.1'
|
||||
implementation 'com.android.support:recyclerview-v7:27.1.1'
|
||||
implementation 'com.vividsolutions:jts-core:1.14.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.osmand.telegram">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
@ -19,6 +20,7 @@
|
|||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
|
@ -30,6 +32,16 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:label="@string/process_location_service"
|
||||
android:name="net.osmand.telegram.LocationService"
|
||||
android:stopWithTask="true">
|
||||
<intent-filter>
|
||||
<action android:name="net.osmand.telegram.LocationService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".notifications.NotificationDismissReceiver" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,4 +1,4 @@
|
|||
package net.osmand.telegram.utils;
|
||||
package net.osmand;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package net.osmand.telegram
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import net.osmand.telegram.notifications.TelegramNotification
|
||||
import net.osmand.PlatformUtil
|
||||
|
||||
class LocationService : Service(), LocationListener {
|
||||
|
||||
private val binder = LocationServiceBinder()
|
||||
|
||||
var handler: Handler? = null
|
||||
|
||||
class LocationServiceBinder : Binder()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return binder
|
||||
}
|
||||
|
||||
fun stopIfNeeded(ctx: Context) {
|
||||
val serviceIntent = Intent(ctx, LocationService::class.java)
|
||||
ctx.stopService(serviceIntent)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
handler = Handler()
|
||||
val app = app()
|
||||
|
||||
app.locationService = this
|
||||
|
||||
// requesting
|
||||
// request location updates
|
||||
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
try {
|
||||
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this@LocationService)
|
||||
} catch (e: SecurityException) {
|
||||
Toast.makeText(this, R.string.no_location_permission, Toast.LENGTH_LONG).show()
|
||||
Log.d(PlatformUtil.TAG, "Location service permission not granted")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(this, R.string.gps_not_available, Toast.LENGTH_LONG).show()
|
||||
Log.d(PlatformUtil.TAG, "GPS location provider not available")
|
||||
}
|
||||
|
||||
// registering icon at top level
|
||||
// Leave icon visible even for navigation for proper display
|
||||
val notification = app.notificationHelper.buildTopNotification()
|
||||
if (notification != null) {
|
||||
startForeground(TelegramNotification.TOP_NOTIFICATION_SERVICE_ID, notification)
|
||||
app.notificationHelper.refreshNotification(TelegramNotification.NotificationType.SHARE_LOCATION)
|
||||
//app.notificationHelper.refreshNotifications()
|
||||
}
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun app() = application as TelegramApplication
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
val app = app()
|
||||
app.locationService = null
|
||||
|
||||
// remove updates
|
||||
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
try {
|
||||
locationManager.removeUpdates(this)
|
||||
} catch (e: SecurityException) {
|
||||
Log.d(PlatformUtil.TAG, "Location service permission not granted")
|
||||
}
|
||||
|
||||
// remove notification
|
||||
stopForeground(java.lang.Boolean.TRUE)
|
||||
app.notificationHelper.updateTopNotification()
|
||||
|
||||
app.runInUIThread({
|
||||
app.notificationHelper.refreshNotification(TelegramNotification.NotificationType.SHARE_LOCATION)
|
||||
//app.notificationHelper.refreshNotifications()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
override fun onLocationChanged(l: Location?) {
|
||||
if (l != null) {
|
||||
val location = convertLocation(l)
|
||||
app().shareLocationHelper.updateLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
Toast.makeText(this, getString(R.string.location_service_no_gps_available), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
override fun onProviderEnabled(provider: String) {}
|
||||
|
||||
|
||||
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
val app = app()
|
||||
app.notificationHelper.removeNotifications()
|
||||
if (app.locationService != null) {
|
||||
this@LocationService.stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun convertLocation(l: Location?): net.osmand.Location? {
|
||||
if (l == null) {
|
||||
return null
|
||||
}
|
||||
val r = net.osmand.Location(l.provider)
|
||||
r.latitude = l.latitude
|
||||
r.longitude = l.longitude
|
||||
r.time = l.time
|
||||
if (l.hasAccuracy()) {
|
||||
r.accuracy = l.accuracy
|
||||
}
|
||||
if (l.hasSpeed()) {
|
||||
r.speed = l.speed
|
||||
}
|
||||
if (l.hasAltitude()) {
|
||||
r.altitude = l.altitude
|
||||
}
|
||||
if (l.hasBearing()) {
|
||||
r.bearing = l.bearing
|
||||
}
|
||||
if (l.hasAltitude()) {
|
||||
r.altitude = l.altitude
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import net.osmand.telegram.utils.AndroidUtils
|
||||
import net.osmand.telegram.utils.PlatformUtil
|
||||
import net.osmand.PlatformUtil
|
||||
|
||||
|
||||
class LoginDialogFragment : DialogFragment() {
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
package net.osmand.telegram
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.*
|
||||
import android.view.*
|
||||
import android.widget.Toast
|
||||
import net.osmand.PlatformUtil
|
||||
import net.osmand.telegram.LoginDialogFragment.LoginDialogType
|
||||
import net.osmand.telegram.TelegramHelper.*
|
||||
import net.osmand.telegram.helpers.TelegramHelper
|
||||
import net.osmand.telegram.helpers.TelegramHelper.*
|
||||
import net.osmand.telegram.utils.AndroidUtils
|
||||
import org.drinkless.td.libcore.telegram.TdApi
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity(), TelegramListener {
|
||||
|
||||
companion object {
|
||||
private const val PERMISSION_REQUEST_LOCATION = 1
|
||||
|
||||
private const val LOGIN_MENU_ID = 0
|
||||
private const val LOGOUT_MENU_ID = 1
|
||||
private const val PROGRESS_MENU_ID = 2
|
||||
}
|
||||
|
||||
private val log = PlatformUtil.getLog(TelegramHelper::class.java)
|
||||
|
||||
private var telegramAuthorizationRequestHandler: TelegramAuthorizationRequestHandler? = null
|
||||
private var paused: Boolean = false
|
||||
|
||||
|
@ -28,8 +38,8 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
private val app: TelegramApplication
|
||||
get() = application as TelegramApplication
|
||||
|
||||
private val telegramHelper: TelegramHelper
|
||||
get() = app.telegramHelper
|
||||
private val telegramHelper get() = app.telegramHelper
|
||||
private val settings get() = app.settings
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -74,6 +84,10 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
|
||||
invalidateOptionsMenu()
|
||||
updateTitle()
|
||||
|
||||
if (settings.hasAnyChatToShareLocation() && AndroidUtils.isLocationPermissionAvailable(this)) {
|
||||
requestLocationPermission()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
@ -81,6 +95,16 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
paused = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
settings.save()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
telegramHelper.close()
|
||||
}
|
||||
|
||||
override fun onTelegramStatusChanged(prevTelegramAuthorizationState: TelegramAuthorizationState,
|
||||
newTelegramAuthorizationState: TelegramAuthorizationState) {
|
||||
runOnUi {
|
||||
|
@ -120,6 +144,10 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onSendLiveLicationError(code: Int, message: String) {
|
||||
log.error("Send live location error: $code - $message")
|
||||
}
|
||||
|
||||
private fun updateChatsList() {
|
||||
val chatList = telegramHelper.getChatList()
|
||||
val chats: MutableList<TdApi.Chat> = mutableListOf()
|
||||
|
@ -237,9 +265,23 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
telegramHelper.close()
|
||||
private fun requestLocationPermission() {
|
||||
if (!AndroidUtils.isLocationPermissionAvailable(this)) {
|
||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_LOCATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
when (requestCode) {
|
||||
PERMISSION_REQUEST_LOCATION -> {
|
||||
if (settings.hasAnyChatToShareLocation()) {
|
||||
app.shareLocationHelper.startSharingLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChatsAdapter :
|
||||
|
@ -258,16 +300,28 @@ class MainActivity : AppCompatActivity(), TelegramListener {
|
|||
val showOnMapSwitch: SwitchCompat? = view.findViewById(R.id.show_on_map_switch)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup,
|
||||
viewType: Int): ChatsAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.chat_list_item, parent, false)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatsAdapter.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.chat_list_item, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val chatId = chats[position].id
|
||||
holder.groupName?.text = chats[position].title
|
||||
holder.shareLocationSwitch?.setOnCheckedChangeListener(null)
|
||||
holder.shareLocationSwitch?.isChecked = settings.isSharingLocationToChat(chatId)
|
||||
holder.shareLocationSwitch?.setOnCheckedChangeListener { view, isChecked ->
|
||||
settings.shareLocationToChat(chatId, isChecked)
|
||||
if (settings.hasAnyChatToShareLocation()) {
|
||||
if (!AndroidUtils.isLocationPermissionAvailable(view.context)) {
|
||||
requestLocationPermission()
|
||||
} else {
|
||||
app.shareLocationHelper.startSharingLocation()
|
||||
}
|
||||
} else {
|
||||
app.shareLocationHelper.stopSharingLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = chats.size
|
||||
|
|
|
@ -2,12 +2,26 @@ package net.osmand.telegram
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import net.osmand.telegram.helpers.ShareLocationHelper
|
||||
import net.osmand.telegram.helpers.TelegramHelper
|
||||
import net.osmand.telegram.notifications.NotificationHelper
|
||||
import net.osmand.telegram.utils.AndroidUtils
|
||||
|
||||
class TelegramApplication : Application() {
|
||||
|
||||
val telegramHelper: TelegramHelper = TelegramHelper.instance
|
||||
val telegramHelper = TelegramHelper.instance
|
||||
lateinit var settings: TelegramSettings private set
|
||||
lateinit var shareLocationHelper: ShareLocationHelper private set
|
||||
lateinit var notificationHelper: NotificationHelper private set
|
||||
|
||||
var locationService: LocationService? = null
|
||||
|
||||
private val uiHandler = Handler()
|
||||
|
||||
private val lastTimeInternetConnectionChecked: Long = 0
|
||||
private var internetConnectionAvailable = true
|
||||
|
@ -15,6 +29,14 @@ class TelegramApplication : Application() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
telegramHelper.appDir = filesDir.absolutePath
|
||||
|
||||
settings = TelegramSettings(this)
|
||||
shareLocationHelper = ShareLocationHelper(this)
|
||||
notificationHelper = NotificationHelper(this)
|
||||
|
||||
if (settings.hasAnyChatToShareLocation() && AndroidUtils.isLocationPermissionAvailable(this)) {
|
||||
shareLocationHelper.startSharingLocation()
|
||||
}
|
||||
}
|
||||
|
||||
val isWifiConnected: Boolean
|
||||
|
@ -47,4 +69,32 @@ class TelegramApplication : Application() {
|
|||
}
|
||||
return internetConnectionAvailable
|
||||
}
|
||||
|
||||
fun startLocationService(restart: Boolean = false) {
|
||||
val serviceIntent = Intent(this, LocationService::class.java)
|
||||
|
||||
val locationService = locationService
|
||||
if (locationService != null && restart) {
|
||||
locationService.stopSelf()
|
||||
}
|
||||
if (locationService == null || restart) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopLocationService() {
|
||||
locationService?.stopIfNeeded(this)
|
||||
}
|
||||
|
||||
fun runInUIThread(action: (() -> Unit)) {
|
||||
uiHandler.post(action)
|
||||
}
|
||||
|
||||
fun runInUIThread(action: (() -> Unit), delay: Long) {
|
||||
uiHandler.postDelayed(action, delay)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package net.osmand.telegram
|
||||
|
||||
import android.content.Context
|
||||
import net.osmand.telegram.utils.OsmandFormatter.MetricsConstants
|
||||
import net.osmand.telegram.utils.OsmandFormatter.SpeedConstants
|
||||
|
||||
class TelegramSettings(private val app: TelegramApplication) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SETTINGS_NAME = "osmand_telegram_settings"
|
||||
|
||||
private const val SHARE_LOCATION_CHATS_KEY = "share_location_chats_key"
|
||||
private const val SHOW_ON_MAP_CHATS_KEY = "show_on_map_chats_key"
|
||||
|
||||
private const val METRICS_CONSTANTS_KEY = "metrics_constants_key"
|
||||
private const val SPEED_CONSTANTS_KEY = "speed_constants_key"
|
||||
|
||||
private const val SHOW_NOTIFICATION_ALWAYS_KEY = "show_notification_always_key"
|
||||
}
|
||||
|
||||
private var shareLocationChats: Set<Long> = emptySet()
|
||||
private var showOnMapChats: Set<Long> = emptySet()
|
||||
|
||||
var metricsConstants = MetricsConstants.KILOMETERS_AND_METERS
|
||||
var speedConstants = SpeedConstants.KILOMETERS_PER_HOUR
|
||||
|
||||
var showNotificationAlways = true
|
||||
|
||||
init {
|
||||
read()
|
||||
}
|
||||
|
||||
fun hasAnyChatToShareLocation(): Boolean {
|
||||
return shareLocationChats.isNotEmpty()
|
||||
}
|
||||
|
||||
fun isSharingLocationToChat(chatId: Long): Boolean {
|
||||
return shareLocationChats.contains(chatId)
|
||||
}
|
||||
|
||||
fun shareLocationToChat(chatId: Long, share: Boolean) {
|
||||
val shareLocationChats = shareLocationChats.toMutableList()
|
||||
if (share) {
|
||||
shareLocationChats.add(chatId)
|
||||
} else {
|
||||
shareLocationChats.remove(chatId)
|
||||
}
|
||||
this.shareLocationChats = shareLocationChats.toHashSet()
|
||||
}
|
||||
|
||||
fun getShareLocationChats() = ArrayList(shareLocationChats)
|
||||
|
||||
fun save() {
|
||||
val prefs = app.getSharedPreferences(SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
val edit = prefs.edit()
|
||||
|
||||
val shareLocationChatsSet = mutableSetOf<String>()
|
||||
val shareLocationChats = ArrayList(shareLocationChats)
|
||||
for (chatId in shareLocationChats) {
|
||||
shareLocationChatsSet.add(chatId.toString())
|
||||
}
|
||||
edit.putStringSet(SHARE_LOCATION_CHATS_KEY, shareLocationChatsSet)
|
||||
|
||||
val showOnMapChatsSet = mutableSetOf<String>()
|
||||
val showOnMapChats = ArrayList(showOnMapChats)
|
||||
for (chatId in showOnMapChats) {
|
||||
showOnMapChatsSet.add(chatId.toString())
|
||||
}
|
||||
edit.putStringSet(SHOW_ON_MAP_CHATS_KEY, showOnMapChatsSet)
|
||||
|
||||
edit.putString(METRICS_CONSTANTS_KEY, metricsConstants.name)
|
||||
edit.putString(SPEED_CONSTANTS_KEY, speedConstants.name)
|
||||
|
||||
edit.putBoolean(SHOW_NOTIFICATION_ALWAYS_KEY, showNotificationAlways)
|
||||
|
||||
edit.apply()
|
||||
}
|
||||
|
||||
fun read() {
|
||||
val prefs = app.getSharedPreferences(SETTINGS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val shareLocationChats = mutableSetOf<Long>()
|
||||
val shareLocationChatsSet = prefs.getStringSet(SHARE_LOCATION_CHATS_KEY, mutableSetOf())
|
||||
for (chatIdStr in shareLocationChatsSet) {
|
||||
val chatId = chatIdStr.toLongOrNull()
|
||||
if (chatId != null) {
|
||||
shareLocationChats.add(chatId)
|
||||
}
|
||||
}
|
||||
this.shareLocationChats = shareLocationChats
|
||||
|
||||
val showOnMapChats = mutableSetOf<Long>()
|
||||
val showOnMapChatsSet = prefs.getStringSet(SHOW_ON_MAP_CHATS_KEY, mutableSetOf())
|
||||
for (chatIdStr in showOnMapChatsSet) {
|
||||
val chatId = chatIdStr.toLongOrNull()
|
||||
if (chatId != null) {
|
||||
showOnMapChats.add(chatId)
|
||||
}
|
||||
}
|
||||
this.showOnMapChats = showOnMapChats
|
||||
|
||||
metricsConstants = MetricsConstants.valueOf(prefs.getString(METRICS_CONSTANTS_KEY, MetricsConstants.KILOMETERS_AND_METERS.name))
|
||||
speedConstants = SpeedConstants.valueOf(prefs.getString(SPEED_CONSTANTS_KEY, SpeedConstants.KILOMETERS_PER_HOUR.name))
|
||||
|
||||
showNotificationAlways = prefs.getBoolean(SHOW_NOTIFICATION_ALWAYS_KEY, true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package net.osmand.telegram.helpers
|
||||
|
||||
import net.osmand.Location
|
||||
import net.osmand.telegram.TelegramApplication
|
||||
import net.osmand.telegram.notifications.TelegramNotification.NotificationType
|
||||
|
||||
class ShareLocationHelper(private val app: TelegramApplication) {
|
||||
|
||||
companion object {
|
||||
const val MAX_LOCATION_MESSAGE_LIVE_PERIOD_SEC = 60 * 60 * 24 - 1 // day
|
||||
}
|
||||
|
||||
var sharingLocation: Boolean = false
|
||||
private set
|
||||
|
||||
var duration: Long = 0
|
||||
private set
|
||||
|
||||
var distance: Int = 0
|
||||
private set
|
||||
|
||||
var lastLocationMessageSentTime: Long = 0
|
||||
|
||||
private var lastTimeInMillis: Long = 0L
|
||||
|
||||
private var lastLocation: Location? = null
|
||||
set(value) {
|
||||
if (lastTimeInMillis == 0L) {
|
||||
lastTimeInMillis = System.currentTimeMillis()
|
||||
} else {
|
||||
val currentTimeInMillis = System.currentTimeMillis()
|
||||
duration += currentTimeInMillis - lastTimeInMillis
|
||||
lastTimeInMillis = currentTimeInMillis
|
||||
}
|
||||
if (lastLocation != null && value != null) {
|
||||
distance += value.distanceTo(lastLocation).toInt()
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
fun updateLocation(location: Location?) {
|
||||
lastLocation = location
|
||||
|
||||
if (location != null) {
|
||||
val shareLocationChats = app.settings.getShareLocationChats()
|
||||
if (shareLocationChats.isNotEmpty()) {
|
||||
app.telegramHelper.sendLiveLocation(shareLocationChats, MAX_LOCATION_MESSAGE_LIVE_PERIOD_SEC, location.latitude, location.longitude)
|
||||
}
|
||||
lastLocationMessageSentTime = System.currentTimeMillis()
|
||||
}
|
||||
refreshNotification()
|
||||
}
|
||||
|
||||
fun startSharingLocation() {
|
||||
sharingLocation = true
|
||||
|
||||
app.startLocationService()
|
||||
|
||||
refreshNotification()
|
||||
}
|
||||
|
||||
fun stopSharingLocation() {
|
||||
sharingLocation = false
|
||||
|
||||
app.stopLocationService()
|
||||
lastLocation = null
|
||||
lastTimeInMillis = 0L
|
||||
distance = 0
|
||||
duration = 0
|
||||
|
||||
refreshNotification()
|
||||
}
|
||||
|
||||
fun pauseSharingLocation() {
|
||||
sharingLocation = false
|
||||
|
||||
app.stopLocationService()
|
||||
lastLocation = null
|
||||
lastTimeInMillis = 0L
|
||||
|
||||
refreshNotification()
|
||||
}
|
||||
|
||||
private fun refreshNotification() {
|
||||
app.runInUIThread {
|
||||
app.notificationHelper.refreshNotification(NotificationType.SHARE_LOCATION)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package net.osmand.telegram
|
||||
package net.osmand.telegram.helpers
|
||||
|
||||
import android.text.TextUtils
|
||||
import net.osmand.telegram.TelegramHelper.TelegramAuthenticationParameterType.*
|
||||
import net.osmand.telegram.utils.CancellableAsyncTask
|
||||
import net.osmand.telegram.utils.PlatformUtil
|
||||
import net.osmand.telegram.helpers.TelegramHelper.TelegramAuthenticationParameterType.*
|
||||
import net.osmand.PlatformUtil
|
||||
import org.drinkless.td.libcore.telegram.Client
|
||||
import org.drinkless.td.libcore.telegram.Client.ResultHandler
|
||||
import org.drinkless.td.libcore.telegram.TdApi
|
||||
|
@ -104,6 +103,7 @@ class TelegramHelper private constructor() {
|
|||
|
||||
fun onTelegramChatsRead()
|
||||
fun onTelegramError(code: Int, message: String)
|
||||
fun onSendLiveLicationError(code: Int, message: String)
|
||||
}
|
||||
|
||||
interface TelegramAuthorizationRequestListener {
|
||||
|
@ -205,6 +205,77 @@ class TelegramHelper private constructor() {
|
|||
listener?.onTelegramChatsRead()
|
||||
}
|
||||
|
||||
/**
|
||||
* @chatId Id of the chat
|
||||
* @livePeriod Period for which the location can be updated, in seconds; should be between 60 and 86400 for a live location and 0 otherwise.
|
||||
* @latitude Latitude of the location
|
||||
* @longitude Longitude of the location
|
||||
*/
|
||||
fun sendLiveLocation(chatIds: List<Long>, livePeriod: Int = 61, latitude: Double, longitude: Double) {
|
||||
|
||||
val lp = livePeriod.coerceAtLeast(61)
|
||||
val location = TdApi.Location(latitude, longitude)
|
||||
val content = TdApi.InputMessageLocation(location, lp)
|
||||
|
||||
client?.send(TdApi.GetActiveLiveLocationMessages(), { obj ->
|
||||
when (obj.constructor) {
|
||||
TdApi.Error.CONSTRUCTOR -> {
|
||||
val error = obj as TdApi.Error
|
||||
listener?.onSendLiveLicationError(error.code, error.message)
|
||||
}
|
||||
TdApi.Messages.CONSTRUCTOR -> {
|
||||
val messages = (obj as TdApi.Messages).messages
|
||||
val processedChatIds = mutableListOf<Long>()
|
||||
if (messages.isNotEmpty()) {
|
||||
for (msg in messages) {
|
||||
if (chatIds.contains(msg.chatId)) {
|
||||
processedChatIds.add(msg.chatId)
|
||||
client?.send(TdApi.EditMessageLiveLocation(msg.chatId, msg.id, null, location), { o->
|
||||
when (o.constructor) {
|
||||
TdApi.Error.CONSTRUCTOR -> {
|
||||
val error = o as TdApi.Error
|
||||
listener?.onSendLiveLicationError(error.code, error.message)
|
||||
}
|
||||
else -> listener?.onSendLiveLicationError(-1, "Receive wrong response from TDLib: $o")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chatIds.size != processedChatIds.size) {
|
||||
val mutableChatIds = chatIds.toMutableList()
|
||||
mutableChatIds.removeAll(processedChatIds)
|
||||
for (chatId in mutableChatIds) {
|
||||
client?.send(TdApi.SendMessage(chatId, 0, false, true, null, content), { o->
|
||||
when (o.constructor) {
|
||||
TdApi.Error.CONSTRUCTOR -> {
|
||||
val error = o as TdApi.Error
|
||||
listener?.onSendLiveLicationError(error.code, error.message)
|
||||
}
|
||||
else -> listener?.onSendLiveLicationError(-1, "Receive wrong response from TDLib: $o")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> listener?.onSendLiveLicationError(-1, "Receive wrong response from TDLib: $obj")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @chatId Id of the chat
|
||||
* @message Text of the message
|
||||
*/
|
||||
fun sendText(chatId: Long, message: String) {
|
||||
// initialize reply markup just for testing
|
||||
//val row = arrayOf(TdApi.InlineKeyboardButton("https://telegram.org?1", TdApi.InlineKeyboardButtonTypeUrl()), TdApi.InlineKeyboardButton("https://telegram.org?2", TdApi.InlineKeyboardButtonTypeUrl()), TdApi.InlineKeyboardButton("https://telegram.org?3", TdApi.InlineKeyboardButtonTypeUrl()))
|
||||
//val replyMarkup = TdApi.ReplyMarkupInlineKeyboard(arrayOf(row, row, row))
|
||||
|
||||
val content = TdApi.InputMessageText(TdApi.FormattedText(message, null), false, true)
|
||||
client?.send(TdApi.SendMessage(chatId, 0, false, true, null, content), defaultHandler)
|
||||
}
|
||||
|
||||
fun logout(): Boolean {
|
||||
return if (libraryLoaded) {
|
||||
isHaveAuthorization = false
|
||||
|
@ -354,9 +425,7 @@ class TelegramHelper private constructor() {
|
|||
chat.order = 0
|
||||
setChatOrder(chat, order)
|
||||
}
|
||||
CancellableAsyncTask.run("onTelegramChatsRead", 100, {
|
||||
listener?.onTelegramChatsRead()
|
||||
})
|
||||
listener?.onTelegramChatsRead()
|
||||
}
|
||||
TdApi.UpdateChatTitle.CONSTRUCTOR -> {
|
||||
val updateChat = obj as TdApi.UpdateChatTitle
|
||||
|
@ -424,6 +493,11 @@ class TelegramHelper private constructor() {
|
|||
chat.unreadMentionCount = updateChat.unreadMentionCount
|
||||
}
|
||||
}
|
||||
|
||||
TdApi.UpdateMessageSendSucceeded.CONSTRUCTOR -> {
|
||||
val updateMessageSent = obj as TdApi.UpdateMessageSendSucceeded
|
||||
}
|
||||
|
||||
TdApi.UpdateChatReplyMarkup.CONSTRUCTOR -> {
|
||||
val updateChat = obj as TdApi.UpdateChatReplyMarkup
|
||||
val chat = chats[updateChat.chatId]
|
|
@ -0,0 +1,39 @@
|
|||
package net.osmand.telegram.notifications
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
|
||||
import net.osmand.telegram.TelegramApplication
|
||||
import net.osmand.telegram.notifications.TelegramNotification.NotificationType
|
||||
|
||||
class NotificationDismissReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val helper = (context.applicationContext as TelegramApplication).notificationHelper
|
||||
val notificationTypeStr = intent.extras!!.getString(NOTIFICATION_TYPE_KEY_NAME)
|
||||
if (!TextUtils.isEmpty(notificationTypeStr)) {
|
||||
try {
|
||||
val notificationType = NotificationType.valueOf(notificationTypeStr)
|
||||
helper.onNotificationDismissed(notificationType)
|
||||
} catch (e: Exception) {
|
||||
//ignored
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val NOTIFICATION_TYPE_KEY_NAME = "net.osmand.telegram.notifications.NotificationType"
|
||||
|
||||
fun createIntent(context: Context, notificationType: NotificationType): PendingIntent {
|
||||
val intent = Intent(context, NotificationDismissReceiver::class.java)
|
||||
intent.putExtra(NOTIFICATION_TYPE_KEY_NAME, notificationType.name)
|
||||
return PendingIntent.getBroadcast(context.applicationContext,
|
||||
0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package net.osmand.telegram.notifications
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
import net.osmand.telegram.R
|
||||
import net.osmand.telegram.TelegramApplication
|
||||
import net.osmand.telegram.notifications.TelegramNotification.NotificationType
|
||||
import java.util.*
|
||||
|
||||
class NotificationHelper(private val app: TelegramApplication) {
|
||||
|
||||
private var shareLocationNotification: ShareLocationNotification? = null
|
||||
private val all = ArrayList<TelegramNotification>()
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
val shareLocationNotification = ShareLocationNotification(app)
|
||||
this.shareLocationNotification = shareLocationNotification
|
||||
all.add(shareLocationNotification)
|
||||
}
|
||||
|
||||
fun buildTopNotification(): Notification? {
|
||||
val notification = acquireTopNotification()
|
||||
if (notification != null) {
|
||||
removeNotification(notification.type)
|
||||
setTopNotification(notification)
|
||||
val notificationBuilder = notification.buildNotification(false)
|
||||
return notificationBuilder?.build()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun acquireTopNotification(): TelegramNotification? {
|
||||
var notification: TelegramNotification? = null
|
||||
if (shareLocationNotification!!.isEnabled && shareLocationNotification!!.isActive) {
|
||||
notification = shareLocationNotification
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
fun updateTopNotification() {
|
||||
val notification = acquireTopNotification()
|
||||
setTopNotification(notification)
|
||||
}
|
||||
|
||||
private fun setTopNotification(notification: TelegramNotification?) {
|
||||
for (n in all) {
|
||||
n.isTop = n === notification
|
||||
}
|
||||
}
|
||||
|
||||
fun showNotifications() {
|
||||
if (!hasAnyTopNotification()) {
|
||||
removeTopNotification()
|
||||
}
|
||||
for (notification in all) {
|
||||
notification.showNotification()
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshNotification(notificationType: NotificationType) {
|
||||
for (notification in all) {
|
||||
if (notification.type == notificationType) {
|
||||
notification.refreshNotification()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onNotificationDismissed(notificationType: NotificationType) {
|
||||
for (notification in all) {
|
||||
if (notification.type == notificationType) {
|
||||
notification.onNotificationDismissed()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasAnyTopNotification(): Boolean {
|
||||
for (notification in all) {
|
||||
if (notification.isTop) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun refreshNotifications() {
|
||||
if (!hasAnyTopNotification()) {
|
||||
removeTopNotification()
|
||||
}
|
||||
for (notification in all) {
|
||||
notification.refreshNotification()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTopNotification() {
|
||||
val notificationManager = NotificationManagerCompat.from(app)
|
||||
notificationManager.cancel(TelegramNotification.TOP_NOTIFICATION_SERVICE_ID)
|
||||
}
|
||||
|
||||
fun removeNotification(notificationType: NotificationType) {
|
||||
for (notification in all) {
|
||||
if (notification.type == notificationType) {
|
||||
notification.removeNotification()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeNotifications() {
|
||||
for (notification in all) {
|
||||
notification.removeNotification()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(26)
|
||||
fun createNotificationChannel() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(NOTIFICATION_CHANEL_ID,
|
||||
app.getString(R.string.osmand_service), NotificationManager.IMPORTANCE_LOW)
|
||||
channel.enableVibration(false)
|
||||
channel.description = app.getString(R.string.osmand_service_descr)
|
||||
val mNotificationManager = app
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
mNotificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NOTIFICATION_CHANEL_ID = "osmand_telegram_background_service"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package net.osmand.telegram.notifications
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import net.osmand.telegram.R
|
||||
import net.osmand.telegram.TelegramApplication
|
||||
import net.osmand.telegram.utils.OsmandFormatter
|
||||
import net.osmand.util.Algorithms
|
||||
|
||||
class ShareLocationNotification(app: TelegramApplication) : TelegramNotification(app, GROUP_NAME) {
|
||||
|
||||
private var wasNoDataDismissed: Boolean = false
|
||||
private var lastBuiltNoData: Boolean = false
|
||||
|
||||
override val type: TelegramNotification.NotificationType
|
||||
get() = TelegramNotification.NotificationType.SHARE_LOCATION
|
||||
|
||||
override val priority: Int
|
||||
get() = NotificationCompat.PRIORITY_DEFAULT
|
||||
|
||||
override val isActive: Boolean
|
||||
get() {
|
||||
val service = app.locationService
|
||||
return isEnabled && service != null
|
||||
}
|
||||
|
||||
override val isEnabled: Boolean
|
||||
get() = app.settings.hasAnyChatToShareLocation()
|
||||
|
||||
override val osmandNotificationId: Int
|
||||
get() = TelegramNotification.SHARE_LOCATION_NOTIFICATION_SERVICE_ID
|
||||
|
||||
override val osmandWearableNotificationId: Int
|
||||
get() = TelegramNotification.WEAR_SHARE_LOCATION_NOTIFICATION_SERVICE_ID
|
||||
|
||||
init {
|
||||
app.registerReceiver(object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
app.shareLocationHelper.startSharingLocation()
|
||||
}
|
||||
}, IntentFilter(OSMAND_START_LOCATION_SHARING_SERVICE_ACTION))
|
||||
|
||||
app.registerReceiver(object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
app.shareLocationHelper.pauseSharingLocation()
|
||||
}
|
||||
}, IntentFilter(OSMAND_PAUSE_LOCATION_SHARING_SERVICE_ACTION))
|
||||
|
||||
app.registerReceiver(object : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
app.shareLocationHelper.stopSharingLocation()
|
||||
}
|
||||
}, IntentFilter(OSMAND_STOP_LOCATION_SHARING_SERVICE_ACTION))
|
||||
}
|
||||
|
||||
override fun onNotificationDismissed() {
|
||||
if (!wasNoDataDismissed) {
|
||||
wasNoDataDismissed = lastBuiltNoData
|
||||
}
|
||||
}
|
||||
|
||||
override fun buildNotification(wearable: Boolean): NotificationCompat.Builder? {
|
||||
if (!isEnabled) {
|
||||
return null
|
||||
}
|
||||
val notificationTitle: String
|
||||
val notificationText: String
|
||||
color = 0
|
||||
icon = R.drawable.ic_action_polygom_dark
|
||||
val shareLocationHelper = app.shareLocationHelper
|
||||
val isSharingLocation = shareLocationHelper.sharingLocation
|
||||
val sharedDistance = shareLocationHelper.distance.toFloat()
|
||||
ongoing = true
|
||||
lastBuiltNoData = false
|
||||
if (isSharingLocation) {
|
||||
color = ContextCompat.getColor(app, R.color.osmand_orange)
|
||||
notificationTitle = (app.getString(R.string.sharing_location) + " • "
|
||||
+ Algorithms.formatDuration((shareLocationHelper.duration / 1000).toInt(), true))
|
||||
notificationText = (app.getString(R.string.shared_string_distance)
|
||||
+ ": " + OsmandFormatter.getFormattedDistance(sharedDistance, app))
|
||||
} else {
|
||||
if (sharedDistance > 0) {
|
||||
notificationTitle = (app.getString(R.string.shared_string_paused) + " • "
|
||||
+ Algorithms.formatDuration((shareLocationHelper.duration / 1000).toInt(), true))
|
||||
notificationText = (app.getString(R.string.shared_string_distance)
|
||||
+ ": " + OsmandFormatter.getFormattedDistance(sharedDistance, app))
|
||||
} else {
|
||||
ongoing = false
|
||||
notificationTitle = app.getString(R.string.share_location)
|
||||
notificationText = app.getString(R.string.shared_string_no_data)
|
||||
lastBuiltNoData = true
|
||||
}
|
||||
}
|
||||
|
||||
if ((wasNoDataDismissed || !app.settings.showNotificationAlways) && !ongoing) {
|
||||
return null
|
||||
}
|
||||
|
||||
val notificationBuilder = createBuilder(wearable)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationText))
|
||||
|
||||
val stopIntent = Intent(OSMAND_STOP_LOCATION_SHARING_SERVICE_ACTION)
|
||||
val stopPendingIntent = PendingIntent.getBroadcast(app, 0, stopIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
if (isSharingLocation) {
|
||||
if (app.shareLocationHelper.distance > 0) {
|
||||
val pauseIntent = Intent(OSMAND_PAUSE_LOCATION_SHARING_SERVICE_ACTION)
|
||||
val pausePendingIntent = PendingIntent.getBroadcast(app, 0, pauseIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
notificationBuilder.addAction(R.drawable.ic_pause,
|
||||
app.getString(R.string.shared_string_pause), pausePendingIntent)
|
||||
notificationBuilder.addAction(R.drawable.ic_action_rec_stop,
|
||||
app.getString(R.string.shared_string_stop), stopPendingIntent)
|
||||
} else {
|
||||
notificationBuilder.addAction(R.drawable.ic_action_rec_stop,
|
||||
app.getString(R.string.shared_string_stop), stopPendingIntent)
|
||||
}
|
||||
} else {
|
||||
val startIntent = Intent(OSMAND_START_LOCATION_SHARING_SERVICE_ACTION)
|
||||
val startPendingIntent = PendingIntent.getBroadcast(app, 0, startIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
if (sharedDistance > 0) {
|
||||
notificationBuilder.addAction(R.drawable.ic_action_rec_start,
|
||||
app.getString(R.string.shared_string_continue), startPendingIntent)
|
||||
notificationBuilder.addAction(R.drawable.ic_action_rec_stop,
|
||||
app.getString(R.string.shared_string_stop), stopPendingIntent)
|
||||
} else {
|
||||
notificationBuilder.addAction(R.drawable.ic_action_rec_start,
|
||||
app.getString(R.string.shared_string_start), startPendingIntent)
|
||||
}
|
||||
}
|
||||
|
||||
return notificationBuilder
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val OSMAND_START_LOCATION_SHARING_SERVICE_ACTION = "osmand_start_location_sharing_service_action"
|
||||
const val OSMAND_PAUSE_LOCATION_SHARING_SERVICE_ACTION = "osmand_pause_location_sharing_service_action"
|
||||
const val OSMAND_STOP_LOCATION_SHARING_SERVICE_ACTION = "osmand_stop_location_sharing_service_action"
|
||||
const val GROUP_NAME = "share_location"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package net.osmand.telegram.notifications
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.NotificationManagerCompat
|
||||
|
||||
import net.osmand.telegram.MainActivity
|
||||
import net.osmand.telegram.TelegramApplication
|
||||
|
||||
|
||||
abstract class TelegramNotification(protected var app: TelegramApplication, val groupName: String) {
|
||||
protected var ongoing = true
|
||||
protected var color: Int = 0
|
||||
protected var icon: Int = 0
|
||||
var isTop: Boolean = false
|
||||
|
||||
abstract val type: NotificationType
|
||||
|
||||
abstract val osmandNotificationId: Int
|
||||
|
||||
abstract val osmandWearableNotificationId: Int
|
||||
|
||||
abstract val priority: Int
|
||||
|
||||
abstract val isActive: Boolean
|
||||
|
||||
abstract val isEnabled: Boolean
|
||||
|
||||
enum class NotificationType {
|
||||
SHARE_LOCATION
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
protected fun createBuilder(wearable: Boolean): NotificationCompat.Builder {
|
||||
val contentIntent = Intent(app, MainActivity::class.java)
|
||||
val contentPendingIntent = PendingIntent.getActivity(app, 0, contentIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
app.notificationHelper.createNotificationChannel()
|
||||
}
|
||||
val builder = NotificationCompat.Builder(app, NotificationHelper.NOTIFICATION_CHANEL_ID)
|
||||
.setVisibility(android.support.v4.app.NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setPriority(if (isTop) NotificationCompat.PRIORITY_HIGH else priority)
|
||||
.setOngoing(ongoing && !wearable)
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setDeleteIntent(NotificationDismissReceiver.createIntent(app, type))
|
||||
.setGroup(groupName).setGroupSummary(!wearable)
|
||||
|
||||
if (color != 0) {
|
||||
builder.color = color
|
||||
}
|
||||
if (icon != 0) {
|
||||
builder.setSmallIcon(icon)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
abstract fun buildNotification(wearable: Boolean): NotificationCompat.Builder?
|
||||
|
||||
fun setupNotification(notification: Notification) {}
|
||||
|
||||
open fun onNotificationDismissed() {}
|
||||
|
||||
private fun notifyWearable(notificationManager: NotificationManagerCompat) {
|
||||
val wearNotificationBuilder = buildNotification(true)
|
||||
if (wearNotificationBuilder != null) {
|
||||
val wearNotification = wearNotificationBuilder.build()
|
||||
notificationManager.notify(osmandWearableNotificationId, wearNotification)
|
||||
}
|
||||
}
|
||||
|
||||
fun showNotification(): Boolean {
|
||||
val notificationManager = NotificationManagerCompat.from(app)
|
||||
if (isEnabled) {
|
||||
val notificationBuilder = buildNotification(false)
|
||||
if (notificationBuilder != null) {
|
||||
val notification = notificationBuilder.build()
|
||||
setupNotification(notification)
|
||||
notificationManager.notify(if (isTop) TOP_NOTIFICATION_SERVICE_ID else osmandNotificationId, notification)
|
||||
notifyWearable(notificationManager)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun refreshNotification(): Boolean {
|
||||
val notificationManager = NotificationManagerCompat.from(app)
|
||||
if (isEnabled) {
|
||||
val notificationBuilder = buildNotification(false)
|
||||
if (notificationBuilder != null) {
|
||||
val notification = notificationBuilder.build()
|
||||
setupNotification(notification)
|
||||
if (isTop) {
|
||||
notificationManager.cancel(osmandNotificationId)
|
||||
notificationManager.notify(TOP_NOTIFICATION_SERVICE_ID, notification)
|
||||
} else {
|
||||
notificationManager.notify(osmandNotificationId, notification)
|
||||
}
|
||||
notifyWearable(notificationManager)
|
||||
return true
|
||||
} else {
|
||||
notificationManager.cancel(osmandNotificationId)
|
||||
}
|
||||
} else {
|
||||
notificationManager.cancel(osmandNotificationId)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun removeNotification() {
|
||||
val notificationManager = NotificationManagerCompat.from(app)
|
||||
notificationManager.cancel(osmandNotificationId)
|
||||
notificationManager.cancel(osmandWearableNotificationId)
|
||||
}
|
||||
|
||||
fun closeSystemDialogs(context: Context) {
|
||||
val it = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
|
||||
context.sendBroadcast(it)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val SHARE_LOCATION_NOTIFICATION_SERVICE_ID = 6
|
||||
const val TOP_NOTIFICATION_SERVICE_ID = 100
|
||||
|
||||
const val WEAR_SHARE_LOCATION_NOTIFICATION_SERVICE_ID = 1006
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package net.osmand.telegram.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
|
@ -32,4 +35,8 @@ object AndroidUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isLocationPermissionAvailable(context: Context): Boolean {
|
||||
return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,267 @@
|
|||
package net.osmand.telegram.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import net.osmand.telegram.R;
|
||||
import net.osmand.telegram.TelegramApplication;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
public class OsmandFormatter {
|
||||
|
||||
public final static float METERS_IN_KILOMETER = 1000f;
|
||||
public final static float METERS_IN_ONE_MILE = 1609.344f; // 1609.344
|
||||
public final static float METERS_IN_ONE_NAUTICALMILE = 1852f; // 1852
|
||||
|
||||
public final static float YARDS_IN_ONE_METER = 1.0936f;
|
||||
public final static float FEET_IN_ONE_METER = YARDS_IN_ONE_METER * 3f;
|
||||
private static final DecimalFormat fixed2 = new DecimalFormat("0.00");
|
||||
private static final DecimalFormat fixed1 = new DecimalFormat("0.0");
|
||||
{
|
||||
fixed2.setMinimumFractionDigits(2);
|
||||
fixed1.setMinimumFractionDigits(1);
|
||||
fixed1.setMinimumIntegerDigits(1);
|
||||
fixed2.setMinimumIntegerDigits(1);
|
||||
}
|
||||
|
||||
public static String getFormattedDuration(int seconds, TelegramApplication ctx) {
|
||||
int hours = seconds / (60 * 60);
|
||||
int minutes = (seconds / 60) % 60;
|
||||
if (hours > 0) {
|
||||
return hours + " "
|
||||
+ ctx.getString(R.string.shared_string_hour_short)
|
||||
+ (minutes > 0 ? " " + minutes + " "
|
||||
+ ctx.getString(R.string.shared_string_minute_short) : "");
|
||||
} else {
|
||||
return minutes + " " + ctx.getString(R.string.shared_string_minute_short);
|
||||
}
|
||||
}
|
||||
|
||||
public static double calculateRoundedDist(double distInMeters, TelegramApplication ctx) {
|
||||
MetricsConstants mc = ctx.getSettings().getMetricsConstants();
|
||||
double mainUnitInMeter = 1;
|
||||
double metersInSecondUnit = METERS_IN_KILOMETER;
|
||||
if (mc == MetricsConstants.MILES_AND_FEET) {
|
||||
mainUnitInMeter = FEET_IN_ONE_METER;
|
||||
metersInSecondUnit = METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_METERS) {
|
||||
mainUnitInMeter = 1;
|
||||
metersInSecondUnit = METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.NAUTICAL_MILES) {
|
||||
mainUnitInMeter = 1;
|
||||
metersInSecondUnit = METERS_IN_ONE_NAUTICALMILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_YARDS) {
|
||||
mainUnitInMeter = YARDS_IN_ONE_METER;
|
||||
metersInSecondUnit = METERS_IN_ONE_MILE;
|
||||
}
|
||||
|
||||
// 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 ...
|
||||
int generator = 1;
|
||||
byte pointer = 1;
|
||||
double point = mainUnitInMeter;
|
||||
double roundDist = 1;
|
||||
while (distInMeters * point > generator) {
|
||||
roundDist = (generator / point);
|
||||
if (pointer++ % 3 == 2) {
|
||||
generator = generator * 5 / 2;
|
||||
} else {
|
||||
generator *= 2;
|
||||
}
|
||||
|
||||
if (point == mainUnitInMeter && metersInSecondUnit * mainUnitInMeter * 0.9f <= generator) {
|
||||
point = 1 / metersInSecondUnit;
|
||||
generator = 1;
|
||||
pointer = 1;
|
||||
}
|
||||
}
|
||||
//Miles exceptions: 2000ft->0.5mi, 1000ft->0.25mi, 1000yd->0.5mi, 500yd->0.25mi, 1000m ->0.5mi, 500m -> 0.25mi
|
||||
if (mc == MetricsConstants.MILES_AND_METERS && roundDist == 1000) {
|
||||
roundDist = 0.5f * METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_METERS && roundDist == 500) {
|
||||
roundDist = 0.25f * METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_FEET && roundDist == 2000 / (double) FEET_IN_ONE_METER) {
|
||||
roundDist = 0.5f * METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_FEET && roundDist == 1000 / (double) FEET_IN_ONE_METER) {
|
||||
roundDist = 0.25f * METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_YARDS && roundDist == 1000 / (double) YARDS_IN_ONE_METER) {
|
||||
roundDist = 0.5f * METERS_IN_ONE_MILE;
|
||||
} else if (mc == MetricsConstants.MILES_AND_YARDS && roundDist == 500 / (double) YARDS_IN_ONE_METER) {
|
||||
roundDist = 0.25f * METERS_IN_ONE_MILE;
|
||||
}
|
||||
return roundDist;
|
||||
}
|
||||
|
||||
public static String getFormattedRoundDistanceKm(float meters, int digits, TelegramApplication ctx) {
|
||||
int mainUnitStr = R.string.km;
|
||||
float mainUnitInMeters = METERS_IN_KILOMETER;
|
||||
if (digits == 0) {
|
||||
return (int) (meters / mainUnitInMeters + 0.5) + " " + ctx.getString(mainUnitStr); //$NON-NLS-1$
|
||||
} else if (digits == 1) {
|
||||
return fixed1.format(((float) meters) / mainUnitInMeters) + " " + ctx.getString(mainUnitStr);
|
||||
} else {
|
||||
return fixed2.format(((float) meters) / mainUnitInMeters) + " " + ctx.getString(mainUnitStr);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFormattedDistance(float meters, TelegramApplication ctx) {
|
||||
return getFormattedDistance(meters, ctx, true);
|
||||
}
|
||||
|
||||
public static String getFormattedDistance(float meters, TelegramApplication ctx, boolean forceTrailingZeros) {
|
||||
String format1 = forceTrailingZeros ? "{0,number,0.0} " : "{0,number,0.#} ";
|
||||
String format2 = forceTrailingZeros ? "{0,number,0.00} " : "{0,number,0.##} ";
|
||||
|
||||
MetricsConstants mc = ctx.getSettings().getMetricsConstants();
|
||||
int mainUnitStr;
|
||||
float mainUnitInMeters;
|
||||
if (mc == MetricsConstants.KILOMETERS_AND_METERS) {
|
||||
mainUnitStr = R.string.km;
|
||||
mainUnitInMeters = METERS_IN_KILOMETER;
|
||||
} else if (mc == MetricsConstants.NAUTICAL_MILES) {
|
||||
mainUnitStr = R.string.nm;
|
||||
mainUnitInMeters = METERS_IN_ONE_NAUTICALMILE;
|
||||
} else {
|
||||
mainUnitStr = R.string.mile;
|
||||
mainUnitInMeters = METERS_IN_ONE_MILE;
|
||||
}
|
||||
|
||||
if (meters >= 100 * mainUnitInMeters) {
|
||||
return (int) (meters / mainUnitInMeters + 0.5) + " " + ctx.getString(mainUnitStr); //$NON-NLS-1$
|
||||
} else if (meters > 9.99f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format1 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else if (meters > 0.999f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.MILES_AND_FEET && meters > 0.249f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.MILES_AND_METERS && meters > 0.249f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.MILES_AND_YARDS && meters > 0.249f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.NAUTICAL_MILES && meters > 0.99f * mainUnitInMeters) {
|
||||
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), ((float) meters) / mainUnitInMeters).replace('\n', ' '); //$NON-NLS-1$
|
||||
} else {
|
||||
if (mc == MetricsConstants.KILOMETERS_AND_METERS || mc == MetricsConstants.MILES_AND_METERS) {
|
||||
return ((int) (meters + 0.5)) + " " + ctx.getString(R.string.m); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.MILES_AND_FEET) {
|
||||
int feet = (int) (meters * FEET_IN_ONE_METER + 0.5);
|
||||
return feet + " " + ctx.getString(R.string.foot); //$NON-NLS-1$
|
||||
} else if (mc == MetricsConstants.MILES_AND_YARDS) {
|
||||
int yards = (int) (meters * YARDS_IN_ONE_METER + 0.5);
|
||||
return yards + " " + ctx.getString(R.string.yard); //$NON-NLS-1$
|
||||
}
|
||||
return ((int) (meters + 0.5)) + " " + ctx.getString(R.string.m); //$NON-NLS-1$
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFormattedAlt(double alt, TelegramApplication ctx) {
|
||||
MetricsConstants mc = ctx.getSettings().getMetricsConstants();
|
||||
if (mc == MetricsConstants.KILOMETERS_AND_METERS) {
|
||||
return ((int) (alt + 0.5)) + " " + ctx.getString(R.string.m);
|
||||
} else {
|
||||
return ((int) (alt * FEET_IN_ONE_METER + 0.5)) + " " + ctx.getString(R.string.foot);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFormattedSpeed(float metersperseconds, TelegramApplication ctx) {
|
||||
SpeedConstants mc = ctx.getSettings().getSpeedConstants();
|
||||
float kmh = metersperseconds * 3.6f;
|
||||
if (mc == SpeedConstants.KILOMETERS_PER_HOUR) {
|
||||
// e.g. car case and for high-speeds: Display rounded to 1 km/h (5% precision at 20 km/h)
|
||||
if (kmh >= 20) {
|
||||
return ((int) Math.round(kmh)) + " " + mc.toShortString(ctx);
|
||||
}
|
||||
// for smaller values display 1 decimal digit x.y km/h, (0.5% precision at 20 km/h)
|
||||
int kmh10 = (int) Math.round(kmh * 10f);
|
||||
return (kmh10 / 10f) + " " + mc.toShortString(ctx);
|
||||
} else if (mc == SpeedConstants.MILES_PER_HOUR) {
|
||||
float mph = kmh * METERS_IN_KILOMETER / METERS_IN_ONE_MILE;
|
||||
if (mph >= 20) {
|
||||
return ((int) Math.round(mph)) + " " + mc.toShortString(ctx);
|
||||
} else {
|
||||
int mph10 = (int) Math.round(mph * 10f);
|
||||
return (mph10 / 10f) + " " + mc.toShortString(ctx);
|
||||
}
|
||||
} else if (mc == SpeedConstants.NAUTICALMILES_PER_HOUR) {
|
||||
float mph = kmh * METERS_IN_KILOMETER / METERS_IN_ONE_NAUTICALMILE;
|
||||
if (mph >= 20) {
|
||||
return ((int) Math.round(mph)) + " " + mc.toShortString(ctx);
|
||||
} else {
|
||||
int mph10 = (int) Math.round(mph * 10f);
|
||||
return (mph10 / 10f) + " " + mc.toShortString(ctx);
|
||||
}
|
||||
} else if (mc == SpeedConstants.MINUTES_PER_KILOMETER) {
|
||||
if (metersperseconds < 0.111111111) {
|
||||
return "-" + mc.toShortString(ctx);
|
||||
}
|
||||
float minperkm = METERS_IN_KILOMETER / (metersperseconds * 60);
|
||||
if (minperkm >= 10) {
|
||||
return ((int) Math.round(minperkm)) + " " + mc.toShortString(ctx);
|
||||
} else {
|
||||
int mph10 = (int) Math.round(minperkm * 10f);
|
||||
return (mph10 / 10f) + " " + mc.toShortString(ctx);
|
||||
}
|
||||
} else if (mc == SpeedConstants.MINUTES_PER_MILE) {
|
||||
if (metersperseconds < 0.111111111) {
|
||||
return "-" + mc.toShortString(ctx);
|
||||
}
|
||||
float minperm = (METERS_IN_ONE_MILE) / (metersperseconds * 60);
|
||||
if (minperm >= 10) {
|
||||
return ((int) Math.round(minperm)) + " " + mc.toShortString(ctx);
|
||||
} else {
|
||||
int mph10 = (int) Math.round(minperm * 10f);
|
||||
return (mph10 / 10f) + " " + mc.toShortString(ctx);
|
||||
}
|
||||
} else /*if (mc == SpeedConstants.METERS_PER_SECOND) */ {
|
||||
if (metersperseconds >= 10) {
|
||||
return ((int) Math.round(metersperseconds)) + " " + SpeedConstants.METERS_PER_SECOND.toShortString(ctx);
|
||||
}
|
||||
// for smaller values display 1 decimal digit x.y km/h, (0.5% precision at 20 km/h)
|
||||
int kmh10 = (int) Math.round(metersperseconds * 10f);
|
||||
return (kmh10 / 10f) + " " + SpeedConstants.METERS_PER_SECOND.toShortString(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public enum MetricsConstants {
|
||||
KILOMETERS_AND_METERS(R.string.si_km_m),
|
||||
MILES_AND_FEET(R.string.si_mi_feet),
|
||||
MILES_AND_METERS(R.string.si_mi_meters),
|
||||
MILES_AND_YARDS(R.string.si_mi_yard),
|
||||
NAUTICAL_MILES(R.string.si_nm);
|
||||
|
||||
private final int key;
|
||||
|
||||
MetricsConstants(int key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String toHumanString(Context ctx) {
|
||||
return ctx.getString(key);
|
||||
}
|
||||
}
|
||||
|
||||
public enum SpeedConstants {
|
||||
KILOMETERS_PER_HOUR(R.string.km_h, R.string.si_kmh),
|
||||
MILES_PER_HOUR(R.string.mile_per_hour, R.string.si_mph),
|
||||
METERS_PER_SECOND(R.string.m_s, R.string.si_m_s),
|
||||
MINUTES_PER_MILE(R.string.min_mile, R.string.si_min_m),
|
||||
MINUTES_PER_KILOMETER(R.string.min_km, R.string.si_min_km),
|
||||
NAUTICALMILES_PER_HOUR(R.string.nm_h, R.string.si_nm_h);
|
||||
|
||||
private final int key;
|
||||
private int descr;
|
||||
|
||||
SpeedConstants(int key, int descr) {
|
||||
this.key = key;
|
||||
this.descr = descr;
|
||||
}
|
||||
|
||||
public String toHumanString(Context ctx) {
|
||||
return ctx.getString(descr);
|
||||
}
|
||||
|
||||
public String toShortString(Context ctx) {
|
||||
return ctx.getString(key);
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
BIN
OsmAnd-telegram/src/main/res/drawable-hdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 188 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1 KiB |
BIN
OsmAnd-telegram/src/main/res/drawable-mdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 174 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.1 KiB |
BIN
OsmAnd-telegram/src/main/res/drawable-xhdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.1 KiB |
BIN
OsmAnd-telegram/src/main/res/drawable-xxhdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 215 B |
|
@ -9,4 +9,6 @@
|
|||
<color name="icon_color_light">#ccc</color>
|
||||
<color name="icon_color_dark">#ff4f4f4f</color>
|
||||
|
||||
<color name="osmand_orange">#ff8f00</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -12,4 +12,48 @@
|
|||
<string name="closing">Closing</string>
|
||||
<string name="shared_string_continue">Continue</string>
|
||||
<string name="shared_string_cancel">Cancel</string>
|
||||
<string name="gps_network_not_enabled">Location service not enabled. Turn it on?</string>
|
||||
<string name="shared_string_settings">Settings</string>
|
||||
<string name="no_location_permission">App has no permission to access location data.</string>
|
||||
<string name="gps_not_available">Please enable GPS in the settings</string>
|
||||
<string name="location_service_no_gps_available">The share location service requires a location provider to be turned on.</string>
|
||||
<string name="osmand_service">Background mode</string>
|
||||
<string name="osmand_service_descr">OsmAnd Telegram runs in the background with the screen off.</string>
|
||||
<string name="shared_string_distance">Distance</string>
|
||||
<string name="share_location">Share location</string>
|
||||
<string name="sharing_location">Sharing location</string>
|
||||
<string name="shared_string_paused">Paused</string>
|
||||
<string name="shared_string_no_data">No data</string>
|
||||
<string name="shared_string_pause">Pause</string>
|
||||
<string name="shared_string_start">Start</string>
|
||||
<string name="shared_string_stop">Stop</string>
|
||||
<string name="process_location_service">OsmAnd Telegram location service</string>
|
||||
|
||||
<string name="yard">yd</string>
|
||||
<string name="foot">ft</string>
|
||||
<string name="mile">mi</string>
|
||||
<string name="km">km</string>
|
||||
<string name="m">m</string>
|
||||
<string name="nm">nmi</string>
|
||||
<string name="min_mile">min/m</string>
|
||||
<string name="min_km">min/km</string>
|
||||
<string name="nm_h">nmi/h</string>
|
||||
<string name="m_s">m/s</string>
|
||||
<string name="km_h">km/h</string>
|
||||
<string name="mile_per_hour">mph</string>
|
||||
<string name="si_kmh">Kilometers per hour</string>
|
||||
<string name="si_mph">Miles per hour</string>
|
||||
<string name="si_m_s">Meters per second</string>
|
||||
<string name="si_min_km">Minutes per kilometer</string>
|
||||
<string name="si_min_m">Minutes per mile</string>
|
||||
<string name="si_nm_h">Nautical miles per hour (knot)</string>
|
||||
<string name="si_mi_feet">Miles/feet</string>
|
||||
<string name="si_mi_yard">Miles/yards</string>
|
||||
<string name="si_km_m">Kilometers/meters</string>
|
||||
<string name="si_nm">Nautical miles</string>
|
||||
<string name="si_mi_meters">Miles/meters</string>
|
||||
<string name="shared_string_hour_short">h</string>
|
||||
<string name="shared_string_minute_short">min</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|