Telegram - introduced live location sharing with chats

This commit is contained in:
crimean 2018-06-09 12:20:21 +03:00
parent 3ca22f8ec3
commit 656faca471
34 changed files with 1338 additions and 21 deletions

View file

@ -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'

View file

@ -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>

View file

@ -1,4 +1,4 @@
package net.osmand.telegram.utils;
package net.osmand;
import org.apache.commons.logging.Log;

View file

@ -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
}
}
}

View file

@ -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() {

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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]

View file

@ -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)
}
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

View file

@ -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>

View file

@ -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>