diff --git a/OsmAnd-telegram/res/layout/chat_list_item.xml b/OsmAnd-telegram/res/layout/chat_list_item.xml index 18882a304b..a47ad13068 100644 --- a/OsmAnd-telegram/res/layout/chat_list_item.xml +++ b/OsmAnd-telegram/res/layout/chat_list_item.xml @@ -19,11 +19,10 @@ diff --git a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt index 811524a9ab..d758f0cfda 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/TelegramApplication.kt @@ -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) diff --git a/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt b/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt index 5769867c1d..92e79c9ddd 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/helpers/TelegramHelper.kt @@ -41,6 +41,8 @@ class TelegramHelper private constructor() { private val chatList = TreeSet() private val chatLiveMessages = ConcurrentHashMap() + private val downloadChatFilesMap = ConcurrentHashMap() + private val usersLiveMessages = ConcurrentHashMap() private val usersFullInfo = ConcurrentHashMap() @@ -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 { 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 diff --git a/OsmAnd-telegram/src/net/osmand/telegram/utils/AndroidUtils.kt b/OsmAnd-telegram/src/net/osmand/telegram/utils/AndroidUtils.kt index 6f2493a548..c7f1a6cfdd 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/utils/AndroidUtils.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/utils/AndroidUtils.kt @@ -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 + } } diff --git a/OsmAnd-telegram/src/net/osmand/telegram/utils/CancellableAsyncTask.kt b/OsmAnd-telegram/src/net/osmand/telegram/utils/CancellableAsyncTask.kt index 92fdaf5430..6f4ff4c822 100644 --- a/OsmAnd-telegram/src/net/osmand/telegram/utils/CancellableAsyncTask.kt +++ b/OsmAnd-telegram/src/net/osmand/telegram/utils/CancellableAsyncTask.kt @@ -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() - private val singleThreadExecutorsMap = ConcurrentHashMap() + companion object { + private const val SLEEP_TIME = 50L + private val requestNumbersMap = ConcurrentHashMap() + private val singleThreadExecutorsMap = ConcurrentHashMap() - 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 + } + } + }) + } } diff --git a/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.java b/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.java deleted file mode 100644 index b2d09a3183..0000000000 --- a/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.java +++ /dev/null @@ -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); - } - } -} diff --git a/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.kt b/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.kt new file mode 100644 index 0000000000..744b7862d7 --- /dev/null +++ b/OsmAnd-telegram/src/net/osmand/telegram/utils/OsmandFormatter.kt @@ -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) + } + } +} diff --git a/OsmAnd-telegram/src/net/osmand/telegram/utils/UiUtils.kt b/OsmAnd-telegram/src/net/osmand/telegram/utils/UiUtils.kt new file mode 100644 index 0000000000..f131c0ff21 --- /dev/null +++ b/OsmAnd-telegram/src/net/osmand/telegram/utils/UiUtils.kt @@ -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() + private val circleBitmapCache = LinkedHashMap() + + 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(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 + } +}