Merge pull request #6221 from osmandapp/AddSharingDevice

Add ability to add new sharing devices
This commit is contained in:
Alexander Sytnyk 2018-10-25 15:21:07 +03:00 committed by GitHub
commit d2c56bb451
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 767 additions and 95 deletions

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_gravity="bottom">
<android.support.design.widget.CoordinatorLayout
android:id="@+id/scroll_view_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v4.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
app:layout_behavior="@string/bottom_sheet_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/card_bg_color"
android:orientation="vertical">
<net.osmand.telegram.ui.views.TextViewEx
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/content_padding_standard"
android:paddingRight="@dimen/content_padding_standard"
android:text="@string/add_device"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/list_item_title_text_size"
app:firstBaselineToTopHeight="28sp"
app:typeface="@string/font_roboto_medium" />
<net.osmand.telegram.ui.views.TextViewEx
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/content_padding_standard"
android:paddingRight="@dimen/content_padding_standard"
android:text="@string/enter_device_name_description"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/list_item_description_text_size"
app:firstBaselineToTopHeight="28sp"
app:lastBaselineToBottomHeight="16sp"
app:typeface="@string/font_roboto_regular" />
<studio.carbonylgroup.textfieldboxes.TextFieldBoxes
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/content_padding_standard"
android:paddingRight="@dimen/content_padding_standard"
app:hasClearButton="true"
app:labelText="@string/device_name">
<studio.carbonylgroup.textfieldboxes.ExtendedEditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</studio.carbonylgroup.textfieldboxes.TextFieldBoxes>
<TextView
android:id="@+id/error_text_descr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_standard"
android:layout_marginRight="@dimen/content_padding_standard"
android:layout_marginTop="@dimen/content_padding_half"
android:letterSpacing="@dimen/text_description_letter_spacing"
android:lineSpacingMultiplier="@dimen/text_description_line_spacing_multiplier"
android:textSize="@dimen/hint_text_size"
android:visibility="invisible"
tools:text="@string/device_name_cannot_be_empty" />
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/buttons_bottom_bar_height"
android:background="?attr/card_bg_color"
android:gravity="center_vertical"
android:paddingLeft="@dimen/content_padding_half"
android:paddingRight="@dimen/content_padding_half">
<include
layout="@layout/secondary_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<View
android:layout_width="@dimen/content_padding_half"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<include
layout="@layout/primary_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="@dimen/progress_bar_size_small"
android:layout_height="@dimen/progress_bar_size_small"
android:layout_gravity="center"
android:visibility="gone"/>
</FrameLayout>
</LinearLayout>
</LinearLayout>

View file

@ -95,11 +95,13 @@
app:typeface="@string/font_roboto_medium" />
<net.osmand.telegram.ui.views.TextViewEx
android:id="@+id/share_as_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:paddingLeft="@dimen/content_padding_standard"
android:paddingRight="@dimen/content_padding_standard"
android:text="@string/share_location_as_descr"
android:text="@string/share_location_as_description"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/list_item_description_text_size"
app:firstBaselineToTopHeight="20sp"
@ -120,6 +122,38 @@
android:layout_height="wrap_content"
android:orientation="vertical" />
<LinearLayout
android:id="@+id/add_new_device_btn"
android:layout_width="match_parent"
android:layout_height="@dimen/dialog_button_height"
android:layout_marginBottom="@dimen/content_padding_standard"
android:layout_marginLeft="@dimen/content_padding_half"
android:layout_marginRight="@dimen/content_padding_half"
android:background="@drawable/btn_round"
android:gravity="center_vertical"
android:paddingLeft="@dimen/content_padding_half"
android:paddingRight="@dimen/content_padding_half">
<ImageView
android:id="@+id/add_new_device_icon"
android:layout_width="@dimen/list_item_icon_size"
android:layout_height="@dimen/list_item_icon_size"
tools:src="@drawable/ic_action_add"
tools:tint="@color/icon_light" />
<net.osmand.telegram.ui.views.TextViewEx
android:id="@+id/add_new_device_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_big"
android:layout_marginRight="@dimen/content_padding_big"
android:text="@string/add_device"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/list_item_title_text_size"
app:typeface="@string/font_roboto_medium" />
</LinearLayout>
</LinearLayout>
<include layout="@layout/list_item_divider" />

View file

@ -1,4 +1,16 @@
<resources>
<string name="enter_another_device_name">Enter another name, you already have the device with the same name</string>
<string name="device_added_successfully">%1$s added successfully.</string>
<string name="shared_string_add">Add</string>
<string name="error_adding_new_device">Error adding new device</string>
<string name="enter_device_name_description">Enter a name for your new device. Max length 200 symbols.</string>
<string name="device_name_is_too_long">Device name is too long</string>
<string name="device_name_cannot_be_empty">Device name cannot be empty</string>
<string name="device_name">Device name</string>
<string name="shared_string_hide">Hide</string>
<string name="share_location_as_description_second_line">You can create and view the Device ID in the telegram client using the %1$s chat bot. %2$s</string>
<string name="share_location_as_description">If you want to connect multiple devices to one telegram account, you need to use different Device to share your location.</string>
<string name="add_new_device_description">Last updated location:</string>
<string name="last_updated_location">Last updated location:</string>
<string name="successfully_sent_and_updated">Successfully sent and updated</string>
<string name="not_possible_to_send_to_telegram_chats">Not possible to send to Telegram chats:</string>

View file

@ -11,6 +11,7 @@ import net.osmand.data.LatLon
import net.osmand.telegram.helpers.OsmandAidlHelper
import net.osmand.telegram.helpers.TelegramHelper
import net.osmand.telegram.utils.AndroidUtils
import net.osmand.telegram.utils.OsmandApiUtils
import net.osmand.telegram.utils.OsmandFormatter
import net.osmand.telegram.utils.OsmandFormatter.MetricsConstants
import net.osmand.telegram.utils.OsmandFormatter.SpeedConstants
@ -23,6 +24,8 @@ import java.util.concurrent.ConcurrentLinkedQueue
val ADDITIONAL_ACTIVE_TIME_VALUES_SEC = listOf(15 * 60L, 30 * 60L, 60 * 60L, 180 * 60L)
const val SHARE_DEVICES_KEY = "devices"
private val SEND_MY_LOC_VALUES_SEC =
listOf(1L, 2L, 3L, 5L, 10L, 15L, 30L, 60L, 90L, 2 * 60L, 3 * 60L, 5 * 60L)
private val STALE_LOC_VALUES_SEC =
@ -76,10 +79,10 @@ class TelegramSettings(private val app: TelegramApplication) {
private var shareChatsInfo = ConcurrentHashMap<Long, ShareChatInfo>()
private var hiddenOnMapChats: Set<Long> = emptySet()
private var shareDevices: Set<DeviceBot> = emptySet()
var sharingStatusChanges = ConcurrentLinkedQueue<SharingStatus>()
var shareDevicesIds = mutableMapOf<String, String>()
var currentSharingMode = ""
var metricsConstants = MetricsConstants.KILOMETERS_AND_METERS
@ -151,16 +154,24 @@ class TelegramSettings(private val app: TelegramApplication) {
}
fun updateShareDevicesIds(list: List<DeviceBot>) {
shareDevicesIds.clear()
list.forEach {
shareDevicesIds[it.externalId] = it.deviceName
}
shareDevices = list.toHashSet()
}
fun getChatLivePeriod(chatId: Long) = shareChatsInfo[chatId]?.livePeriod
fun getChatsShareInfo() = shareChatsInfo
fun getShareDevices() = shareDevices
fun containsShareDeviceWithName(name: String): Boolean {
shareDevices.forEach {
if (it.deviceName == name) {
return true
}
}
return false
}
fun getLastSuccessfulSendTime() = shareChatsInfo.values.maxBy { it.lastSuccessfulSendTimeMs }?.lastSuccessfulSendTimeMs ?: -1
fun stopSharingLocationToChats() {
@ -216,11 +227,15 @@ class TelegramSettings(private val app: TelegramApplication) {
statusChangeTime = newSharingStatus.statusChangeTime
locationTime = newSharingStatus.locationTime
chatsTitles = newSharingStatus.chatsTitles
title = newSharingStatus.title
if (statusType == SharingStatusType.INITIALIZING
&& newSharingStatus.statusType == SharingStatusType.INITIALIZING
&& !lastSharingStatus.description.contains(newSharingStatus.description)) {
lastSharingStatus.description = "${lastSharingStatus.description}, ${newSharingStatus.description}"
&& newSharingStatus.statusType == SharingStatusType.INITIALIZING) {
if (!description.contains(newSharingStatus.description)) {
description = "$description, ${newSharingStatus.description}"
}
} else {
description = newSharingStatus.description
}
}
}
@ -278,7 +293,7 @@ class TelegramSettings(private val app: TelegramApplication) {
locationTime = getLastSuccessfulSendTime()
title = app.getString(R.string.successfully_sent_and_updated)
description = app.getString(R.string.last_updated_location)
statusType = SharingStatusType.SUCCESSFULLY_SENT
statusType = SharingStatusType.SENDING
}
}
} else {
@ -332,22 +347,14 @@ class TelegramSettings(private val app: TelegramApplication) {
edit.putBoolean(BATTERY_OPTIMISATION_ASKED, batteryOptimisationAsked)
try {
val jArray = JSONArray()
shareChatsInfo.forEach { (chatId, chatInfo) ->
val obj = JSONObject()
obj.put(ShareChatInfo.CHAT_ID_KEY, chatId)
obj.put(ShareChatInfo.START_KEY, chatInfo.start)
obj.put(ShareChatInfo.LIVE_PERIOD_KEY, chatInfo.livePeriod)
obj.put(ShareChatInfo.LIMIT_KEY, chatInfo.currentMessageLimit)
obj.put(ShareChatInfo.CURRENT_MESSAGE_ID_KEY, chatInfo.currentMessageId)
obj.put(ShareChatInfo.USER_SET_LIVE_PERIOD_KEY, chatInfo.userSetLivePeriod)
obj.put(ShareChatInfo.USER_SET_LIVE_PERIOD_START_KEY, chatInfo.userSetLivePeriodStart)
jArray.put(obj)
}
val jArray = convertShareChatsInfoToJson()
if (jArray != null) {
edit.putString(SHARE_CHATS_INFO_KEY, jArray.toString())
} catch (e: JSONException) {
e.printStackTrace()
}
val jsonObject = convertShareDevicesToJson()
if (jsonObject != null) {
edit.putString(SHARE_DEVICES_KEY, jsonObject.toString())
}
edit.apply()
@ -376,6 +383,8 @@ class TelegramSettings(private val app: TelegramApplication) {
e.printStackTrace()
}
parseShareDevices(prefs.getString(SHARE_DEVICES_KEY, ""))
val sendMyLocDef = SEND_MY_LOC_VALUES_SEC[SEND_MY_LOC_DEFAULT_INDEX]
sendMyLocInterval = prefs.getLong(SEND_MY_LOC_INTERVAL_KEY, sendMyLocDef)
val staleLocDef = STALE_LOC_VALUES_SEC[STALE_LOC_DEFAULT_INDEX]
@ -394,6 +403,48 @@ class TelegramSettings(private val app: TelegramApplication) {
batteryOptimisationAsked = prefs.getBoolean(BATTERY_OPTIMISATION_ASKED,false)
}
private fun convertShareDevicesToJson():JSONObject?{
return try {
val jsonObject = JSONObject()
val jArray = JSONArray()
shareDevices.forEach { device ->
val obj = JSONObject()
obj.put(DeviceBot.DEVICE_ID, device.id)
obj.put(DeviceBot.USER_ID, device.userId)
obj.put(DeviceBot.CHAT_ID, device.chatId)
obj.put(DeviceBot.DEVICE_NAME, device.deviceName)
obj.put(DeviceBot.EXTERNAL_ID, device.externalId)
obj.put(DeviceBot.DATA, JSONObject(device.data))
jArray.put(obj)
}
jsonObject.put(SHARE_DEVICES_KEY, jArray)
} catch (e: JSONException) {
e.printStackTrace()
null
}
}
private fun convertShareChatsInfoToJson(): JSONArray? {
return try {
val jArray = JSONArray()
shareChatsInfo.forEach { (chatId, chatInfo) ->
val obj = JSONObject()
obj.put(ShareChatInfo.CHAT_ID_KEY, chatId)
obj.put(ShareChatInfo.START_KEY, chatInfo.start)
obj.put(ShareChatInfo.LIVE_PERIOD_KEY, chatInfo.livePeriod)
obj.put(ShareChatInfo.LIMIT_KEY, chatInfo.currentMessageLimit)
obj.put(ShareChatInfo.CURRENT_MESSAGE_ID_KEY, chatInfo.currentMessageId)
obj.put(ShareChatInfo.USER_SET_LIVE_PERIOD_KEY, chatInfo.userSetLivePeriod)
obj.put(ShareChatInfo.USER_SET_LIVE_PERIOD_START_KEY, chatInfo.userSetLivePeriodStart)
jArray.put(obj)
}
jArray
} catch (e: JSONException) {
e.printStackTrace()
null
}
}
private fun parseShareChatsInfo(json: JSONArray) {
for (i in 0 until json.length()) {
val obj = json.getJSONObject(i)
@ -410,6 +461,10 @@ class TelegramSettings(private val app: TelegramApplication) {
}
}
private fun parseShareDevices(json: String) {
shareDevices = OsmandApiUtils.parseJsonContents(json).toHashSet()
}
private fun getLiveNowChats() = app.telegramHelper.getMessagesByChatIds(locHistoryTime).keys
private fun updatePrefs() {
@ -567,11 +622,6 @@ class TelegramSettings(private val app: TelegramApplication) {
R.color.sharing_status_icon_error,
true
),
SUCCESSFULLY_SENT(
R.drawable.ic_action_share_location,
R.color.sharing_status_icon_success,
false
),
SENDING(
R.drawable.ic_action_share_location,
R.color.sharing_status_icon_success,
@ -601,6 +651,16 @@ class TelegramSettings(private val app: TelegramApplication) {
var deviceName: String = ""
var externalId: String = ""
var data: String = ""
companion object {
internal const val DEVICE_ID = "id"
internal const val USER_ID = "userId"
internal const val CHAT_ID = "chatId"
internal const val DEVICE_NAME = "deviceName"
internal const val EXTERNAL_ID = "externalId"
internal const val DATA = "data"
}
}
class SharingStatus {

View file

@ -5,6 +5,7 @@ import net.osmand.PlatformUtil
import net.osmand.telegram.TelegramApplication
import net.osmand.telegram.notifications.TelegramNotification.NotificationType
import net.osmand.telegram.utils.AndroidNetworkUtils
import net.osmand.telegram.utils.BASE_URL
private const val USER_SET_LIVE_PERIOD_DELAY_MS = 5000 // 5 sec
@ -51,8 +52,8 @@ class ShareLocationHelper(private val app: TelegramApplication) {
if (user != null && sharingMode == user.id.toString()) {
app.telegramHelper.sendLiveLocationMessage(chatsShareInfo, location.latitude, location.longitude)
} else if (sharingMode.isNotEmpty()) {
val url = "https://live.osmand.net/device/$sharingMode/send?lat=${location.latitude}&lon=${location.longitude}"
AndroidNetworkUtils.sendRequestAsync(url, null)
val url = "$BASE_URL/device/$sharingMode/send?lat=${location.latitude}&lon=${location.longitude}"
AndroidNetworkUtils.sendRequestAsync(app, url, null, "Send Location", false, false, null)
}
}
lastLocationMessageSentTime = System.currentTimeMillis()

View file

@ -0,0 +1,191 @@
package net.osmand.telegram.ui
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.BottomSheetBehavior
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.TextView
import net.osmand.telegram.R
import net.osmand.telegram.TelegramSettings
import net.osmand.telegram.ui.views.BottomSheetDialog
import net.osmand.telegram.utils.AndroidNetworkUtils
import net.osmand.telegram.utils.OsmandApiUtils
import org.drinkless.td.libcore.telegram.TdApi
import org.json.JSONException
import org.json.JSONObject
class AddNewDeviceBottomSheet : BaseDialogFragment() {
private lateinit var editText: EditText
private lateinit var errorTextDescription: TextView
private lateinit var primaryBtn: TextView
private lateinit var progressBar: ProgressBar
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = BottomSheetDialog(context!!)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
return dialog
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val mainView = inflater.inflate(R.layout.bottom_sheet_add_new_device, container, false)
mainView.findViewById<View>(R.id.scroll_view_container).setOnClickListener { dismiss() }
BottomSheetBehavior.from(mainView.findViewById<View>(R.id.scroll_view))
.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
editText = mainView.findViewById<EditText>(R.id.edit_text).apply {
addTextChangedListener(object :
TextWatcher {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable) {
updateErrorTextDescription(s.toString())
}
})
}
errorTextDescription = mainView.findViewById<TextView>(R.id.error_text_descr)
progressBar = mainView.findViewById<ProgressBar>(R.id.progressBar).apply {
indeterminateDrawable.setColorFilter(ContextCompat.getColor(app, R.color.primary_btn_text_light), android.graphics.PorterDuff.Mode.MULTIPLY)
}
mainView.findViewById<TextView>(R.id.secondary_btn).apply {
setText(R.string.shared_string_cancel)
setOnClickListener { dismiss() }
}
primaryBtn = mainView.findViewById<TextView>(R.id.primary_btn).apply {
setText(R.string.shared_string_add)
setOnClickListener {
val deviceName = editText.text.toString()
updateErrorTextDescription(deviceName)
if (deviceName.isNotEmpty() && deviceName.length < MAX_DEVICE_NAME_LENGTH
&& !app.settings.containsShareDeviceWithName(deviceName)) {
val user = app.telegramHelper.getCurrentUser()
if (user != null) {
updatePrimaryBtnAndProgress(true)
createNewDeviceWithName(user, deviceName)
}
}
}
}
return mainView
}
private fun createNewDeviceWithName(user: TdApi.User, deviceName: String) {
OsmandApiUtils.createNewDevice(app, user, app.telegramHelper.isBot(user.id), deviceName, 0,
object : AndroidNetworkUtils.OnRequestResultListener {
override fun onResult(result: String?) {
updatePrimaryBtnAndProgress(false)
val deviceBot = getDeviceFromJson(result)
if (deviceBot != null) {
targetFragment?.also { target ->
val intent = Intent().putExtra(DEVICE_NAME, deviceBot.deviceName).putExtra(DEVICE_EXTERNAL_ID, deviceBot.externalId)
target.onActivityResult(targetRequestCode, NEW_DEVICE_REQUEST_CODE, intent)
}
dismiss()
} else {
updateErrorTextDescription(null)
}
}
})
}
private fun updateErrorTextDescription(text: String?) {
when {
text == null -> {
errorTextDescription.visibility = View.VISIBLE
errorTextDescription.text = getString(R.string.error_adding_new_device)
}
text.isEmpty() -> {
errorTextDescription.visibility = View.VISIBLE
errorTextDescription.text = getString(R.string.device_name_cannot_be_empty)
}
text.length > MAX_DEVICE_NAME_LENGTH -> {
errorTextDescription.visibility = View.VISIBLE
errorTextDescription.text = getString(R.string.device_name_is_too_long)
}
app.settings.containsShareDeviceWithName(text.toString()) -> {
errorTextDescription.visibility = View.VISIBLE
errorTextDescription.text = getString(R.string.enter_another_device_name)
}
else -> errorTextDescription.visibility = View.INVISIBLE
}
}
private fun updatePrimaryBtnAndProgress(showProgress: Boolean) {
if (showProgress) {
progressBar.visibility = View.VISIBLE
primaryBtn.text = ""
} else {
progressBar.visibility = View.GONE
primaryBtn.setText(R.string.shared_string_add)
}
}
private fun getDeviceFromJson(json: String?): TelegramSettings.DeviceBot? {
var device: TelegramSettings.DeviceBot? = null
if (json != null) {
device = try {
val jsonResult = JSONObject(json)
val status = jsonResult.getString("status")
if (status == "OK") {
OsmandApiUtils.parseDeviceBot(jsonResult.getJSONObject("device"))
} else {
null
}
} catch (e: JSONException) {
null
}
}
return device
}
companion object {
const val NEW_DEVICE_REQUEST_CODE = 5
const val DEVICE_NAME = "DEVICE_NAME"
const val DEVICE_EXTERNAL_ID = "DEVICE_EXTERNAL_ID"
const val MAX_DEVICE_NAME_LENGTH = 200
private const val TAG = "AddNewDeviceBottomSheet"
fun showInstance(fm: FragmentManager, target: Fragment): Boolean {
return try {
AddNewDeviceBottomSheet().apply {
setTargetFragment(target, NEW_DEVICE_REQUEST_CODE)
show(fm, TAG)
}
true
} catch (e: RuntimeException) {
false
}
}
}
}

View file

@ -1,11 +1,14 @@
package net.osmand.telegram.ui
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.support.v7.widget.ListPopupWindow
import android.support.v7.widget.Toolbar
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -14,6 +17,7 @@ import android.widget.*
import net.osmand.telegram.R
import net.osmand.telegram.TelegramSettings
import net.osmand.telegram.TelegramSettings.DurationPref
import net.osmand.telegram.helpers.TelegramHelper.Companion.OSMAND_BOT_USERNAME
import net.osmand.telegram.helpers.TelegramUiHelper
import net.osmand.telegram.utils.AndroidUtils
import org.drinkless.td.libcore.telegram.TdApi
@ -21,6 +25,10 @@ import org.drinkless.td.libcore.telegram.TdApi
class SettingsDialogFragment : BaseDialogFragment() {
private val uiUtils get() = app.uiUtils
private lateinit var shareAsContainer: ViewGroup
private lateinit var shareAsDescription: TextView
private var shareAsDescriptionHidden = true
override fun onCreateView(
inflater: LayoutInflater,
@ -65,6 +73,36 @@ class SettingsDialogFragment : BaseDialogFragment() {
}
}
shareAsDescription = mainView.findViewById<TextView>(R.id.share_as_description).apply {
text = getText(R.string.share_location_as_description)
setOnClickListener {
updateShareAsDescription()
}
}
shareAsContainer = mainView.findViewById(R.id.share_as_container)
val user = telegramHelper.getCurrentUser()
if (user != null) {
addItemToContainer(inflater, shareAsContainer, user.id.toString(), TelegramUiHelper.getUserName(user))
}
settings.getShareDevices().forEach {
addItemToContainer(inflater, shareAsContainer, it.externalId, it.deviceName)
}
mainView.findViewById<TextView>(R.id.add_new_device_title)
.setTextColor(AndroidUtils.createPressedColorStateList(app, true, R.color.ctrl_active_light, R.color.ctrl_light))
mainView.findViewById<ImageView>(R.id.add_new_device_icon)
.setImageDrawable(getAddNewDeviceIcon())
mainView.findViewById<LinearLayout>(R.id.add_new_device_btn).apply {
setOnClickListener {
fragmentManager?.also { fm ->
AddNewDeviceBottomSheet.showInstance(fm, this@SettingsDialogFragment)
}
}
}
container = mainView.findViewById(R.id.osmand_connect_container)
for (appConn in TelegramSettings.AppConnect.values()) {
val pack = appConn.appPackage
@ -106,15 +144,6 @@ class SettingsDialogFragment : BaseDialogFragment() {
}
updateSelectedAppConn()
container = mainView.findViewById(R.id.share_as_container)
val user = telegramHelper.getCurrentUser()
if (user != null) {
addItemToContainer(inflater, container, user.id.toString(), TelegramUiHelper.getUserName(user))
}
settings.shareDevicesIds.forEach {
addItemToContainer(inflater, container, it.key, it.value)
}
if (user != null) {
TelegramUiHelper.setupPhoto(
app,
@ -151,6 +180,19 @@ class SettingsDialogFragment : BaseDialogFragment() {
logoutTelegram()
dismiss()
}
AddNewDeviceBottomSheet.NEW_DEVICE_REQUEST_CODE -> {
val user = app.telegramHelper.getCurrentUser()
if (user != null && data != null) {
val deviceName = data.getStringExtra(AddNewDeviceBottomSheet.DEVICE_NAME)
val deviceExternalId = data.getStringExtra(AddNewDeviceBottomSheet.DEVICE_EXTERNAL_ID)
val inflater = activity?.layoutInflater
if (inflater != null && deviceName != null && deviceExternalId != null) {
addItemToContainer(inflater, shareAsContainer, deviceExternalId, deviceName)
Toast.makeText(app, getString(R.string.device_added_successfully, deviceName), Toast.LENGTH_SHORT).show()
}
}
}
}
}
@ -235,6 +277,46 @@ class SettingsDialogFragment : BaseDialogFragment() {
}
}
private fun updateShareAsDescription() {
if (shareAsDescriptionHidden) {
shareAsDescription.text = getFullShareAsDescriptionText()
} else {
shareAsDescription.text = getText(R.string.share_location_as_description)
}
shareAsDescriptionHidden = !shareAsDescriptionHidden
}
private fun getFullShareAsDescriptionText(): CharSequence {
val textHide = "${getString(R.string.shared_string_hide)}."
val spannableString = SpannableStringBuilder(getText(R.string.share_location_as_description))
val newSpannable = SpannableStringBuilder(getString(R.string.share_location_as_description_second_line, OSMAND_BOT_USERNAME, textHide))
spannableString.append("\n\n")
var startIndex = newSpannable.indexOf(OSMAND_BOT_USERNAME)
var endIndex = startIndex + OSMAND_BOT_USERNAME.length
newSpannable.setSpan(ForegroundColorSpan(app.uiUtils.getActiveColor()), startIndex, endIndex, 0)
startIndex = newSpannable.indexOf(textHide)
endIndex = startIndex + textHide.length
newSpannable.setSpan(ForegroundColorSpan(app.uiUtils.getActiveColor()), startIndex, endIndex, 0)
spannableString.append(newSpannable)
return spannableString
}
private fun getAddNewDeviceIcon(): Drawable? {
val normal = app.uiUtils.getActiveIcon(R.drawable.ic_action_add)
if (Build.VERSION.SDK_INT >= 21) {
val active = app.uiUtils.getIcon(R.drawable.ic_action_add, R.color.ctrl_light)
if (normal != null && active != null) {
return AndroidUtils.createPressedStateListDrawable(normal, active)
}
}
return normal
}
private fun logoutTelegram() {
val act = activity ?: return
(act as MainActivity).logoutTelegram()

View file

@ -56,7 +56,7 @@ class SharingStatusBottomSheet : DialogFragment() {
findViewById<TextView>(R.id.last_location_line).text = sharingStatus.description
if (sharingStatusType != TelegramSettings.SharingStatusType.INITIALIZING
&& sharingStatusType != TelegramSettings.SharingStatusType.SENDING) {
&& (sharingStatusType == TelegramSettings.SharingStatusType.SENDING && time != -1L)) {
val descriptionTime = when {
time > 0 -> OsmandFormatter.getFormattedTime(time, false)
sharingStatusType == TelegramSettings.SharingStatusType.NO_GPS -> getString(

View file

@ -2,8 +2,14 @@ package net.osmand.telegram.utils
import android.os.AsyncTask
import net.osmand.PlatformUtil
import java.io.*
import java.net.*
import net.osmand.telegram.TelegramApplication
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.URL
object AndroidNetworkUtils {
@ -11,21 +17,34 @@ object AndroidNetworkUtils {
private val log = PlatformUtil.getLog(AndroidNetworkUtils::class.java)
interface OnRequestResultListener {
fun onResult(result: String)
fun onResult(result: String?)
}
fun sendRequestAsync(urlText: String, listener: OnRequestResultListener?) {
SendRequestTask(urlText, listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
fun sendRequestAsync(
app: TelegramApplication,
urlText: String,
json: String?,
userOperation: String,
toastAllowed: Boolean,
post: Boolean,
listener: OnRequestResultListener?
) {
SendRequestTask(app, urlText, json, userOperation, toastAllowed, post, listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
private class SendRequestTask(
private val urlText: String,
private val app: TelegramApplication,
private val url: String,
private val json: String?,
private val userOperation: String,
private val toastAllowed: Boolean,
private val post: Boolean,
private val listener: OnRequestResultListener?
) : AsyncTask<Void, Void, String?>() {
override fun doInBackground(vararg params: Void): String? {
return try {
sendRequest(urlText)
sendRequest(app, url, json, userOperation, toastAllowed, post)
} catch (e: Exception) {
log.error(e.message, e)
null
@ -33,47 +52,99 @@ object AndroidNetworkUtils {
}
override fun onPostExecute(response: String?) {
if (response != null) {
listener?.onResult(response)
}
listener?.onResult(response)
}
}
fun sendRequest(urlText: String): String? {
fun sendRequest(
app: TelegramApplication,
url: String,
jsonBody: String?,
userOperation: String,
toastAllowed: Boolean,
post: Boolean
): String? {
var connection: HttpURLConnection? = null
try {
log.info("GET : $urlText")
val conn = getHttpURLConnection(urlText)
conn.doInput = true
conn.doOutput = false
conn.requestMethod = "GET"
conn.setRequestProperty("User-Agent", "OsmAnd Sharing")
log.info("Response code and message : " + conn.responseCode + " " + conn.responseMessage)
if (conn.responseCode != 200) {
return conn.responseMessage
connection = getHttpURLConnection(url)
connection.setRequestProperty("Accept-Charset", "UTF-8")
connection.setRequestProperty("User-Agent", app.packageName)
connection.connectTimeout = 15000
if (jsonBody != null && post) {
connection.doInput = true
connection.doOutput = true
connection.useCaches = false
connection.requestMethod = "POST"
connection.setRequestProperty("Accept", "application/json")
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Content-Length", jsonBody.toByteArray(charset("UTF-8")).size.toString())
connection.setFixedLengthStreamingMode(jsonBody.toByteArray(charset("UTF-8")).size)
val output = BufferedOutputStream(connection.outputStream)
output.write(jsonBody.toByteArray(charset("UTF-8")))
output.flush()
output.close()
} else {
connection.requestMethod = "GET"
connection.connect()
}
val inputStream = conn.inputStream
val responseBody = StringBuilder()
responseBody.setLength(0)
if (inputStream != null) {
val bufferedInput = BufferedReader(InputStreamReader(inputStream, "UTF-8"))
var s = bufferedInput.readLine()
var first = true
while (s != null) {
if (first) {
first = false
} else {
responseBody.append("\n")
}
responseBody.append(s)
s = bufferedInput.readLine()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
if (toastAllowed) {
val msg = (userOperation + " " + "Failed: " + connection.responseMessage)
log.error(msg)
}
inputStream.close()
} else {
val responseBody = StringBuilder()
responseBody.setLength(0)
val i = connection.inputStream
if (i != null) {
val input = BufferedReader(InputStreamReader(i, "UTF-8"), 256)
var s: String? = input.readLine()
var f = true
while (s != null) {
if (!f) {
responseBody.append("\n")
} else {
f = false
}
responseBody.append(s)
s = input.readLine()
}
try {
input.close()
i.close()
} catch (e: Exception) {
// ignore exception
}
}
return responseBody.toString()
}
} catch (e: NullPointerException) {
if (toastAllowed) {
val msg = (userOperation + " " + "Failed - $e" + ": " + connection?.responseMessage)
log.error(msg)
}
} catch (e: MalformedURLException) {
if (toastAllowed) {
val msg = (userOperation + " " + "Failed - $e" + ": " + connection?.responseMessage)
log.error(msg)
}
return responseBody.toString()
} catch (e: IOException) {
log.error(e.message, e)
return e.message
if (toastAllowed) {
val msg = (userOperation + " " + "Failed - $e" + ": " + connection?.responseMessage)
log.error(msg)
}
} finally {
connection?.disconnect()
}
return null
}
@Throws(MalformedURLException::class, IOException::class)

View file

@ -6,16 +6,21 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.graphics.drawable.StateListDrawable
import android.net.Uri
import android.os.Build
import android.support.annotation.AttrRes
import android.support.annotation.ColorInt
import android.support.annotation.ColorRes
import android.support.v4.app.ActivityCompat
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat
import android.support.v4.content.FileProvider
import android.util.TypedValue
import android.util.TypedValue.COMPLEX_UNIT_DIP
@ -131,6 +136,46 @@ object AndroidUtils {
return ctx.resources.getDimensionPixelSize(R.dimen.list_popup_window_height)
}
fun createPressedColorStateList(
ctx: Context, light: Boolean,
@ColorRes lightNormal: Int, @ColorRes lightPressed: Int,
@ColorRes darkNormal: Int = 0, @ColorRes darkPressed: Int = 0
): ColorStateList {
return createColorStateList(
ctx, light, android.R.attr.state_pressed,
lightNormal, lightPressed, darkNormal, darkPressed
)
}
fun createColorStateList(
ctx: Context, light: Boolean, state: Int,
@ColorRes lightNormal: Int, @ColorRes lightState: Int,
@ColorRes darkNormal: Int, @ColorRes darkState: Int
): ColorStateList {
return ColorStateList(
arrayOf(intArrayOf(state), intArrayOf()),
intArrayOf(
ContextCompat.getColor(ctx, if (light) lightState else darkState),
ContextCompat.getColor(ctx, if (light) lightNormal else darkNormal)
)
)
}
fun createPressedStateListDrawable(normal: Drawable, pressed: Drawable): StateListDrawable {
return createStateListDrawable(normal, pressed, android.R.attr.state_pressed)
}
fun createStateListDrawable(
normal: Drawable,
stateDrawable: Drawable,
state: Int
): StateListDrawable {
val res = StateListDrawable()
res.addState(intArrayOf(state), stateDrawable)
res.addState(intArrayOf(), normal)
return res
}
@ColorInt
fun getAttrColor(ctx: Context, @AttrRes attrId: Int, @ColorInt defaultColor: Int = 0): Int {
val ta = ctx.theme.obtainStyledAttributes(intArrayOf(attrId))

View file

@ -1,46 +1,95 @@
package net.osmand.telegram.utils
import net.osmand.PlatformUtil
import net.osmand.telegram.SHARE_DEVICES_KEY
import net.osmand.telegram.TelegramApplication
import net.osmand.telegram.TelegramSettings
import org.drinkless.td.libcore.telegram.TdApi
import org.json.JSONException
import org.json.JSONObject
const val BASE_URL = "https://live.osmand.net"
object OsmandApiUtils {
private val log = PlatformUtil.getLog(OsmandApiUtils::class.java)
fun updateSharingDevices(app: TelegramApplication, userId: Int) {
AndroidNetworkUtils.sendRequestAsync(
"https://osmand.net/device/send-devices?uid=$userId",
AndroidNetworkUtils.sendRequestAsync(app, "$BASE_URL/device/send-devices?uid=$userId", null, "Get Devices", true, false,
object : AndroidNetworkUtils.OnRequestResultListener {
override fun onResult(result: String) {
val list = parseJsonContents(result)
app.settings.updateShareDevicesIds(list)
override fun onResult(result: String?) {
if (result != null) {
val list = parseJsonContents(result)
app.settings.updateShareDevicesIds(list)
}
}
}
)
}
fun createNewDevice(
app: TelegramApplication,
user: TdApi.User,
isBot: Boolean,
deviceName: String,
chatId: Long,
listener: AndroidNetworkUtils.OnRequestResultListener
) {
val json = getNewDeviceJson(user, isBot, deviceName, chatId)
if (json != null) {
AndroidNetworkUtils.sendRequestAsync(app, "$BASE_URL/device/new", json.toString(), "add Device", true, true, listener)
}
}
fun parseDeviceBot(deviceJSON: JSONObject): TelegramSettings.DeviceBot? {
return try {
TelegramSettings.DeviceBot().apply {
id = deviceJSON.optLong(TelegramSettings.DeviceBot.DEVICE_ID)
userId = deviceJSON.optLong(TelegramSettings.DeviceBot.USER_ID)
chatId = deviceJSON.optLong(TelegramSettings.DeviceBot.CHAT_ID)
deviceName = deviceJSON.optString(TelegramSettings.DeviceBot.DEVICE_NAME)
externalId = deviceJSON.optString(TelegramSettings.DeviceBot.EXTERNAL_ID)
data = deviceJSON.optString(TelegramSettings.DeviceBot.DATA)
}
} catch (e: JSONException) {
log.error(e.message, e)
null
}
}
fun parseJsonContents(contentsJson: String): List<TelegramSettings.DeviceBot> {
val list = mutableListOf<TelegramSettings.DeviceBot>()
try {
val jArray = JSONObject(contentsJson).getJSONArray("devices")
val jArray = JSONObject(contentsJson).getJSONArray(SHARE_DEVICES_KEY)
for (i in 0 until jArray.length()) {
val deviceJSON = jArray.getJSONObject(i)
val deviceBot = TelegramSettings.DeviceBot().apply {
id = deviceJSON.getLong("id")
userId = deviceJSON.getLong("userId")
chatId = deviceJSON.getLong("chatId")
deviceName = deviceJSON.getString("deviceName")
externalId = deviceJSON.getString("externalId")
data = deviceJSON.getString("data")
val deviceBot = parseDeviceBot(deviceJSON)
if (deviceBot != null) {
list.add(deviceBot)
}
list.add(deviceBot)
}
} catch (e: JSONException) {
log.error(e.message, e)
}
return list
}
private fun getNewDeviceJson(user: TdApi.User, isBot: Boolean, deviceName: String, chatId: Long): JSONObject? {
return try {
val json = JSONObject()
json.put("deviceName", deviceName)
json.put("chatId", chatId)
val jsonUser = JSONObject()
jsonUser.put("id", user.id)
jsonUser.put("firstName", user.firstName)
jsonUser.put("isBot", isBot)
jsonUser.put("lastName", user.lastName)
jsonUser.put("userName", user.username)
jsonUser.put("languageCode", user.languageCode)
json.put("user", jsonUser)
} catch (e: JSONException) {
log.error(e)
null
}
}
}