/* * 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 . */ package com.nextcloud.ui import android.annotation.SuppressLint import android.app.Dialog import android.content.Context import android.os.Bundle import android.util.Log 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 android.widget.ImageView import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.card.MaterialCardView import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.newm3.ViewThemeUtils import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.google.GoogleEmojiProvider 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 private const val CLEAR_AT_TYPE_PERIOD = "period" private const val CLEAR_AT_TYPE_END_OF = "end-of" 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 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 @Inject lateinit var viewThemeUtils: ViewThemeUtils 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>() {}.type predefinedStatus = Gson().fromJson(json, myType) } } EmojiManager.install(GoogleEmojiProvider()) } @SuppressLint("InflateParams") override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { binding = DialogSetStatusBinding.inflate(layoutInflater) val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.statusView.context, builder) return builder.create() } @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.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 { popup.show() } 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(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.colorMaterialButtonText(binding.clearStatus) viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.setStatus) viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer) viewThemeUtils.platform.themeDialog(binding.root) } private fun updateCurrentStatusViews(it: Status) { 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() .replaceFirstChar { it.lowercase(Locale.getDefault()) } setOnClickListener { visibility = View.GONE binding.clearStatusAfterSpinner.visibility = View.VISIBLE binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_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 setStatus(statusType: StatusType) { visualizeStatus(statusType) asyncRunner.postQuickTask( SetStatusTask( statusType, accountManager.currentOwnCloudAccount?.savedAccount, context ), { if (!it) { clearTopStatus() } }, { clearTopStatus() } ) } private fun visualizeStatus(statusType: StatusType) { clearTopStatus() val views: Triple = when (statusType) { StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon) StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon) StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon) StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon) else -> { Log.d(TAG, "unknown status") return } } viewThemeUtils.material.colorCardViewBackground(views.first) viewThemeUtils.platform.colorPrimaryTextViewElement(views.second) } private fun clearTopStatus() { context?.let { val grey = it.resources.getColor(R.color.grey_200) binding.onlineStatus.setCardBackgroundColor(grey) binding.awayStatus.setCardBackgroundColor(grey) binding.dndStatus.setCardBackgroundColor(grey) binding.invisibleStatus.setCardBackgroundColor(grey) binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) binding.onlineIcon.imageTintList = null binding.awayIcon.imageTintList = null binding.dndIcon.imageTintList = null binding.invisibleIcon.imageTintList = null } } 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 { private val TAG = SetStatusDialogFragment::class.simpleName @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 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) 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) } } @VisibleForTesting fun setPredefinedStatus(predefinedStatus: ArrayList) { adapter.list = predefinedStatus binding.predefinedStatusList.adapter?.notifyDataSetChanged() } }