package refactor

This commit is contained in:
rhunk
2023-08-06 22:07:53 +02:00
parent 8e87e7c84d
commit 289afce4a5
13 changed files with 12 additions and 569 deletions

View File

@ -4,13 +4,12 @@ import android.content.Intent
import android.os.Bundle
import me.rhunk.snapenhance.action.AbstractAction
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.ui.map.MapActivity
class OpenMap: AbstractAction("action.open_map") {
override fun run() {
context.runOnUiThread {
val mapActivityIntent = Intent()
mapActivityIntent.setClassName(BuildConfig.APPLICATION_ID, MapActivity::class.java.name)
mapActivityIntent.setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.ui.MapActivity")
mapActivityIntent.putExtra("location", Bundle().apply {
putDouble("latitude", context.config.spoof.location.latitude.get().toDouble())
putDouble("longitude", context.config.spoof.location.longitude.get().toDouble())

View File

@ -6,7 +6,7 @@ import android.database.sqlite.SQLiteDatabase
import me.rhunk.snapenhance.download.data.DownloadMetadata
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.ui.download.MediaFilter
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.util.getIntOrNull
import me.rhunk.snapenhance.util.getStringOrNull

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.ui.download
package me.rhunk.snapenhance.download.data
enum class MediaFilter(
val key: String,

View File

@ -32,7 +32,7 @@ import me.rhunk.snapenhance.hook.HookAdapter
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.Hooker
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.download.MediaFilter
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
import me.rhunk.snapenhance.util.getObjectField
import me.rhunk.snapenhance.util.protobuf.ProtoReader

View File

@ -1,104 +0,0 @@
package me.rhunk.snapenhance.ui.download
import android.app.AlertDialog
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ListView
import android.widget.TextView
import android.widget.Toast
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.core.R
import java.io.File
class ActionListAdapter(
private val activity: DownloadManagerActivity,
private val layoutId: Int,
private val actions: Array<Pair<String, () -> Unit>>
) : ArrayAdapter<Pair<String, () -> Unit>>(activity, layoutId, actions) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: activity.layoutInflater.inflate(layoutId, parent, false)
val action = actions[position]
view.isClickable = true
view.findViewById<TextView>(R.id.feature_text).text = action.first
view.setOnClickListener {
action.second()
}
return view
}
}
class DebugSettingsLayoutInflater(
private val activity: DownloadManagerActivity
) {
private fun confirmAction(title: String, message: String, action: () -> Unit) {
activity.runOnUiThread {
AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setPositiveButton(SharedContext.translation["button.positive"]) { _, _ ->
action()
}
.setNegativeButton(SharedContext.translation["button.negative"]) { _, _ -> }
.show()
}
}
private fun showSuccessToast() {
Toast.makeText(activity, "Success", Toast.LENGTH_SHORT).show()
}
fun inflate(parent: ViewGroup) {
val debugSettingsLayout = activity.layoutInflater.inflate(R.layout.debug_settings_page, parent, false)
val debugSettingsTranslation = activity.translation.getCategory("debug_settings_page")
debugSettingsLayout.findViewById<ImageButton>(R.id.back_button).setOnClickListener {
parent.removeView(debugSettingsLayout)
}
debugSettingsLayout.findViewById<TextView>(R.id.title).text = activity.translation["debug_settings"]
debugSettingsLayout.findViewById<ListView>(R.id.setting_page_list).apply {
adapter = ActionListAdapter(activity, R.layout.debug_setting_item, mutableListOf<Pair<String, () -> Unit>>().apply {
add(debugSettingsTranslation["clear_cache_title"] to {
context.cacheDir.listFiles()?.forEach {
it.deleteRecursively()
}
showSuccessToast()
})
BridgeFileType.values().forEach { fileType ->
val actionName = debugSettingsTranslation.format("clear_file_title", "file_name" to fileType.displayName)
add(actionName to {
confirmAction(actionName, debugSettingsTranslation.format("clear_file_confirmation", "file_name" to fileType.displayName)) {
fileType.resolve(context).deleteRecursively()
showSuccessToast()
}
})
}
add(debugSettingsTranslation["reset_all_title"] to {
confirmAction(debugSettingsTranslation["reset_all_title"], debugSettingsTranslation["reset_all_confirmation"]) {
arrayOf(context.cacheDir, context.filesDir, File(context.dataDir, "databases"), File(context.dataDir, "shared_prefs")).forEach {
it.listFiles()?.forEach { file ->
file.deleteRecursively()
}
}
showSuccessToast()
}
})
}.toTypedArray())
}
activity.registerBackCallback {
parent.removeView(debugSettingsLayout)
}
parent.addView(debugSettingsLayout)
}
}

View File

@ -1,242 +0,0 @@
package me.rhunk.snapenhance.ui.download
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Adapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.download.data.DownloadObject
import me.rhunk.snapenhance.download.data.DownloadStage
import me.rhunk.snapenhance.util.snap.PreviewUtils
import java.io.File
import java.io.FileInputStream
import java.net.URL
import kotlin.concurrent.thread
import kotlin.coroutines.coroutineContext
class DownloadListAdapter(
private val activity: DownloadManagerActivity,
private val downloadList: MutableList<DownloadObject>
): Adapter<DownloadListAdapter.ViewHolder>() {
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val previewJobs = mutableMapOf<Int, Job>()
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val bitmojiIcon: ImageView = view.findViewById(R.id.bitmoji_icon)
val title: TextView = view.findViewById(R.id.item_title)
val subtitle: TextView = view.findViewById(R.id.item_subtitle)
val status: TextView = view.findViewById(R.id.item_status)
val actionButton: Button = view.findViewById(R.id.item_action_button)
val radius by lazy {
view.context.resources.getDimensionPixelSize(R.dimen.download_manager_item_preview_radius)
}
val viewWidth by lazy {
view.resources.displayMetrics.widthPixels
}
val viewHeight by lazy {
view.layoutParams.height
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.download_manager_item, parent, false))
}
override fun getItemCount(): Int {
return downloadList.size
}
@SuppressLint("Recycle")
private suspend fun handlePreview(download: DownloadObject, holder: ViewHolder) {
download.outputFile?.let {
val uri = Uri.parse(it)
runCatching {
if (uri.scheme == "content") {
val fileType = activity.contentResolver.openInputStream(uri)!!.use { stream ->
FileType.fromInputStream(stream)
}
fileType to activity.contentResolver.openInputStream(uri)
} else {
FileType.fromFile(File(it)) to FileInputStream(it)
}
}.getOrNull()
}?.also { (fileType, assetStream) ->
val previewBitmap = assetStream?.use { stream ->
//don't preview files larger than 30MB
if (stream.available() > 30 * 1024 * 1024) return@also
val tempFile = File.createTempFile("preview", ".${fileType.fileExtension}")
tempFile.outputStream().use { output ->
stream.copyTo(output)
}
runCatching {
PreviewUtils.createPreviewFromFile(tempFile)?.let { preview ->
val offsetY = (preview.height / 2 - holder.viewHeight / 2).coerceAtLeast(0)
Bitmap.createScaledBitmap(
Bitmap.createBitmap(
preview, 0, offsetY,
preview.width.coerceAtMost(holder.viewWidth),
preview.height.coerceAtMost(holder.viewHeight)
),
holder.viewWidth,
holder.viewHeight,
false
)
}
}.onFailure {
Logger.error("failed to create preview $fileType", it)
}.also {
tempFile.delete()
}.getOrNull()
} ?: return@also
if (coroutineContext.job.isCancelled) return@also
Handler(holder.view.context.mainLooper).post {
holder.view.background = RoundedBitmapDrawableFactory.create(
holder.view.context.resources,
previewBitmap
).also {
it.cornerRadius = holder.radius.toFloat()
}
}
}
}
private fun updateViewHolder(download: DownloadObject, holder: ViewHolder) {
holder.status.text = download.downloadStage.toString()
holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background)
coroutineScope.launch {
withTimeout(2000) {
handlePreview(download, holder)
}
}
val isSaved = download.downloadStage == DownloadStage.SAVED
//if the download is in progress, the user can cancel it
val canInteract = if (download.job != null) !download.downloadStage.isFinalStage || isSaved
else isSaved
holder.status.visibility = if (isSaved) View.GONE else View.VISIBLE
with(holder.actionButton) {
isEnabled = canInteract
alpha = if (canInteract) 1f else 0.5f
background = context.getDrawable(if (isSaved) R.drawable.action_button_success else R.drawable.action_button_cancel)
setTextColor(context.getColor(if (isSaved) R.color.successColor else R.color.actionBarColor))
text = if (isSaved)
SharedContext.translation["button.open"]
else
SharedContext.translation["button.cancel"]
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pendingDownload = downloadList[position]
pendingDownload.changeListener = { _, _ ->
Handler(holder.view.context.mainLooper).post {
updateViewHolder(pendingDownload, holder)
notifyItemChanged(position)
}
}
// holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank)
pendingDownload.metadata.iconUrl?.let { url ->
thread(start = true) {
runCatching {
val iconBitmap = URL(url).openStream().use {
BitmapFactory.decodeStream(it)
}
Handler(holder.view.context.mainLooper).post {
holder.bitmojiIcon.setImageBitmap(iconBitmap)
}
}
}
}
holder.title.visibility = View.GONE
holder.subtitle.visibility = View.GONE
pendingDownload.metadata.mediaDisplayType?.let {
holder.title.text = it
holder.title.visibility = View.VISIBLE
}
pendingDownload.metadata.mediaDisplaySource?.let {
holder.subtitle.text = it
holder.subtitle.visibility = View.VISIBLE
}
holder.actionButton.setOnClickListener {
if (pendingDownload.downloadStage != DownloadStage.SAVED) {
pendingDownload.cancel()
pendingDownload.downloadStage = DownloadStage.CANCELLED
updateViewHolder(pendingDownload, holder)
notifyItemChanged(position);
return@setOnClickListener
}
pendingDownload.outputFile?.let {
fun showFileNotFound() {
Toast.makeText(holder.view.context, SharedContext.translation["download_manager_activity.file_not_found_toast"], Toast.LENGTH_SHORT).show()
}
val uri = Uri.parse(it)
val fileType = runCatching {
if (uri.scheme == "content") {
activity.contentResolver.openInputStream(uri)?.use { input ->
FileType.fromInputStream(input)
} ?: run {
showFileNotFound()
return@setOnClickListener
}
} else {
val file = File(it)
if (!file.exists()) {
showFileNotFound()
return@setOnClickListener
}
FileType.fromFile(file)
}
}.onFailure { exception ->
Logger.error("Failed to open file", exception)
}.getOrDefault(FileType.UNKNOWN)
if (fileType == FileType.UNKNOWN) {
showFileNotFound()
return@setOnClickListener
}
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.setDataAndType(uri, fileType.mimeType)
holder.view.context.startActivity(intent)
}
}
updateViewHolder(pendingDownload, holder)
}
}

View File

@ -1,214 +0,0 @@
package me.rhunk.snapenhance.ui.download
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import me.rhunk.snapenhance.SharedContext
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.R
import me.rhunk.snapenhance.download.data.DownloadObject
class DownloadManagerActivity : Activity() {
lateinit var translation: LocaleWrapper
private val backCallbacks = mutableListOf<() -> Unit>()
private val fetchedDownloadTasks = mutableListOf<DownloadObject>()
private var listFilter = MediaFilter.NONE
private val preferences by lazy {
getSharedPreferences("settings", Context.MODE_PRIVATE)
}
private fun updateNoDownloadText() {
findViewById<TextView>(R.id.no_download_title).let {
it.text = translation["no_downloads"]
it.visibility = if (fetchedDownloadTasks.isEmpty()) View.VISIBLE else View.GONE
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateListContent() {
fetchedDownloadTasks.clear()
fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryFirstTasks(filter = listFilter).values)
with(findViewById<RecyclerView>(R.id.download_list)) {
adapter?.notifyDataSetChanged()
scrollToPosition(0)
}
updateNoDownloadText()
}
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onBackPressed() {
backCallbacks.lastOrNull()?.let {
it()
backCallbacks.removeLast()
} ?: super.onBackPressed()
}
fun registerBackCallback(callback: () -> Unit) {
backCallbacks.add(callback)
}
@SuppressLint("BatteryLife", "NotifyDataSetChanged", "SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SharedContext.ensureInitialized(this)
translation = SharedContext.translation.getCategory("download_manager_activity")
setContentView(R.layout.download_manager_activity)
findViewById<TextView>(R.id.title).text = resources.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME
findViewById<ImageButton>(R.id.debug_settings_button).setOnClickListener {
DebugSettingsLayoutInflater(this).inflate(findViewById(android.R.id.content))
}
with(findViewById<RecyclerView>(R.id.download_list)) {
layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this@DownloadManagerActivity)
adapter = DownloadListAdapter(this@DownloadManagerActivity, fetchedDownloadTasks).apply {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
updateNoDownloadText()
}
})
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val download = fetchedDownloadTasks[viewHolder.absoluteAdapterPosition]
return if (download.isJobActive()) {
0
} else {
super.getMovementFlags(recyclerView, viewHolder)
}
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
@SuppressLint("NotifyDataSetChanged")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
fetchedDownloadTasks.removeAt(viewHolder.absoluteAdapterPosition).let {
SharedContext.downloadTaskManager.removeTask(it)
}
adapter?.notifyItemRemoved(viewHolder.absoluteAdapterPosition)
}
}).attachToRecyclerView(this)
var isLoading = false
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
val layoutManager = recyclerView.layoutManager as androidx.recyclerview.widget.LinearLayoutManager
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
if (lastVisibleItemPosition == RecyclerView.NO_POSITION) {
return
}
if (lastVisibleItemPosition == fetchedDownloadTasks.size - 1 && !isLoading) {
isLoading = true
SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().downloadId, filter = listFilter).forEach {
fetchedDownloadTasks.add(it.value)
adapter?.notifyItemInserted(fetchedDownloadTasks.size - 1)
}
isLoading = false
}
}
})
arrayOf(
Pair(R.id.all_category, MediaFilter.NONE),
Pair(R.id.pending_category, MediaFilter.PENDING),
Pair(R.id.snap_category, MediaFilter.CHAT_MEDIA),
Pair(R.id.story_category, MediaFilter.STORY),
Pair(R.id.spotlight_category, MediaFilter.SPOTLIGHT)
).let { categoryPairs ->
categoryPairs.forEach { pair ->
this@DownloadManagerActivity.findViewById<TextView>(pair.first).apply {
text = translation["category.${resources.getResourceEntryName(pair.first)}"]
}.setOnClickListener { view ->
listFilter = pair.second
updateListContent()
categoryPairs.map { this@DownloadManagerActivity.findViewById<TextView>(it.first) }.forEach {
it.setTextColor(getColor(R.color.primaryText))
}
(view as TextView).setTextColor(getColor(R.color.focusedCategoryColor))
}
}
}
this@DownloadManagerActivity.findViewById<Button>(R.id.remove_all_button).also {
it.text = translation["remove_all"]
}.setOnClickListener {
with(AlertDialog.Builder(this@DownloadManagerActivity)) {
setTitle(translation["remove_all_title"])
setMessage(translation["remove_all_text"])
setPositiveButton(SharedContext.translation["button.positive"]) { _, _ ->
SharedContext.downloadTaskManager.removeAllTasks()
fetchedDownloadTasks.removeIf {
if (it.isJobActive()) it.cancel()
true
}
adapter?.notifyDataSetChanged()
updateNoDownloadText()
}
setNegativeButton(SharedContext.translation["button.negative"]) { dialog, _ ->
dialog.dismiss()
}
show()
}
}
}
updateListContent()
if (!preferences.getBoolean("ask_battery_optimisations", true) ||
!(getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName)) return
with(Intent()) {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:$packageName")
startActivityForResult(this, 1)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == 1) {
preferences.edit().putBoolean("ask_battery_optimisations", false).apply()
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
updateListContent()
}
}

View File

@ -1,98 +0,0 @@
package me.rhunk.snapenhance.ui.map
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.view.MotionEvent
import android.widget.Button
import android.widget.EditText
import me.rhunk.snapenhance.core.R
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.Projection
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Overlay
class MapActivity : Activity() {
private lateinit var mapView: MapView
@SuppressLint("MissingInflatedId", "ResourceType")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val contextBundle = intent.extras?.getBundle("location") ?: return
val locationLatitude = contextBundle.getDouble("latitude")
val locationLongitude = contextBundle.getDouble("longitude")
Configuration.getInstance().load(applicationContext, getSharedPreferences("osmdroid", Context.MODE_PRIVATE))
setContentView(R.layout.map)
mapView = findViewById(R.id.mapView)
mapView.setMultiTouchControls(true);
mapView.setTileSource(TileSourceFactory.MAPNIK)
val startPoint = GeoPoint(locationLatitude, locationLongitude)
mapView.controller.setZoom(10.0)
mapView.controller.setCenter(startPoint)
val marker = Marker(mapView)
marker.isDraggable = true
marker.position = startPoint
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
mapView.overlays.add(object: Overlay() {
override fun onSingleTapConfirmed(e: MotionEvent?, mapView: MapView?): Boolean {
val proj: Projection = mapView!!.projection
val loc = proj.fromPixels(e!!.x.toInt(), e.y.toInt()) as GeoPoint
marker.position = loc
mapView.invalidate()
return true
}
})
mapView.overlays.add(marker)
val applyButton = findViewById<Button>(R.id.apply_location_button)
applyButton.setOnClickListener {
val bundle = Bundle()
bundle.putFloat("latitude", marker.position.latitude.toFloat())
bundle.putFloat("longitude", marker.position.longitude.toFloat())
setResult(RESULT_OK, intent.putExtra("location", bundle))
finish()
}
val setPreciseLocationButton = findViewById<Button>(R.id.set_precise_location_button)
setPreciseLocationButton.setOnClickListener {
val locationDialog = layoutInflater.inflate(R.layout.precise_location_dialog, null)
val dialogLatitude = locationDialog.findViewById<EditText>(R.id.dialog_latitude).also { it.setText(marker.position.latitude.toString()) }
val dialogLongitude = locationDialog.findViewById<EditText>(R.id.dialog_longitude).also { it.setText(marker.position.longitude.toString()) }
AlertDialog.Builder(this)
.setView(locationDialog)
.setTitle("Set a precise location")
.setPositiveButton("Set") { _, _ ->
val latitude = dialogLatitude.text.toString().toDoubleOrNull()
val longitude = dialogLongitude.text.toString().toDoubleOrNull()
if (latitude != null && longitude != null) {
val preciseLocation = GeoPoint(latitude, longitude)
mapView.controller.setCenter(preciseLocation)
marker.position = preciseLocation
mapView.invalidate()
}
}.setNegativeButton("Cancel") { _, _ -> }.show()
}
}
override fun onDestroy() {
super.onDestroy()
mapView.onDetach()
}
}