Telegram - added chat photos

This commit is contained in:
crimean 2018-06-13 20:14:08 +03:00
parent 668d3e9835
commit ae2aa88578
9 changed files with 579 additions and 357 deletions

View file

@ -19,11 +19,10 @@
<android.support.v7.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerInside"
android:src="@drawable/ic_group"
android:tint="?attr/icon_color"
android:visibility="visible" />
<android.support.v7.widget.AppCompatTextView

View file

@ -4,6 +4,8 @@ import android.Manifest
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.ActivityCompat
@ -164,6 +166,12 @@ class MainActivity : AppCompatActivity(), TelegramListener {
}
}
override fun onTelegramChatChanged(chat: TdApi.Chat) {
runOnUi {
updateChat(chat)
}
}
override fun onTelegramError(code: Int, message: String) {
runOnUi {
Toast.makeText(this@MainActivity, "$code - $message", Toast.LENGTH_LONG).show()
@ -192,6 +200,15 @@ class MainActivity : AppCompatActivity(), TelegramListener {
chatViewAdapter.chats = chats
}
private fun updateChat(chat: TdApi.Chat) {
val chatIndex = telegramHelper.getChatIndex(chat.id)
if (chatIndex != -1) {
chatViewAdapter.notifyItemChanged(chatIndex)
} else {
updateChatsList()
}
}
fun logoutTelegram(silent: Boolean = false) {
if (telegramHelper.getTelegramAuthorizationState() == TelegramAuthorizationState.READY) {
telegramHelper.logout()
@ -358,9 +375,24 @@ class MainActivity : AppCompatActivity(), TelegramListener {
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val chatTitle = chats[position].title
val chat = chats[position]
val chatTitle = chat.title
holder.groupName?.text = chatTitle
var drawable: Drawable? = null
var bitmap: Bitmap? = null
val chatPhoto = chat.photo?.small
if (chatPhoto != null && chatPhoto.local.path.isNotEmpty()) {
bitmap = app.uiUtils.getCircleBitmap(chatPhoto.local.path)
}
if (bitmap == null) {
drawable = app.uiUtils.getThemedIcon(R.drawable.ic_group)
}
if (bitmap != null) {
holder.icon?.setImageBitmap(bitmap)
} else {
holder.icon?.setImageDrawable(drawable)
}
holder.shareLocationSwitch?.setOnCheckedChangeListener(null)
holder.shareLocationSwitch?.isChecked = settings.isSharingLocationToChat(chatTitle)
holder.shareLocationSwitch?.setOnCheckedChangeListener { view, isChecked ->

View file

@ -14,11 +14,13 @@ import net.osmand.telegram.helpers.ShowLocationHelper
import net.osmand.telegram.helpers.TelegramHelper
import net.osmand.telegram.notifications.NotificationHelper
import net.osmand.telegram.utils.AndroidUtils
import net.osmand.telegram.utils.UiUtils
class TelegramApplication : Application(), OsmandHelperListener {
val telegramHelper = TelegramHelper.instance
lateinit var settings: TelegramSettings private set
lateinit var uiUtils: UiUtils private set
lateinit var shareLocationHelper: ShareLocationHelper private set
lateinit var showLocationHelper: ShowLocationHelper private set
lateinit var notificationHelper: NotificationHelper private set
@ -36,6 +38,7 @@ class TelegramApplication : Application(), OsmandHelperListener {
telegramHelper.appDir = filesDir.absolutePath
settings = TelegramSettings(this)
uiUtils = UiUtils(this)
osmandHelper = OsmandAidlHelper(this)
shareLocationHelper = ShareLocationHelper(this)
showLocationHelper = ShowLocationHelper(this)

View file

@ -41,6 +41,8 @@ class TelegramHelper private constructor() {
private val chatList = TreeSet<OrderedChat>()
private val chatLiveMessages = ConcurrentHashMap<Long, Long>()
private val downloadChatFilesMap = ConcurrentHashMap<String, TdApi.Chat>()
private val usersLiveMessages = ConcurrentHashMap<Long, TdApi.Message>()
private val usersFullInfo = ConcurrentHashMap<Int, TdApi.UserFullInfo>()
@ -72,6 +74,17 @@ class TelegramHelper private constructor() {
}
}
fun getChatIndex(chatId: Long): Int {
synchronized(chatList) {
for ((i, chat) in chatList.withIndex()) {
if (chat.chatId == chatId) {
return i
}
}
}
return -1
}
fun getChatTitles(): List<String> {
return chatTitles.keys().toList()
}
@ -126,6 +139,7 @@ class TelegramHelper private constructor() {
fun onTelegramChatsRead()
fun onTelegramChatsChanged()
fun onTelegramChatChanged(chat: TdApi.Chat)
fun onTelegramError(code: Int, message: String)
fun onSendLiveLicationError(code: Int, message: String)
}
@ -434,8 +448,8 @@ class TelegramHelper private constructor() {
parameters.databaseDirectory = File(appDir, "tdlib").absolutePath
parameters.useMessageDatabase = true
parameters.useSecretChats = true
parameters.apiId = 94575
parameters.apiHash = "a3406de8d171bb422bb6ddf3bbd800e2"
parameters.apiId = 293148
parameters.apiHash = "d1942abd0f1364efe5020e2bfed2ed15"
parameters.systemLanguageCode = "en"
parameters.deviceModel = "Android"
parameters.systemVersion = "OsmAnd Telegram"
@ -551,7 +565,34 @@ class TelegramHelper private constructor() {
synchronized(chat!!) {
if (chat.type !is TdApi.ChatTypeSupergroup || !(chat.type as TdApi.ChatTypeSupergroup).isChannel) {
chats[chat.id] = chat
val localPhoto = chat.photo?.small?.local
val hasLocalPhoto = if (localPhoto != null) {
localPhoto.canBeDownloaded && localPhoto.isDownloadingCompleted && localPhoto.path.isNotEmpty()
} else {
false
}
if (!hasLocalPhoto) {
val remotePhoto = chat.photo?.small?.remote
if (remotePhoto != null && remotePhoto.id.isNotEmpty()) {
downloadChatFilesMap[remotePhoto.id] = chat
client!!.send(TdApi.GetRemoteFile(remotePhoto.id, null), { obj ->
when (obj.constructor) {
TdApi.Error.CONSTRUCTOR -> {
val error = obj as TdApi.Error
val code = error.code
if (code != IGNORED_ERROR_CODE) {
listener?.onTelegramError(code, error.message)
}
}
TdApi.File.CONSTRUCTOR -> {
val file = obj as TdApi.File
client!!.send(TdApi.DownloadFile(file.id, 10), defaultHandler)
}
else -> listener?.onTelegramError(-1, "Receive wrong response from TDLib: $obj")
}
})
}
}
val order = chat.order
chat.order = 0
setChatOrder(chat, order)
@ -567,7 +608,7 @@ class TelegramHelper private constructor() {
chat.title = updateChat.title
}
updateChatTitles()
listener?.onTelegramChatsChanged()
listener?.onTelegramChatChanged(chat)
}
TdApi.UpdateChatPhoto.CONSTRUCTOR -> {
val updateChat = obj as TdApi.UpdateChatPhoto
@ -575,7 +616,7 @@ class TelegramHelper private constructor() {
synchronized(chat!!) {
chat.photo = updateChat.photo
}
listener?.onTelegramChatsChanged()
listener?.onTelegramChatChanged(chat)
}
TdApi.UpdateChatLastMessage.CONSTRUCTOR -> {
val updateChat = obj as TdApi.UpdateChatLastMessage
@ -697,6 +738,18 @@ class TelegramHelper private constructor() {
}
}
TdApi.UpdateFile.CONSTRUCTOR -> {
val updateFile = obj as TdApi.UpdateFile
if (updateFile.file.local.isDownloadingCompleted) {
val remoteId = updateFile.file.remote.id
val chat = downloadChatFilesMap.remove(remoteId)
if (chat != null) {
chat.photo?.small = updateFile.file
listener?.onTelegramChatChanged(chat)
}
}
}
TdApi.UpdateUserFullInfo.CONSTRUCTOR -> {
val updateUserFullInfo = obj as TdApi.UpdateUserFullInfo
usersFullInfo[updateUserFullInfo.userId] = updateUserFullInfo.userFullInfo

View file

@ -5,38 +5,60 @@ import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.support.annotation.AttrRes
import android.support.annotation.ColorInt
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.util.TypedValue
import android.util.TypedValue.COMPLEX_UNIT_DIP
import android.view.View
import android.view.inputmethod.InputMethodManager
object AndroidUtils {
private fun isHardwareKeyboardAvailable(context: Context): Boolean {
return context.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
}
private fun isHardwareKeyboardAvailable(context: Context): Boolean {
return context.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
}
fun softKeyboardDelayed(view: View) {
view.post {
if (!isHardwareKeyboardAvailable(view.context)) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
}
}
fun softKeyboardDelayed(view: View) {
view.post {
if (!isHardwareKeyboardAvailable(view.context)) {
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT)
}
}
}
fun hideSoftKeyboard(activity: Activity, input: View?) {
val inputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
if (inputMethodManager != null) {
if (input != null) {
val windowToken = input.windowToken
if (windowToken != null) {
inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
}
}
}
}
fun hideSoftKeyboard(activity: Activity, input: View?) {
val inputMethodManager = activity.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
if (inputMethodManager != null) {
if (input != null) {
val windowToken = input.windowToken
if (windowToken != null) {
inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
}
}
}
}
fun isLocationPermissionAvailable(context: Context): Boolean {
return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
fun isLocationPermissionAvailable(context: Context): Boolean {
return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
fun dpToPx(ctx: Context, dp: Float): Int {
val r = ctx.resources
return TypedValue.applyDimension(
COMPLEX_UNIT_DIP,
dp,
r.displayMetrics
).toInt()
}
@ColorInt
fun getAttrColor(ctx: Context, @AttrRes attrId: Int, @ColorInt defaultColor: Int = 0): Int {
val ta = ctx.theme.obtainStyledAttributes(intArrayOf(attrId))
val color = ta.getColor(0, defaultColor)
ta.recycle()
return color
}
}

View file

@ -7,70 +7,70 @@ import java.util.concurrent.atomic.AtomicInteger
class CancellableAsyncTask(val taskId: String, val executeTimeout: Long = 0) {
companion object {
private const val SLEEP_TIME = 50L
private val requestNumbersMap = ConcurrentHashMap<String, AtomicInteger>()
private val singleThreadExecutorsMap = ConcurrentHashMap<String, ExecutorService>()
companion object {
private const val SLEEP_TIME = 50L
private val requestNumbersMap = ConcurrentHashMap<String, AtomicInteger>()
private val singleThreadExecutorsMap = ConcurrentHashMap<String, ExecutorService>()
fun run(taskId: String, executeTimeout: Long = 0, action: (() -> Unit)) {
CancellableAsyncTask(taskId, executeTimeout).run(action)
}
fun run(taskId: String, executeTimeout: Long = 0, action: (() -> Unit)) {
CancellableAsyncTask(taskId, executeTimeout).run(action)
}
fun clearResources(taskId: String) {
requestNumbersMap.remove(taskId)
singleThreadExecutorsMap.remove(taskId)
}
}
fun clearResources(taskId: String) {
requestNumbersMap.remove(taskId)
singleThreadExecutorsMap.remove(taskId)
}
}
private val singleThreadExecutor: ExecutorService
private var requestNumber: AtomicInteger
private val singleThreadExecutor: ExecutorService
private var requestNumber: AtomicInteger
var isCancelled: Boolean = false
var isCancelled: Boolean = false
init {
val requestNumber = requestNumbersMap[taskId]
if (requestNumber == null) {
this.requestNumber = AtomicInteger()
requestNumbersMap[taskId] = this.requestNumber
} else {
this.requestNumber = requestNumber
}
init {
val requestNumber = requestNumbersMap[taskId]
if (requestNumber == null) {
this.requestNumber = AtomicInteger()
requestNumbersMap[taskId] = this.requestNumber
} else {
this.requestNumber = requestNumber
}
val singleThreadExecutor = singleThreadExecutorsMap[taskId]
if (singleThreadExecutor == null) {
this.singleThreadExecutor = Executors.newSingleThreadExecutor()
singleThreadExecutorsMap[taskId] = this.singleThreadExecutor
} else {
this.singleThreadExecutor = singleThreadExecutor
}
}
val singleThreadExecutor = singleThreadExecutorsMap[taskId]
if (singleThreadExecutor == null) {
this.singleThreadExecutor = Executors.newSingleThreadExecutor()
singleThreadExecutorsMap[taskId] = this.singleThreadExecutor
} else {
this.singleThreadExecutor = singleThreadExecutor
}
}
fun run(action: (() -> Unit)) {
val req = requestNumber.incrementAndGet()
fun run(action: (() -> Unit)) {
val req = requestNumber.incrementAndGet()
singleThreadExecutor.submit(object : Runnable {
singleThreadExecutor.submit(object : Runnable {
private val isCancelled: Boolean
get() = requestNumber.get() != req || this@CancellableAsyncTask.isCancelled
private val isCancelled: Boolean
get() = requestNumber.get() != req || this@CancellableAsyncTask.isCancelled
override fun run() {
try {
if (executeTimeout > 0) {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime <= executeTimeout) {
if (isCancelled) {
return
}
Thread.sleep(SLEEP_TIME)
}
}
if (!isCancelled) {
action.invoke()
}
} catch (e: InterruptedException) {
// ignore
}
}
})
}
override fun run() {
try {
if (executeTimeout > 0) {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime <= executeTimeout) {
if (isCancelled) {
return
}
Thread.sleep(SLEEP_TIME)
}
}
if (!isCancelled) {
action.invoke()
}
} catch (e: InterruptedException) {
// ignore
}
}
})
}
}

View file

@ -1,267 +0,0 @@
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);
}
}
}

View file

@ -0,0 +1,255 @@
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
object OsmandFormatter {
val METERS_IN_KILOMETER = 1000f
val METERS_IN_ONE_MILE = 1609.344f // 1609.344
val METERS_IN_ONE_NAUTICALMILE = 1852f // 1852
val YARDS_IN_ONE_METER = 1.0936f
val FEET_IN_ONE_METER = YARDS_IN_ONE_METER * 3f
private val fixed2 = DecimalFormat("0.00")
private val fixed1 = DecimalFormat("0.0")
init {
fixed2.minimumFractionDigits = 2
fixed1.minimumFractionDigits = 1
fixed1.minimumIntegerDigits = 1
fixed2.minimumIntegerDigits = 1
}
fun getFormattedDuration(seconds: Int, ctx: TelegramApplication): String {
val hours = seconds / (60 * 60)
val minutes = seconds / 60 % 60
return if (hours > 0) {
(hours.toString() + " "
+ ctx.getString(R.string.shared_string_hour_short)
+ if (minutes > 0)
" " + minutes + " "
+ ctx.getString(R.string.shared_string_minute_short)
else
"")
} else {
minutes.toString() + " " + ctx.getString(R.string.shared_string_minute_short)
}
}
fun calculateRoundedDist(distInMeters: Double, ctx: TelegramApplication): Double {
val mc = ctx.settings.metricsConstants
var mainUnitInMeter = 1.0
var metersInSecondUnit = METERS_IN_KILOMETER.toDouble()
if (mc == MetricsConstants.MILES_AND_FEET) {
mainUnitInMeter = FEET_IN_ONE_METER.toDouble()
metersInSecondUnit = METERS_IN_ONE_MILE.toDouble()
} else if (mc == MetricsConstants.MILES_AND_METERS) {
mainUnitInMeter = 1.0
metersInSecondUnit = METERS_IN_ONE_MILE.toDouble()
} else if (mc == MetricsConstants.NAUTICAL_MILES) {
mainUnitInMeter = 1.0
metersInSecondUnit = METERS_IN_ONE_NAUTICALMILE.toDouble()
} else if (mc == MetricsConstants.MILES_AND_YARDS) {
mainUnitInMeter = YARDS_IN_ONE_METER.toDouble()
metersInSecondUnit = METERS_IN_ONE_MILE.toDouble()
}
// 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000 ...
var generator = 1
var pointer: Byte = 1
var point = mainUnitInMeter
var roundDist = 1.0
while (distInMeters * point > generator) {
roundDist = generator / point
if (pointer++ % 3 == 2) {
generator = generator * 5 / 2
} else {
generator *= 2
}
if (point == mainUnitInMeter && metersInSecondUnit * mainUnitInMeter * 0.9 <= 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.0) {
roundDist = (0.5f * METERS_IN_ONE_MILE).toDouble()
} else if (mc == MetricsConstants.MILES_AND_METERS && roundDist == 500.0) {
roundDist = (0.25f * METERS_IN_ONE_MILE).toDouble()
} else if (mc == MetricsConstants.MILES_AND_FEET && roundDist == 2000 / FEET_IN_ONE_METER.toDouble()) {
roundDist = (0.5f * METERS_IN_ONE_MILE).toDouble()
} else if (mc == MetricsConstants.MILES_AND_FEET && roundDist == 1000 / FEET_IN_ONE_METER.toDouble()) {
roundDist = (0.25f * METERS_IN_ONE_MILE).toDouble()
} else if (mc == MetricsConstants.MILES_AND_YARDS && roundDist == 1000 / YARDS_IN_ONE_METER.toDouble()) {
roundDist = (0.5f * METERS_IN_ONE_MILE).toDouble()
} else if (mc == MetricsConstants.MILES_AND_YARDS && roundDist == 500 / YARDS_IN_ONE_METER.toDouble()) {
roundDist = (0.25f * METERS_IN_ONE_MILE).toDouble()
}
return roundDist
}
fun getFormattedRoundDistanceKm(meters: Float, digits: Int, ctx: TelegramApplication): String {
val mainUnitStr = R.string.km
val mainUnitInMeters = METERS_IN_KILOMETER
return if (digits == 0) {
(meters / mainUnitInMeters + 0.5).toInt().toString() + " " + ctx.getString(mainUnitStr) //$NON-NLS-1$
} else if (digits == 1) {
fixed1.format((meters / mainUnitInMeters).toDouble()) + " " + ctx.getString(mainUnitStr)
} else {
fixed2.format((meters / mainUnitInMeters).toDouble()) + " " + ctx.getString(mainUnitStr)
}
}
@JvmOverloads
fun getFormattedDistance(meters: Float, ctx: TelegramApplication, forceTrailingZeros: Boolean = true): String {
val format1 = if (forceTrailingZeros) "{0,number,0.0} " else "{0,number,0.#} "
val format2 = if (forceTrailingZeros) "{0,number,0.00} " else "{0,number,0.##} "
val mc = ctx.settings.metricsConstants
val mainUnitStr: Int
val mainUnitInMeters: Float
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 (meters / mainUnitInMeters + 0.5).toInt().toString() + " " + ctx.getString(mainUnitStr) //$NON-NLS-1$
} else if (meters > 9.99f * mainUnitInMeters) {
return MessageFormat.format(format1 + ctx.getString(mainUnitStr), meters / mainUnitInMeters).replace('\n', ' ') //$NON-NLS-1$
} else if (meters > 0.999f * mainUnitInMeters) {
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), 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), 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), 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), meters / mainUnitInMeters).replace('\n', ' ') //$NON-NLS-1$
} else if (mc == MetricsConstants.NAUTICAL_MILES && meters > 0.99f * mainUnitInMeters) {
return MessageFormat.format(format2 + ctx.getString(mainUnitStr), meters / mainUnitInMeters).replace('\n', ' ') //$NON-NLS-1$
} else {
if (mc == MetricsConstants.KILOMETERS_AND_METERS || mc == MetricsConstants.MILES_AND_METERS) {
return (meters + 0.5).toInt().toString() + " " + ctx.getString(R.string.m) //$NON-NLS-1$
} else if (mc == MetricsConstants.MILES_AND_FEET) {
val feet = (meters * FEET_IN_ONE_METER + 0.5).toInt()
return feet.toString() + " " + ctx.getString(R.string.foot) //$NON-NLS-1$
} else if (mc == MetricsConstants.MILES_AND_YARDS) {
val yards = (meters * YARDS_IN_ONE_METER + 0.5).toInt()
return yards.toString() + " " + ctx.getString(R.string.yard) //$NON-NLS-1$
}
return (meters + 0.5).toInt().toString() + " " + ctx.getString(R.string.m) //$NON-NLS-1$
}
}
fun getFormattedAlt(alt: Double, ctx: TelegramApplication): String {
val mc = ctx.settings.metricsConstants
return if (mc == MetricsConstants.KILOMETERS_AND_METERS) {
(alt + 0.5).toInt().toString() + " " + ctx.getString(R.string.m)
} else {
(alt * FEET_IN_ONE_METER + 0.5).toInt().toString() + " " + ctx.getString(R.string.foot)
}
}
fun getFormattedSpeed(metersperseconds: Float, ctx: TelegramApplication): String {
val mc = ctx.settings.speedConstants
val 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 Math.round(kmh).toString() + " " + mc.toShortString(ctx)
}
// for smaller values display 1 decimal digit x.y km/h, (0.5% precision at 20 km/h)
val kmh10 = Math.round(kmh * 10f)
return (kmh10 / 10f).toString() + " " + mc.toShortString(ctx)
} else if (mc == SpeedConstants.MILES_PER_HOUR) {
val mph = kmh * METERS_IN_KILOMETER / METERS_IN_ONE_MILE
if (mph >= 20) {
return Math.round(mph).toString() + " " + mc.toShortString(ctx)
} else {
val mph10 = Math.round(mph * 10f)
return (mph10 / 10f).toString() + " " + mc.toShortString(ctx)
}
} else if (mc == SpeedConstants.NAUTICALMILES_PER_HOUR) {
val mph = kmh * METERS_IN_KILOMETER / METERS_IN_ONE_NAUTICALMILE
if (mph >= 20) {
return Math.round(mph).toString() + " " + mc.toShortString(ctx)
} else {
val mph10 = Math.round(mph * 10f)
return (mph10 / 10f).toString() + " " + mc.toShortString(ctx)
}
} else if (mc == SpeedConstants.MINUTES_PER_KILOMETER) {
if (metersperseconds < 0.111111111) {
return "-" + mc.toShortString(ctx)
}
val minperkm = METERS_IN_KILOMETER / (metersperseconds * 60)
if (minperkm >= 10) {
return Math.round(minperkm).toString() + " " + mc.toShortString(ctx)
} else {
val mph10 = Math.round(minperkm * 10f)
return (mph10 / 10f).toString() + " " + mc.toShortString(ctx)
}
} else if (mc == SpeedConstants.MINUTES_PER_MILE) {
if (metersperseconds < 0.111111111) {
return "-" + mc.toShortString(ctx)
}
val minperm = METERS_IN_ONE_MILE / (metersperseconds * 60)
if (minperm >= 10) {
return Math.round(minperm).toString() + " " + mc.toShortString(ctx)
} else {
val mph10 = Math.round(minperm * 10f)
return (mph10 / 10f).toString() + " " + mc.toShortString(ctx)
}
} else
/*if (mc == SpeedConstants.METERS_PER_SECOND) */ {
if (metersperseconds >= 10) {
return Math.round(metersperseconds).toString() + " " + SpeedConstants.METERS_PER_SECOND.toShortString(ctx)
}
// for smaller values display 1 decimal digit x.y km/h, (0.5% precision at 20 km/h)
val kmh10 = Math.round(metersperseconds * 10f)
return (kmh10 / 10f).toString() + " " + SpeedConstants.METERS_PER_SECOND.toShortString(ctx)
}
}
enum class MetricsConstants private constructor(private val key: Int) {
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);
fun toHumanString(ctx: Context): String {
return ctx.getString(key)
}
}
enum class SpeedConstants private constructor(private val key: Int, private val descr: Int) {
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);
fun toHumanString(ctx: Context): String {
return ctx.getString(descr)
}
fun toShortString(ctx: Context): String {
return ctx.getString(key)
}
}
}

View file

@ -0,0 +1,125 @@
package net.osmand.telegram.utils
import android.graphics.*
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.support.annotation.ColorInt
import android.support.annotation.ColorRes
import android.support.annotation.DrawableRes
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
import net.osmand.telegram.R
import net.osmand.telegram.TelegramApplication
import java.util.*
class UiUtils(private val app: TelegramApplication) {
private val drawableCache = LinkedHashMap<Long, Drawable>()
private val circleBitmapCache = LinkedHashMap<String, Bitmap>()
private val isLightContent: Boolean
get() = true
fun getCircleBitmap(path: String): Bitmap? {
var bmp: Bitmap? = circleBitmapCache[path]
if (bmp == null) {
bmp = BitmapFactory.decodeFile(path)
if (bmp != null) {
bmp = app.uiUtils.createCircleBitmap(bmp, true)
circleBitmapCache[path] = bmp
}
}
return bmp
}
private fun getDrawable(@DrawableRes resId: Int, @ColorRes clrId: Int): Drawable? {
val hash = (resId.toLong() shl 31) + clrId
var d: Drawable? = drawableCache[hash]
if (d == null) {
d = ContextCompat.getDrawable(app, resId)
if (d != null) {
d = DrawableCompat.wrap(d)
d!!.mutate()
if (clrId != 0) {
DrawableCompat.setTint(d, ContextCompat.getColor(app, clrId))
}
drawableCache[hash] = d
}
}
return d
}
private fun getPaintedDrawable(@DrawableRes resId: Int, @ColorInt color: Int): Drawable? {
val hash = (resId.toLong() shl 31) + color
var d: Drawable? = drawableCache[hash]
if (d == null) {
d = ContextCompat.getDrawable(app, resId)
if (d != null) {
d = DrawableCompat.wrap(d)
d!!.mutate()
DrawableCompat.setTint(d, color)
drawableCache[hash] = d
}
}
return d
}
fun getPaintedIcon(@DrawableRes id: Int, @ColorInt color: Int): Drawable? {
return getPaintedDrawable(id, color)
}
fun getIcon(@DrawableRes id: Int, @ColorRes colorId: Int): Drawable? {
return getDrawable(id, colorId)
}
fun getIcon(@DrawableRes backgroundId: Int, @DrawableRes id: Int, @ColorRes colorId: Int): Drawable {
val b = getDrawable(backgroundId, 0)
val f = getDrawable(id, colorId)
val layers = arrayOfNulls<Drawable>(2)
layers[0] = b
layers[1] = f
return LayerDrawable(layers)
}
fun getThemedIcon(@DrawableRes id: Int): Drawable? {
return getDrawable(id, if (isLightContent) R.color.icon_color_light else 0)
}
fun getIcon(@DrawableRes id: Int): Drawable? {
return getDrawable(id, 0)
}
fun getIcon(@DrawableRes id: Int, light: Boolean): Drawable? {
return getDrawable(id, if (light) R.color.icon_color_light else 0)
}
fun createCircleBitmap(source: Bitmap, recycleSource: Boolean = false): Bitmap {
val size = Math.min(source.width, source.height)
val width = (source.width - size) / 2
val height = (source.height - size) / 2
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint()
val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
if (width != 0 || height != 0) {
// source isn't square, move viewport to center
val matrix = Matrix()
matrix.setTranslate((-width).toFloat(), (-height).toFloat())
shader.setLocalMatrix(matrix)
}
paint.shader = shader
paint.isAntiAlias = true
val r = size / 2f
canvas.drawCircle(r, r, r, paint)
if (recycleSource) {
source.recycle()
}
return bitmap
}
}