c2c-sync/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt

345 lines
13 KiB
Kotlin
Raw Normal View History

2025-09-18 18:43:03 +02:00
/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.nextcloud.ui
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.core.AsyncRunner
import com.nextcloud.client.di.Injectable
import com.owncloud.android.R
import com.owncloud.android.databinding.SetStatusMessageBottomSheetBinding
import com.owncloud.android.datamodel.ArbitraryDataProvider
import com.owncloud.android.lib.resources.users.ClearAt
import com.owncloud.android.lib.resources.users.PredefinedStatus
import com.owncloud.android.lib.resources.users.Status
import com.owncloud.android.ui.activity.BaseActivity
import com.owncloud.android.ui.adapter.PredefinedStatusClickListener
import com.owncloud.android.ui.adapter.PredefinedStatusListAdapter
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.theme.ViewThemeUtils
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.EmojiPopup
import com.vanniktech.emoji.google.GoogleEmojiProvider
import com.vanniktech.emoji.installDisableKeyboardInput
import com.vanniktech.emoji.installForceSingleEmoji
import java.util.Calendar
import java.util.Locale
import javax.inject.Inject
private const val POS_DONT_CLEAR = 0
private const val POS_HALF_AN_HOUR = 1
private const val POS_AN_HOUR = 2
private const val POS_FOUR_HOURS = 3
private const val POS_TODAY = 4
private const val POS_END_OF_WEEK = 5
private const val ONE_SECOND_IN_MILLIS = 1000
private const val ONE_MINUTE_IN_SECONDS = 60
private const val THIRTY_MINUTES = 30
private const val FOUR_HOURS = 4
private const val LAST_HOUR_OF_DAY = 23
private const val LAST_MINUTE_OF_HOUR = 59
private const val LAST_SECOND_OF_MINUTE = 59
private const val CLEAR_AT_TYPE_PERIOD = "period"
private const val CLEAR_AT_TYPE_END_OF = "end-of"
class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) :
BottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet),
PredefinedStatusClickListener,
Injectable {
private lateinit var binding: SetStatusMessageBottomSheetBinding
private lateinit var accountManager: UserAccountManager
private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
private lateinit var adapter: PredefinedStatusListAdapter
private var selectedPredefinedMessageId: String? = null
private var clearAt: Long? = -1
private lateinit var popup: EmojiPopup
@Inject
lateinit var arbitraryDataProvider: ArbitraryDataProvider
@Inject
lateinit var asyncRunner: AsyncRunner
@Inject
lateinit var viewThemeUtils: ViewThemeUtils
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val json = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.PREDEFINED_STATUS)
if (json.isNotEmpty()) {
val myType = object : TypeToken<ArrayList<PredefinedStatus>>() {}.type
predefinedStatus = Gson().fromJson(json, myType)
}
EmojiManager.install(GoogleEmojiProvider())
}
@SuppressLint("DefaultLocale")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
accountManager = (activity as BaseActivity).userAccountManager
currentStatus?.let {
updateCurrentStatusViews(it)
}
adapter = PredefinedStatusListAdapter(this, requireContext())
if (this::predefinedStatus.isInitialized) {
adapter.list = predefinedStatus
}
binding.predefinedStatusList.adapter = adapter
binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
binding.clearStatus.setOnClickListener { clearStatus() }
binding.setStatus.setOnClickListener { setStatusMessage() }
binding.emoji.setOnClickListener { popup.show() }
popup = EmojiPopup(view, binding.emoji, onEmojiClickListener = { _ ->
popup.dismiss()
binding.emoji.clearFocus()
val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
InputMethodManager
imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
})
binding.emoji.installForceSingleEmoji()
binding.emoji.installDisableKeyboardInput(popup)
val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
adapter.add(getString(R.string.dontClear))
adapter.add(getString(R.string.thirtyMinutes))
adapter.add(getString(R.string.oneHour))
adapter.add(getString(R.string.fourHours))
adapter.add(getString(R.string.today))
adapter.add(getString(R.string.thisWeek))
binding.clearStatusAfterSpinner.apply {
this.adapter = adapter
onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
setClearStatusAfterValue(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// nothing to do
}
}
}
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.clearStatus)
viewThemeUtils.material.colorMaterialButtonPrimaryTonal(binding.setStatus)
viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer)
viewThemeUtils.platform.themeDialog(binding.root)
}
private fun updateCurrentStatusViews(it: Status) {
if (it.icon.isNullOrBlank()) {
binding.emoji.setText("😀")
} else {
binding.emoji.setText(it.icon)
}
binding.customStatusInput.text?.clear()
binding.customStatusInput.setText(it.message)
if (it.clearAt > 0) {
binding.clearStatusAfterSpinner.visibility = View.GONE
binding.remainingClearTime.apply {
binding.clearStatusMessageTextView.text = getString(R.string.clear)
visibility = View.VISIBLE
text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
.toString()
.replaceFirstChar { it.lowercase(Locale.getDefault()) }
setOnClickListener {
visibility = View.GONE
binding.clearStatusAfterSpinner.visibility = View.VISIBLE
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after)
}
}
}
}
private fun setClearStatusAfterValue(item: Int) {
clearAt = when (item) {
POS_DONT_CLEAR -> null // don't clear
POS_HALF_AN_HOUR -> {
// 30 minutes
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
}
POS_AN_HOUR -> {
// one hour
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
}
POS_FOUR_HOURS -> {
// four hours
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS +
FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
}
POS_TODAY -> {
// today
val date = getLastSecondOfToday()
dateToSeconds(date)
}
POS_END_OF_WEEK -> {
// end of week
val date = getLastSecondOfToday()
while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
date.add(Calendar.DAY_OF_YEAR, 1)
}
dateToSeconds(date)
}
else -> clearAt
}
}
private fun clearAtToUnixTime(clearAt: ClearAt?): Long = when {
clearAt?.type == CLEAR_AT_TYPE_PERIOD -> {
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
}
clearAt?.type == CLEAR_AT_TYPE_END_OF && clearAt.time == "day" -> {
val date = getLastSecondOfToday()
dateToSeconds(date)
}
else -> -1
}
private fun getLastSecondOfToday(): Calendar {
val date = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
}
return date
}
private fun dateToSeconds(date: Calendar) = date.timeInMillis / ONE_SECOND_IN_MILLIS
private fun clearStatus() {
asyncRunner.postQuickTask(
ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
{ dismiss(it) }
)
}
private fun setStatusMessage() {
if (selectedPredefinedMessageId != null) {
asyncRunner.postQuickTask(
SetPredefinedCustomStatusTask(
selectedPredefinedMessageId!!,
clearAt,
accountManager.currentOwnCloudAccount?.savedAccount,
context
),
{ dismiss(it) }
)
} else {
asyncRunner.postQuickTask(
SetUserDefinedCustomStatusTask(
binding.customStatusInput.text.toString(),
binding.emoji.text.toString(),
clearAt,
accountManager.currentOwnCloudAccount?.savedAccount,
context
),
{ dismiss(it) }
)
}
}
private fun dismiss(boolean: Boolean) {
if (boolean) {
dismiss()
} else {
DisplayUtils.showSnackMessage(view, view?.resources?.getString(R.string.error_setting_status_message))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = SetStatusMessageBottomSheetBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onClick(predefinedStatus: PredefinedStatus) {
selectedPredefinedMessageId = predefinedStatus.id
clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
binding.emoji.setText(predefinedStatus.icon)
binding.customStatusInput.text?.clear()
binding.customStatusInput.text?.append(predefinedStatus.message)
binding.remainingClearTime.visibility = View.GONE
binding.clearStatusAfterSpinner.visibility = View.VISIBLE
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after)
val clearAt = predefinedStatus.clearAt
if (clearAt == null) {
binding.clearStatusAfterSpinner.setSelection(0)
} else {
when (clearAt.type) {
CLEAR_AT_TYPE_PERIOD -> updateClearAtViewsForPeriod(clearAt)
CLEAR_AT_TYPE_END_OF -> updateClearAtViewsForEndOf(clearAt)
}
}
setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
}
private fun updateClearAtViewsForPeriod(clearAt: ClearAt) {
when (clearAt.time) {
"1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR)
"3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR)
"14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS)
else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
}
}
private fun updateClearAtViewsForEndOf(clearAt: ClearAt) {
when (clearAt.time) {
"day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
"week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
}
}
2025-11-20 16:16:40 +01:00
@SuppressLint("NotifyDataSetChanged")
2025-09-18 18:43:03 +02:00
@VisibleForTesting
fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
adapter.list = predefinedStatus
binding.predefinedStatusList.adapter?.notifyDataSetChanged()
}
}