524 lines
21 KiB
Kotlin
524 lines
21 KiB
Kotlin
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.adapter
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.database.CursorIndexOutOfBoundsException
|
|
import android.support.v4.widget.Space
|
|
import android.support.v7.widget.RecyclerView
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.widget.TextView
|
|
import com.bumptech.glide.RequestManager
|
|
import org.mariotaku.kpreferences.get
|
|
import org.mariotaku.ktextension.contains
|
|
import org.mariotaku.ktextension.rangeOfSize
|
|
import org.mariotaku.ktextension.safeGetLong
|
|
import org.mariotaku.ktextension.safeMoveToPosition
|
|
import org.mariotaku.library.objectcursor.ObjectCursor
|
|
import org.mariotaku.microblog.library.twitter.model.Activity
|
|
import org.mariotaku.twidere.R
|
|
import org.mariotaku.twidere.adapter.iface.IActivitiesAdapter
|
|
import org.mariotaku.twidere.adapter.iface.IGapSupportedAdapter
|
|
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
|
|
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
|
|
import org.mariotaku.twidere.annotation.Referral
|
|
import org.mariotaku.twidere.constant.newDocumentApiKey
|
|
import org.mariotaku.twidere.exception.UnsupportedCountIndexException
|
|
import org.mariotaku.twidere.extension.model.activityStatus
|
|
import org.mariotaku.twidere.fragment.CursorActivitiesFragment
|
|
import org.mariotaku.twidere.model.*
|
|
import org.mariotaku.twidere.model.util.ParcelableActivityUtils
|
|
import org.mariotaku.twidere.provider.TwidereDataStore.Activities
|
|
import org.mariotaku.twidere.util.IntentUtils
|
|
import org.mariotaku.twidere.util.JsonSerializer
|
|
import org.mariotaku.twidere.util.OnLinkClickHandler
|
|
import org.mariotaku.twidere.util.TwidereLinkify
|
|
import org.mariotaku.twidere.view.holder.*
|
|
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
|
|
import java.lang.ref.WeakReference
|
|
import java.util.*
|
|
|
|
/**
|
|
* Created by mariotaku on 15/1/3.
|
|
*/
|
|
class ParcelableActivitiesAdapter(
|
|
context: Context,
|
|
requestManager: RequestManager
|
|
) : LoadMoreSupportAdapter<RecyclerView.ViewHolder>(context, requestManager),
|
|
IActivitiesAdapter<List<ParcelableActivity>>, IItemCountsAdapter {
|
|
|
|
override val itemCounts: ItemCounts = ItemCounts(2)
|
|
|
|
override var loadMoreIndicatorPosition: Long
|
|
get() = super.loadMoreIndicatorPosition
|
|
set(value) {
|
|
super.loadMoreIndicatorPosition = value
|
|
updateItemCount()
|
|
}
|
|
|
|
override var loadMoreSupportedPosition: Long
|
|
get() = super.loadMoreSupportedPosition
|
|
set(value) {
|
|
super.loadMoreSupportedPosition = value
|
|
updateItemCount()
|
|
}
|
|
|
|
override val activityEventListener: IActivitiesAdapter.ActivityEventListener?
|
|
get() = eventListener
|
|
|
|
override val gapClickListener: IGapSupportedAdapter.GapClickListener?
|
|
get() = eventListener
|
|
|
|
override val mediaPreviewStyle: Int
|
|
get() = statusAdapterDelegate.mediaPreviewStyle
|
|
|
|
override val useStarsForLikes: Boolean
|
|
get() = statusAdapterDelegate.useStarsForLikes
|
|
|
|
override val mediaPreviewEnabled: Boolean
|
|
get() = statusAdapterDelegate.mediaPreviewEnabled
|
|
|
|
override val lightFont: Boolean
|
|
get() = statusAdapterDelegate.lightFont
|
|
|
|
override var showAccountsColor: Boolean
|
|
get() = statusAdapterDelegate.showAccountsColor
|
|
set(value) {
|
|
statusAdapterDelegate.showAccountsColor = value
|
|
notifyDataSetChanged()
|
|
}
|
|
|
|
val isNameFirst: Boolean
|
|
get() = statusAdapterDelegate.nameFirst
|
|
|
|
val activityStartIndex: Int
|
|
get() = getItemStartPosition(ITEM_INDEX_ACTIVITY)
|
|
|
|
var followingOnly: Boolean = false
|
|
set(value) {
|
|
field = value
|
|
notifyDataSetChanged()
|
|
}
|
|
var mentionsOnly: Boolean = false
|
|
set(value) {
|
|
field = value
|
|
notifyDataSetChanged()
|
|
}
|
|
|
|
private val inflater = LayoutInflater.from(context)
|
|
private val twidereLinkify = TwidereLinkify(OnLinkClickHandler(context, null, preferences))
|
|
private val statusAdapterDelegate = DummyItemAdapter(context, twidereLinkify, this, requestManager)
|
|
private val eventListener: EventListener
|
|
private var data: List<ParcelableActivity>? = null
|
|
private var activityAdapterListener: ActivityAdapterListener? = null
|
|
private var filteredUserKeys: Array<UserKey>? = null
|
|
private val gapLoadingIds: MutableSet<ObjectId> = HashSet()
|
|
private val reuseActivity = ParcelableActivity()
|
|
private var infoCache: Array<ActivityInfo?>? = null
|
|
|
|
init {
|
|
eventListener = EventListener(this)
|
|
statusAdapterDelegate.updateOptions()
|
|
setHasStableIds(true)
|
|
}
|
|
|
|
override fun isGapItem(position: Int): Boolean {
|
|
return getFieldValue(position, readInfoValueAction = {
|
|
it.gap
|
|
}, readStatusValueAction = { activity ->
|
|
activity.is_gap
|
|
}, defValue = false, raw = false)
|
|
}
|
|
|
|
override fun getItemId(position: Int): Long {
|
|
val countIndex = itemCounts.getItemCountIndex(position)
|
|
when (countIndex) {
|
|
ITEM_INDEX_ACTIVITY -> {
|
|
return getRowId(position, false)
|
|
}
|
|
else -> {
|
|
return (countIndex.toLong() shl 32) or getItemViewType(position).toLong()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun getActivity(position: Int, raw: Boolean): ParcelableActivity {
|
|
return getActivityInternal(position, raw, false)
|
|
}
|
|
|
|
override fun getActivityCount(raw: Boolean): Int {
|
|
if (data == null) return 0
|
|
return data!!.size
|
|
}
|
|
|
|
override fun setData(data: List<ParcelableActivity>?) {
|
|
if (data is CursorActivitiesFragment.CursorActivitiesLoader.ActivityCursor) {
|
|
filteredUserKeys = data.filteredUserIds
|
|
}
|
|
this.data = data
|
|
infoCache = if (data != null) arrayOfNulls(data.size) else null
|
|
gapLoadingIds.clear()
|
|
updateItemCount()
|
|
notifyDataSetChanged()
|
|
}
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
when (viewType) {
|
|
ITEM_VIEW_TYPE_STATUS -> {
|
|
val holder = ListParcelableStatusesAdapter.createStatusViewHolder(statusAdapterDelegate,
|
|
inflater, parent)
|
|
holder.setStatusClickListener(eventListener)
|
|
return holder
|
|
}
|
|
ITEM_VIEW_TYPE_TITLE_SUMMARY -> {
|
|
val view = inflater.inflate(R.layout.list_item_activity_summary_compact, parent, false)
|
|
val holder = ActivityTitleSummaryViewHolder(view, this)
|
|
holder.setOnClickListeners()
|
|
holder.setupViewOptions()
|
|
return holder
|
|
}
|
|
ITEM_VIEW_TYPE_GAP -> {
|
|
val view = inflater.inflate(GapViewHolder.layoutResource, parent, false)
|
|
return GapViewHolder(this, view)
|
|
}
|
|
ITEM_VIEW_TYPE_LOAD_INDICATOR -> {
|
|
val view = inflater.inflate(R.layout.list_item_load_indicator, parent, false)
|
|
return LoadIndicatorViewHolder(view)
|
|
}
|
|
ITEM_VIEW_TYPE_STUB -> {
|
|
val view = inflater.inflate(R.layout.list_item_two_line, parent, false)
|
|
return StubViewHolder(view)
|
|
}
|
|
ITEM_VIEW_TYPE_EMPTY -> {
|
|
return EmptyViewHolder(Space(context))
|
|
}
|
|
}
|
|
throw UnsupportedOperationException("Unsupported viewType " + viewType)
|
|
}
|
|
|
|
|
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
when (holder.itemViewType) {
|
|
ITEM_VIEW_TYPE_STATUS -> {
|
|
val activity = getActivityInternal(position, raw = false, reuse = true)
|
|
(holder as IStatusViewHolder).display(activity, displayInReplyTo = true)
|
|
}
|
|
ITEM_VIEW_TYPE_TITLE_SUMMARY -> {
|
|
val activity = getActivityInternal(position, raw = false, reuse = true)
|
|
val sources = getAfterFilteredSources(position, false)
|
|
activity.after_filtered_sources = sources
|
|
(holder as ActivityTitleSummaryViewHolder).displayActivity(activity)
|
|
}
|
|
ITEM_VIEW_TYPE_STUB -> {
|
|
val activity = getActivityInternal(position, raw = false, reuse = true)
|
|
(holder as StubViewHolder).displayActivity(activity)
|
|
}
|
|
ITEM_VIEW_TYPE_GAP -> {
|
|
val activity = getActivityInternal(position, raw = false, reuse = true)
|
|
val loading = gapLoadingIds.any { it.accountKey == activity.account_key && it.id == activity.id }
|
|
(holder as GapViewHolder).display(loading)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun getItemViewType(position: Int): Int {
|
|
val countIndex = getItemCountIndex(position)
|
|
when (countIndex) {
|
|
ITEM_INDEX_ACTIVITY -> {
|
|
if (isGapItem(position)) {
|
|
return ITEM_VIEW_TYPE_GAP
|
|
}
|
|
val action = getAction(position)
|
|
when (action) {
|
|
Activity.Action.MENTION, Activity.Action.QUOTE, Activity.Action.REPLY -> {
|
|
return ITEM_VIEW_TYPE_STATUS
|
|
}
|
|
Activity.Action.FOLLOW, Activity.Action.FAVORITE, Activity.Action.RETWEET,
|
|
Activity.Action.FAVORITED_RETWEET, Activity.Action.RETWEETED_RETWEET,
|
|
Activity.Action.RETWEETED_MENTION, Activity.Action.FAVORITED_MENTION,
|
|
Activity.Action.LIST_CREATED, Activity.Action.LIST_MEMBER_ADDED,
|
|
Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED,
|
|
Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> {
|
|
if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY
|
|
val afterFiltered = getAfterFilteredSources(position, false)
|
|
if (afterFiltered != null && afterFiltered.isEmpty()) {
|
|
return ITEM_VIEW_TYPE_EMPTY
|
|
}
|
|
return ITEM_VIEW_TYPE_TITLE_SUMMARY
|
|
}
|
|
}
|
|
return ITEM_VIEW_TYPE_STUB
|
|
}
|
|
ITEM_INDEX_LOAD_MORE_INDICATOR -> {
|
|
return ITEM_VIEW_TYPE_LOAD_INDICATOR
|
|
}
|
|
else -> throw UnsupportedCountIndexException(countIndex, position)
|
|
}
|
|
}
|
|
|
|
override fun addGapLoadingId(id: ObjectId) {
|
|
gapLoadingIds.add(id)
|
|
}
|
|
|
|
override fun removeGapLoadingId(id: ObjectId) {
|
|
gapLoadingIds.remove(id)
|
|
}
|
|
|
|
override fun getItemCount(): Int {
|
|
return itemCounts.itemCount
|
|
}
|
|
|
|
fun setListener(listener: ActivityAdapterListener) {
|
|
activityAdapterListener = listener
|
|
}
|
|
|
|
fun isActivity(position: Int, raw: Boolean = false): Boolean {
|
|
return position < getActivityCount(raw)
|
|
}
|
|
|
|
fun findPositionBySortTimestamp(timestamp: Long, raw: Boolean = false): Int {
|
|
if (timestamp <= 0) return RecyclerView.NO_POSITION
|
|
val range = rangeOfSize(activityStartIndex, getActivityCount(raw))
|
|
if (range.isEmpty()) return RecyclerView.NO_POSITION
|
|
if (timestamp < getTimestamp(range.last, raw)) {
|
|
return range.last
|
|
}
|
|
return range.indexOfFirst { timestamp >= getTimestamp(it, raw) }
|
|
}
|
|
|
|
fun getTimestamp(adapterPosition: Int, raw: Boolean = false): Long {
|
|
return getFieldValue(adapterPosition, readInfoValueAction = {
|
|
it.timestamp
|
|
}, readStatusValueAction = { activity ->
|
|
activity.timestamp
|
|
}, defValue = -1, raw = raw)
|
|
}
|
|
|
|
fun getAction(adapterPosition: Int, raw: Boolean = false): String? {
|
|
return getFieldValue(adapterPosition, readInfoValueAction = {
|
|
it.action
|
|
}, readStatusValueAction = { activity ->
|
|
activity.action
|
|
}, defValue = null, raw = raw)
|
|
}
|
|
|
|
fun getRowId(adapterPosition: Int, raw: Boolean = false): Long {
|
|
return getFieldValue(adapterPosition, readInfoValueAction = {
|
|
it._id
|
|
}, readStatusValueAction = { activity ->
|
|
activity.hashCode().toLong()
|
|
}, defValue = -1L, raw = raw)
|
|
}
|
|
|
|
fun getData(): List<ParcelableActivity>? {
|
|
return data
|
|
}
|
|
|
|
private fun updateItemCount() {
|
|
itemCounts[0] = getActivityCount(false)
|
|
itemCounts[1] = if (ILoadMoreSupportAdapter.END in loadMoreIndicatorPosition) 1 else 0
|
|
}
|
|
|
|
private fun getActivityInternal(position: Int, raw: Boolean, reuse: Boolean): ParcelableActivity {
|
|
val dataPosition = position - activityStartIndex
|
|
val activityCount = getActivityCount(raw)
|
|
if (dataPosition < 0 || dataPosition >= activityCount) {
|
|
val validRange = rangeOfSize(activityStartIndex, getActivityCount(raw))
|
|
throw IndexOutOfBoundsException("index: $position, valid range is $validRange")
|
|
}
|
|
val data = this.data!!
|
|
if (reuse && data is ObjectCursor) {
|
|
val activity = data.setInto(dataPosition, reuseActivity)
|
|
activity.after_filtered_sources = null
|
|
return activity
|
|
} else {
|
|
return data[dataPosition]
|
|
}
|
|
}
|
|
|
|
private fun getAfterFilteredSources(position: Int, raw: Boolean): Array<ParcelableLiteUser>? {
|
|
return getFieldValue(position, readInfoValueAction = {
|
|
it.filteredSources
|
|
}, readStatusValueAction = lambda2@ { activity ->
|
|
if (activity.after_filtered_sources != null) return@lambda2 activity.after_filtered_sources
|
|
val sources = ParcelableActivityUtils.filterSources(activity.sources_lite,
|
|
filteredUserKeys, followingOnly)
|
|
activity.after_filtered_sources = sources
|
|
return@lambda2 sources
|
|
}, defValue = null, raw = raw)
|
|
}
|
|
|
|
private inline fun <T> getFieldValue(position: Int,
|
|
readInfoValueAction: (ActivityInfo) -> T,
|
|
readStatusValueAction: (status: ParcelableActivity) -> T,
|
|
defValue: T, raw: Boolean = false): T {
|
|
val data = this.data
|
|
if (data is ObjectCursor) {
|
|
val dataPosition = position - activityStartIndex
|
|
if (dataPosition < 0 || dataPosition >= getActivityCount(true)) {
|
|
throw CursorIndexOutOfBoundsException("index: $position, valid range is $0..${getActivityCount(true)}")
|
|
}
|
|
val info = infoCache?.get(dataPosition) ?: run {
|
|
val cursor = data.cursor
|
|
if (!cursor.safeMoveToPosition(dataPosition)) return defValue
|
|
val indices = data.indices
|
|
val _id = cursor.safeGetLong(indices[Activities._ID])
|
|
val timestamp = cursor.safeGetLong(indices[Activities.TIMESTAMP])
|
|
val action = cursor.getString(indices[Activities.ACTION])
|
|
val gap = cursor.getInt(indices[Activities.IS_GAP]) == 1
|
|
val sources = cursor.getString(indices[Activities.SOURCES_LITE])?.let {
|
|
JsonSerializer.parseArray(it, ParcelableLiteUser::class.java)
|
|
}
|
|
val filteredSources = ParcelableActivityUtils.filterSources(sources, filteredUserKeys,
|
|
followingOnly)
|
|
val newInfo = ActivityInfo(_id, timestamp, gap, action, filteredSources)
|
|
infoCache?.set(dataPosition, newInfo)
|
|
return@run newInfo
|
|
}
|
|
return readInfoValueAction(info)
|
|
}
|
|
return readStatusValueAction(getActivityInternal(position, raw, false))
|
|
}
|
|
|
|
interface ActivityAdapterListener {
|
|
fun onGapClick(holder: GapViewHolder, position: Int)
|
|
|
|
fun onActivityClick(holder: ActivityTitleSummaryViewHolder, position: Int)
|
|
|
|
fun onStatusActionClick(holder: IStatusViewHolder, id: Int, position: Int)
|
|
|
|
fun onStatusActionLongClick(holder: IStatusViewHolder, id: Int, position: Int): Boolean
|
|
|
|
fun onStatusMenuClick(holder: IStatusViewHolder, menuView: View, position: Int)
|
|
|
|
fun onMediaClick(holder: IStatusViewHolder, view: View, media: ParcelableMedia, position: Int)
|
|
|
|
fun onStatusClick(holder: IStatusViewHolder, position: Int)
|
|
|
|
fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int)
|
|
|
|
}
|
|
|
|
internal class StubViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
|
|
internal val text1 = itemView.findViewById(android.R.id.text1) as TextView
|
|
internal val text2 = itemView.findViewById(android.R.id.text2) as TextView
|
|
|
|
init {
|
|
|
|
text2.setSingleLine(false)
|
|
}
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
fun displayActivity(activity: ParcelableActivity) {
|
|
text1.text = text1.resources.getString(R.string.unsupported_activity_action_title,
|
|
activity.action)
|
|
text2.text = "host: ${activity.account_key.host}, id: ${activity.id}\n"
|
|
text2.append(itemView.context.getString(R.string.unsupported_activity_action_summary))
|
|
}
|
|
}
|
|
|
|
internal class EventListener(adapter: ParcelableActivitiesAdapter) :
|
|
IStatusViewHolder.StatusClickListener, IGapSupportedAdapter.GapClickListener,
|
|
IActivitiesAdapter.ActivityEventListener {
|
|
|
|
val adapterRef = WeakReference(adapter)
|
|
|
|
override fun onGapClick(holder: GapViewHolder, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
val activity = adapter.getActivity(position)
|
|
adapter.addGapLoadingId(ObjectId(activity.account_key, activity.id))
|
|
adapter.activityAdapterListener?.onGapClick(holder, position)
|
|
}
|
|
|
|
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
|
|
val listener = adapterRef.get()?.activityAdapterListener ?: return
|
|
listener.onStatusActionClick(holder as IStatusViewHolder, id, position)
|
|
}
|
|
|
|
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
|
|
val listener = adapterRef.get()?.activityAdapterListener ?: return false
|
|
return listener.onStatusActionLongClick(holder as IStatusViewHolder, id, position)
|
|
}
|
|
|
|
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
|
|
return true
|
|
}
|
|
|
|
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
val status = adapter.getActivity(position).activityStatus ?: return
|
|
IntentUtils.openUserProfile(adapter.context, status.account_key, status.user_key,
|
|
status.user_screen_name, status.extras?.user_statusnet_profile_url,
|
|
adapter.preferences[newDocumentApiKey], Referral.TIMELINE_STATUS, null)
|
|
}
|
|
|
|
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
adapter.activityAdapterListener?.onStatusClick(holder, position)
|
|
}
|
|
|
|
|
|
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
adapter.activityAdapterListener?.onQuotedStatusClick(holder, position)
|
|
}
|
|
|
|
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
adapter.activityAdapterListener?.onMediaClick(holder, view, current, statusPosition)
|
|
}
|
|
|
|
override fun onActivityClick(holder: ActivityTitleSummaryViewHolder, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
adapter.activityAdapterListener?.onActivityClick(holder, position)
|
|
}
|
|
|
|
override fun onItemMenuClick(holder: RecyclerView.ViewHolder, menuView: View, position: Int) {
|
|
val adapter = adapterRef.get() ?: return
|
|
adapter.activityAdapterListener?.onStatusMenuClick(holder as StatusViewHolder, menuView, position)
|
|
}
|
|
}
|
|
|
|
data class ActivityInfo(
|
|
val _id: Long,
|
|
val timestamp: Long,
|
|
val gap: Boolean,
|
|
val action: String,
|
|
val filteredSources: Array<ParcelableLiteUser>?
|
|
)
|
|
|
|
companion object {
|
|
const val ITEM_VIEW_TYPE_STUB = 0
|
|
const val ITEM_VIEW_TYPE_GAP = 1
|
|
const val ITEM_VIEW_TYPE_LOAD_INDICATOR = 2
|
|
const val ITEM_VIEW_TYPE_TITLE_SUMMARY = 3
|
|
const val ITEM_VIEW_TYPE_STATUS = 4
|
|
const val ITEM_VIEW_TYPE_EMPTY = 5
|
|
|
|
const val ITEM_INDEX_ACTIVITY = 0
|
|
const val ITEM_INDEX_LOAD_MORE_INDICATOR = 1
|
|
|
|
}
|
|
}
|