SetStatusDialogFragment.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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.view.LayoutInflater
  26. import android.view.View
  27. import android.view.ViewGroup
  28. import android.view.inputmethod.InputMethodManager
  29. import android.widget.AdapterView
  30. import android.widget.AdapterView.OnItemSelectedListener
  31. import android.widget.ArrayAdapter
  32. import androidx.annotation.VisibleForTesting
  33. import androidx.appcompat.app.AlertDialog
  34. import androidx.fragment.app.DialogFragment
  35. import androidx.recyclerview.widget.LinearLayoutManager
  36. import com.google.gson.Gson
  37. import com.google.gson.reflect.TypeToken
  38. import com.nextcloud.client.account.User
  39. import com.nextcloud.client.account.UserAccountManager
  40. import com.nextcloud.client.core.AsyncRunner
  41. import com.nextcloud.client.di.Injectable
  42. import com.nextcloud.client.network.ClientFactory
  43. import com.owncloud.android.R
  44. import com.owncloud.android.databinding.DialogSetStatusBinding
  45. import com.owncloud.android.datamodel.ArbitraryDataProvider
  46. import com.owncloud.android.lib.resources.users.ClearAt
  47. import com.owncloud.android.lib.resources.users.PredefinedStatus
  48. import com.owncloud.android.lib.resources.users.Status
  49. import com.owncloud.android.lib.resources.users.StatusType
  50. import com.owncloud.android.ui.activity.BaseActivity
  51. import com.owncloud.android.ui.adapter.PredefinedStatusClickListener
  52. import com.owncloud.android.ui.adapter.PredefinedStatusListAdapter
  53. import com.owncloud.android.utils.DisplayUtils
  54. import com.owncloud.android.utils.theme.ThemeButtonUtils
  55. import com.owncloud.android.utils.theme.ThemeColorUtils
  56. import com.owncloud.android.utils.theme.ThemeTextInputUtils
  57. import com.vanniktech.emoji.EmojiManager
  58. import com.vanniktech.emoji.EmojiPopup
  59. import com.vanniktech.emoji.google.GoogleEmojiProvider
  60. import java.util.Calendar
  61. import java.util.Locale
  62. import javax.inject.Inject
  63. private const val ARG_CURRENT_USER_PARAM = "currentUser"
  64. private const val ARG_CURRENT_STATUS_PARAM = "currentStatus"
  65. private const val POS_DONT_CLEAR = 0
  66. private const val POS_HALF_AN_HOUR = 1
  67. private const val POS_AN_HOUR = 2
  68. private const val POS_FOUR_HOURS = 3
  69. private const val POS_TODAY = 4
  70. private const val POS_END_OF_WEEK = 5
  71. private const val ONE_SECOND_IN_MILLIS = 1000
  72. private const val ONE_MINUTE_IN_SECONDS = 60
  73. private const val THIRTY_MINUTES = 30
  74. private const val FOUR_HOURS = 4
  75. private const val LAST_HOUR_OF_DAY = 23
  76. private const val LAST_MINUTE_OF_HOUR = 59
  77. private const val LAST_SECOND_OF_MINUTE = 59
  78. class SetStatusDialogFragment :
  79. DialogFragment(),
  80. PredefinedStatusClickListener,
  81. Injectable {
  82. private lateinit var binding: DialogSetStatusBinding
  83. private var currentUser: User? = null
  84. private var currentStatus: Status? = null
  85. private lateinit var accountManager: UserAccountManager
  86. private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
  87. private lateinit var adapter: PredefinedStatusListAdapter
  88. private var selectedPredefinedMessageId: String? = null
  89. private var clearAt: Long? = -1
  90. private lateinit var popup: EmojiPopup
  91. @Inject
  92. lateinit var arbitraryDataProvider: ArbitraryDataProvider
  93. @Inject
  94. lateinit var asyncRunner: AsyncRunner
  95. @Inject
  96. lateinit var clientFactory: ClientFactory
  97. @Inject
  98. lateinit var themeColorUtils: ThemeColorUtils
  99. @Inject
  100. lateinit var themeButtonUtils: ThemeButtonUtils
  101. @Inject
  102. lateinit var themeTextInputUtils: ThemeTextInputUtils
  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. return AlertDialog.Builder(requireContext())
  120. .setView(binding.root)
  121. .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. binding.emoji.setText(it.icon)
  129. binding.customStatusInput.text?.clear()
  130. binding.customStatusInput.setText(it.message)
  131. visualizeStatus(it.status)
  132. if (it.clearAt > 0) {
  133. binding.clearStatusAfterSpinner.visibility = View.GONE
  134. binding.remainingClearTime.apply {
  135. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message)
  136. visibility = View.VISIBLE
  137. text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true)
  138. .toString()
  139. .decapitalize(Locale.getDefault())
  140. setOnClickListener {
  141. visibility = View.GONE
  142. binding.clearStatusAfterSpinner.visibility = View.VISIBLE
  143. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
  144. }
  145. }
  146. }
  147. }
  148. adapter = PredefinedStatusListAdapter(this, requireContext())
  149. if (this::predefinedStatus.isInitialized) {
  150. adapter.list = predefinedStatus
  151. }
  152. binding.predefinedStatusList.adapter = adapter
  153. binding.predefinedStatusList.layoutManager = LinearLayoutManager(context)
  154. binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
  155. binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) }
  156. binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) }
  157. binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) }
  158. binding.clearStatus.setOnClickListener { clearStatus() }
  159. binding.setStatus.setOnClickListener { setStatusMessage() }
  160. binding.emoji.setOnClickListener { openEmojiPopup() }
  161. popup = EmojiPopup.Builder
  162. .fromRootView(view)
  163. .setOnEmojiClickListener { _, _ ->
  164. popup.dismiss()
  165. binding.emoji.clearFocus()
  166. val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
  167. InputMethodManager
  168. imm.hideSoftInputFromWindow(binding.emoji.windowToken, 0)
  169. }
  170. .build(binding.emoji)
  171. binding.emoji.disableKeyboardInput(popup)
  172. binding.emoji.forceSingleEmoji()
  173. val adapter = ArrayAdapter<String>(requireContext(), android.R.layout.simple_spinner_item)
  174. adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
  175. adapter.add(getString(R.string.dontClear))
  176. adapter.add(getString(R.string.thirtyMinutes))
  177. adapter.add(getString(R.string.oneHour))
  178. adapter.add(getString(R.string.fourHours))
  179. adapter.add(getString(R.string.today))
  180. adapter.add(getString(R.string.thisWeek))
  181. binding.clearStatusAfterSpinner.apply {
  182. this.adapter = adapter
  183. onItemSelectedListener = object : OnItemSelectedListener {
  184. override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
  185. setClearStatusAfterValue(position)
  186. }
  187. override fun onNothingSelected(parent: AdapterView<*>?) {
  188. // nothing to do
  189. }
  190. }
  191. }
  192. binding.clearStatus.setTextColor(themeColorUtils.primaryColor(context, true))
  193. themeButtonUtils.colorPrimaryButton(binding.setStatus, context, themeColorUtils)
  194. themeTextInputUtils.colorTextInput(
  195. binding.customStatusInputContainer,
  196. binding.customStatusInput,
  197. themeColorUtils.primaryColor(activity)
  198. )
  199. }
  200. @Suppress("ComplexMethod")
  201. private fun setClearStatusAfterValue(item: Int) {
  202. when (item) {
  203. POS_DONT_CLEAR -> {
  204. // don't clear
  205. clearAt = null
  206. }
  207. POS_HALF_AN_HOUR -> {
  208. // 30 minutes
  209. clearAt = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS
  210. }
  211. POS_AN_HOUR -> {
  212. // one hour
  213. clearAt =
  214. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
  215. }
  216. POS_FOUR_HOURS -> {
  217. // four hours
  218. clearAt =
  219. System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
  220. +FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS
  221. }
  222. POS_TODAY -> {
  223. // today
  224. val date = Calendar.getInstance().apply {
  225. set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
  226. set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
  227. set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
  228. }
  229. clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
  230. }
  231. POS_END_OF_WEEK -> {
  232. // end of week
  233. val date = Calendar.getInstance().apply {
  234. set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
  235. set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
  236. set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
  237. }
  238. while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
  239. date.add(Calendar.DAY_OF_YEAR, 1)
  240. }
  241. clearAt = date.timeInMillis / ONE_SECOND_IN_MILLIS
  242. }
  243. }
  244. }
  245. @Suppress("ReturnCount")
  246. private fun clearAtToUnixTime(clearAt: ClearAt?): Long {
  247. if (clearAt != null) {
  248. if (clearAt.type.equals("period")) {
  249. return System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong()
  250. } else if (clearAt.type.equals("end-of")) {
  251. if (clearAt.time.equals("day")) {
  252. val date = Calendar.getInstance().apply {
  253. set(Calendar.HOUR_OF_DAY, LAST_HOUR_OF_DAY)
  254. set(Calendar.MINUTE, LAST_MINUTE_OF_HOUR)
  255. set(Calendar.SECOND, LAST_SECOND_OF_MINUTE)
  256. }
  257. return date.timeInMillis / ONE_SECOND_IN_MILLIS
  258. }
  259. }
  260. }
  261. return -1
  262. }
  263. private fun openEmojiPopup() {
  264. popup.show()
  265. }
  266. private fun clearStatus() {
  267. asyncRunner.postQuickTask(
  268. ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
  269. { dismiss(it) }
  270. )
  271. }
  272. private fun setStatus(statusType: StatusType) {
  273. visualizeStatus(statusType)
  274. asyncRunner.postQuickTask(
  275. SetStatusTask(
  276. statusType,
  277. accountManager.currentOwnCloudAccount?.savedAccount,
  278. context
  279. ),
  280. {
  281. if (!it) {
  282. clearTopStatus()
  283. }
  284. },
  285. { clearTopStatus() }
  286. )
  287. }
  288. private fun visualizeStatus(statusType: StatusType) {
  289. when (statusType) {
  290. StatusType.ONLINE -> {
  291. clearTopStatus()
  292. binding.onlineStatus.setBackgroundColor(themeColorUtils.primaryColor(context))
  293. }
  294. StatusType.AWAY -> {
  295. clearTopStatus()
  296. binding.awayStatus.setBackgroundColor(themeColorUtils.primaryColor(context))
  297. }
  298. StatusType.DND -> {
  299. clearTopStatus()
  300. binding.dndStatus.setBackgroundColor(themeColorUtils.primaryColor(context))
  301. }
  302. StatusType.INVISIBLE -> {
  303. clearTopStatus()
  304. binding.invisibleStatus.setBackgroundColor(themeColorUtils.primaryColor(context))
  305. }
  306. else -> clearTopStatus()
  307. }
  308. }
  309. private fun clearTopStatus() {
  310. context?.let {
  311. val grey = it.resources.getColor(R.color.grey_200)
  312. binding.onlineStatus.setBackgroundColor(grey)
  313. binding.awayStatus.setBackgroundColor(grey)
  314. binding.dndStatus.setBackgroundColor(grey)
  315. binding.invisibleStatus.setBackgroundColor(grey)
  316. }
  317. }
  318. private fun setStatusMessage() {
  319. if (selectedPredefinedMessageId != null) {
  320. asyncRunner.postQuickTask(
  321. SetPredefinedCustomStatusTask(
  322. selectedPredefinedMessageId!!,
  323. clearAt,
  324. accountManager.currentOwnCloudAccount?.savedAccount,
  325. context
  326. ),
  327. { dismiss(it) }
  328. )
  329. } else {
  330. asyncRunner.postQuickTask(
  331. SetUserDefinedCustomStatusTask(
  332. binding.customStatusInput.text.toString(),
  333. binding.emoji.text.toString(),
  334. clearAt,
  335. accountManager.currentOwnCloudAccount?.savedAccount,
  336. context
  337. ),
  338. { dismiss(it) }
  339. )
  340. }
  341. }
  342. private fun dismiss(boolean: Boolean) {
  343. if (boolean) {
  344. dismiss()
  345. }
  346. }
  347. /**
  348. * Fragment creator
  349. */
  350. companion object {
  351. @JvmStatic
  352. fun newInstance(user: User, status: Status?): SetStatusDialogFragment {
  353. val args = Bundle()
  354. args.putParcelable(ARG_CURRENT_USER_PARAM, user)
  355. args.putParcelable(ARG_CURRENT_STATUS_PARAM, status)
  356. val dialogFragment = SetStatusDialogFragment()
  357. dialogFragment.arguments = args
  358. dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog)
  359. return dialogFragment
  360. }
  361. }
  362. override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
  363. return binding.root
  364. }
  365. override fun onClick(predefinedStatus: PredefinedStatus) {
  366. selectedPredefinedMessageId = predefinedStatus.id
  367. clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
  368. binding.emoji.setText(predefinedStatus.icon)
  369. binding.customStatusInput.text?.clear()
  370. binding.customStatusInput.text?.append(predefinedStatus.message)
  371. binding.remainingClearTime.visibility = View.GONE
  372. binding.clearStatusAfterSpinner.visibility = View.VISIBLE
  373. binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
  374. if (predefinedStatus.clearAt == null) {
  375. binding.clearStatusAfterSpinner.setSelection(0)
  376. } else {
  377. val clearAt = predefinedStatus.clearAt!!
  378. if (clearAt.type.equals("period")) {
  379. when (clearAt.time) {
  380. "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR)
  381. "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR)
  382. "14400" -> binding.clearStatusAfterSpinner.setSelection(POS_FOUR_HOURS)
  383. else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
  384. }
  385. } else if (clearAt.type.equals("end-of")) {
  386. when (clearAt.time) {
  387. "day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY)
  388. "week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK)
  389. else -> binding.clearStatusAfterSpinner.setSelection(POS_DONT_CLEAR)
  390. }
  391. }
  392. }
  393. setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition)
  394. }
  395. @VisibleForTesting
  396. fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
  397. adapter.list = predefinedStatus
  398. binding.predefinedStatusList.adapter?.notifyDataSetChanged()
  399. }
  400. }