SetStatusDialogFragment.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Tobias Kaminsky
  5. * Copyright (C) 2020 Nextcloud GmbH
  6. *
  7. * This program is free software; you can redistribute it and/or
  8. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  9. * License as published by the Free Software Foundation; either
  10. * version 3 of the License, or any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public
  18. * License along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.ui
  21. import android.annotation.SuppressLint
  22. import android.app.Dialog
  23. import android.content.Context
  24. import android.os.Bundle
  25. import android.util.Log
  26. import android.view.LayoutInflater
  27. import android.view.View
  28. import android.view.ViewGroup
  29. import android.view.inputmethod.InputMethodManager
  30. import android.widget.AdapterView
  31. import android.widget.AdapterView.OnItemSelectedListener
  32. import android.widget.ArrayAdapter
  33. import android.widget.ImageView
  34. import android.widget.TextView
  35. import androidx.annotation.VisibleForTesting
  36. import androidx.fragment.app.DialogFragment
  37. import androidx.recyclerview.widget.LinearLayoutManager
  38. import com.google.android.material.card.MaterialCardView
  39. import com.google.android.material.dialog.MaterialAlertDialogBuilder
  40. import com.google.gson.Gson
  41. import com.google.gson.reflect.TypeToken
  42. import com.nextcloud.client.account.User
  43. import com.nextcloud.client.account.UserAccountManager
  44. import com.nextcloud.client.core.AsyncRunner
  45. import com.nextcloud.client.di.Injectable
  46. import com.nextcloud.client.network.ClientFactory
  47. import com.owncloud.android.R
  48. import com.owncloud.android.databinding.DialogSetStatusBinding
  49. import com.owncloud.android.datamodel.ArbitraryDataProvider
  50. import com.owncloud.android.lib.resources.users.ClearAt
  51. import com.owncloud.android.lib.resources.users.PredefinedStatus
  52. import com.owncloud.android.lib.resources.users.Status
  53. import com.owncloud.android.lib.resources.users.StatusType
  54. import com.owncloud.android.ui.activity.BaseActivity
  55. import com.owncloud.android.ui.adapter.PredefinedStatusClickListener
  56. import com.owncloud.android.ui.adapter.PredefinedStatusListAdapter
  57. import com.owncloud.android.utils.DisplayUtils
  58. import com.owncloud.android.utils.theme.newm3.ViewThemeUtils
  59. import com.vanniktech.emoji.EmojiManager
  60. import com.vanniktech.emoji.EmojiPopup
  61. import com.vanniktech.emoji.google.GoogleEmojiProvider
  62. import java.util.Calendar
  63. import java.util.Locale
  64. import javax.inject.Inject
  65. private const val ARG_CURRENT_USER_PARAM = "currentUser"
  66. private const val ARG_CURRENT_STATUS_PARAM = "currentStatus"
  67. private const val POS_DONT_CLEAR = 0
  68. private const val POS_HALF_AN_HOUR = 1
  69. private const val POS_AN_HOUR = 2
  70. private const val POS_FOUR_HOURS = 3
  71. private const val POS_TODAY = 4
  72. private const val POS_END_OF_WEEK = 5
  73. private const val ONE_SECOND_IN_MILLIS = 1000
  74. private const val ONE_MINUTE_IN_SECONDS = 60
  75. private const val THIRTY_MINUTES = 30
  76. private const val FOUR_HOURS = 4
  77. private const val LAST_HOUR_OF_DAY = 23
  78. private const val LAST_MINUTE_OF_HOUR = 59
  79. private const val LAST_SECOND_OF_MINUTE = 59
  80. private const val CLEAR_AT_TYPE_PERIOD = "period"
  81. private const val CLEAR_AT_TYPE_END_OF = "end-of"
  82. class SetStatusDialogFragment :
  83. DialogFragment(),
  84. PredefinedStatusClickListener,
  85. Injectable {
  86. private lateinit var binding: DialogSetStatusBinding
  87. private var currentUser: User? = null
  88. private var currentStatus: Status? = null
  89. private lateinit var accountManager: UserAccountManager
  90. private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
  91. private lateinit var adapter: PredefinedStatusListAdapter
  92. private var selectedPredefinedMessageId: String? = null
  93. private var clearAt: Long? = -1
  94. private lateinit var popup: EmojiPopup
  95. @Inject
  96. lateinit var arbitraryDataProvider: ArbitraryDataProvider
  97. @Inject
  98. lateinit var asyncRunner: AsyncRunner
  99. @Inject
  100. lateinit var clientFactory: ClientFactory
  101. @Inject
  102. lateinit var viewThemeUtils: ViewThemeUtils
  103. override fun onCreate(savedInstanceState: Bundle?) {
  104. super.onCreate(savedInstanceState)
  105. arguments?.let {
  106. currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM)
  107. currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
  108. val json = arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PREDEFINED_STATUS)
  109. if (json.isNotEmpty()) {
  110. val myType = object : TypeToken<ArrayList<PredefinedStatus>>() {}.type
  111. predefinedStatus = Gson().fromJson(json, myType)
  112. }
  113. }
  114. EmojiManager.install(GoogleEmojiProvider())
  115. }
  116. @SuppressLint("InflateParams")
  117. override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
  118. binding = DialogSetStatusBinding.inflate(layoutInflater)
  119. val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
  120. viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.statusView.context, builder)
  121. return builder.create()
  122. }
  123. @SuppressLint("DefaultLocale")
  124. override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  125. super.onViewCreated(view, savedInstanceState)
  126. accountManager = (activity as BaseActivity).userAccountManager
  127. currentStatus?.let {
  128. updateCurrentStatusViews(it)
  129. }
  130. adapter = PredefinedStatusListAdapter(this, requireContext())
  131. if (this::predefinedStatus.isInitialized) {
  132. adapter.list = predefinedStatus
  133. }
  134. binding.predefinedStatusList.adapter = adapter
  135. binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
  136. binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
  137. binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) }
  138. binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) }
  139. binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) }
  140. binding.clearStatus.setOnClickListener { clearStatus() }
  141. binding.setStatus.setOnClickListener { setStatusMessage() }
  142. binding.emoji.setOnClickListener { popup.show() }
  143. popup = EmojiPopup.Builder
  144. .fromRootView(view)
  145. .setOnEmojiClickListener { _, _ ->
  146. popup.dismiss()
  147. binding.emoji.clearFocus()
  148. val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
  149. InputMethodManager
  150. imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
  151. }
  152. .build(binding.emoji)
  153. binding.emoji.disableKeyboardInput(popup)
  154. binding.emoji.forceSingleEmoji()
  155. val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item)
  156. adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
  157. adapter.add(getString(R.string.dontClear))
  158. adapter.add(getString(R.string.thirtyMinutes))
  159. adapter.add(getString(R.string.oneHour))
  160. adapter.add(getString(R.string.fourHours))
  161. adapter.add(getString(R.string.today))
  162. adapter.add(getString(R.string.thisWeek))
  163. binding.clearStatusAfterSpinner.apply {
  164. this.adapter = adapter
  165. onItemSelectedListener = object : OnItemSelectedListener {
  166. override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
  167. setClearStatusAfterValue(position)
  168. }
  169. override fun onNothingSelected(parent: AdapterView<*>?) {
  170. // nothing to do
  171. }
  172. }
  173. }
  174. viewThemeUtils.material.colorMaterialButtonText(binding.clearStatus)
  175. viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.setStatus)
  176. viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer)
  177. viewThemeUtils.platform.themeDialog(binding.root)
  178. }
  179. private fun updateCurrentStatusViews(it: Status) {
  180. binding.emoji.setText(it.icon)
  181. binding.customStatusInput.text?.clear()
  182. binding.customStatusInput.setText(it.message)
  183. visualizeStatus(it.status)
  184. if (it.clearAt > 0) {
  185. binding.clearStatusAfterSpinner.visibility = View.GONE
  186. binding.remainingClearTime.apply {
  187. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message)
  188. visibility = View.VISIBLE
  189. text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
  190. .toString()
  191. .replaceFirstChar { it.lowercase(Locale.getDefault()) }
  192. setOnClickListener {
  193. visibility = View.GONE
  194. binding.clearStatusAfterSpinner.visibility = View.VISIBLE
  195. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
  196. }
  197. }
  198. }
  199. }
  200. private fun setClearStatusAfterValue(item: Int) {
  201. clearAt = when (item) {
  202. POS_DONT_CLEAR -> null // don't clear
  203. POS_HALF_AN_HOUR -> {
  204. // 30 minutes
  205. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
  206. }
  207. POS_AN_HOUR -> {
  208. // one hour
  209. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
  210. }
  211. POS_FOUR_HOURS -> {
  212. // four hours
  213. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS +
  214. FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
  215. }
  216. POS_TODAY -> {
  217. // today
  218. val date = getLastSecondOfToday()
  219. dateToSeconds(date)
  220. }
  221. POS_END_OF_WEEK -> {
  222. // end of week
  223. val date = getLastSecondOfToday()
  224. while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
  225. date.add(Calendar.DAY_OF_YEAR, 1)
  226. }
  227. dateToSeconds(date)
  228. }
  229. else -> clearAt
  230. }
  231. }
  232. private fun clearAtToUnixTime(clearAt: ClearAt?): Long = when {
  233. clearAt?.type == CLEAR_AT_TYPE_PERIOD -> {
  234. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
  235. }
  236. clearAt?.type == CLEAR_AT_TYPE_END_OF && clearAt.time == "day" -> {
  237. val date = getLastSecondOfToday()
  238. dateToSeconds(date)
  239. }
  240. else -> -1
  241. }
  242. private fun getLastSecondOfToday(): Calendar {
  243. val date = Calendar.getInstance().apply {
  244. set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
  245. set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
  246. set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
  247. }
  248. return date
  249. }
  250. private fun dateToSeconds(date: Calendar) = date.timeInMillis / ONE_SECOND_IN_MILLIS
  251. private fun clearStatus() {
  252. asyncRunner.postQuickTask(
  253. ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
  254. { dismiss(it) }
  255. )
  256. }
  257. private fun setStatus(statusType: StatusType) {
  258. visualizeStatus(statusType)
  259. asyncRunner.postQuickTask(
  260. SetStatusTask(
  261. statusType,
  262. accountManager.currentOwnCloudAccount?.savedAccount,
  263. context
  264. ),
  265. {
  266. if (!it) {
  267. clearTopStatus()
  268. }
  269. },
  270. { clearTopStatus() }
  271. )
  272. }
  273. private fun visualizeStatus(statusType: StatusType) {
  274. clearTopStatus()
  275. val views: Triple<MaterialCardView, TextView, ImageView> = when (statusType) {
  276. StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon)
  277. StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon)
  278. StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon)
  279. StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon)
  280. else -> {
  281. Log.d(TAG, "unknown status")
  282. return
  283. }
  284. }
  285. viewThemeUtils.material.colorCardViewBackground(views.first)
  286. viewThemeUtils.platform.colorPrimaryTextViewElement(views.second)
  287. }
  288. private fun clearTopStatus() {
  289. context?.let {
  290. val grey = it.resources.getColor(R.color.grey_200)
  291. binding.onlineStatus.setCardBackgroundColor(grey)
  292. binding.awayStatus.setCardBackgroundColor(grey)
  293. binding.dndStatus.setCardBackgroundColor(grey)
  294. binding.invisibleStatus.setCardBackgroundColor(grey)
  295. binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
  296. binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
  297. binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
  298. binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text))
  299. binding.onlineIcon.imageTintList = null
  300. binding.awayIcon.imageTintList = null
  301. binding.dndIcon.imageTintList = null
  302. binding.invisibleIcon.imageTintList = null
  303. }
  304. }
  305. private fun setStatusMessage() {
  306. if (selectedPredefinedMessageId != null) {
  307. asyncRunner.postQuickTask(
  308. SetPredefinedCustomStatusTask(
  309. selectedPredefinedMessageId!!,
  310. clearAt,
  311. accountManager.currentOwnCloudAccount?.savedAccount,
  312. context
  313. ),
  314. { dismiss(it) }
  315. )
  316. } else {
  317. asyncRunner.postQuickTask(
  318. SetUserDefinedCustomStatusTask(
  319. binding.customStatusInput.text.toString(),
  320. binding.emoji.text.toString(),
  321. clearAt,
  322. accountManager.currentOwnCloudAccount?.savedAccount,
  323. context
  324. ),
  325. { dismiss(it) }
  326. )
  327. }
  328. }
  329. private fun dismiss(boolean: Boolean) {
  330. if (boolean) {
  331. dismiss()
  332. }
  333. }
  334. /**
  335. * Fragment creator
  336. */
  337. companion object {
  338. private val TAG = SetStatusDialogFragment::class.simpleName
  339. @JvmStatic
  340. fun newInstance(user: User, status: Status?): SetStatusDialogFragment {
  341. val args = Bundle()
  342. args.putParcelable(ARG_CURRENT_USER_PARAM, user)
  343. args.putParcelable(ARG_CURRENT_STATUS_PARAM, status)
  344. val dialogFragment = SetStatusDialogFragment()
  345. dialogFragment.arguments = args
  346. return dialogFragment
  347. }
  348. }
  349. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
  350. return binding.root
  351. }
  352. override fun onClick(predefinedStatus: PredefinedStatus) {
  353. selectedPredefinedMessageId = predefinedStatus.id
  354. clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
  355. binding.emoji.setText(predefinedStatus.icon)
  356. binding.customStatusInput.text?.clear()
  357. binding.customStatusInput.text?.append(predefinedStatus.message)
  358. binding.remainingClearTime.visibility = View.GONE
  359. binding.clearStatusAfterSpinner.visibility = View.VISIBLE
  360. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
  361. val clearAt = predefinedStatus.clearAt
  362. if (clearAt == null) {
  363. binding.clearStatusAfterSpinner.setSelection(0)
  364. } else {
  365. when (clearAt.type) {
  366. CLEAR_AT_TYPE_PERIOD -> updateClearAtViewsForPeriod(clearAt)
  367. CLEAR_AT_TYPE_END_OF -> updateClearAtViewsForEndOf(clearAt)
  368. }
  369. }
  370. setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
  371. }
  372. private fun updateClearAtViewsForPeriod(clearAt: ClearAt) {
  373. when (clearAt.time) {
  374. "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR)
  375. "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR)
  376. "14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS)
  377. else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
  378. }
  379. }
  380. private fun updateClearAtViewsForEndOf(clearAt: ClearAt) {
  381. when (clearAt.time) {
  382. "day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
  383. "week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
  384. else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
  385. }
  386. }
  387. @VisibleForTesting
  388. fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
  389. adapter.list = predefinedStatus
  390. binding.predefinedStatusList.adapter?.notifyDataSetChanged()
  391. }
  392. }