nextcloud-android/src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt

447 lines
17 KiB
Kotlin

/*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2020 Nextcloud GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.ui
import android.annotation.SuppressLint
import android.app.Dialog
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.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.LinearLayoutManager
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.nextcloud.client.network.ClientFactory
import com.owncloud.android.R
import com.owncloud.android.databinding.DialogSetStatusBinding
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.lib.resources.users.StatusType
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.ThemeButtonUtils
import com.owncloud.android.utils.theme.ThemeColorUtils
import com.owncloud.android.utils.theme.ThemeTextInputUtils
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.EmojiPopup
import com.vanniktech.emoji.google.GoogleEmojiProvider
import java.util.ArrayList
import java.util.Calendar
import java.util.Locale
import javax.inject.Inject
private const val ARG_CURRENT_USER_PARAM = "currentUser"
private const val ARG_CURRENT_STATUS_PARAM = "currentStatus"
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
class SetStatusDialogFragment :
DialogFragment(),
PredefinedStatusClickListener,
Injectable {
private lateinit var binding: DialogSetStatusBinding
private var currentUser: User? = null
private var currentStatus: Status? = null
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 clientFactory: ClientFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM)
currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
val json = arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PREDEFINED_STATUS)
if (json.isNotEmpty()) {
val myType = object : TypeToken<ArrayList<PredefinedStatus>>() {}.type
predefinedStatus = Gson().fromJson(json, myType)
}
}
EmojiManager.install(GoogleEmojiProvider())
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogSetStatusBinding.inflate(LayoutInflater.from(context))
return AlertDialog.Builder(requireContext())
.setView(binding.root)
.create()
}
@SuppressLint("DefaultLocale")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
accountManager = (activity as BaseActivity).userAccountManager
currentStatus?.let {
binding.emoji.setText(it.icon)
binding.customStatusInput.text?.clear()
binding.customStatusInput.setText(it.message)
visualizeStatus(it.status)
if (it.clearAt > 0) {
binding.clearStatusAfterSpinner.visibility = View.GONE
binding.remainingClearTime.apply {
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message)
visibility = View.VISIBLE
text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
.toString()
.decapitalize(Locale.getDefault())
setOnClickListener {
visibility = View.GONE
binding.clearStatusAfterSpinner.visibility = View.VISIBLE
binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
}
}
}
}
adapter = PredefinedStatusListAdapter(this, requireContext())
if (this::predefinedStatus.isInitialized) {
adapter.list = predefinedStatus
}
binding.predefinedStatusList.adapter = adapter
binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) }
binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) }
binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) }
binding.clearStatus.setOnClickListener { clearStatus() }
binding.setStatus.setOnClickListener { setStatusMessage() }
binding.emoji.setOnClickListener { openEmojiPopup() }
popup = EmojiPopup.Builder
.fromRootView(view)
.setOnEmojiClickListener { _, _ ->
popup.dismiss()
binding.emoji.clearFocus()
val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
InputMethodManager
imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
}
.build(binding.emoji)
binding.emoji.disableKeyboardInput(popup)
binding.emoji.forceSingleEmoji()
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
}
}
}
binding.clearStatus.setTextColor(ThemeColorUtils.primaryColor(context, true))
ThemeButtonUtils.colorPrimaryButton(binding.setStatus, context)
ThemeTextInputUtils.colorTextInput(
binding.customStatusInputContainer,
binding.customStatusInput,
ThemeColorUtils.primaryColor(activity)
)
}
private fun setClearStatusAfterValue(item: Int) {
when (item) {
POS_DONT_CLEAR -> {
// don't clear
clearAt = null
}
POS_HALF_AN_HOUR -> {
// 30 minutes
clearAt = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
}
POS_AN_HOUR -> {
// one hour
clearAt =
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
}
POS_FOUR_HOURS -> {
// four hours
clearAt =
System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
+FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
}
POS_TODAY -> {
// today
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)
}
clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
}
POS_END_OF_WEEK -> {
// end of week
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)
}
while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
date.add(Calendar.DAY_OF_YEAR, 1)
}
clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
}
}
}
@Suppress("ReturnCount")
private fun clearAtToUnixTime(clearAt: ClearAt?): Long {
if (clearAt != null) {
if (clearAt.type.equals("period")) {
return System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
} else if (clearAt.type.equals("end-of")) {
if (clearAt.time.equals("day")) {
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.timeInMillis / ONE_SECOND_IN_MILLIS
}
}
}
return -1
}
private fun openEmojiPopup() {
popup.show()
}
private fun clearStatus() {
asyncRunner.postQuickTask(
ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
{ dismiss(it) }
)
}
private fun setStatus(statusType: StatusType) {
visualizeStatus(statusType)
asyncRunner.postQuickTask(
SetStatusTask(
statusType,
accountManager.currentOwnCloudAccount?.savedAccount,
context
),
{
if (!it) {
clearTopStatus()
}
},
{ clearTopStatus() }
)
}
private fun visualizeStatus(statusType: StatusType) {
when (statusType) {
StatusType.ONLINE -> {
clearTopStatus()
binding.onlineStatus.setBackgroundColor(ThemeColorUtils.primaryColor(context))
}
StatusType.AWAY -> {
clearTopStatus()
binding.awayStatus.setBackgroundColor(ThemeColorUtils.primaryColor(context))
}
StatusType.DND -> {
clearTopStatus()
binding.dndStatus.setBackgroundColor(ThemeColorUtils.primaryColor(context))
}
StatusType.INVISIBLE -> {
clearTopStatus()
binding.invisibleStatus.setBackgroundColor(ThemeColorUtils.primaryColor(context))
}
else -> clearTopStatus()
}
}
private fun clearTopStatus() {
context?.let {
val grey = it.resources.getColor(R.color.grey_200)
binding.onlineStatus.setBackgroundColor(grey)
binding.awayStatus.setBackgroundColor(grey)
binding.dndStatus.setBackgroundColor(grey)
binding.invisibleStatus.setBackgroundColor(grey)
}
}
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()
}
}
/**
* Fragment creator
*/
companion object {
@JvmStatic
fun newInstance(user: User, status: Status?): SetStatusDialogFragment {
val args = Bundle()
args.putParcelable(ARG_CURRENT_USER_PARAM, user)
args.putParcelable(ARG_CURRENT_STATUS_PARAM, status)
val dialogFragment = SetStatusDialogFragment()
dialogFragment.arguments = args
dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog)
return dialogFragment
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
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_message_after)
if (predefinedStatus.clearAt == null) {
binding.clearStatusAfterSpinner.setSelection(0)
} else {
val clearAt = predefinedStatus.clearAt!!
if (clearAt.type.equals("period")) {
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)
}
} else if (clearAt.type.equals("end-of")) {
when (clearAt.time) {
"day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
"week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
}
}
}
setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
}
@VisibleForTesting
fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
adapter.list = predefinedStatus
binding.predefinedStatusList.adapter?.notifyDataSetChanged()
}
}