Search initial commit

This commit is contained in:
Chumva 2019-04-10 18:00:45 +03:00
parent 688d193803
commit fca39521cc
15 changed files with 946 additions and 74 deletions

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<gradient
android:angle="0"
android:centerColor="#00ffffff"
android:centerX="0.2"
android:startColor="@color/card_bg_light" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<gradient
android:angle="180"
android:centerColor="#00ffffff"
android:centerX="0.2"
android:startColor="@color/card_bg_light" />
</shape>
</item>
</layer-list>

View file

@ -28,34 +28,10 @@
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
<LinearLayout <include
android:id="@+id/buttons_bar" layout="@layout/bottom_buttons_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/buttons_bottom_bar_height" 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"
android:visibility="gone"
tools:visibility="visible">
<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"/>
<include
layout="@layout/primary_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<android.support.design.widget.BottomNavigationView <android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_navigation" android:id="@+id/bottom_navigation"

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/buttons_bar"
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"
android:visibility="gone"
tools:visibility="visible">
<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" />
<include
layout="@layout/primary_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:osmand="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:id="@+id/empty_state_image_container"
android:layout_width="match_parent"
android:layout_height="@dimen/my_location_image_height"
android:layout_marginBottom="16dp"
android:foreground="@drawable/gradient_both_sides_light">
<ImageView
android:id="@+id/empty_state_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="center"
android:src="@drawable/img_my_location_roadbg" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/content_padding_standard">
<ImageView
android:id="@+id/empty_state_placeholder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/img_my_location_user" />
<ImageView
android:id="@+id/empty_state_user_icon"
android:layout_width="@dimen/my_location_user_icon_size"
android:layout_height="@dimen/my_location_user_icon_size"
android:layout_gravity="center"
android:src="@drawable/img_user_placeholder" />
</FrameLayout>
</FrameLayout>
<net.osmand.telegram.ui.views.TextViewEx
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/content_padding_half"
android:background="@null"
android:text="@string/search_contacts"
android:textColor="?android:attr/textColorSecondary"
android:textSize="20sp"
osmand:typeface="@string/font_roboto_medium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="68dp"
android:layout_marginRight="68dp"
android:background="@null"
android:gravity="center"
android:text="@string/search_contacts_descr"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp" />
</LinearLayout>

View file

@ -101,49 +101,6 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/search_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_half"
android:layout_marginRight="@dimen/content_padding_half"
android:visibility="gone"
tools:background="@drawable/btn_round">
<LinearLayout
android:id="@+id/search_button"
android:layout_width="match_parent"
android:layout_height="@dimen/search_box_height"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical">
<TextView
android:id="@+id/search_hint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_standard"
android:layout_marginRight="@dimen/content_padding_standard"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:text="@string/my_location_search_hint"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/descr_text_size"/>
<ImageView
android:id="@+id/search_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_padding_half"
android:layout_marginRight="@dimen/content_padding_half"
tools:src="@drawable/ic_action_search_dark"
tools:tint="@color/icon_light"/>
</LinearLayout>
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/title_container" android:id="@+id/title_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -296,6 +253,48 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/search_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_half"
android:layout_marginRight="@dimen/content_padding_half"
tools:background="@drawable/btn_round">
<LinearLayout
android:id="@+id/search_button"
android:layout_width="match_parent"
android:layout_height="@dimen/search_box_height"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical">
<TextView
android:id="@+id/search_hint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/content_padding_standard"
android:layout_marginRight="@dimen/content_padding_standard"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:text="@string/my_location_search_hint"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/descr_text_size"/>
<ImageView
android:id="@+id/search_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_padding_half"
android:layout_marginRight="@dimen/content_padding_half"
tools:src="@drawable/ic_action_search_dark"
tools:tint="@color/icon_light"/>
</LinearLayout>
</FrameLayout>
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
<FrameLayout <FrameLayout

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/card_bg_color">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/action_bar_height">
<LinearLayout
android:id="@+id/title_row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical">
<EditText
android:id="@+id/searchEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/content_padding_standard"
android:layout_marginRight="@dimen/content_padding_standard"
android:layout_weight="1"
android:background="@null"
android:ellipsize="end"
android:gravity="center_vertical"
android:hint="@string/type_contact_or_group_name"
android:maxLines="1"
android:textColor="@color/app_bar_title_light"
android:textSize="@dimen/title_text_size" />
<ImageView
android:id="@+id/search_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/shared_string_search"
android:padding="@dimen/content_padding_standard"
android:src="@drawable/ic_action_search_dark"
android:tint="@color/icon_light" />
</LinearLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<net.osmand.telegram.ui.views.EmptyStateRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/buttons_bottom_bar_height" />
<include
android:id="@+id/empty_view"
layout="@layout/empty_state_search" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<android.support.v7.widget.AppCompatImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:src="?attr/bottom_nav_shadow" />
<include
layout="@layout/bottom_buttons_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/buttons_bottom_bar_height"
android:layout_gravity="bottom" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View file

@ -7,6 +7,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<include
android:id="@+id/top_divider"
layout="@layout/list_item_divider"/>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="search_contacts">Search contacts</string>
<string name="search_contacts_descr">Search across all of your groups and contacts.</string>
<string name="type_contact_or_group_name">Type contact or group name</string>
<string name="shared_string_search">Search</string>
<string name="shared_string_ok">OK</string> <string name="shared_string_ok">OK</string>
<string name="timeline_available_for_free_now">Timeline is a feature available now for free.</string> <string name="timeline_available_for_free_now">Timeline is a feature available now for free.</string>
<string name="disable_monitoring">Disable monitoring</string> <string name="disable_monitoring">Disable monitoring</string>

View file

@ -35,6 +35,8 @@ class TelegramHelper private constructor() {
private const val IGNORED_ERROR_CODE = 406 private const val IGNORED_ERROR_CODE = 406
private const val MESSAGE_CANNOT_BE_EDITED_ERROR_CODE = 5 private const val MESSAGE_CANNOT_BE_EDITED_ERROR_CODE = 5
private const val MAX_SEARCH_ITEMS = 100
// min and max values for the Telegram API // min and max values for the Telegram API
const val MIN_LOCATION_MESSAGE_LIVE_PERIOD_SEC = 61 const val MIN_LOCATION_MESSAGE_LIVE_PERIOD_SEC = 61
const val MAX_LOCATION_MESSAGE_LIVE_PERIOD_SEC = 60 * 60 * 24 - 1 // one day const val MAX_LOCATION_MESSAGE_LIVE_PERIOD_SEC = 60 * 60 * 24 - 1 // one day
@ -98,6 +100,7 @@ class TelegramHelper private constructor() {
private val incomingMessagesListeners = HashSet<TelegramIncomingMessagesListener>() private val incomingMessagesListeners = HashSet<TelegramIncomingMessagesListener>()
private val outgoingMessagesListeners = HashSet<TelegramOutgoingMessagesListener>() private val outgoingMessagesListeners = HashSet<TelegramOutgoingMessagesListener>()
private val fullInfoUpdatesListeners = HashSet<FullInfoUpdatesListener>() private val fullInfoUpdatesListeners = HashSet<FullInfoUpdatesListener>()
private val searchListeners = HashSet<TelegramSearchListener>()
fun addIncomingMessagesListener(listener: TelegramIncomingMessagesListener) { fun addIncomingMessagesListener(listener: TelegramIncomingMessagesListener) {
incomingMessagesListeners.add(listener) incomingMessagesListeners.add(listener)
@ -123,6 +126,14 @@ class TelegramHelper private constructor() {
fullInfoUpdatesListeners.remove(listener) fullInfoUpdatesListeners.remove(listener)
} }
fun addSearchListener(listener: TelegramSearchListener) {
searchListeners.add(listener)
}
fun removeSearchListener(listener: TelegramSearchListener) {
searchListeners.remove(listener)
}
fun getChatList(): TreeSet<OrderedChat> { fun getChatList(): TreeSet<OrderedChat> {
synchronized(chatList) { synchronized(chatList) {
return TreeSet(chatList.filter { !it.isChannel }) return TreeSet(chatList.filter { !it.isChannel })
@ -193,7 +204,7 @@ class TelegramHelper private constructor() {
fun isSecretChat(chat: TdApi.Chat): Boolean = chat.type is TdApi.ChatTypeSecret fun isSecretChat(chat: TdApi.Chat): Boolean = chat.type is TdApi.ChatTypeSecret
private fun isChannel(chat: TdApi.Chat): Boolean { fun isChannel(chat: TdApi.Chat): Boolean {
return chat.type is TdApi.ChatTypeSupergroup && (chat.type as TdApi.ChatTypeSupergroup).isChannel return chat.type is TdApi.ChatTypeSupergroup && (chat.type as TdApi.ChatTypeSupergroup).isChannel
} }
@ -249,6 +260,12 @@ class TelegramHelper private constructor() {
fun onTelegramAuthorizationRequestError(code: Int, message: String) fun onTelegramAuthorizationRequestError(code: Int, message: String)
} }
interface TelegramSearchListener {
fun onSearchChatsFinished(obj: TdApi.Chats)
fun onSearchPublicChatsFinished(obj: TdApi.Chats)
fun onSearchContactsFinished(obj: TdApi.Users)
}
inner class TelegramAuthorizationRequestHandler(val telegramAuthorizationRequestListener: TelegramAuthorizationRequestListener) { inner class TelegramAuthorizationRequestHandler(val telegramAuthorizationRequestListener: TelegramAuthorizationRequestListener) {
fun applyAuthenticationParameter(parameterType: TelegramAuthenticationParameterType, parameterValue: String) { fun applyAuthenticationParameter(parameterType: TelegramAuthenticationParameterType, parameterValue: String) {
@ -628,7 +645,7 @@ class TelegramHelper private constructor() {
} }
} }
private fun requestUser(id: Int) { fun requestUser(id: Int) {
client?.send(TdApi.GetUser(id)) { obj -> client?.send(TdApi.GetUser(id)) { obj ->
when (obj.constructor) { when (obj.constructor) {
TdApi.Error.CONSTRUCTOR -> { TdApi.Error.CONSTRUCTOR -> {
@ -648,6 +665,24 @@ class TelegramHelper private constructor() {
} }
} }
fun requestChat(id: Long) {
client?.send(TdApi.GetChat(id)) { obj ->
when (obj.constructor) {
TdApi.Error.CONSTRUCTOR -> {
val error = obj as TdApi.Error
if (error.code != IGNORED_ERROR_CODE) {
listener?.onTelegramError(error.code, error.message)
}
}
TdApi.Chat.CONSTRUCTOR -> {
val chat = obj as TdApi.Chat
chats[chat.id] = chat
listener?.onTelegramChatChanged(chat)
}
}
}
}
fun createPrivateChatWithUser( fun createPrivateChatWithUser(
userId: Int, userId: Int,
shareInfo: TelegramSettings.ShareChatInfo, shareInfo: TelegramSettings.ShareChatInfo,
@ -965,6 +1000,53 @@ class TelegramHelper private constructor() {
} }
} }
fun searchChats(searchTerm: String) {
client?.send(TdApi.SearchChats(searchTerm, MAX_SEARCH_ITEMS)) { obj ->
checkChatsAndUsersSearch(obj)
}
}
fun searchChatsOnServer(searchTerm: String) {
client?.send(TdApi.SearchChatsOnServer(searchTerm, MAX_SEARCH_ITEMS)) { obj ->
checkChatsAndUsersSearch(obj)
}
}
fun searchPublicChats(searchTerm: String) {
client?.send(TdApi.SearchPublicChats(searchTerm)) { obj ->
checkChatsAndUsersSearch(obj, true)
}
}
fun searchContacts(searchTerm: String) {
client?.send(TdApi.SearchContacts(searchTerm, MAX_SEARCH_ITEMS)) { obj ->
checkChatsAndUsersSearch(obj)
}
}
private fun checkChatsAndUsersSearch(obj: TdApi.Object, publicChats: Boolean = false) {
when (obj.constructor) {
TdApi.Error.CONSTRUCTOR -> {
val error = obj as TdApi.Error
if (error.code != IGNORED_ERROR_CODE) {
listener?.onTelegramError(error.code, error.message)
}
}
TdApi.Chats.CONSTRUCTOR -> {
val chats = obj as TdApi.Chats
if (publicChats) {
searchListeners.forEach { it.onSearchPublicChatsFinished(chats) }
} else {
searchListeners.forEach { it.onSearchChatsFinished(chats) }
}
}
TdApi.Users.CONSTRUCTOR -> {
val users = obj as TdApi.Users
searchListeners.forEach { it.onSearchContactsFinished(users) }
}
}
}
fun logout(): Boolean { fun logout(): Boolean {
return if (libraryLoaded) { return if (libraryLoaded) {
haveAuthorization = false haveAuthorization = false

View file

@ -510,6 +510,7 @@ class LiveNowTabFragment : Fragment(), TelegramListener, TelegramIncomingMessage
holder.topDivider?.visibility = if (!sortByGroup && position != 0) View.GONE else View.VISIBLE holder.topDivider?.visibility = if (!sortByGroup && position != 0) View.GONE else View.VISIBLE
} else if (item is LocationItem && holder is ContactViewHolder) { } else if (item is LocationItem && holder is ContactViewHolder) {
holder.description?.text = OsmandFormatter.getListItemLiveTimeDescr(app, item.lastUpdated, lastResponseStr) holder.description?.text = OsmandFormatter.getListItemLiveTimeDescr(app, item.lastUpdated, lastResponseStr)
holder.topShadowDivider?.visibility = View.GONE
} }
} }
@ -601,6 +602,7 @@ class LiveNowTabFragment : Fragment(), TelegramListener, TelegramIncomingMessage
val description: TextView? = view.findViewById(R.id.description) val description: TextView? = view.findViewById(R.id.description)
val receivedGpxPointsContainer: View? = view.findViewById(R.id.received_gps_points_container) val receivedGpxPointsContainer: View? = view.findViewById(R.id.received_gps_points_container)
val receivedGpxPointsDescr: TextView? = view.findViewById(R.id.received_gps_points_description) val receivedGpxPointsDescr: TextView? = view.findViewById(R.id.received_gps_points_description)
val topShadowDivider: View? = view.findViewById(R.id.top_divider)
val bottomShadow: View? = view.findViewById(R.id.bottom_shadow) val bottomShadow: View? = view.findViewById(R.id.bottom_shadow)
val lastTelegramUpdateTime: TextView? = view.findViewById(R.id.last_telegram_update_time) val lastTelegramUpdateTime: TextView? = view.findViewById(R.id.last_telegram_update_time)

View file

@ -130,6 +130,7 @@ class MyLocationTabFragment : Fragment(), TelegramListener {
appBarCollapsed = collapsed appBarCollapsed = collapsed
adjustText() adjustText()
adjustAppbar() adjustAppbar()
adjustSearchBox()
optionsBtn.visibility = if (collapsed) View.VISIBLE else View.GONE optionsBtn.visibility = if (collapsed) View.VISIBLE else View.GONE
} }
}) })
@ -183,7 +184,7 @@ class MyLocationTabFragment : Fragment(), TelegramListener {
setBackgroundDrawable(searchBoxBg) setBackgroundDrawable(searchBoxBg)
} }
findViewById<View>(R.id.search_button).setOnClickListener { findViewById<View>(R.id.search_button).setOnClickListener {
Toast.makeText(context, "Search", Toast.LENGTH_SHORT).show() activity.supportFragmentManager?.also { SearchDialogFragment.showInstance(it, this@MyLocationTabFragment) }
} }
findViewById<ImageView>(R.id.search_icon) findViewById<ImageView>(R.id.search_icon)
.setImageDrawable(app.uiUtils.getThemedIcon(R.drawable.ic_action_search_dark)) .setImageDrawable(app.uiUtils.getThemedIcon(R.drawable.ic_action_search_dark))
@ -635,6 +636,7 @@ class MyLocationTabFragment : Fragment(), TelegramListener {
} }
} }
} }
holder.topShadowDivider?.visibility = View.GONE
holder.bottomShadow?.visibility = if (lastItem) View.VISIBLE else View.GONE holder.bottomShadow?.visibility = if (lastItem) View.VISIBLE else View.GONE
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
if (live) { if (live) {
@ -754,6 +756,7 @@ class MyLocationTabFragment : Fragment(), TelegramListener {
inner class ChatViewHolder(val view: View) : BaseViewHolder(view) { inner class ChatViewHolder(val view: View) : BaseViewHolder(view) {
val checkBox: CheckBox? = view.findViewById(R.id.check_box) val checkBox: CheckBox? = view.findViewById(R.id.check_box)
val topShadowDivider: View? = view.findViewById(R.id.top_divider)
val bottomShadow: View? = view.findViewById(R.id.bottom_shadow) val bottomShadow: View? = view.findViewById(R.id.bottom_shadow)
} }

View file

@ -0,0 +1,527 @@
package net.osmand.telegram.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import net.osmand.Location
import net.osmand.PlatformUtil
import net.osmand.data.LatLon
import net.osmand.telegram.R
import net.osmand.telegram.TelegramLocationProvider.TelegramCompassListener
import net.osmand.telegram.TelegramLocationProvider.TelegramLocationListener
import net.osmand.telegram.helpers.TelegramHelper
import net.osmand.telegram.helpers.TelegramUiHelper
import net.osmand.telegram.ui.views.EmptyStateRecyclerView
import net.osmand.telegram.utils.AndroidUtils
import net.osmand.telegram.utils.OsmandFormatter
import net.osmand.telegram.utils.OsmandLocationUtils
import net.osmand.telegram.utils.UiUtils
import net.osmand.util.MapUtils
import org.drinkless.td.libcore.telegram.TdApi
class SearchDialogFragment : BaseDialogFragment(), TelegramHelper.TelegramSearchListener,
TelegramLocationListener, TelegramCompassListener {
private val log = PlatformUtil.getLog(SearchDialogFragment::class.java)
private val uiUtils get() = app.uiUtils
private val adapter = SearchAdapter()
private lateinit var locationViewCache: UiUtils.UpdateLocationViewCache
private lateinit var searchBox: EditText
private lateinit var buttonsBar: LinearLayout
private val searchedChatsIds = mutableSetOf<Long>()
private val searchedPublicChatsIds = mutableSetOf<Long>()
private val searchedContactsIds = mutableSetOf<Int>()
private val selectedChats = HashSet<Long>()
private val selectedUsers = HashSet<Long>()
private var searchQuery: String = ""
private var location: Location? = null
private var heading: Float? = null
private var locationUiUpdateAllowed: Boolean = true
override fun onCreateView(
inflater: LayoutInflater,
parent: ViewGroup?,
savedInstanceState: Bundle?
): View {
val mainView = inflater.inflate(R.layout.fragment_search_dialog, parent)
val appBarLayout = mainView.findViewById<View>(R.id.app_bar_layout)
AndroidUtils.addStatusBarPadding19v(context!!, appBarLayout)
mainView.findViewById<Toolbar>(R.id.toolbar).apply {
navigationIcon = uiUtils.getThemedIcon(R.drawable.ic_arrow_back)
setNavigationOnClickListener { dismiss() }
}
searchBox = mainView.findViewById<EditText>(R.id.searchEditText).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) {
val newQueryText = s.toString()
if (!searchQuery.equals(newQueryText, true)) {
searchQuery = newQueryText
clearSearchedItems()
if (searchQuery.isNotBlank()) {
runSearch()
} else {
updateList()
}
}
}
})
}
mainView.findViewById<ImageView>(R.id.search_icon).setOnClickListener {
runSearch()
}
val emptyView = mainView.findViewById<LinearLayout>(R.id.empty_view)
mainView.findViewById<EmptyStateRecyclerView>(R.id.recycler_view).apply {
layoutManager = LinearLayoutManager(context)
adapter = this@SearchDialogFragment.adapter
setEmptyView(emptyView)
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
locationUiUpdateAllowed = newState == RecyclerView.SCROLL_STATE_IDLE
}
})
}
val matrix = ColorMatrix()
matrix.setSaturation(0f)
val filter = ColorMatrixColorFilter(matrix)
mainView.findViewById<ImageView>(R.id.empty_state_background).colorFilter = filter
mainView.findViewById<ImageView>(R.id.empty_state_placeholder).colorFilter = filter
buttonsBar = mainView.findViewById<LinearLayout>(R.id.buttons_bar).apply {
findViewById<TextView>(R.id.primary_btn).apply {
text = getString(R.string.shared_string_continue)
setOnClickListener {
onPrimaryBtnClick()
}
}
findViewById<TextView>(R.id.secondary_btn).apply {
text = getString(R.string.shared_string_cancel)
setOnClickListener {
onSecondaryBtnClick()
}
}
}
return mainView
}
private fun clearSearchedItems() {
searchedChatsIds.clear()
searchedPublicChatsIds.clear()
searchedContactsIds.clear()
}
private fun runSearch() {
if (searchQuery.isNotBlank()) {
runSearch(searchQuery)
}
}
private fun runSearch(text: String) {
telegramHelper.searchChats(text)
telegramHelper.searchChatsOnServer(text)
telegramHelper.searchContacts(text)
if (text.length > 4) {
telegramHelper.searchPublicChats(text)
}
}
override fun onResume() {
super.onResume()
telegramHelper.addSearchListener(this)
locationViewCache = app.uiUtils.getUpdateLocationViewCache()
startLocationUpdate()
}
override fun onPause() {
super.onPause()
telegramHelper.removeSearchListener(this)
stopLocationUpdate()
}
override fun updateLocation(location: Location?) {
val loc = this.location
val newLocation = loc == null && location != null
val locationChanged = loc != null && location != null
&& loc.latitude != location.latitude
&& loc.longitude != location.longitude
if (newLocation || locationChanged) {
this.location = location
updateLocationUi()
}
}
override fun updateCompassValue(value: Float) {
// 99 in next line used to one-time initialize arrows (with reference vs. fixed-north direction)
// on non-compass devices
val lastHeading = heading ?: 99f
heading = value
if (Math.abs(MapUtils.degreesDiff(lastHeading.toDouble(), value.toDouble())) > 5) {
updateLocationUi()
} else {
heading = lastHeading
}
}
private fun startLocationUpdate() {
app.locationProvider.addLocationListener(this)
app.locationProvider.addCompassListener(this)
updateLocationUi()
}
private fun stopLocationUpdate() {
app.locationProvider.removeLocationListener(this)
app.locationProvider.removeCompassListener(this)
}
private fun updateLocationUi() {
if (locationUiUpdateAllowed) {
app.runInUIThread { updateList() }
}
}
private fun updateList() {
val items: MutableList<TdApi.Object> = mutableListOf()
val currentUserId = telegramHelper.getCurrentUserId()
val chats: MutableList<TdApi.Chat> = getChats(currentUserId)
items.addAll(chats)
val users: MutableList<TdApi.User> = getContacts(currentUserId, chats)
items.addAll(sortUsers(users))
items.addAll(getChats(currentUserId, chats))
adapter.items = items
}
private fun getChats(
currentUserId: Int,
addedChats: List<TdApi.Chat>? = null
): MutableList<TdApi.Chat> {
val chats: MutableList<TdApi.Chat> = mutableListOf()
searchedChatsIds.forEach {
val chat = telegramHelper.getChat(it)
if (chat != null) {
if (!telegramHelper.isChannel(chat)
&& telegramHelper.getUserIdFromChatType(chat.type) != currentUserId
&& (addedChats == null || (!addedChats.contains(chat)))
) {
chats.add(chat)
}
} else {
telegramHelper.requestChat(it)
}
}
return chats
}
private fun getContacts(currentUserId: Int, chats: List<TdApi.Chat>): MutableList<TdApi.User> {
val users: MutableList<TdApi.User> = mutableListOf()
searchedContactsIds.forEach { userId ->
val user = telegramHelper.getUser(userId)
if (user != null) {
if (user.id != currentUserId && !chats.any { telegramHelper.getUserIdFromChatType(it.type) == user.id })
users.add(user)
} else {
telegramHelper.requestUser(userId)
}
}
return users
}
private fun sortUsers(list: MutableList<TdApi.User>): MutableList<TdApi.User> {
list.sortWith(Comparator { o1, o2 ->
val title1 = TelegramUiHelper.getUserName(o1)
val title2 = TelegramUiHelper.getUserName(o2)
title1.compareTo(title2)
})
return list
}
override fun onSearchContactsFinished(obj: TdApi.Users) {
log.debug("searchContactsFinished $obj")
val ids = obj.userIds
if (ids.isNotEmpty()) {
searchedContactsIds.addAll(ids.toList())
app.runInUIThread { updateList() }
}
}
override fun onSearchChatsFinished(obj: TdApi.Chats) {
log.debug("searchChatsFinished $obj")
val ids = obj.chatIds
if (ids.isNotEmpty()) {
searchedChatsIds.addAll(ids.toList())
app.runInUIThread { updateList() }
}
}
override fun onSearchPublicChatsFinished(obj: TdApi.Chats) {
log.debug("onSearchPublicChatsFinished $obj")
val ids = obj.chatIds
if (ids.isNotEmpty()) {
searchedPublicChatsIds.addAll(ids.toList())
app.runInUIThread { updateList() }
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
LogoutBottomSheet.LOGOUT_REQUEST_CODE -> {
dismiss()
}
SetTimeDialogFragment.LOCATION_SHARED_REQUEST_CODE -> {
if (resultCode == SetTimeDialogFragment.LOCATION_SHARED_REQUEST_CODE) {
targetFragment?.also {
it.onActivityResult(targetRequestCode, resultCode, null)
}
dismiss()
}
}
}
}
inner class SearchAdapter : RecyclerView.Adapter<SearchAdapter.ChatViewHolder>() {
var items = mutableListOf<TdApi.Object>()
set(value) {
field = value
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.user_list_item, parent, false)
return ChatViewHolder(view)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) {
val item = items[position]
val isChat = item is TdApi.Chat
val itemId = if (isChat) {
(item as TdApi.Chat).id
} else {
(item as TdApi.User).id.toLong()
}
val latLon = getItemLastLocation(item)
val lastUpdate = getLastUpdateTime(item)
val lastItem = position == itemCount - 1
val placeholderId = if (isChat && telegramHelper.isGroup(item as TdApi.Chat)) R.drawable.img_group_picture else R.drawable.img_user_picture
val live = (isChat && settings.isSharingLocationToChat(itemId))
val shareInfo = if (isChat) settings.getChatsShareInfo()[itemId] else null
val photoPath = when (item) {
is TdApi.Chat -> item.photo?.small?.local?.path
is TdApi.User -> item.profilePhoto?.small?.local?.path
else -> null
}
TelegramUiHelper.setupPhoto(app, holder.icon, photoPath, placeholderId, false)
val title = when (item) {
is TdApi.Chat -> item.title
is TdApi.User -> TelegramUiHelper.getUserName(item)
else -> null
}
holder.title?.text = title
holder.checkBox?.apply {
visibility = if (live) View.GONE else View.VISIBLE
setOnCheckedChangeListener(null)
isChecked = if (isChat) {
selectedChats.contains(itemId)
} else {
selectedUsers.contains(itemId)
}
setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
if (isChat) {
selectedChats.add(itemId)
} else {
selectedUsers.add(itemId)
}
} else {
if (isChat) {
selectedChats.remove(itemId)
} else {
selectedUsers.remove(itemId)
}
}
switchButtonsVisibility(selectedChats.isNotEmpty() || selectedUsers.isNotEmpty())
}
}
holder.topShadowDivider?.visibility = if (position == 0) View.VISIBLE else View.GONE
holder.bottomShadow?.visibility = if (lastItem) View.VISIBLE else View.GONE
holder.itemView.setOnClickListener {
if (!live) {
holder.checkBox?.apply {
isChecked = !isChecked
}
}
}
if (location != null && latLon != null && lastUpdate != null) {
val staleLocation = System.currentTimeMillis() / 1000 - lastUpdate > settings.staleLocTime
holder.locationViewContainer?.visibility = if (lastUpdate > 0) View.VISIBLE else View.GONE
locationViewCache.outdatedLocation = staleLocation
app.uiUtils.updateLocationView(holder.directionIcon, holder.distanceText, latLon, locationViewCache)
} else {
holder.locationViewContainer?.visibility = View.GONE
}
val expiresIn = shareInfo?.getChatLiveMessageExpireTime() ?: 0
holder.textInArea?.apply {
visibility = if (live) View.VISIBLE else View.GONE
text = OsmandFormatter.getFormattedDuration(app, expiresIn)
}
holder.description?.apply {
val description = getItemDescription(item, lastUpdate)
text = description
visibility = if (description != null) View.VISIBLE else View.GONE
}
}
private fun getItemLastMessage(item: TdApi.Object): TdApi.Message? {
when (item) {
is TdApi.User -> {
return telegramHelper.getUserMessage(item)
}
is TdApi.Chat -> {
return telegramHelper.getChatMessages(item.id).firstOrNull() ?: item.lastMessage
}
}
return null
}
private fun getItemLastLocation(item: TdApi.Object): LatLon? {
val message = getItemLastMessage(item)
if (message != null && OsmandLocationUtils.getSenderMessageId(message) != telegramHelper.getCurrentUserId()) {
val messageLocation = OsmandLocationUtils.parseMessageContent(message, telegramHelper)
if (messageLocation != null) {
return LatLon(messageLocation.lat, messageLocation.lon)
}
}
return null
}
private fun getLastUpdateTime(item: TdApi.Object): Int? {
val message = getItemLastMessage(item)
if (message != null && OsmandLocationUtils.getSenderMessageId(message) != telegramHelper.getCurrentUserId()) {
return OsmandLocationUtils.getLastUpdatedTime(message)
}
return null
}
private fun getItemDescription(item: TdApi.Object, lastUpdateTime: Int?): String? {
if (lastUpdateTime != null) {
return OsmandFormatter.getListItemLiveTimeDescr(app, lastUpdateTime)
}
if (item is TdApi.Chat && telegramHelper.isGroup(item)) {
return getString(R.string.shared_string_group)
}
return null
}
override fun getItemCount() = items.size
inner class ChatViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val icon: ImageView? = view.findViewById(R.id.icon)
val title: TextView? = view.findViewById(R.id.title)
val locationViewContainer: View? = view.findViewById(R.id.location_view_container)
val directionIcon: ImageView? = view.findViewById(R.id.direction_icon)
val distanceText: TextView? = view.findViewById(R.id.distance_text)
val description: TextView? = view.findViewById(R.id.description)
val checkBox: CheckBox? = view.findViewById(R.id.check_box)
val textInArea: TextView? = view.findViewById(R.id.text_in_area)
val topShadowDivider: View? = view.findViewById(R.id.top_divider)
val bottomShadow: View? = view.findViewById(R.id.bottom_shadow)
}
}
private fun onPrimaryBtnClick() {
if (selectedChats.isNotEmpty() || selectedUsers.isNotEmpty()) {
fragmentManager?.also {
SetTimeDialogFragment.showInstance(it, selectedChats, selectedUsers, this)
}
}
}
private fun onSecondaryBtnClick() {
clearSelection()
adapter.notifyDataSetChanged()
adapter.notifyDataSetChanged()
switchButtonsVisibility(false)
}
private fun clearSelection() {
selectedChats.clear()
selectedUsers.clear()
}
private fun switchButtonsVisibility(visible: Boolean) {
val buttonsVisibility = if (visible) View.VISIBLE else View.GONE
if (buttonsBar.visibility != buttonsVisibility) {
buttonsBar.visibility = buttonsVisibility
}
}
companion object {
const val TAG = "SearchDialogFragment"
private val SEARCH_ITEM = 1
fun showInstance(fm: FragmentManager, target: Fragment?): Boolean {
return try {
SearchDialogFragment().apply {
if (target != null) {
setTargetFragment(target, SetTimeDialogFragment.LOCATION_SHARED_REQUEST_CODE)
}
show(fm, TAG)
}
true
} catch (e: RuntimeException) {
false
}
}
}
}

View file

@ -79,6 +79,9 @@ class SetTimeDialogFragment : BaseDialogFragment(), TelegramLocationListener, Te
view.findViewById<TextView>(R.id.secondary_btn).apply { view.findViewById<TextView>(R.id.secondary_btn).apply {
text = getString(R.string.shared_string_back) text = getString(R.string.shared_string_back)
setOnClickListener { setOnClickListener {
targetFragment?.also {
it.onActivityResult(targetRequestCode, LOCATION_SHARING_CANCELED_CODE, null)
}
dismiss() dismiss()
} }
} }
@ -357,6 +360,7 @@ class SetTimeDialogFragment : BaseDialogFragment(), TelegramLocationListener, Te
userLivePeriods[itemId]?.also { text = formatLivePeriod(it) } userLivePeriods[itemId]?.also { text = formatLivePeriod(it) }
} }
} }
holder.topShadowDivider?.visibility = View.GONE
holder.bottomShadow?.visibility = View.GONE holder.bottomShadow?.visibility = View.GONE
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
selectDuration(itemId, isChat) selectDuration(itemId, isChat)
@ -373,6 +377,7 @@ class SetTimeDialogFragment : BaseDialogFragment(), TelegramLocationListener, Te
val locationViewContainer: View? = view.findViewById(R.id.location_view_container) val locationViewContainer: View? = view.findViewById(R.id.location_view_container)
val description: TextView? = view.findViewById(R.id.description) val description: TextView? = view.findViewById(R.id.description)
val textInArea: TextView? = view.findViewById(R.id.text_in_area) val textInArea: TextView? = view.findViewById(R.id.text_in_area)
val topShadowDivider: View? = view.findViewById(R.id.top_divider)
val bottomShadow: View? = view.findViewById(R.id.bottom_shadow) val bottomShadow: View? = view.findViewById(R.id.bottom_shadow)
} }
} }
@ -380,6 +385,7 @@ class SetTimeDialogFragment : BaseDialogFragment(), TelegramLocationListener, Te
companion object { companion object {
const val LOCATION_SHARED_REQUEST_CODE = 0 const val LOCATION_SHARED_REQUEST_CODE = 0
const val LOCATION_SHARING_CANCELED_CODE = 1
private const val TAG = "SetTimeDialogFragment" private const val TAG = "SetTimeDialogFragment"
private const val CHATS_KEY = "chats_key" private const val CHATS_KEY = "chats_key"

View file

@ -0,0 +1,56 @@
package net.osmand.telegram.ui.views
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
class EmptyStateRecyclerView : RecyclerView {
private var emptyView: View? = null
private val emptyStateObserver = object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
checkIfEmpty()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
checkIfEmpty()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
checkIfEmpty()
}
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
context,
attrs,
defStyle
)
override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
val oldAdapter = getAdapter()
oldAdapter?.unregisterAdapterDataObserver(emptyStateObserver)
super.setAdapter(adapter)
adapter?.registerAdapterDataObserver(emptyStateObserver)
checkIfEmpty()
}
fun setEmptyView(emptyView: View) {
this.emptyView = emptyView
checkIfEmpty()
}
private fun checkIfEmpty() {
adapter?.apply {
val empty = itemCount == 0
visibility = if (empty) View.GONE else View.VISIBLE
emptyView?.visibility = if (empty) View.VISIBLE else View.GONE
}
}
}

View file

@ -169,7 +169,10 @@ object OsmandLocationUtils {
} }
} }
fun parseTextLocation(text: TdApi.FormattedText, botLocation: Boolean): MessageLocation { fun parseTextLocation(text: TdApi.FormattedText, botLocation: Boolean): MessageLocation? {
if (botLocation && !text.text.startsWith(DEVICE_PREFIX) || !botLocation && !text.text.startsWith(USER_TEXT_LOCATION_TITLE)) {
return null
}
val res = if (botLocation) MessageOsmAndBotLocation() else MessageUserLocation() val res = if (botLocation) MessageOsmAndBotLocation() else MessageUserLocation()
res.type = LocationMessages.TYPE_TEXT res.type = LocationMessages.TYPE_TEXT
var locationNA = false var locationNA = false