From ccd89d3a304704a30630b61d207e67326c3680db Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 22 Oct 2025 12:18:04 +0200 Subject: [PATCH 01/15] feat: status choosing Signed-off-by: alperozturk --- app/build.gradle | 3 + .../AccountSwitcherDialog.java | 15 + .../SetStatusMessageBottomSheet.kt | 339 ++++++++++++++++++ .../adapter/PredefinedStatusClickListener.kt | 14 + .../adapter/PredefinedStatusListAdapter.kt | 31 ++ .../adapter/PredefinedStatusViewHolder.kt | 44 +++ .../accountswitcher/task/ClearStatusTask.kt | 27 ++ .../task/SetPredefinedCustomStatusTask.kt | 32 ++ .../accountswitcher/task/SetStatusTask.kt | 28 ++ .../task/SetUserDefinedCustomStatusTask.kt | 35 ++ .../BrandedBottomSheetDialogFragment.kt | 21 ++ .../notes/shared/util/DisplayUtils.java | 52 +++ .../owncloud/notes/util/runner/AsyncRunner.kt | 60 ++++ .../notes/util/runner/ManualAsyncRunner.kt | 86 +++++ .../owncloud/notes/util/runner/Task.kt | 59 +++ .../util/runner/ThreadPoolAsyncRunner.kt | 59 +++ .../notes/util/storage/UserStorage.kt | 26 ++ app/src/main/res/drawable/borderless_btn.xml | 14 + app/src/main/res/drawable/chat_bubble.xml | 16 + app/src/main/res/drawable/ic_check_circle.xml | 18 + .../res/layout/dialog_account_switcher.xml | 51 +++ app/src/main/res/layout/predefined_status.xml | 49 +++ .../set_status_message_bottom_sheet.xml | 149 ++++++++ app/src/main/res/values-night/colors.xml | 2 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 19 + app/src/main/res/values/styles.xml | 27 ++ 28 files changed, 1282 insertions(+) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialogFragment.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt create mode 100644 app/src/main/res/drawable/borderless_btn.xml create mode 100644 app/src/main/res/drawable/chat_bubble.xml create mode 100644 app/src/main/res/drawable/ic_check_circle.xml create mode 100644 app/src/main/res/layout/predefined_status.xml create mode 100644 app/src/main/res/layout/set_status_message_bottom_sheet.xml diff --git a/app/build.gradle b/app/build.gradle index 658288fbd..495520a0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,6 +152,9 @@ dependencies { implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.work:work-runtime:2.10.5' implementation 'com.google.android.material:material:1.13.0' + + // Vanitech + implementation 'com.vanniktech:emoji-google:0.21.0' // Database implementation "androidx.room:room-runtime:${roomVersion}" diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index c11b8d311..b2f89abf8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -14,6 +14,7 @@ import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; +import android.view.View; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; @@ -75,6 +76,20 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost()); AvatarLoader.INSTANCE.load(requireContext(), binding.currentAccountItemAvatar, currentLocalAccount); binding.accountLayout.setOnClickListener((v) -> dismiss()); + binding.onlineStatus.setOnClickListener(v -> { + /* + val setStatusDialog = SetOnlineStatusBottomSheet(currentStatus) + setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status") + */ + + dismiss(); + }); + + binding.statusMessage.setOnClickListener(v -> { + final var setStatusMessageDialog = new SetStatusMessageBottomSheet(accountManager.user, currentStatus); + setStatusMessageDialog.show(requireActivity().getSupportFragmentManager(), "fragment_set_status_message"); + dismiss(); + }); final var adapter = new AccountSwitcherAdapter((localAccount -> { accountSwitcherListener.onAccountChosen(localAccount); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt new file mode 100644 index 000000000..cb29b9173 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt @@ -0,0 +1,339 @@ +/* + * 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 it.niedermann.owncloud.notes.accountswitcher + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +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.nextcloud.common.User +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.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 it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener +import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter +import it.niedermann.owncloud.notes.accountswitcher.task.ClearStatusTask +import it.niedermann.owncloud.notes.accountswitcher.task.SetPredefinedCustomStatusTask +import it.niedermann.owncloud.notes.accountswitcher.task.SetUserDefinedCustomStatusTask +import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.SetStatusMessageBottomSheetBinding +import it.niedermann.owncloud.notes.shared.util.DisplayUtils +import it.niedermann.owncloud.notes.util.runner.AsyncRunner +import it.niedermann.owncloud.notes.util.runner.ThreadPoolAsyncRunner +import it.niedermann.owncloud.notes.util.storage.UserStorage +import java.util.Calendar +import java.util.Locale + +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?) : + BrandedBottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet), + PredefinedStatusClickListener, + Injectable { + + private lateinit var binding: SetStatusMessageBottomSheetBinding + + 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 + + lateinit var asyncRunner: AsyncRunner + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val uiHandler = Handler(Looper.getMainLooper()) + asyncRunner = ThreadPoolAsyncRunner(uiHandler, 4, "io") + predefinedStatus = UserStorage.readPredefinedStatus(requireContext()) + 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(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 + onItemClickListener = object : OnItemSelectedListener, AdapterView.OnItemClickListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + setClearStatusAfterValue(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + + override fun onItemClick( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) = Unit + } + } + } + + override fun applyBrand(color: Int) { + val viewThemeUtils = BrandingUtil.of(color, requireContext()) + 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) + } + } + + @VisibleForTesting + fun setPredefinedStatus(predefinedStatus: ArrayList) { + adapter.list = predefinedStatus + binding.predefinedStatusList.adapter?.notifyDataSetChanged() + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt new file mode 100644 index 000000000..b4de418b5 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.adapter + +import com.owncloud.android.lib.resources.users.PredefinedStatus + +interface PredefinedStatusClickListener { + fun onClick(predefinedStatus: PredefinedStatus) +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt new file mode 100644 index 000000000..815708a31 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.lib.resources.users.PredefinedStatus +import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding + +class PredefinedStatusListAdapter(private val clickListener: PredefinedStatusClickListener, val context: Context) : + RecyclerView.Adapter() { + internal var list: List = emptyList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder { + val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PredefinedStatusViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: PredefinedStatusViewHolder, position: Int) { + holder.bind(list[position], clickListener, context) + } + + override fun getItemCount(): Int = list.size +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt new file mode 100644 index 000000000..9cb524f3b --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.adapter + +import android.content.Context +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.lib.resources.users.PredefinedStatus +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding +import it.niedermann.owncloud.notes.shared.util.DisplayUtils + +private const val ONE_SECOND_IN_MILLIS = 1000 + +class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) { + binding.root.setOnClickListener { clickListener.onClick(status) } + binding.icon.text = status.icon + binding.name.text = status.message + + if (status.clearAt == null) { + binding.clearAt.text = context.getString(R.string.dontClear) + } else { + val clearAt = status.clearAt!! + if (clearAt.type == "period") { + binding.clearAt.text = DisplayUtils.getRelativeTimestamp( + context, + System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS, + true + ) + } else { + // end-of + if (clearAt.time == "day") { + binding.clearAt.text = context.getString(R.string.today) + } + } + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt new file mode 100644 index 000000000..1fb9ec9d6 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.task + +import android.accounts.Account +import android.content.Context +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.ClearStatusMessageRemoteOperation + +class ClearStatusTask(val account: Account?, val context: Context?) : Function0 { + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) + + ClearStatusMessageRemoteOperation().execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error clearing status", e) + + false + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt new file mode 100644 index 000000000..13f8c1da0 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.task + +import android.accounts.Account +import android.content.Context +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.SetPredefinedCustomStatusMessageRemoteOperation + +class SetPredefinedCustomStatusTask( + val messageId: String, + val clearAt: Long?, + val account: Account?, + val context: Context? +) : Function0 { + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) + + SetPredefinedCustomStatusMessageRemoteOperation(messageId, clearAt).execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error setting predefined status", e) + + false + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt new file mode 100644 index 000000000..12b8b1459 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.task + +import android.accounts.Account +import android.content.Context +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.SetStatusRemoteOperation +import com.owncloud.android.lib.resources.users.StatusType + +class SetStatusTask(val statusType: StatusType, val account: Account?, val context: Context?) : Function0 { + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) + + SetStatusRemoteOperation(statusType).execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error setting status", e) + + false + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt new file mode 100644 index 000000000..fb3d1d7c3 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.accountswitcher.task + +import android.accounts.Account +import android.content.Context +import com.owncloud.android.lib.common.OwnCloudClientFactory +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.SetUserDefinedCustomStatusMessageRemoteOperation + +class SetUserDefinedCustomStatusTask( + val message: String, + val icon: String, + val clearAt: Long?, + val account: Account?, + val context: Context? +) : Function0 { + override fun invoke(): Boolean { + return try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) + + return SetUserDefinedCustomStatusMessageRemoteOperation(message, icon, clearAt).execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error setting user defined custom status", e) + + false + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialogFragment.kt b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialogFragment.kt new file mode 100644 index 000000000..21e16e2a7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/branding/BrandedBottomSheetDialogFragment.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.branding + +import androidx.annotation.ColorInt +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +abstract class BrandedBottomSheetDialogFragment(contentLayoutId: Int) : + BottomSheetDialogFragment(contentLayoutId), Branded { + + override fun onStart() { + super.onStart() + + @ColorInt val color = BrandingUtil.readBrandMainColor(requireContext()) + applyBrand(color) + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java index 1790add08..47b332c51 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/DisplayUtils.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Build; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.TypedValue; import android.view.View; import android.view.WindowInsets; @@ -29,7 +30,9 @@ import com.google.android.material.snackbar.Snackbar; +import java.text.DateFormat; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; @@ -43,6 +46,8 @@ public class DisplayUtils { + private static final int DATE_TIME_PARTS_SIZE = 2; + private static final Map> SPECIAL_CATEGORY_REPLACEMENTS = Map.of( R.drawable.selector_music, singletonList(R.string.category_music), R.drawable.selector_movies, asList(R.string.category_movies, R.string.category_movie), @@ -189,4 +194,51 @@ public static Snackbar showSnackMessage(View view, String message) { return snackbar; } + public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) { + return getRelativeDateTimeString(context, + modificationTimestamp, + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + showFuture); + } + + public static CharSequence getRelativeDateTimeString(Context c, + long time, + long minResolution, + long transitionResolution, + int flags, + boolean showFuture) { + + + // in Future + if (!showFuture && time > System.currentTimeMillis()) { + return DisplayUtils.unixTimeToHumanReadable(time); + } + // < 60 seconds -> seconds ago + long diff = System.currentTimeMillis() - time; + if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) { + return c.getString(R.string.file_list_seconds_ago); + } else { + CharSequence dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags); + + String[] parts = dateString.toString().split(","); + if (parts.length == DATE_TIME_PARTS_SIZE) { + if (parts[1].contains(":") && !parts[0].contains(":")) { + return parts[0]; + } else if (parts[0].contains(":") && !parts[1].contains(":")) { + return parts[1]; + } + } + // dateString contains unexpected format. fallback: use relative date time string from android api as is. + return dateString.toString(); + } + } + + public static String unixTimeToHumanReadable(long milliseconds) { + Date date = new Date(milliseconds); + DateFormat df = DateFormat.getDateTimeInstance(); + return df.format(date); + } + } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt new file mode 100644 index 000000000..005b5b01d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt @@ -0,0 +1,60 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.util.runner + +import io.reactivex.functions.Cancellable + +typealias OnResultCallback = (result: T) -> Unit +typealias OnErrorCallback = (error: Throwable) -> Unit +typealias OnProgressCallback

= (progress: P) -> Unit +typealias IsCancelled = () -> Boolean +typealias TaskFunction = ( + onProgress: OnProgressCallback, + isCancelled: IsCancelled +) -> RESULT + +/** + * This interface allows to post background tasks that report results via callbacks invoked on main thread. + * It is provided as an alternative for heavy, platform specific and virtually untestable [android.os.AsyncTask] + * + * Please note that as of Android R, [android.os.AsyncTask] is deprecated and [java.util.concurrent] is a recommended + * alternative. + */ +interface AsyncRunner { + + /** + * Post a quick background task and return immediately returning task cancellation interface. + * + * Quick task is a short piece of code that does not support interruption nor progress monitoring. + * + * @param task Task function returning result T; error shall be signalled by throwing an exception. + * @param onResult Callback called when task function returns a result. + * @param onError Callback called when task function throws an exception. + * @return Cancellable interface, allowing cancellation of a running task. + */ + fun postQuickTask( + task: () -> RESULT, + onResult: OnResultCallback? = null, + onError: OnErrorCallback? = null + ): Cancellable + + /** + * Post a background task and return immediately returning task cancellation interface. + * + * @param task Task function returning result T; error shall be signalled by throwing an exception. + * @param onResult Callback called when task function returns a result, + * @param onError Callback called when task function throws an exception. + * @param onProgress Callback called when task function reports progress update. + * @return Cancellable interface, allowing cancellation of a running task. + */ + fun postTask( + task: TaskFunction, + onResult: OnResultCallback? = null, + onError: OnErrorCallback? = null, + onProgress: OnProgressCallback? = null + ): Cancellable +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt new file mode 100644 index 000000000..355ef3ade --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt @@ -0,0 +1,86 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.util.runner + +import io.reactivex.functions.Cancellable +import java.util.ArrayDeque + +/** + * This async runner is suitable for tests, where manual simulation of + * asynchronous operations is desirable. + */ +class ManualAsyncRunner : AsyncRunner { + + private val queue: ArrayDeque = ArrayDeque() + + override fun postQuickTask( + task: () -> T, + onResult: OnResultCallback?, + onError: OnErrorCallback? + ): Cancellable = postTask( + task = { _: OnProgressCallback, _: IsCancelled -> task.invoke() }, + onResult = onResult, + onError = onError, + onProgress = null + ) + + override fun postTask( + task: TaskFunction, + onResult: OnResultCallback?, + onError: OnErrorCallback?, + onProgress: OnProgressCallback

? + ): Cancellable { + val remove: Function1 = queue::remove + val taskWrapper = Task( + postResult = { + it.run() + true + }, + removeFromQueue = remove, + taskBody = task, + onSuccess = onResult, + onError = onError, + onProgress = onProgress + ) + queue.push(taskWrapper) + return taskWrapper + } + + val size: Int get() = queue.size + val isEmpty: Boolean get() = queue.size == 0 + + /** + * Run all enqueued tasks until queue is empty. This will run also tasks + * enqueued by task callbacks. + * + * @param maximum max number of tasks to run to avoid infinite loopss + * @return number of executed tasks + */ + fun runAll(maximum: Int = 100): Int { + var c = 0 + while (queue.size > 0) { + val t = queue.remove() + t.run() + c++ + if (c > maximum) { + throw IllegalStateException("Maximum number of tasks run. Are you in infinite loop?") + } + } + return c + } + + /** + * Run one pending task + * + * @return true if task has been run + */ + fun runOne(): Boolean { + val t = queue.pollFirst() + t?.run() + return t != null + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt new file mode 100644 index 000000000..56b686b79 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.util.runner + +import io.reactivex.functions.Cancellable +import java.util.concurrent.atomic.AtomicBoolean + +/** + * This is a wrapper for a task function running in background. + * It executes task function and handles result or error delivery. + */ +@Suppress("LongParameterList") +internal class Task( + private val postResult: (Runnable) -> Boolean, + private val removeFromQueue: (Runnable) -> Boolean, + private val taskBody: TaskFunction, + private val onSuccess: OnResultCallback?, + private val onError: OnErrorCallback?, + private val onProgress: OnProgressCallback

? +) : Runnable, + Cancellable { + + val isCancelled: Boolean + get() = cancelled.get() + + private val cancelled = AtomicBoolean(false) + + private fun postProgress(p: P) { + postResult(Runnable { onProgress?.invoke(p) }) + } + + @Suppress("TooGenericExceptionCaught") // this is exactly what we want here + override fun run() { + try { + val result = taskBody.invoke({ postProgress(it) }, this::isCancelled) + if (!cancelled.get()) { + postResult.invoke( + Runnable { + onSuccess?.invoke(result) + } + ) + } + } catch (t: Throwable) { + if (!cancelled.get()) { + postResult(Runnable { onError?.invoke(t) }) + } + } + removeFromQueue(this) + } + + override fun cancel() { + cancelled.set(true) + removeFromQueue(this) + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt new file mode 100644 index 000000000..f55e11230 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package it.niedermann.owncloud.notes.util.runner + +import android.os.Handler +import io.reactivex.functions.Cancellable +import java.util.concurrent.ScheduledThreadPoolExecutor + +/** + * This async runner uses [ScheduledThreadPoolExecutor] to run tasks + * asynchronously. + * + * Tasks are run on multi-threaded pool. If serialized execution is desired, set [corePoolSize] to 1. + */ +internal class ThreadPoolAsyncRunner( + private val uiThreadHandler: Handler, + corePoolSize: Int, + val tag: String = "default" +) : AsyncRunner { + + private val executor = ScheduledThreadPoolExecutor(corePoolSize) + + override fun postQuickTask( + task: () -> T, + onResult: OnResultCallback?, + onError: OnErrorCallback? + ): Cancellable { + val taskAdapter = { _: OnProgressCallback, _: IsCancelled -> task.invoke() } + return postTask( + taskAdapter, + onResult, + onError, + null + ) + } + + override fun postTask( + task: TaskFunction, + onResult: OnResultCallback?, + onError: OnErrorCallback?, + onProgress: OnProgressCallback

? + ): Cancellable { + val remove: Function1 = executor::remove + val taskWrapper = Task( + postResult = uiThreadHandler::post, + removeFromQueue = remove, + taskBody = task, + onSuccess = onResult, + onError = onError, + onProgress = onProgress + ) + executor.execute(taskWrapper) + return taskWrapper + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt new file mode 100644 index 000000000..23aa19cb7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt @@ -0,0 +1,26 @@ +package it.niedermann.owncloud.notes.util.storage + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.owncloud.android.lib.resources.users.PredefinedStatus + +object UserStorage { + private const val APP = "NextcloudNotes" + private const val PREDEFINED_STATUS = "PREDEFINED_STATUS" + + private fun getSharedPreferences(context: Context): SharedPreferences { + return context.getSharedPreferences(APP, Context.MODE_PRIVATE) + } + + fun readPredefinedStatus(context: Context): ArrayList { + val json = getSharedPreferences(context).getString(PREDEFINED_STATUS, null) + if (json == null) { + return arrayListOf() + } + + val type = object : TypeToken>() {}.type + return Gson().fromJson(json, type) + } +} diff --git a/app/src/main/res/drawable/borderless_btn.xml b/app/src/main/res/drawable/borderless_btn.xml new file mode 100644 index 000000000..b56094c0d --- /dev/null +++ b/app/src/main/res/drawable/borderless_btn.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_bubble.xml b/app/src/main/res/drawable/chat_bubble.xml new file mode 100644 index 000000000..23e3c9c37 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..1c7459e3b --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml index 76b3a9a6d..08c293542 100644 --- a/app/src/main/res/layout/dialog_account_switcher.xml +++ b/app/src/main/res/layout/dialog_account_switcher.xml @@ -70,6 +70,57 @@ app:srcCompat="@drawable/check" /> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/set_status_message_bottom_sheet.xml b/app/src/main/res/layout/set_status_message_bottom_sheet.xml new file mode 100644 index 000000000..247717c1d --- /dev/null +++ b/app/src/main/res/layout/set_status_message_bottom_sheet.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 34d35a722..fd7704512 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -40,4 +40,6 @@ #ffffff #1E1E1E + @android:color/white + #000000 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c0c740047..c7356558d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -49,4 +49,9 @@ #222222 #ededed + #666666 + #00000000 + #D6D7D7 + #000000 + #ffffff diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 943dce10e..e1c075414 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -72,4 +72,5 @@ 48dp 56dp + 60dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 919f5fe49..5cf5c63b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -509,4 +509,23 @@ Switch to plain editing Back Mozilla/5.0 (Android) %1$s-android/%2$s + Online status + Status message + ๐Ÿ˜ƒ + What is your status? + ๐Ÿ“† + In a meeting + an hour + โ€” + Clear status after + Clear + Set message + Don\'t clear + Today + 30 minutes + 1 hour + 4 hours + This week + Error setting status message! + seconds ago diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b84323cdc..66c2c59d8 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -124,4 +124,31 @@ tools:ignore="ResourceCycle"> @layout/preference_switch + + + + + + + + \ No newline at end of file From d765f03c65772d7ae498cb5dc28c192e8bcbfd11 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 22 Oct 2025 15:39:56 +0200 Subject: [PATCH 02/15] add user status repo Signed-off-by: alperozturk --- .../SetStatusMessageBottomSheet.kt | 14 ++++- .../repository/UserStatusRepository.kt | 63 +++++++++++++++++++ .../notes/persistence/sync/OcsAPI.java | 17 +++++ .../notes/util/storage/UserStorage.kt | 4 +- 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt index cb29b9173..8a5624fe1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt @@ -22,6 +22,7 @@ import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import androidx.annotation.VisibleForTesting +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.nextcloud.common.User import com.owncloud.android.lib.resources.users.ClearAt @@ -35,6 +36,7 @@ import com.vanniktech.emoji.installForceSingleEmoji import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter +import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository import it.niedermann.owncloud.notes.accountswitcher.task.ClearStatusTask import it.niedermann.owncloud.notes.accountswitcher.task.SetPredefinedCustomStatusTask import it.niedermann.owncloud.notes.accountswitcher.task.SetUserDefinedCustomStatusTask @@ -45,6 +47,8 @@ import it.niedermann.owncloud.notes.shared.util.DisplayUtils import it.niedermann.owncloud.notes.util.runner.AsyncRunner import it.niedermann.owncloud.notes.util.runner.ThreadPoolAsyncRunner import it.niedermann.owncloud.notes.util.storage.UserStorage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.Calendar import java.util.Locale @@ -68,12 +72,11 @@ private const val CLEAR_AT_TYPE_END_OF = "end-of" class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : BrandedBottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet), - PredefinedStatusClickListener, - Injectable { + PredefinedStatusClickListener{ private lateinit var binding: SetStatusMessageBottomSheetBinding - private lateinit var accountManager: UserAccountManager + private lateinit var repository: UserStatusRepository private lateinit var predefinedStatus: ArrayList private lateinit var adapter: PredefinedStatusListAdapter private var selectedPredefinedMessageId: String? = null @@ -88,6 +91,11 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : val uiHandler = Handler(Looper.getMainLooper()) asyncRunner = ThreadPoolAsyncRunner(uiHandler, 4, "io") predefinedStatus = UserStorage.readPredefinedStatus(requireContext()) + + lifecycleScope.launch(Dispatchers.IO) { + + } + repository = UserStatusRepository(requireContext()) EmojiManager.install(GoogleEmojiProvider()) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt new file mode 100644 index 000000000..2328ea732 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -0,0 +1,63 @@ +package it.niedermann.owncloud.notes.accountswitcher.repository + +import android.content.Context +import android.util.Log +import androidx.annotation.WorkerThread +import com.nextcloud.android.sso.model.SingleSignOnAccount +import it.niedermann.owncloud.notes.persistence.ApiProvider +import it.niedermann.owncloud.notes.shared.model.OcsResponse +import retrofit2.Response +import kotlin.getValue + +class UserStatusRepository( + private val context: Context, + private val ssoAccount: SingleSignOnAccount +) { + companion object { + private const val TAG = "UserStatusRepository" + } + + private val ocsAPI by lazy { ApiProvider.getInstance().getOcsAPI(context, ssoAccount) } + + @WorkerThread + fun clearStatus(): Boolean { + return try { + val response: Response> = ocsAPI.clearStatusMessage().execute() + response.isSuccessful + } catch (t: Throwable) { + Log.e(TAG, "Clearing status failed", t) + false + } + } + + @WorkerThread + fun setPredefinedStatus(messageId: String, clearAt: Long? = null): Boolean { + val body = mutableMapOf("messageId" to messageId) + clearAt?.let { body["clearAt"] = it.toString() } + + return try { + val response: Response> = ocsAPI.setPredefinedStatusMessage(body).execute() + response.isSuccessful + } catch (t: Throwable) { + Log.e(TAG, "Setting predefined status failed", t) + false + } + } + + @WorkerThread + fun setCustomStatus(message: String, statusIcon: String, clearAt: Long? = null): Boolean { + val body = mutableMapOf( + "message" to message, + "statusIcon" to statusIcon + ) + clearAt?.let { body["clearAt"] = it.toString() } + + return try { + val response: Response> = ocsAPI.setUserDefinedStatusMessage(body).execute() + response.isSuccessful + } catch (t: Throwable) { + Log.e(TAG, "Setting custom status failed", t) + false + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java index b6304cb2e..a98313a54 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java @@ -8,13 +8,19 @@ import com.nextcloud.android.sso.api.ParsedResponse; +import java.util.Map; + import io.reactivex.Observable; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.OcsResponse; import it.niedermann.owncloud.notes.shared.model.OcsUser; import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.PUT; import retrofit2.http.Path; /** @@ -27,4 +33,15 @@ public interface OcsAPI { @GET("users/{userId}?format=json") Call> getUser(@Path("userId") String userId); + + @DELETE("apps/user_status/api/v1/user_status/message?format=json") + Call> clearStatusMessage(); + + @PUT("apps/user_status/api/v1/user_status/message/predefined?format=json") + @Headers("Content-Type: application/json") + Call> setPredefinedStatusMessage(@Body Map body); + + @PUT("apps/user_status/api/v1/user_status/message/custom?format=json") + @Headers("Content-Type: application/json") + Call> setUserDefinedStatusMessage(@Body Map body); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt index 23aa19cb7..f602811d9 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt @@ -2,16 +2,16 @@ package it.niedermann.owncloud.notes.util.storage import android.content.Context import android.content.SharedPreferences +import androidx.preference.PreferenceManager import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.owncloud.android.lib.resources.users.PredefinedStatus object UserStorage { - private const val APP = "NextcloudNotes" private const val PREDEFINED_STATUS = "PREDEFINED_STATUS" private fun getSharedPreferences(context: Context): SharedPreferences { - return context.getSharedPreferences(APP, Context.MODE_PRIVATE) + return PreferenceManager.getDefaultSharedPreferences(context) } fun readPredefinedStatus(context: Context): ArrayList { From 7c7f464abc534b6024a90516908e1d01f2872413 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 11:30:40 +0200 Subject: [PATCH 03/15] add SetStatusMessageBottomSheet Signed-off-by: alperozturk --- .../AccountSwitcherDialog.java | 3 +- .../SetStatusMessageBottomSheet.kt | 183 +++++++++--------- .../adapter/PredefinedStatusClickListener.kt | 4 +- .../adapter/PredefinedStatusListAdapter.kt | 4 +- .../adapter/PredefinedStatusViewHolder.kt | 8 +- .../model/ExposedPredefinedStatus.kt | 32 +++ .../repository/UserStatusRepository.kt | 124 ++++++++---- .../accountswitcher/task/ClearStatusTask.kt | 27 --- .../task/SetPredefinedCustomStatusTask.kt | 32 --- .../accountswitcher/task/SetStatusTask.kt | 28 --- .../task/SetUserDefinedCustomStatusTask.kt | 35 ---- .../notes/persistence/ApiProvider.java | 17 +- .../notes/persistence/sync/OcsAPI.java | 17 -- .../notes/persistence/sync/UserStatusAPI.kt | 31 +++ .../notes/util/storage/UserStorage.kt | 8 + .../res/layout/dialog_account_switcher.xml | 2 +- 16 files changed, 278 insertions(+), 277 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index b2f89abf8..f51a35df5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -14,7 +14,6 @@ import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; -import android.view.View; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; @@ -86,7 +85,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { }); binding.statusMessage.setOnClickListener(v -> { - final var setStatusMessageDialog = new SetStatusMessageBottomSheet(accountManager.user, currentStatus); + final var setStatusMessageDialog = new SetStatusMessageBottomSheet(); setStatusMessageDialog.show(requireActivity().getSupportFragmentManager(), "fragment_set_status_message"); dismiss(); }); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt index 8a5624fe1..b9e980c64 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt @@ -12,8 +12,6 @@ package it.niedermann.owncloud.notes.accountswitcher import android.annotation.SuppressLint import android.content.Context import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -21,13 +19,12 @@ import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter -import androidx.annotation.VisibleForTesting import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.nextcloud.common.User -import com.owncloud.android.lib.resources.users.ClearAt -import com.owncloud.android.lib.resources.users.PredefinedStatus +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.google.GoogleEmojiProvider @@ -36,19 +33,16 @@ import com.vanniktech.emoji.installForceSingleEmoji import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedClearAt +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository -import it.niedermann.owncloud.notes.accountswitcher.task.ClearStatusTask -import it.niedermann.owncloud.notes.accountswitcher.task.SetPredefinedCustomStatusTask -import it.niedermann.owncloud.notes.accountswitcher.task.SetUserDefinedCustomStatusTask import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.SetStatusMessageBottomSheetBinding import it.niedermann.owncloud.notes.shared.util.DisplayUtils -import it.niedermann.owncloud.notes.util.runner.AsyncRunner -import it.niedermann.owncloud.notes.util.runner.ThreadPoolAsyncRunner -import it.niedermann.owncloud.notes.util.storage.UserStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Calendar import java.util.Locale @@ -70,50 +64,53 @@ 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?) : +class SetStatusMessageBottomSheet : BrandedBottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet), - PredefinedStatusClickListener{ + PredefinedStatusClickListener { + companion object { + private const val TAG = "SetStatusMessageBottomSheet" + } private lateinit var binding: SetStatusMessageBottomSheetBinding - private lateinit var repository: UserStatusRepository - private lateinit var predefinedStatus: ArrayList + private var repository: UserStatusRepository? = null private lateinit var adapter: PredefinedStatusListAdapter private var selectedPredefinedMessageId: String? = null private var clearAt: Long? = -1 private lateinit var popup: EmojiPopup - lateinit var asyncRunner: AsyncRunner - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - val uiHandler = Handler(Looper.getMainLooper()) - asyncRunner = ThreadPoolAsyncRunner(uiHandler, 4, "io") - predefinedStatus = UserStorage.readPredefinedStatus(requireContext()) - - lifecycleScope.launch(Dispatchers.IO) { - - } - repository = UserStatusRepository(requireContext()) 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) + private fun initRepository() { + lifecycleScope.launch(Dispatchers.IO) { + val ssoAccount = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) ?: return@launch + repository = UserStatusRepository(requireContext(), ssoAccount) + val predefinedStatus = repository?.fetchPredefinedStatuses() ?: arrayListOf() + val currentStatus = repository?.fetchUserStatus() ?: Status(StatusType.OFFLINE, "", "", -1) + + withContext(Dispatchers.Main) { + updateCurrentStatusViews(currentStatus) + initPredefinedStatusAdapter(predefinedStatus) + } } + } + private fun initPredefinedStatusAdapter(predefinedStatus: ArrayList) { adapter = PredefinedStatusListAdapter(this, requireContext()) - if (this::predefinedStatus.isInitialized) { - adapter.list = predefinedStatus - } + Log_OC.d(TAG, "PredefinedStatusListAdapter initialized") + adapter.list = predefinedStatus binding.predefinedStatusList.adapter = adapter binding.predefinedStatusList.layoutManager = LinearLayoutManager(context) + } + + @SuppressLint("DefaultLocale") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initRepository() binding.clearStatus.setOnClickListener { clearStatus() } binding.setStatus.setOnClickListener { setStatusMessage() } @@ -129,30 +126,34 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : binding.emoji.installForceSingleEmoji() binding.emoji.installDisableKeyboardInput(popup) - 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)) + clearStatusAdapter() + } + + private fun clearStatusAdapter() { + val adapter = + ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + add(getString(R.string.dontClear)) + add(getString(R.string.thirtyMinutes)) + add(getString(R.string.oneHour)) + add(getString(R.string.fourHours)) + add(getString(R.string.today)) + add(getString(R.string.thisWeek)) + } binding.clearStatusAfterSpinner.apply { this.adapter = adapter - onItemClickListener = object : OnItemSelectedListener, AdapterView.OnItemClickListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + onItemSelectedListener = object : OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View?, + position: Int, + id: Long + ) { setClearStatusAfterValue(position) } override fun onNothingSelected(parent: AdapterView<*>?) = Unit - - override fun onItemClick( - parent: AdapterView<*>?, - view: View?, - position: Int, - id: Long - ) = Unit } } } @@ -180,7 +181,11 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : binding.remainingClearTime.apply { binding.clearStatusMessageTextView.text = getString(R.string.clear) visibility = View.VISIBLE - text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true) + text = DisplayUtils.getRelativeTimestamp( + context, + it.clearAt * ONE_SECOND_IN_MILLIS, + true + ) .toString() .replaceFirstChar { it.lowercase(Locale.getDefault()) } setOnClickListener { @@ -208,7 +213,7 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : POS_FOUR_HOURS -> { // four hours System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + - FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS + FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS } POS_TODAY -> { @@ -230,7 +235,7 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : } } - private fun clearAtToUnixTime(clearAt: ClearAt?): Long = when { + private fun clearAtToUnixTime(clearAt: ExposedClearAt?): Long = when { clearAt?.type == CLEAR_AT_TYPE_PERIOD -> { System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong() } @@ -255,53 +260,51 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : private fun dateToSeconds(date: Calendar) = date.timeInMillis / ONE_SECOND_IN_MILLIS private fun clearStatus() { - asyncRunner.postQuickTask( - ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context), - { dismiss(it) } - ) + lifecycleScope.launch(Dispatchers.IO) { + val result = repository?.clearStatus() + dismiss(result) + } } private fun setStatusMessage() { if (selectedPredefinedMessageId != null) { - asyncRunner.postQuickTask( - SetPredefinedCustomStatusTask( - selectedPredefinedMessageId!!, - clearAt, - accountManager.currentOwnCloudAccount?.savedAccount, - context - ), - { dismiss(it) } - ) + lifecycleScope.launch(Dispatchers.IO) { + val result = repository?.setPredefinedStatus(selectedPredefinedMessageId!!, clearAt) + dismiss(result) + } } else { - asyncRunner.postQuickTask( - SetUserDefinedCustomStatusTask( + lifecycleScope.launch(Dispatchers.IO) { + val result = repository?.setCustomStatus( binding.customStatusInput.text.toString(), binding.emoji.text.toString(), - clearAt, - accountManager.currentOwnCloudAccount?.savedAccount, - context - ), - { dismiss(it) } - ) + clearAt + ) + dismiss(result) + } } } - private fun dismiss(boolean: Boolean) { - if (boolean) { + private suspend fun dismiss(boolean: Boolean?) = withContext(Dispatchers.Main) { + if (boolean == true) { dismiss() } else { - DisplayUtils.showSnackMessage(view, view?.resources?.getString(R.string.error_setting_status_message)) + val message = view?.resources?.getString(R.string.error_setting_status_message) + DisplayUtils.showSnackMessage(view, message) } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { binding = SetStatusMessageBottomSheetBinding.inflate(layoutInflater, container, false) return binding.root } - override fun onClick(predefinedStatus: PredefinedStatus) { + override fun onClick(predefinedStatus: ExposedPredefinedStatus) { selectedPredefinedMessageId = predefinedStatus.id - clearAt = clearAtToUnixTime(predefinedStatus.clearAt) + clearAt = clearAtToUnixTime(predefinedStatus.exposedClearAt) binding.emoji.setText(predefinedStatus.icon) binding.customStatusInput.text?.clear() binding.customStatusInput.text?.append(predefinedStatus.message) @@ -310,7 +313,7 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : binding.clearStatusAfterSpinner.visibility = View.VISIBLE binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after) - val clearAt = predefinedStatus.clearAt + val clearAt = predefinedStatus.exposedClearAt if (clearAt == null) { binding.clearStatusAfterSpinner.setSelection(0) } else { @@ -322,7 +325,7 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition) } - private fun updateClearAtViewsForPeriod(clearAt: ClearAt) { + private fun updateClearAtViewsForPeriod(clearAt: ExposedClearAt) { when (clearAt.time) { "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR) "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR) @@ -331,17 +334,11 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : } } - private fun updateClearAtViewsForEndOf(clearAt: ClearAt) { + private fun updateClearAtViewsForEndOf(clearAt: ExposedClearAt) { 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() - } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt index b4de418b5..1ad81a1f6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt @@ -7,8 +7,8 @@ */ package it.niedermann.owncloud.notes.accountswitcher.adapter -import com.owncloud.android.lib.resources.users.PredefinedStatus +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus interface PredefinedStatusClickListener { - fun onClick(predefinedStatus: PredefinedStatus) + fun onClick(predefinedStatus: ExposedPredefinedStatus) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt index 815708a31..da6f3842f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt @@ -11,12 +11,12 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.owncloud.android.lib.resources.users.PredefinedStatus +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding class PredefinedStatusListAdapter(private val clickListener: PredefinedStatusClickListener, val context: Context) : RecyclerView.Adapter() { - internal var list: List = emptyList() + internal var list: List = emptyList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder { val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt index 9cb524f3b..4e6f98077 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt @@ -9,8 +9,8 @@ package it.niedermann.owncloud.notes.accountswitcher.adapter import android.content.Context import androidx.recyclerview.widget.RecyclerView -import com.owncloud.android.lib.resources.users.PredefinedStatus import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding import it.niedermann.owncloud.notes.shared.util.DisplayUtils @@ -18,15 +18,15 @@ private const val ONE_SECOND_IN_MILLIS = 1000 class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) { + fun bind(status: ExposedPredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) { binding.root.setOnClickListener { clickListener.onClick(status) } binding.icon.text = status.icon binding.name.text = status.message - if (status.clearAt == null) { + if (status.exposedClearAt == null) { binding.clearAt.text = context.getString(R.string.dontClear) } else { - val clearAt = status.clearAt!! + val clearAt = status.exposedClearAt if (clearAt.type == "period") { binding.clearAt.text = DisplayUtils.getRelativeTimestamp( context, diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt new file mode 100644 index 000000000..c3f9866eb --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt @@ -0,0 +1,32 @@ +package it.niedermann.owncloud.notes.accountswitcher.model + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class ExposedPredefinedStatus( + @Expose + @SerializedName("id") + val id: String, + + @Expose + @SerializedName("icon") + val icon: String, + + @Expose + @SerializedName("message") + val message: String, + + @Expose + @SerializedName("clearAt") + val exposedClearAt: ExposedClearAt? +) + +data class ExposedClearAt( + @Expose + @SerializedName("type") + val type: String, + + @Expose + @SerializedName("time") + val time: String +) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt index 2328ea732..977162f3c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -1,13 +1,14 @@ package it.niedermann.owncloud.notes.accountswitcher.repository import android.content.Context -import android.util.Log -import androidx.annotation.WorkerThread import com.nextcloud.android.sso.model.SingleSignOnAccount +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.persistence.ApiProvider -import it.niedermann.owncloud.notes.shared.model.OcsResponse -import retrofit2.Response -import kotlin.getValue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class UserStatusRepository( private val context: Context, @@ -17,47 +18,104 @@ class UserStatusRepository( private const val TAG = "UserStatusRepository" } - private val ocsAPI by lazy { ApiProvider.getInstance().getOcsAPI(context, ssoAccount) } + private val api by lazy { ApiProvider.getInstance().getUserStatusAPI(context, ssoAccount) } - @WorkerThread - fun clearStatus(): Boolean { - return try { - val response: Response> = ocsAPI.clearStatusMessage().execute() - response.isSuccessful + suspend fun fetchPredefinedStatuses(): ArrayList = withContext(Dispatchers.IO) { + try { + val response = api.fetchPredefinedStatuses().execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… fetching predefined statuses successfully completed") + response.body()?.ocs?.data ?: arrayListOf() + } else { + Log_OC.e(TAG, "โŒ fetching predefined statuses failed") + arrayListOf() + } } catch (t: Throwable) { - Log.e(TAG, "Clearing status failed", t) + Log_OC.e(TAG, "โŒ fetching predefined statuses failed", t) + arrayListOf() + } + } + + suspend fun clearStatus(): Boolean = withContext(Dispatchers.IO) { + try { + val call = api.clearStatusMessage() + val response = call.execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… clearing status successfully completed") + true + } else { + Log_OC.e(TAG, "โŒ clearing status failed") + false + } + } catch (t: Throwable) { + Log_OC.e(TAG, "โŒ clearing status failed", t) false } } - @WorkerThread - fun setPredefinedStatus(messageId: String, clearAt: Long? = null): Boolean { - val body = mutableMapOf("messageId" to messageId) - clearAt?.let { body["clearAt"] = it.toString() } + suspend fun setPredefinedStatus(messageId: String, clearAt: Long? = null): Boolean = + withContext(Dispatchers.IO) { + try { + val body = mutableMapOf("messageId" to messageId) + clearAt?.let { body["clearAt"] = it.toString() } + val call = api.setPredefinedStatusMessage(body) + val response = call.execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… predefined status successfully set") + true + } else { + Log_OC.e(TAG, "โŒ setting predefined status failed") + false + } + } catch (t: Throwable) { + Log_OC.e(TAG, "โŒ setting predefined status failed", t) + false + } + } + + suspend fun setCustomStatus( + message: String, + statusIcon: String, + clearAt: Long? = null + ): Boolean = withContext(Dispatchers.IO) { + try { + val body = mutableMapOf( + "message" to message, + "statusIcon" to statusIcon + ) + clearAt?.let { body["clearAt"] = it.toString() } - return try { - val response: Response> = ocsAPI.setPredefinedStatusMessage(body).execute() - response.isSuccessful + val call = api.setUserDefinedStatusMessage(body) + val response = call.execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… setting custom status successfully completed") + true + } else { + Log_OC.e(TAG, "โŒ setting custom status failed") + false + } } catch (t: Throwable) { - Log.e(TAG, "Setting predefined status failed", t) + Log_OC.e(TAG, "โŒsetting custom status failed", t) false } } - @WorkerThread - fun setCustomStatus(message: String, statusIcon: String, clearAt: Long? = null): Boolean { - val body = mutableMapOf( - "message" to message, - "statusIcon" to statusIcon - ) - clearAt?.let { body["clearAt"] = it.toString() } - - return try { - val response: Response> = ocsAPI.setUserDefinedStatusMessage(body).execute() - response.isSuccessful + suspend fun fetchUserStatus(): Status? = withContext(Dispatchers.IO) { + val offlineStatus = Status(StatusType.OFFLINE, "", "", -1) + try { + val call = api.fetchUserStatus() + val response = call.execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… fetching user status successfully completed") + response.body()?.ocs?.data ?: offlineStatus + } else { + Log_OC.e(TAG, "โŒ fetching user status failed") + offlineStatus + } + } catch (t: Throwable) { - Log.e(TAG, "Setting custom status failed", t) - false + Log_OC.e(TAG, "โŒ fetching user status failed $t") + offlineStatus } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt deleted file mode 100644 index 1fb9ec9d6..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/ClearStatusTask.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.accountswitcher.task - -import android.accounts.Account -import android.content.Context -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.common.accounts.AccountUtils -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.users.ClearStatusMessageRemoteOperation - -class ClearStatusTask(val account: Account?, val context: Context?) : Function0 { - override fun invoke(): Boolean = try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) - - ClearStatusMessageRemoteOperation().execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error clearing status", e) - - false - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt deleted file mode 100644 index 13f8c1da0..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetPredefinedCustomStatusTask.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.accountswitcher.task - -import android.accounts.Account -import android.content.Context -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.common.accounts.AccountUtils -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.users.SetPredefinedCustomStatusMessageRemoteOperation - -class SetPredefinedCustomStatusTask( - val messageId: String, - val clearAt: Long?, - val account: Account?, - val context: Context? -) : Function0 { - override fun invoke(): Boolean = try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) - - SetPredefinedCustomStatusMessageRemoteOperation(messageId, clearAt).execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error setting predefined status", e) - - false - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt deleted file mode 100644 index 12b8b1459..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetStatusTask.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.accountswitcher.task - -import android.accounts.Account -import android.content.Context -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.common.accounts.AccountUtils -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.users.SetStatusRemoteOperation -import com.owncloud.android.lib.resources.users.StatusType - -class SetStatusTask(val statusType: StatusType, val account: Account?, val context: Context?) : Function0 { - override fun invoke(): Boolean = try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) - - SetStatusRemoteOperation(statusType).execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error setting status", e) - - false - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt deleted file mode 100644 index fb3d1d7c3..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/task/SetUserDefinedCustomStatusTask.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.accountswitcher.task - -import android.accounts.Account -import android.content.Context -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.common.accounts.AccountUtils -import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.lib.resources.users.SetUserDefinedCustomStatusMessageRemoteOperation - -class SetUserDefinedCustomStatusTask( - val message: String, - val icon: String, - val clearAt: Long?, - val account: Account?, - val context: Context? -) : Function0 { - override fun invoke(): Boolean { - return try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) - - return SetUserDefinedCustomStatusMessageRemoteOperation(message, icon, clearAt).execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error setting user defined custom status", e) - - false - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index e183b4c18..95c63f163 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -31,9 +31,9 @@ import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; import it.niedermann.owncloud.notes.persistence.sync.OcsAPI; import it.niedermann.owncloud.notes.persistence.sync.ShareAPI; +import it.niedermann.owncloud.notes.persistence.sync.UserStatusAPI; import it.niedermann.owncloud.notes.shared.model.ApiVersion; import it.niedermann.owncloud.notes.shared.model.Capabilities; -import okhttp3.ResponseBody; import retrofit2.NextcloudRetrofitApiBuilder; import retrofit2.Retrofit; @@ -49,12 +49,14 @@ public class ApiProvider { private static final ApiProvider INSTANCE = new ApiProvider(); private static final String API_ENDPOINT_OCS = "/ocs/v2.php/cloud/"; + private static final String API_USER_STATUS = "/ocs/v2.php/apps/user_status/api/v1/"; private static final String API_ENDPOINT_FILES ="/ocs/v2.php/apps/files/api/v1/"; private static final String API_ENDPOINT_FILES_SHARING ="/ocs/v2.php/apps/files_sharing/api/v1/"; private static final Map API_CACHE = new ConcurrentHashMap<>(); private static final Map API_CACHE_OCS = new ConcurrentHashMap<>(); + private static final Map API_CACHE_USER_STATUS = new ConcurrentHashMap<>(); private static final Map API_CACHE_NOTES = new ConcurrentHashMap<>(); private static final Map API_CACHE_FILES = new ConcurrentHashMap<>(); private static final Map API_CACHE_FILES_SHARING = new ConcurrentHashMap<>(); @@ -80,6 +82,15 @@ public synchronized OcsAPI getOcsAPI(@NonNull Context context, @NonNull SingleSi return ocsAPI; } + public synchronized UserStatusAPI getUserStatusAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { + if (API_CACHE_USER_STATUS.containsKey(ssoAccount.name)) { + return API_CACHE_USER_STATUS.get(ssoAccount.name); + } + final var result = new NextcloudRetrofitApiBuilder(getNextcloudAPI(context, ssoAccount), API_USER_STATUS).create(UserStatusAPI.class); + API_CACHE_USER_STATUS.put(ssoAccount.name, result); + return result; + } + /** * In case the {@param preferredApiVersion} changes, call {@link #invalidateAPICache(SingleSignOnAccount)} or {@link #invalidateAPICache()} to make sure that this call returns a {@link NotesAPI} that uses the correct compatibility layer. */ @@ -151,6 +162,8 @@ public synchronized void invalidateAPICache(@NonNull SingleSignOnAccount ssoAcco } API_CACHE_NOTES.remove(ssoAccount.name); API_CACHE_OCS.remove(ssoAccount.name); + API_CACHE_USER_STATUS.remove(ssoAccount.name); + API_CACHE_FILES_SHARING.remove(ssoAccount.name); } /** @@ -169,5 +182,7 @@ public synchronized void invalidateAPICache() { } API_CACHE_NOTES.clear(); API_CACHE_OCS.clear(); + API_CACHE_USER_STATUS.clear(); + API_CACHE_FILES_SHARING.clear(); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java index a98313a54..b6304cb2e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/OcsAPI.java @@ -8,19 +8,13 @@ import com.nextcloud.android.sso.api.ParsedResponse; -import java.util.Map; - import io.reactivex.Observable; import it.niedermann.owncloud.notes.shared.model.Capabilities; import it.niedermann.owncloud.notes.shared.model.OcsResponse; import it.niedermann.owncloud.notes.shared.model.OcsUser; import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Header; -import retrofit2.http.Headers; -import retrofit2.http.PUT; import retrofit2.http.Path; /** @@ -33,15 +27,4 @@ public interface OcsAPI { @GET("users/{userId}?format=json") Call> getUser(@Path("userId") String userId); - - @DELETE("apps/user_status/api/v1/user_status/message?format=json") - Call> clearStatusMessage(); - - @PUT("apps/user_status/api/v1/user_status/message/predefined?format=json") - @Headers("Content-Type: application/json") - Call> setPredefinedStatusMessage(@Body Map body); - - @PUT("apps/user_status/api/v1/user_status/message/custom?format=json") - @Headers("Content-Type: application/json") - Call> setUserDefinedStatusMessage(@Body Map body); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt new file mode 100644 index 000000000..72b467bc7 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt @@ -0,0 +1,31 @@ +package it.niedermann.owncloud.notes.persistence.sync + +import com.owncloud.android.lib.resources.users.PredefinedStatus +import com.owncloud.android.lib.resources.users.Status +import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus +import it.niedermann.owncloud.notes.shared.model.OcsResponse +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.PUT + +interface UserStatusAPI { + @GET("user_status?format=json") + fun fetchUserStatus(): Call> + + @DELETE("user_status/message?format=json") + fun clearStatusMessage(): Call> + + @GET("predefined_statuses?format=json") + fun fetchPredefinedStatuses(): Call>> + + @PUT("user_status/message/predefined?format=json") + @Headers("Content-Type: application/json") + fun setPredefinedStatusMessage(@Body body: Map): Call> + + @PUT("user_status/message/custom?format=json") + @Headers("Content-Type: application/json") + fun setUserDefinedStatusMessage(@Body body: Map): Call> +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt index f602811d9..8efff5a49 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt @@ -23,4 +23,12 @@ object UserStorage { val type = object : TypeToken>() {}.type return Gson().fromJson(json, type) } + + fun storePredefinedStatus(context: Context, statuses: ArrayList) { + val json = Gson().toJson(statuses) + getSharedPreferences(context).edit().apply { + putString(PREDEFINED_STATUS, json) + apply() + } + } } diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml index 08c293542..0f10e5d1d 100644 --- a/app/src/main/res/layout/dialog_account_switcher.xml +++ b/app/src/main/res/layout/dialog_account_switcher.xml @@ -77,7 +77,7 @@ android:orientation="horizontal" android:paddingStart="@dimen/spacer_1x" android:paddingEnd="@dimen/zero" - android:visibility="gone" + android:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" tools:visibility="visible"> From 956d4f515f7b0586dacd9ddbacbc6dbcddd68e11 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 11:39:36 +0200 Subject: [PATCH 04/15] add SetStatusMessageBottomSheet Signed-off-by: alperozturk --- .../SetStatusMessageBottomSheet.kt | 31 ++++++++++-------- .../adapter/PredefinedStatusClickListener.kt | 4 +-- .../adapter/PredefinedStatusListAdapter.kt | 4 +-- .../adapter/PredefinedStatusViewHolder.kt | 12 +++---- .../model/ExposedPredefinedStatus.kt | 32 ------------------- .../repository/UserStatusRepository.kt | 4 +-- .../notes/persistence/ApiProvider.java | 1 - .../notes/persistence/sync/UserStatusAPI.kt | 3 +- 8 files changed, 30 insertions(+), 61 deletions(-) delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt index b9e980c64..270a11cd5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt @@ -23,6 +23,8 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.nextcloud.android.sso.helper.SingleAccountHelper import com.owncloud.android.lib.common.utils.Log_OC +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.vanniktech.emoji.EmojiManager @@ -33,8 +35,6 @@ import com.vanniktech.emoji.installForceSingleEmoji import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedClearAt -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment import it.niedermann.owncloud.notes.branding.BrandingUtil @@ -99,7 +99,7 @@ class SetStatusMessageBottomSheet : } } - private fun initPredefinedStatusAdapter(predefinedStatus: ArrayList) { + private fun initPredefinedStatusAdapter(predefinedStatus: ArrayList) { adapter = PredefinedStatusListAdapter(this, requireContext()) Log_OC.d(TAG, "PredefinedStatusListAdapter initialized") adapter.list = predefinedStatus @@ -159,11 +159,14 @@ class SetStatusMessageBottomSheet : } override fun applyBrand(color: Int) { - val viewThemeUtils = BrandingUtil.of(color, requireContext()) - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(binding.clearStatus) - viewThemeUtils.material.colorMaterialButtonPrimaryTonal(binding.setStatus) - viewThemeUtils.material.colorTextInputLayout(binding.customStatusInputContainer) - viewThemeUtils.platform.themeDialog(binding.root) + BrandingUtil.of(color, requireContext()).run { + platform.themeDialog(binding.root) + material.run { + colorMaterialButtonPrimaryBorderless(binding.clearStatus) + colorMaterialButtonPrimaryTonal(binding.setStatus) + colorTextInputLayout(binding.customStatusInputContainer) + } + } } private fun updateCurrentStatusViews(it: Status) { @@ -235,7 +238,7 @@ class SetStatusMessageBottomSheet : } } - private fun clearAtToUnixTime(clearAt: ExposedClearAt?): Long = when { + private fun clearAtToUnixTime(clearAt: ClearAt?): Long = when { clearAt?.type == CLEAR_AT_TYPE_PERIOD -> { System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong() } @@ -302,9 +305,9 @@ class SetStatusMessageBottomSheet : return binding.root } - override fun onClick(predefinedStatus: ExposedPredefinedStatus) { + override fun onClick(predefinedStatus: PredefinedStatus) { selectedPredefinedMessageId = predefinedStatus.id - clearAt = clearAtToUnixTime(predefinedStatus.exposedClearAt) + clearAt = clearAtToUnixTime(predefinedStatus.clearAt) binding.emoji.setText(predefinedStatus.icon) binding.customStatusInput.text?.clear() binding.customStatusInput.text?.append(predefinedStatus.message) @@ -313,7 +316,7 @@ class SetStatusMessageBottomSheet : binding.clearStatusAfterSpinner.visibility = View.VISIBLE binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after) - val clearAt = predefinedStatus.exposedClearAt + val clearAt = predefinedStatus.clearAt if (clearAt == null) { binding.clearStatusAfterSpinner.setSelection(0) } else { @@ -325,7 +328,7 @@ class SetStatusMessageBottomSheet : setClearStatusAfterValue(binding.clearStatusAfterSpinner.selectedItemPosition) } - private fun updateClearAtViewsForPeriod(clearAt: ExposedClearAt) { + private fun updateClearAtViewsForPeriod(clearAt: ClearAt) { when (clearAt.time) { "1800" -> binding.clearStatusAfterSpinner.setSelection(POS_HALF_AN_HOUR) "3600" -> binding.clearStatusAfterSpinner.setSelection(POS_AN_HOUR) @@ -334,7 +337,7 @@ class SetStatusMessageBottomSheet : } } - private fun updateClearAtViewsForEndOf(clearAt: ExposedClearAt) { + private fun updateClearAtViewsForEndOf(clearAt: ClearAt) { when (clearAt.time) { "day" -> binding.clearStatusAfterSpinner.setSelection(POS_TODAY) "week" -> binding.clearStatusAfterSpinner.setSelection(POS_END_OF_WEEK) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt index 1ad81a1f6..b4de418b5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt @@ -7,8 +7,8 @@ */ package it.niedermann.owncloud.notes.accountswitcher.adapter -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus +import com.owncloud.android.lib.resources.users.PredefinedStatus interface PredefinedStatusClickListener { - fun onClick(predefinedStatus: ExposedPredefinedStatus) + fun onClick(predefinedStatus: PredefinedStatus) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt index da6f3842f..815708a31 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt @@ -11,12 +11,12 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus +import com.owncloud.android.lib.resources.users.PredefinedStatus import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding class PredefinedStatusListAdapter(private val clickListener: PredefinedStatusClickListener, val context: Context) : RecyclerView.Adapter() { - internal var list: List = emptyList() + internal var list: List = emptyList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PredefinedStatusViewHolder { val itemBinding = PredefinedStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt index 4e6f98077..19965fda1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt @@ -9,8 +9,8 @@ package it.niedermann.owncloud.notes.accountswitcher.adapter import android.content.Context import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.lib.resources.users.PredefinedStatus import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.databinding.PredefinedStatusBinding import it.niedermann.owncloud.notes.shared.util.DisplayUtils @@ -18,16 +18,16 @@ private const val ONE_SECOND_IN_MILLIS = 1000 class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(status: ExposedPredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) { + fun bind(status: PredefinedStatus, clickListener: PredefinedStatusClickListener, context: Context) { binding.root.setOnClickListener { clickListener.onClick(status) } binding.icon.text = status.icon binding.name.text = status.message - if (status.exposedClearAt == null) { + if (status.clearAt == null) { binding.clearAt.text = context.getString(R.string.dontClear) } else { - val clearAt = status.exposedClearAt - if (clearAt.type == "period") { + val clearAt = status.clearAt + if (clearAt?.type == "period") { binding.clearAt.text = DisplayUtils.getRelativeTimestamp( context, System.currentTimeMillis() + clearAt.time.toInt() * ONE_SECOND_IN_MILLIS, @@ -35,7 +35,7 @@ class PredefinedStatusViewHolder(private val binding: PredefinedStatusBinding) : ) } else { // end-of - if (clearAt.time == "day") { + if (clearAt?.time == "day") { binding.clearAt.text = context.getString(R.string.today) } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt deleted file mode 100644 index c3f9866eb..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/model/ExposedPredefinedStatus.kt +++ /dev/null @@ -1,32 +0,0 @@ -package it.niedermann.owncloud.notes.accountswitcher.model - -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName - -data class ExposedPredefinedStatus( - @Expose - @SerializedName("id") - val id: String, - - @Expose - @SerializedName("icon") - val icon: String, - - @Expose - @SerializedName("message") - val message: String, - - @Expose - @SerializedName("clearAt") - val exposedClearAt: ExposedClearAt? -) - -data class ExposedClearAt( - @Expose - @SerializedName("type") - val type: String, - - @Expose - @SerializedName("time") - val time: String -) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt index 977162f3c..aa6bc7ffe 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -3,9 +3,9 @@ package it.niedermann.owncloud.notes.accountswitcher.repository import android.content.Context import com.nextcloud.android.sso.model.SingleSignOnAccount import com.owncloud.android.lib.common.utils.Log_OC +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 it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.persistence.ApiProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -20,7 +20,7 @@ class UserStatusRepository( private val api by lazy { ApiProvider.getInstance().getUserStatusAPI(context, ssoAccount) } - suspend fun fetchPredefinedStatuses(): ArrayList = withContext(Dispatchers.IO) { + suspend fun fetchPredefinedStatuses(): ArrayList = withContext(Dispatchers.IO) { try { val response = api.fetchPredefinedStatuses().execute() if (response.isSuccessful) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 95c63f163..8143a9a9e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -129,7 +129,6 @@ private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @Non final var nextcloudAPI = new NextcloudAPI(context.getApplicationContext(), ssoAccount, new GsonBuilder() .setStrictness(Strictness.LENIENT) - .excludeFieldsWithoutExposeAnnotation() .registerTypeHierarchyAdapter(Calendar.class, (JsonSerializer) (src, typeOfSrc, ctx) -> new JsonPrimitive(src.getTimeInMillis() / 1_000)) .registerTypeHierarchyAdapter(Calendar.class, (JsonDeserializer) (src, typeOfSrc, ctx) -> { final var calendar = Calendar.getInstance(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt index 72b467bc7..6b2a319a8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt @@ -2,7 +2,6 @@ package it.niedermann.owncloud.notes.persistence.sync import com.owncloud.android.lib.resources.users.PredefinedStatus import com.owncloud.android.lib.resources.users.Status -import it.niedermann.owncloud.notes.accountswitcher.model.ExposedPredefinedStatus import it.niedermann.owncloud.notes.shared.model.OcsResponse import retrofit2.Call import retrofit2.http.Body @@ -19,7 +18,7 @@ interface UserStatusAPI { fun clearStatusMessage(): Call> @GET("predefined_statuses?format=json") - fun fetchPredefinedStatuses(): Call>> + fun fetchPredefinedStatuses(): Call>> @PUT("user_status/message/predefined?format=json") @Headers("Content-Type: application/json") From 0e78de0e9c2e19ccef070a5ce31dd8e89d5eaf96 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 12:37:02 +0200 Subject: [PATCH 05/15] add SetOnlineStatusBottomSheet Signed-off-by: alperozturk --- .../AccountSwitcherDialog.java | 7 +- .../SetOnlineStatusBottomSheet.kt | 177 ++++++++ .../repository/UserStatusRepository.kt | 24 ++ .../notes/persistence/NotesDatabase.java | 13 +- .../sync/CapabilitiesDeserializer.java | 12 + .../notes/persistence/sync/UserStatusAPI.kt | 4 + .../notes/shared/model/Capabilities.java | 11 +- .../util/FilesSpecificViewThemeUtils.kt | 49 +++ .../owncloud/notes/util/runner/AsyncRunner.kt | 60 --- .../notes/util/runner/ManualAsyncRunner.kt | 86 ---- .../owncloud/notes/util/runner/Task.kt | 59 --- .../util/runner/ThreadPoolAsyncRunner.kt | 59 --- .../notes/util/storage/UserStorage.kt | 34 -- .../main/res/drawable/ic_user_status_away.xml | 15 + .../main/res/drawable/ic_user_status_busy.xml | 18 + .../main/res/drawable/ic_user_status_dnd.xml | 15 + .../res/drawable/ic_user_status_invisible.xml | 15 + .../res/drawable/ic_user_status_online.xml | 18 + .../layout/set_online_status_bottom_sheet.xml | 391 ++++++++++++++++++ app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 9 + 22 files changed, 771 insertions(+), 309 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt delete mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt create mode 100644 app/src/main/res/drawable/ic_user_status_away.xml create mode 100644 app/src/main/res/drawable/ic_user_status_busy.xml create mode 100644 app/src/main/res/drawable/ic_user_status_dnd.xml create mode 100644 app/src/main/res/drawable/ic_user_status_invisible.xml create mode 100644 app/src/main/res/drawable/ic_user_status_online.xml create mode 100644 app/src/main/res/layout/set_online_status_bottom_sheet.xml diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index f51a35df5..cfd20d72f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -76,11 +76,8 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { AvatarLoader.INSTANCE.load(requireContext(), binding.currentAccountItemAvatar, currentLocalAccount); binding.accountLayout.setOnClickListener((v) -> dismiss()); binding.onlineStatus.setOnClickListener(v -> { - /* - val setStatusDialog = SetOnlineStatusBottomSheet(currentStatus) - setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status") - */ - + final var setOnlineStatusBottomSheet = new SetOnlineStatusBottomSheet(); + setOnlineStatusBottomSheet.show(requireActivity().getSupportFragmentManager(), "fragment_set_status"); dismiss(); }); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt new file mode 100644 index 000000000..d98073d27 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt @@ -0,0 +1,177 @@ +package it.niedermann.owncloud.notes.accountswitcher + + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import com.google.android.material.card.MaterialCardView +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository +import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.SetOnlineStatusBottomSheetBinding +import it.niedermann.owncloud.notes.shared.util.DisplayUtils +import it.niedermann.owncloud.notes.shared.util.FilesSpecificViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SetOnlineStatusBottomSheet : + BrandedBottomSheetDialogFragment(R.layout.set_online_status_bottom_sheet) { + + companion object { + private val TAG = SetOnlineStatusBottomSheet::class.simpleName + } + + private lateinit var binding: SetOnlineStatusBottomSheetBinding + private var cardViews: Triple? = null + private var repository: UserStatusRepository? = null + + @SuppressLint("DefaultLocale") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initRepository() + setupStatusClickListeners() + } + + private fun setupStatusClickListeners() { + val statusMap = mapOf( + binding.onlineStatus to StatusType.ONLINE, + binding.awayStatus to StatusType.AWAY, + binding.busyStatus to StatusType.BUSY, + binding.dndStatus to StatusType.DND, + binding.invisibleStatus to StatusType.INVISIBLE + ) + + statusMap.forEach { (view, statusType) -> + view.setOnClickListener { setStatus(statusType) } + } + } + + private fun initRepository() { + lifecycleScope.launch(Dispatchers.IO) { + val ssoAccount = + SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) ?: return@launch + repository = UserStatusRepository(requireContext(), ssoAccount) + val currentStatus = + repository?.fetchUserStatus() ?: Status(StatusType.OFFLINE, "", "", -1) + + val capabilities = repository?.getCapabilities() + + if (capabilities?.isUserStatusSupportsBusy == true) { + binding.busyStatus.visibility = View.VISIBLE + } else { + binding.busyStatus.visibility = View.GONE + } + + withContext(Dispatchers.Main) { + updateCurrentStatusViews(currentStatus) + } + } + } + + private fun updateCurrentStatusViews(it: Status) { + visualizeStatus(it.status) + } + + private fun setStatus(statusType: StatusType) { + lifecycleScope.launch(Dispatchers.IO) { + val result = repository?.setStatusType(statusType) + withContext(Dispatchers.Main) { + if (result == true) { + dismiss() + } else { + showErrorSnackbar() + } + } + } + } + + private fun showErrorSnackbar() { + DisplayUtils.showSnackMessage(view, R.string.status_set_fail_message) + clearTopStatus() + } + + private fun visualizeStatus(statusType: StatusType) { + clearTopStatus() + cardViews = when (statusType) { + StatusType.ONLINE -> Triple( + binding.onlineStatus, + binding.onlineHeadline, + binding.onlineIcon + ) + + StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon) + StatusType.BUSY -> Triple(binding.busyStatus, binding.busyHeadline, binding.busyIcon) + 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 + } + } + cardViews?.first?.isChecked = true + } + + private fun clearTopStatus() { + context?.let { ctx -> + binding.run { + val headlines = listOf( + onlineHeadline, + awayHeadline, + busyHeadline, + dndHeadline, + invisibleHeadline + ) + val color = ContextCompat.getColor(ctx, com.nextcloud.android.common.ui.R.color.high_emphasis_text) + headlines.forEach { it.setTextColor(color) } + listOf(awayIcon, dndIcon, invisibleIcon).forEach { it.imageTintList = null } + listOf(onlineStatus, awayStatus, busyStatus, dndStatus, invisibleStatus).forEach { it.isChecked = false } + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SetOnlineStatusBottomSheetBinding.inflate(layoutInflater, container, false) + return binding.root + } + + override fun applyBrand(color: Int) { + BrandingUtil.of(color, requireContext()).run { + platform.themeDialog(binding.root) + + cardViews?.let { + platform.colorTextView(it.second, ColorRole.ON_SECONDARY_CONTAINER) + } + } + + FilesSpecificViewThemeUtils.run { + themeStatusCardView(binding.onlineStatus, color) + themeStatusCardView(binding.awayStatus, color) + themeStatusCardView(binding.busyStatus, color) + themeStatusCardView(binding.dndStatus, color) + themeStatusCardView(binding.invisibleStatus, color) + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt index aa6bc7ffe..84c0281dd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -7,6 +7,8 @@ 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 it.niedermann.owncloud.notes.persistence.ApiProvider +import it.niedermann.owncloud.notes.persistence.NotesRepository +import it.niedermann.owncloud.notes.shared.model.Capabilities import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -18,8 +20,11 @@ class UserStatusRepository( private const val TAG = "UserStatusRepository" } + private val notesRepository: NotesRepository by lazy { NotesRepository.getInstance(context) } private val api by lazy { ApiProvider.getInstance().getUserStatusAPI(context, ssoAccount) } + fun getCapabilities(): Capabilities = notesRepository.capabilities + suspend fun fetchPredefinedStatuses(): ArrayList = withContext(Dispatchers.IO) { try { val response = api.fetchPredefinedStatuses().execute() @@ -118,4 +123,23 @@ class UserStatusRepository( offlineStatus } } + + suspend fun setStatusType(statusType: StatusType): Boolean = withContext(Dispatchers.IO) { + try { + val body = mutableMapOf( + "statusType" to statusType.string, + ) + val response = api.setStatusType(body).execute() + if (response.isSuccessful) { + Log_OC.d(TAG, "โœ… status type successfully set") + true + } else { + Log_OC.e(TAG, "โŒ setting status type failed") + false + } + } catch (t: Throwable) { + Log_OC.e(TAG, "โŒ setting status type failed", t) + false + } + } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java index b1fae2161..6fd0b6cb4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java @@ -58,8 +58,11 @@ NotesListWidgetData.class, ShareEntity.class, Capabilities.class - }, version = 26, - autoMigrations = { @AutoMigration(from = 25, to = 26) } + }, version = 27, + autoMigrations = { + @AutoMigration(from = 25, to = 26), + @AutoMigration(from = 26, to = 27), + } ) @TypeConverters({Converters.class}) public abstract class NotesDatabase extends RoomDatabase { @@ -77,9 +80,9 @@ public static NotesDatabase getInstance(@NonNull Context context) { private static NotesDatabase create(final Context context) { return Room.databaseBuilder( - context, - NotesDatabase.class, - NOTES_DB_NAME) + context, + NotesDatabase.class, + NOTES_DB_NAME) .addMigrations( new Migration_9_10(), // v2.0.0 new Migration_10_11(context), diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java index 7e158363d..e814d07be 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/CapabilitiesDeserializer.java @@ -32,6 +32,7 @@ public class CapabilitiesDeserializer implements JsonDeserializer { private static final String CAPABILITIES = "capabilities"; + private static final String CAPABILITIES_USER_STATUS = "user_status"; private static final String CAPABILITIES_NOTES = "notes"; private static final String CAPABILITIES_NOTES_API_VERSION = "api_version"; private static final String CAPABILITIES_THEMING = "theming"; @@ -42,6 +43,7 @@ public class CapabilitiesDeserializer implements JsonDeserializer private static final String CAPABILITIES_FILES_DIRECT_EDITING_SUPPORTS_FILE_ID = "supportsFileId"; private static final String CAPABILITIES_FILES_SHARING = "files_sharing"; private static final String VERSION = "version"; + private static final String CAPABILITIES_SUPPORTS_BUSY = "supports_busy"; @Override public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { @@ -110,6 +112,16 @@ public Capabilities deserialize(JsonElement json, Type typeOfT, JsonDeserializat } } response.setDirectEditingAvailable(hasDirectEditingCapability(capabilities)); + + if (capabilities.has(CAPABILITIES_USER_STATUS)) { + final var userStatus = capabilities.getAsJsonObject(CAPABILITIES_USER_STATUS); + if (userStatus.has(CAPABILITIES_SUPPORTS_BUSY)) { + final var userStatusSupportsBusy = userStatus.getAsJsonPrimitive(CAPABILITIES_SUPPORTS_BUSY); + if (userStatusSupportsBusy != null) { + response.setUserStatusSupportsBusy(userStatusSupportsBusy.getAsBoolean()); + } + } + } } return response; } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt index 6b2a319a8..73c2250fc 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt @@ -27,4 +27,8 @@ interface UserStatusAPI { @PUT("user_status/message/custom?format=json") @Headers("Content-Type: application/json") fun setUserDefinedStatusMessage(@Body body: Map): Call> + + @PUT("user_status/status?format=json") + @Headers("Content-Type: application/json") + fun setStatusType(@Body body: Map): Call> } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java index e7f1f98d2..1e62e41c5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java @@ -45,6 +45,7 @@ public class Capabilities implements Serializable { private boolean askForOptionalPassword; private boolean isReSharingAllowed; private int defaultPermission = OCShare.NO_PERMISSION; + private Boolean userStatusSupportsBusy = null; public boolean isReSharingAllowed() { return isReSharingAllowed; @@ -146,7 +147,6 @@ public void setTextColor(@ColorInt int textColor) { this.textColor = textColor; } - public boolean isDirectEditingAvailable() { return directEditingAvailable; } @@ -155,6 +155,14 @@ public void setDirectEditingAvailable(boolean directEditingAvailable) { this.directEditingAvailable = directEditingAvailable; } + public boolean isUserStatusSupportsBusy() { + return userStatusSupportsBusy; + } + + public void setUserStatusSupportsBusy(boolean value) { + userStatusSupportsBusy = value; + } + @Override public String toString() { return "Capabilities{" + @@ -169,6 +177,7 @@ public String toString() { ", textColor=" + textColor + ", eTag='" + eTag + '\'' + ", hasDirectEditing=" + directEditingAvailable + + ", userStatusSupportsBusy=" + userStatusSupportsBusy + '}'; } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt index 92978cc18..c2c18d571 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/FilesSpecificViewThemeUtils.kt @@ -7,6 +7,8 @@ package it.niedermann.owncloud.notes.shared.util import android.content.Context +import android.content.res.ColorStateList +import android.view.View import android.widget.ImageView import androidx.annotation.DrawableRes import androidx.annotation.Px @@ -14,8 +16,13 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat +import com.google.android.material.card.MaterialCardView +import com.nextcloud.android.common.ui.theme.MaterialSchemes +import com.nextcloud.android.common.ui.util.PlatformThemeUtil +import com.nextcloud.android.common.ui.util.extensions.toColorScheme import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.ShareType +import dynamiccolor.DynamicScheme import it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.branding.BrandingUtil @@ -31,6 +38,48 @@ object FilesSpecificViewThemeUtils { const val LARGE = 8 } + private fun getSchemeInternal(context: Context, color: Int): DynamicScheme { + val scheme = MaterialSchemes.Companion.fromColor(color) + return when { + PlatformThemeUtil.isDarkMode(context) -> scheme.darkScheme + else -> scheme.lightScheme + } + } + + private fun withScheme( + view: View, + color: Int, + block: (DynamicScheme) -> R + ): R = block(getSchemeInternal(view.context, color)) + + fun themeStatusCardView(cardView: MaterialCardView, color: Int) { + withScheme(cardView, color) { scheme -> + cardView.backgroundTintList = + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + scheme.surfaceContainerHighest, + scheme.surface + ) + ) + cardView.setStrokeColor( + ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + scheme.onSecondaryContainer, + scheme.outlineVariant + ) + ) + ) + } + } + fun createAvatar(type: ShareType?, avatar: ImageView, context: Context) { fun createAvatarBase(@DrawableRes icon: Int, padding: Int = AvatarPadding.SMALL) { avatar.setImageResource(icon) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt deleted file mode 100644 index 005b5b01d..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/AsyncRunner.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.util.runner - -import io.reactivex.functions.Cancellable - -typealias OnResultCallback = (result: T) -> Unit -typealias OnErrorCallback = (error: Throwable) -> Unit -typealias OnProgressCallback

= (progress: P) -> Unit -typealias IsCancelled = () -> Boolean -typealias TaskFunction = ( - onProgress: OnProgressCallback, - isCancelled: IsCancelled -) -> RESULT - -/** - * This interface allows to post background tasks that report results via callbacks invoked on main thread. - * It is provided as an alternative for heavy, platform specific and virtually untestable [android.os.AsyncTask] - * - * Please note that as of Android R, [android.os.AsyncTask] is deprecated and [java.util.concurrent] is a recommended - * alternative. - */ -interface AsyncRunner { - - /** - * Post a quick background task and return immediately returning task cancellation interface. - * - * Quick task is a short piece of code that does not support interruption nor progress monitoring. - * - * @param task Task function returning result T; error shall be signalled by throwing an exception. - * @param onResult Callback called when task function returns a result. - * @param onError Callback called when task function throws an exception. - * @return Cancellable interface, allowing cancellation of a running task. - */ - fun postQuickTask( - task: () -> RESULT, - onResult: OnResultCallback? = null, - onError: OnErrorCallback? = null - ): Cancellable - - /** - * Post a background task and return immediately returning task cancellation interface. - * - * @param task Task function returning result T; error shall be signalled by throwing an exception. - * @param onResult Callback called when task function returns a result, - * @param onError Callback called when task function throws an exception. - * @param onProgress Callback called when task function reports progress update. - * @return Cancellable interface, allowing cancellation of a running task. - */ - fun postTask( - task: TaskFunction, - onResult: OnResultCallback? = null, - onError: OnErrorCallback? = null, - onProgress: OnProgressCallback? = null - ): Cancellable -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt deleted file mode 100644 index 355ef3ade..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ManualAsyncRunner.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.util.runner - -import io.reactivex.functions.Cancellable -import java.util.ArrayDeque - -/** - * This async runner is suitable for tests, where manual simulation of - * asynchronous operations is desirable. - */ -class ManualAsyncRunner : AsyncRunner { - - private val queue: ArrayDeque = ArrayDeque() - - override fun postQuickTask( - task: () -> T, - onResult: OnResultCallback?, - onError: OnErrorCallback? - ): Cancellable = postTask( - task = { _: OnProgressCallback, _: IsCancelled -> task.invoke() }, - onResult = onResult, - onError = onError, - onProgress = null - ) - - override fun postTask( - task: TaskFunction, - onResult: OnResultCallback?, - onError: OnErrorCallback?, - onProgress: OnProgressCallback

? - ): Cancellable { - val remove: Function1 = queue::remove - val taskWrapper = Task( - postResult = { - it.run() - true - }, - removeFromQueue = remove, - taskBody = task, - onSuccess = onResult, - onError = onError, - onProgress = onProgress - ) - queue.push(taskWrapper) - return taskWrapper - } - - val size: Int get() = queue.size - val isEmpty: Boolean get() = queue.size == 0 - - /** - * Run all enqueued tasks until queue is empty. This will run also tasks - * enqueued by task callbacks. - * - * @param maximum max number of tasks to run to avoid infinite loopss - * @return number of executed tasks - */ - fun runAll(maximum: Int = 100): Int { - var c = 0 - while (queue.size > 0) { - val t = queue.remove() - t.run() - c++ - if (c > maximum) { - throw IllegalStateException("Maximum number of tasks run. Are you in infinite loop?") - } - } - return c - } - - /** - * Run one pending task - * - * @return true if task has been run - */ - fun runOne(): Boolean { - val t = queue.pollFirst() - t?.run() - return t != null - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt deleted file mode 100644 index 56b686b79..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/Task.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.util.runner - -import io.reactivex.functions.Cancellable -import java.util.concurrent.atomic.AtomicBoolean - -/** - * This is a wrapper for a task function running in background. - * It executes task function and handles result or error delivery. - */ -@Suppress("LongParameterList") -internal class Task( - private val postResult: (Runnable) -> Boolean, - private val removeFromQueue: (Runnable) -> Boolean, - private val taskBody: TaskFunction, - private val onSuccess: OnResultCallback?, - private val onError: OnErrorCallback?, - private val onProgress: OnProgressCallback

? -) : Runnable, - Cancellable { - - val isCancelled: Boolean - get() = cancelled.get() - - private val cancelled = AtomicBoolean(false) - - private fun postProgress(p: P) { - postResult(Runnable { onProgress?.invoke(p) }) - } - - @Suppress("TooGenericExceptionCaught") // this is exactly what we want here - override fun run() { - try { - val result = taskBody.invoke({ postProgress(it) }, this::isCancelled) - if (!cancelled.get()) { - postResult.invoke( - Runnable { - onSuccess?.invoke(result) - } - ) - } - } catch (t: Throwable) { - if (!cancelled.get()) { - postResult(Runnable { onError?.invoke(t) }) - } - } - removeFromQueue(this) - } - - override fun cancel() { - cancelled.set(true) - removeFromQueue(this) - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt deleted file mode 100644 index f55e11230..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/runner/ThreadPoolAsyncRunner.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package it.niedermann.owncloud.notes.util.runner - -import android.os.Handler -import io.reactivex.functions.Cancellable -import java.util.concurrent.ScheduledThreadPoolExecutor - -/** - * This async runner uses [ScheduledThreadPoolExecutor] to run tasks - * asynchronously. - * - * Tasks are run on multi-threaded pool. If serialized execution is desired, set [corePoolSize] to 1. - */ -internal class ThreadPoolAsyncRunner( - private val uiThreadHandler: Handler, - corePoolSize: Int, - val tag: String = "default" -) : AsyncRunner { - - private val executor = ScheduledThreadPoolExecutor(corePoolSize) - - override fun postQuickTask( - task: () -> T, - onResult: OnResultCallback?, - onError: OnErrorCallback? - ): Cancellable { - val taskAdapter = { _: OnProgressCallback, _: IsCancelled -> task.invoke() } - return postTask( - taskAdapter, - onResult, - onError, - null - ) - } - - override fun postTask( - task: TaskFunction, - onResult: OnResultCallback?, - onError: OnErrorCallback?, - onProgress: OnProgressCallback

? - ): Cancellable { - val remove: Function1 = executor::remove - val taskWrapper = Task( - postResult = uiThreadHandler::post, - removeFromQueue = remove, - taskBody = task, - onSuccess = onResult, - onError = onError, - onProgress = onProgress - ) - executor.execute(taskWrapper) - return taskWrapper - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt deleted file mode 100644 index 8efff5a49..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/storage/UserStorage.kt +++ /dev/null @@ -1,34 +0,0 @@ -package it.niedermann.owncloud.notes.util.storage - -import android.content.Context -import android.content.SharedPreferences -import androidx.preference.PreferenceManager -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import com.owncloud.android.lib.resources.users.PredefinedStatus - -object UserStorage { - private const val PREDEFINED_STATUS = "PREDEFINED_STATUS" - - private fun getSharedPreferences(context: Context): SharedPreferences { - return PreferenceManager.getDefaultSharedPreferences(context) - } - - fun readPredefinedStatus(context: Context): ArrayList { - val json = getSharedPreferences(context).getString(PREDEFINED_STATUS, null) - if (json == null) { - return arrayListOf() - } - - val type = object : TypeToken>() {}.type - return Gson().fromJson(json, type) - } - - fun storePredefinedStatus(context: Context, statuses: ArrayList) { - val json = Gson().toJson(statuses) - getSharedPreferences(context).edit().apply { - putString(PREDEFINED_STATUS, json) - apply() - } - } -} diff --git a/app/src/main/res/drawable/ic_user_status_away.xml b/app/src/main/res/drawable/ic_user_status_away.xml new file mode 100644 index 000000000..68fdf4102 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_away.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_busy.xml b/app/src/main/res/drawable/ic_user_status_busy.xml new file mode 100644 index 000000000..bdf9611a8 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_busy.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_user_status_dnd.xml b/app/src/main/res/drawable/ic_user_status_dnd.xml new file mode 100644 index 000000000..b9be2e6e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_dnd.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_invisible.xml b/app/src/main/res/drawable/ic_user_status_invisible.xml new file mode 100644 index 000000000..d79901b69 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_invisible.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_status_online.xml b/app/src/main/res/drawable/ic_user_status_online.xml new file mode 100644 index 000000000..df952c4de --- /dev/null +++ b/app/src/main/res/drawable/ic_user_status_online.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/set_online_status_bottom_sheet.xml b/app/src/main/res/layout/set_online_status_bottom_sheet.xml new file mode 100644 index 000000000..a8e5677be --- /dev/null +++ b/app/src/main/res/layout/set_online_status_bottom_sheet.xml @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c7356558d..b09134d24 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -54,4 +54,7 @@ #D6D7D7 #000000 #ffffff + #2D7B41 + #DB0606 + @color/high_emphasis_text diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e1c075414..1cf1f054c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -73,4 +73,5 @@ 56dp 60dp + 24dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5cf5c63b2..e4e2c9efb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -528,4 +528,13 @@ This week Error setting status message! seconds ago + Online status + Online + Do not disturb + Away + Invisible + Busy + Mute all notifications + Appear offline + Failed to set status! From 48d1e9c52794a31be78fcd11e5d75f11993a0c93 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 14:03:42 +0200 Subject: [PATCH 06/15] use showBottomSheetDialog Signed-off-by: alperozturk --- .../AccountSwitcherBottomSheetTag.kt | 25 +++++++++++ .../AccountSwitcherDialog.java | 19 ++++++++ .../SetOnlineStatusBottomSheet.kt | 44 +++++++++---------- .../SetStatusMessageBottomSheet.kt | 24 ++++------ .../owncloud/notes/util/ActivityExtensions.kt | 25 +++++++++++ 5 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt new file mode 100644 index 000000000..5dd9bf59d --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt @@ -0,0 +1,25 @@ +package it.niedermann.owncloud.notes.accountswitcher + +import com.owncloud.android.lib.resources.users.Status +import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository +import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment + +enum class AccountSwitcherBottomSheetTag(tag: String) { + ONLINE_STATUS("fragment_set_status"), + MESSAGE_STATUS("fragment_set_status_message"); + + fun fragment( + repository: UserStatusRepository, + currentStatus: Status + ): BrandedBottomSheetDialogFragment { + return when (this) { + ONLINE_STATUS -> { + SetOnlineStatusBottomSheet(repository, currentStatus) + } + + MESSAGE_STATUS -> { + SetStatusMessageBottomSheet(repository, currentStatus) + } + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index cfd20d72f..5234ffc52 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -22,6 +22,8 @@ import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository; +import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding; @@ -29,6 +31,8 @@ import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.share.helper.AvatarLoader; +import it.niedermann.owncloud.notes.util.ActivityExtensionsKt; +import kotlin.Unit; /** * Displays all available {@link Account} entries and provides basic operations for them, like adding or switching @@ -75,6 +79,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { binding.accountHost.setText(Uri.parse(currentLocalAccount.getUrl()).getHost()); AvatarLoader.INSTANCE.load(requireContext(), binding.currentAccountItemAvatar, currentLocalAccount); binding.accountLayout.setOnClickListener((v) -> dismiss()); + binding.onlineStatus.setOnClickListener(v -> { final var setOnlineStatusBottomSheet = new SetOnlineStatusBottomSheet(); setOnlineStatusBottomSheet.show(requireActivity().getSupportFragmentManager(), "fragment_set_status"); @@ -123,6 +128,20 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { return builder.create(); } + private void showBottomSheetDialog(@NonNull AccountSwitcherBottomSheetTag tag) { + ActivityExtensionsKt.ssoAccount(requireActivity(), account -> { + if (account == null) { + return Unit.INSTANCE; + } + final var repository = new UserStatusRepository(requireContext(), account); + + final var fragment = tag.fragment(repository, ) + fragment.show(requireActivity().getSupportFragmentManager(), tag.name()); + dismiss(); + return Unit.INSTANCE; + }); + } + public static DialogFragment newInstance(long currentAccountId) { final var dialog = new AccountSwitcherDialog(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt index d98073d27..6c7a8b452 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt @@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import com.google.android.material.card.MaterialCardView import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.android.sso.helper.SingleAccountHelper import com.owncloud.android.lib.resources.users.Status import com.owncloud.android.lib.resources.users.StatusType import it.niedermann.owncloud.notes.R @@ -27,7 +26,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class SetOnlineStatusBottomSheet : +class SetOnlineStatusBottomSheet( + private val repository: UserStatusRepository, + private val currentStatus: Status +) : BrandedBottomSheetDialogFragment(R.layout.set_online_status_bottom_sheet) { companion object { @@ -36,12 +38,12 @@ class SetOnlineStatusBottomSheet : private lateinit var binding: SetOnlineStatusBottomSheetBinding private var cardViews: Triple? = null - private var repository: UserStatusRepository? = null @SuppressLint("DefaultLocale") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + visualizeStatus(currentStatus.status) initRepository() setupStatusClickListeners() } @@ -62,35 +64,20 @@ class SetOnlineStatusBottomSheet : private fun initRepository() { lifecycleScope.launch(Dispatchers.IO) { - val ssoAccount = - SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) ?: return@launch - repository = UserStatusRepository(requireContext(), ssoAccount) - val currentStatus = - repository?.fetchUserStatus() ?: Status(StatusType.OFFLINE, "", "", -1) - - val capabilities = repository?.getCapabilities() - - if (capabilities?.isUserStatusSupportsBusy == true) { + val capabilities = repository.getCapabilities() + if (capabilities.isUserStatusSupportsBusy) { binding.busyStatus.visibility = View.VISIBLE } else { binding.busyStatus.visibility = View.GONE } - - withContext(Dispatchers.Main) { - updateCurrentStatusViews(currentStatus) - } } } - private fun updateCurrentStatusViews(it: Status) { - visualizeStatus(it.status) - } - private fun setStatus(statusType: StatusType) { lifecycleScope.launch(Dispatchers.IO) { - val result = repository?.setStatusType(statusType) + val result = repository.setStatusType(statusType) withContext(Dispatchers.Main) { - if (result == true) { + if (result) { dismiss() } else { showErrorSnackbar() @@ -140,10 +127,19 @@ class SetOnlineStatusBottomSheet : dndHeadline, invisibleHeadline ) - val color = ContextCompat.getColor(ctx, com.nextcloud.android.common.ui.R.color.high_emphasis_text) + val color = ContextCompat.getColor( + ctx, + com.nextcloud.android.common.ui.R.color.high_emphasis_text + ) headlines.forEach { it.setTextColor(color) } listOf(awayIcon, dndIcon, invisibleIcon).forEach { it.imageTintList = null } - listOf(onlineStatus, awayStatus, busyStatus, dndStatus, invisibleStatus).forEach { it.isChecked = false } + listOf( + onlineStatus, + awayStatus, + busyStatus, + dndStatus, + invisibleStatus + ).forEach { it.isChecked = false } } } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt index 270a11cd5..a120bb516 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt @@ -21,12 +21,10 @@ import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import com.nextcloud.android.sso.helper.SingleAccountHelper import com.owncloud.android.lib.common.utils.Log_OC 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.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.google.GoogleEmojiProvider @@ -64,7 +62,10 @@ 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 : +class SetStatusMessageBottomSheet( + private val repository: UserStatusRepository, + private val currentStatus: Status +) : BrandedBottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet), PredefinedStatusClickListener { companion object { @@ -73,7 +74,6 @@ class SetStatusMessageBottomSheet : private lateinit var binding: SetStatusMessageBottomSheetBinding - private var repository: UserStatusRepository? = null private lateinit var adapter: PredefinedStatusListAdapter private var selectedPredefinedMessageId: String? = null private var clearAt: Long? = -1 @@ -86,14 +86,8 @@ class SetStatusMessageBottomSheet : private fun initRepository() { lifecycleScope.launch(Dispatchers.IO) { - val ssoAccount = - SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext()) ?: return@launch - repository = UserStatusRepository(requireContext(), ssoAccount) - val predefinedStatus = repository?.fetchPredefinedStatuses() ?: arrayListOf() - val currentStatus = repository?.fetchUserStatus() ?: Status(StatusType.OFFLINE, "", "", -1) - + val predefinedStatus = repository.fetchPredefinedStatuses() withContext(Dispatchers.Main) { - updateCurrentStatusViews(currentStatus) initPredefinedStatusAdapter(predefinedStatus) } } @@ -111,7 +105,7 @@ class SetStatusMessageBottomSheet : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initRepository() - + updateCurrentStatusViews(currentStatus) binding.clearStatus.setOnClickListener { clearStatus() } binding.setStatus.setOnClickListener { setStatusMessage() } binding.emoji.setOnClickListener { popup.show() } @@ -264,7 +258,7 @@ class SetStatusMessageBottomSheet : private fun clearStatus() { lifecycleScope.launch(Dispatchers.IO) { - val result = repository?.clearStatus() + val result = repository.clearStatus() dismiss(result) } } @@ -272,12 +266,12 @@ class SetStatusMessageBottomSheet : private fun setStatusMessage() { if (selectedPredefinedMessageId != null) { lifecycleScope.launch(Dispatchers.IO) { - val result = repository?.setPredefinedStatus(selectedPredefinedMessageId!!, clearAt) + val result = repository.setPredefinedStatus(selectedPredefinedMessageId!!, clearAt) dismiss(result) } } else { lifecycleScope.launch(Dispatchers.IO) { - val result = repository?.setCustomStatus( + val result = repository.setCustomStatus( binding.customStatusInput.text.toString(), binding.emoji.text.toString(), clearAt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt new file mode 100644 index 000000000..155f17db8 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt @@ -0,0 +1,25 @@ +package it.niedermann.owncloud.notes.util + +import androidx.core.app.ComponentActivity +import androidx.lifecycle.lifecycleScope +import com.nextcloud.android.sso.helper.SingleAccountHelper +import com.nextcloud.android.sso.model.SingleSignOnAccount +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +fun ComponentActivity.ssoAccount(onCompleted: (SingleSignOnAccount?) -> Unit) { + lifecycleScope.launch(Dispatchers.IO) { + val result = try { + val account = SingleAccountHelper.getCurrentSingleSignOnAccount(this@ssoAccount) + account + } catch (t: Throwable) { + Log_OC.e("ComponentActivityExtension", "cant get sso account: $t") + null + } + withContext(Dispatchers.Main) { + onCompleted(result) + } + } +} From 9628af5d32194bd92308c024d8841a627dd6f148 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 14:09:38 +0200 Subject: [PATCH 07/15] use showBottomSheetDialog Signed-off-by: alperozturk --- .../AccountSwitcherDialog.java | 27 ++++++++++++------- .../repository/UserStatusRepository.kt | 4 +-- .../owncloud/notes/util/ActivityExtensions.kt | 13 +++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index 5234ffc52..57376d605 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -20,10 +20,12 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository; -import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment; import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.DialogAccountSwitcherBinding; @@ -45,6 +47,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { private DialogAccountSwitcherBinding binding; private AccountSwitcherListener accountSwitcherListener; private long currentAccountId; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); @Override public void onAttach(@NonNull Context context) { @@ -81,15 +84,11 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { binding.accountLayout.setOnClickListener((v) -> dismiss()); binding.onlineStatus.setOnClickListener(v -> { - final var setOnlineStatusBottomSheet = new SetOnlineStatusBottomSheet(); - setOnlineStatusBottomSheet.show(requireActivity().getSupportFragmentManager(), "fragment_set_status"); - dismiss(); + showBottomSheetDialog(AccountSwitcherBottomSheetTag.ONLINE_STATUS); }); binding.statusMessage.setOnClickListener(v -> { - final var setStatusMessageDialog = new SetStatusMessageBottomSheet(); - setStatusMessageDialog.show(requireActivity().getSupportFragmentManager(), "fragment_set_status_message"); - dismiss(); + showBottomSheetDialog(AccountSwitcherBottomSheetTag.MESSAGE_STATUS); }); final var adapter = new AccountSwitcherAdapter((localAccount -> { @@ -134,10 +133,18 @@ private void showBottomSheetDialog(@NonNull AccountSwitcherBottomSheetTag tag) { return Unit.INSTANCE; } final var repository = new UserStatusRepository(requireContext(), account); + executor.execute(() -> { + final var currentStatus = repository.fetchUserStatus(); + if (currentStatus == null) { + return; + } - final var fragment = tag.fragment(repository, ) - fragment.show(requireActivity().getSupportFragmentManager(), tag.name()); - dismiss(); + requireActivity().runOnUiThread(() -> { + final var fragment = tag.fragment(repository, currentStatus); + fragment.show(requireActivity().getSupportFragmentManager(), tag.name()); + dismiss(); + }); + }); return Unit.INSTANCE; }); } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt index 84c0281dd..87f8355e1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -105,9 +105,9 @@ class UserStatusRepository( } } - suspend fun fetchUserStatus(): Status? = withContext(Dispatchers.IO) { + fun fetchUserStatus(): Status? { val offlineStatus = Status(StatusType.OFFLINE, "", "", -1) - try { + return try { val call = api.fetchUserStatus() val response = call.execute() if (response.isSuccessful) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt index 155f17db8..548a7d73c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt @@ -9,6 +9,19 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +/** + * Retrieves the currently active Single Sign-On (SSO) account associated with this [ComponentActivity]. + * + * This function runs asynchronously using a coroutine: + * - The SSO account lookup is performed on the **IO dispatcher** (background thread). + * - Once the result is available, the [onCompleted] callback is invoked on the **main thread**. + * + * If fetching the account fails for any reason (e.g., no account found, SSO error, etc.), + * the callback will receive `null` and an error will be logged. + * + * @param onCompleted A callback that receives the retrieved [SingleSignOnAccount], + * or `null` if no valid account was found. + */ fun ComponentActivity.ssoAccount(onCompleted: (SingleSignOnAccount?) -> Unit) { lifecycleScope.launch(Dispatchers.IO) { val result = try { From 2daad49a703a6d31a4cc3d6b4b6499398494a5bd Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 14:35:28 +0200 Subject: [PATCH 08/15] use showBottomSheetDialog Signed-off-by: alperozturk --- .../AccountSwitcherDialog.java | 58 +++++++++++++------ .../res/layout/dialog_account_switcher.xml | 42 +++++++++++--- app/src/main/res/values-v31/dimens.xml | 2 +- app/src/main/res/values/dimens.xml | 4 +- app/src/main/res/values/strings.xml | 2 + 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index 57376d605..b29ba9122 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -14,11 +14,13 @@ import android.graphics.drawable.LayerDrawable; import android.net.Uri; import android.os.Bundle; +import android.view.View; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.owncloud.android.lib.resources.users.Status; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -33,6 +35,7 @@ import it.niedermann.owncloud.notes.persistence.NotesRepository; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.share.helper.AvatarLoader; +import it.niedermann.owncloud.notes.shared.util.DisplayUtils; import it.niedermann.owncloud.notes.util.ActivityExtensionsKt; import kotlin.Unit; @@ -47,6 +50,8 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { private DialogAccountSwitcherBinding binding; private AccountSwitcherListener accountSwitcherListener; private long currentAccountId; + private UserStatusRepository repository; + private Status currentStatus; private final ExecutorService executor = Executors.newSingleThreadExecutor(); @Override @@ -67,6 +72,34 @@ public void onAttach(@NonNull Context context) { } repo = NotesRepository.getInstance(requireContext()); + initRepositoryAndFetchCurrentStatus(); + } + + private void initRepositoryAndFetchCurrentStatus() { + ActivityExtensionsKt.ssoAccount(requireActivity(), account -> { + if (account != null) { + repository = new UserStatusRepository(requireContext(), account); + } else { + DisplayUtils.showSnackMessage(requireView(), R.string.account_switch_dialog_status_fetching_error_message); + } + executor.execute(() -> { + currentStatus = repository.fetchUserStatus(); + requireActivity().runOnUiThread(() -> { + final var message = currentStatus.getMessage(); + if (message != null) { + binding.accountStatus.setVisibility(View.VISIBLE); + binding.accountStatus.setText(message); + } + + final var emoji = currentStatus.getIcon(); + if (emoji != null) { + binding.accountStatusEmoji.setVisibility(View.VISIBLE); + binding.accountStatusEmoji.setText(emoji); + } + }); + }); + return Unit.INSTANCE; + }); } @NonNull @@ -128,25 +161,14 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { } private void showBottomSheetDialog(@NonNull AccountSwitcherBottomSheetTag tag) { - ActivityExtensionsKt.ssoAccount(requireActivity(), account -> { - if (account == null) { - return Unit.INSTANCE; - } - final var repository = new UserStatusRepository(requireContext(), account); - executor.execute(() -> { - final var currentStatus = repository.fetchUserStatus(); - if (currentStatus == null) { - return; - } + if (repository == null || currentStatus == null) { + DisplayUtils.showSnackMessage(requireView(), R.string.account_switch_dialog_status_fetching_error_message); + return; + } - requireActivity().runOnUiThread(() -> { - final var fragment = tag.fragment(repository, currentStatus); - fragment.show(requireActivity().getSupportFragmentManager(), tag.name()); - dismiss(); - }); - }); - return Unit.INSTANCE; - }); + final var fragment = tag.fragment(repository, currentStatus); + fragment.show(requireActivity().getSupportFragmentManager(), tag.name()); + dismiss(); } public static DialogFragment newInstance(long currentAccountId) { diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml index 0f10e5d1d..4b3eaa341 100644 --- a/app/src/main/res/layout/dialog_account_switcher.xml +++ b/app/src/main/res/layout/dialog_account_switcher.xml @@ -22,15 +22,31 @@ android:padding="@dimen/spacer_2x" android:paddingHorizontal="@dimen/spacer_1x"> - + android:layout_marginStart="@dimen/spacer_activity_sides"> + + + + + + + diff --git a/app/src/main/res/values-v31/dimens.xml b/app/src/main/res/values-v31/dimens.xml index 559394ce3..a6cdbf9fe 100644 --- a/app/src/main/res/values-v31/dimens.xml +++ b/app/src/main/res/values-v31/dimens.xml @@ -15,7 +15,7 @@ 24dp 40dp - 36dp + 40dp 0dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 1cf1f054c..6e65c7b17 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -25,7 +25,7 @@ 180dp 100dp - 36dp + 40dp 196dip 48dp @@ -51,6 +51,8 @@ 16sp 14sp + 14sp + 12sp 16sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4e2c9efb..07140003f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -537,4 +537,6 @@ Mute all notifications Appear offline Failed to set status! + Failed to fetch status, please try again. + Online From ab9a3d84106c2929ed048a9b45eafbec0b06c1db Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 15:35:04 +0200 Subject: [PATCH 09/15] add missing license Signed-off-by: alperozturk --- .../accountswitcher/AccountSwitcherBottomSheetTag.kt | 6 ++++++ .../notes/accountswitcher/SetOnlineStatusBottomSheet.kt | 9 ++++++++- .../accountswitcher/repository/UserStatusRepository.kt | 6 ++++++ .../owncloud/notes/persistence/sync/UserStatusAPI.kt | 8 +++++++- .../niedermann/owncloud/notes/util/ActivityExtensions.kt | 6 ++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt index 5dd9bf59d..fcc4a95cb 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ package it.niedermann.owncloud.notes.accountswitcher import com.owncloud.android.lib.resources.users.Status diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt index 6c7a8b452..a32276d23 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt @@ -1,6 +1,13 @@ +/* + * 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 it.niedermann.owncloud.notes.accountswitcher - import android.annotation.SuppressLint import android.os.Bundle import android.util.Log diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt index 87f8355e1..bc816dd78 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/repository/UserStatusRepository.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ package it.niedermann.owncloud.notes.accountswitcher.repository import android.content.Context diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt index 73c2250fc..c408c8283 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/UserStatusAPI.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ package it.niedermann.owncloud.notes.persistence.sync import com.owncloud.android.lib.resources.users.PredefinedStatus @@ -15,7 +21,7 @@ interface UserStatusAPI { fun fetchUserStatus(): Call> @DELETE("user_status/message?format=json") - fun clearStatusMessage(): Call> + fun clearStatusMessage(): Call>> @GET("predefined_statuses?format=json") fun fetchPredefinedStatuses(): Call>> diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt index 548a7d73c..57e0cb361 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/ActivityExtensions.kt @@ -1,3 +1,9 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ package it.niedermann.owncloud.notes.util import androidx.core.app.ComponentActivity From 4a1deea69edc89699a733099476eae700818003c Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 15:39:59 +0200 Subject: [PATCH 10/15] move package places Signed-off-by: alperozturk --- .../notes/accountswitcher/AccountSwitcherDialog.java | 2 ++ .../accountSwitcher}/AccountSwitcherAdapter.java | 2 +- .../accountSwitcher}/AccountSwitcherViewHolder.java | 2 +- .../{ => predefinedStatus}/PredefinedStatusClickListener.kt | 2 +- .../{ => predefinedStatus}/PredefinedStatusListAdapter.kt | 2 +- .../{ => predefinedStatus}/PredefinedStatusViewHolder.kt | 2 +- .../{ => bottomSheet}/AccountSwitcherBottomSheetTag.kt | 4 ++-- .../{ => bottomSheet}/SetOnlineStatusBottomSheet.kt | 2 +- .../{ => bottomSheet}/SetStatusMessageBottomSheet.kt | 6 +++--- 9 files changed, 13 insertions(+), 11 deletions(-) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/{ => adapter/accountSwitcher}/AccountSwitcherAdapter.java (95%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/{ => adapter/accountSwitcher}/AccountSwitcherViewHolder.java (94%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/{ => predefinedStatus}/PredefinedStatusClickListener.kt (83%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/{ => predefinedStatus}/PredefinedStatusListAdapter.kt (93%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/{ => predefinedStatus}/PredefinedStatusViewHolder.kt (95%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/{ => bottomSheet}/AccountSwitcherBottomSheetTag.kt (93%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/{ => bottomSheet}/SetOnlineStatusBottomSheet.kt (98%) rename app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/{ => bottomSheet}/SetStatusMessageBottomSheet.kt (97%) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index b29ba9122..4f2787fec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -27,6 +27,8 @@ import it.niedermann.owncloud.notes.NotesApplication; import it.niedermann.owncloud.notes.R; +import it.niedermann.owncloud.notes.accountswitcher.adapter.accountSwitcher.AccountSwitcherAdapter; +import it.niedermann.owncloud.notes.accountswitcher.bottomSheet.AccountSwitcherBottomSheetTag; import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository; import it.niedermann.owncloud.notes.branding.BrandedDialogFragment; import it.niedermann.owncloud.notes.branding.BrandingUtil; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherAdapter.java similarity index 95% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherAdapter.java index 660c22154..a2af6a748 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherAdapter.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2020 Stefan Niedermann * SPDX-License-Identifier: GPL-3.0-or-later */ -package it.niedermann.owncloud.notes.accountswitcher; +package it.niedermann.owncloud.notes.accountswitcher.adapter.accountSwitcher; import android.view.LayoutInflater; import android.view.ViewGroup; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherViewHolder.java similarity index 94% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherViewHolder.java index d128e07ef..bb4878fc3 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/accountSwitcher/AccountSwitcherViewHolder.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2020-2021 Stefan Niedermann * SPDX-License-Identifier: GPL-3.0-or-later */ -package it.niedermann.owncloud.notes.accountswitcher; +package it.niedermann.owncloud.notes.accountswitcher.adapter.accountSwitcher; import android.net.Uri; import android.view.View; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt similarity index 83% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt index b4de418b5..5375eaa9f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package it.niedermann.owncloud.notes.accountswitcher.adapter +package it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus import com.owncloud.android.lib.resources.users.PredefinedStatus diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusListAdapter.kt similarity index 93% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusListAdapter.kt index 815708a31..1a1c551ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusListAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusListAdapter.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package it.niedermann.owncloud.notes.accountswitcher.adapter +package it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus import android.content.Context import android.view.LayoutInflater diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt similarity index 95% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt index 19965fda1..e8e76f72c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/PredefinedStatusViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package it.niedermann.owncloud.notes.accountswitcher.adapter +package it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus import android.content.Context import androidx.recyclerview.widget.RecyclerView diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/AccountSwitcherBottomSheetTag.kt similarity index 93% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/AccountSwitcherBottomSheetTag.kt index fcc4a95cb..08f8ba62e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherBottomSheetTag.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/AccountSwitcherBottomSheetTag.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-3.0-or-later */ -package it.niedermann.owncloud.notes.accountswitcher +package it.niedermann.owncloud.notes.accountswitcher.bottomSheet import com.owncloud.android.lib.resources.users.Status import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository @@ -28,4 +28,4 @@ enum class AccountSwitcherBottomSheetTag(tag: String) { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt similarity index 98% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt index a32276d23..778d73005 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetOnlineStatusBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt @@ -6,7 +6,7 @@ * * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package it.niedermann.owncloud.notes.accountswitcher +package it.niedermann.owncloud.notes.accountswitcher.bottomSheet import android.annotation.SuppressLint import android.os.Bundle diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt similarity index 97% rename from app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt rename to app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt index a120bb516..54087c7e4 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt @@ -7,7 +7,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ -package it.niedermann.owncloud.notes.accountswitcher +package it.niedermann.owncloud.notes.accountswitcher.bottomSheet import android.annotation.SuppressLint import android.content.Context @@ -31,8 +31,8 @@ import com.vanniktech.emoji.google.GoogleEmojiProvider import com.vanniktech.emoji.installDisableKeyboardInput import com.vanniktech.emoji.installForceSingleEmoji import it.niedermann.owncloud.notes.R -import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusClickListener -import it.niedermann.owncloud.notes.accountswitcher.adapter.PredefinedStatusListAdapter +import it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus.PredefinedStatusClickListener +import it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus.PredefinedStatusListAdapter import it.niedermann.owncloud.notes.accountswitcher.repository.UserStatusRepository import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialogFragment import it.niedermann.owncloud.notes.branding.BrandingUtil From ed92712bd1641307baf393f2985461e37db6106e Mon Sep 17 00:00:00 2001 From: alperozturk Date: Fri, 24 Oct 2025 16:05:18 +0200 Subject: [PATCH 11/15] fix tests Signed-off-by: alperozturk --- .../it/niedermann/owncloud/notes/shared/model/Capabilities.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java index 1e62e41c5..d4f29d364 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/Capabilities.java @@ -156,7 +156,7 @@ public void setDirectEditingAvailable(boolean directEditingAvailable) { } public boolean isUserStatusSupportsBusy() { - return userStatusSupportsBusy; + return userStatusSupportsBusy != null && userStatusSupportsBusy; } public void setUserStatusSupportsBusy(boolean value) { From ed35a10e6970977a91a38521803cd95202cbdd0e Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 27 Oct 2025 16:10:17 +0100 Subject: [PATCH 12/15] update license header Signed-off-by: alperozturk --- .../predefinedStatus/PredefinedStatusClickListener.kt | 7 +++---- .../predefinedStatus/PredefinedStatusListAdapter.kt | 4 ++-- .../predefinedStatus/PredefinedStatusViewHolder.kt | 4 ++-- .../bottomSheet/SetOnlineStatusBottomSheet.kt | 8 +++----- .../bottomSheet/SetStatusMessageBottomSheet.kt | 11 +++++------ 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt index 5375eaa9f..891397457 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt @@ -1,9 +1,8 @@ /* - * Nextcloud - Android Client + * Nextcloud Talk - Android Client * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2022 Tim Krรผger * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-License-Identifier: GPL-3.0-or-later */ package it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt index e8e76f72c..e587a2fea 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt @@ -1,9 +1,9 @@ /* - * Nextcloud - Android Client + * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-License-Identifier: GPL-3.0-or-later */ package it.niedermann.owncloud.notes.accountswitcher.adapter.predefinedStatus diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt index 778d73005..7b1ec14ec 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt @@ -1,10 +1,8 @@ /* - * Nextcloud Android client application + * Nextcloud Talk - Android Client * - * @author Tobias Kaminsky - * Copyright (C) 2020 Nextcloud GmbH - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2025 Sowjanya Kota + * SPDX-License-Identifier: GPL-3.0-or-later */ package it.niedermann.owncloud.notes.accountswitcher.bottomSheet diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt index 54087c7e4..9a1d6e49b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt @@ -1,12 +1,11 @@ /* - * Nextcloud Android client application + * Nextcloud Talk - Android Client * - * @author Tobias Kaminsky - * Copyright (C) 2020 Nextcloud GmbH - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-License-Identifier: GPL-3.0-or-later */ - package it.niedermann.owncloud.notes.accountswitcher.bottomSheet import android.annotation.SuppressLint From f242b1a2d30c209a964e013d9ef04b46613d3273 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 27 Oct 2025 16:13:56 +0100 Subject: [PATCH 13/15] update license header Signed-off-by: alperozturk --- app/src/main/res/drawable/borderless_btn.xml | 6 +++--- app/src/main/res/layout/predefined_status.xml | 7 +++---- app/src/main/res/layout/set_online_status_bottom_sheet.xml | 7 +++---- .../main/res/layout/set_status_message_bottom_sheet.xml | 7 +++---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/drawable/borderless_btn.xml b/app/src/main/res/drawable/borderless_btn.xml index b56094c0d..e6384d893 100644 --- a/app/src/main/res/drawable/borderless_btn.xml +++ b/app/src/main/res/drawable/borderless_btn.xml @@ -1,9 +1,9 @@ diff --git a/app/src/main/res/layout/predefined_status.xml b/app/src/main/res/layout/predefined_status.xml index 9ef5b054c..20b1c5be4 100644 --- a/app/src/main/res/layout/predefined_status.xml +++ b/app/src/main/res/layout/predefined_status.xml @@ -1,11 +1,10 @@ + ~ SPDX-FileCopyrightText: 2020 Andy Scherzinger ~ SPDX-FileCopyrightText: 2020 Tobias Kaminsky - ~ SPDX-FileCopyrightText: 2020 Nextcloud GmbH - ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + ~ SPDX-License-Identifier: GPL-3.0-or-later --> + ~ SPDX-FileCopyrightText: 2020 Andy Scherzinger ~ SPDX-FileCopyrightText: 2020 Tobias Kaminsky - ~ SPDX-FileCopyrightText: 2020 Nextcloud GmbH - ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + ~ SPDX-License-Identifier: GPL-3.0-or-later --> Date: Tue, 28 Oct 2025 14:32:02 +0100 Subject: [PATCH 14/15] add status icon Signed-off-by: alperozturk --- .../AccountSwitcherDialog.java | 6 ++++++ .../notes/util/StatusTypeExtensions.kt | 20 +++++++++++++++++++ .../res/layout/dialog_account_switcher.xml | 11 ++++++++-- app/src/main/res/values/dimens.xml | 1 + 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/util/StatusTypeExtensions.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java index 4f2787fec..546fba440 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/AccountSwitcherDialog.java @@ -21,6 +21,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.owncloud.android.lib.resources.users.Status; +import com.owncloud.android.lib.resources.users.StatusType; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -39,6 +40,7 @@ import it.niedermann.owncloud.notes.share.helper.AvatarLoader; import it.niedermann.owncloud.notes.shared.util.DisplayUtils; import it.niedermann.owncloud.notes.util.ActivityExtensionsKt; +import it.niedermann.owncloud.notes.util.StatusTypeExtensionsKt; import kotlin.Unit; /** @@ -97,6 +99,10 @@ private void initRepositoryAndFetchCurrentStatus() { if (emoji != null) { binding.accountStatusEmoji.setVisibility(View.VISIBLE); binding.accountStatusEmoji.setText(emoji); + } else { + final var status = currentStatus.getStatus(); + binding.accountStatusIcon.setVisibility(View.VISIBLE); + binding.accountStatusIcon.setImageResource(StatusTypeExtensionsKt.getImageResource(status)); } }); }); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/StatusTypeExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/StatusTypeExtensions.kt new file mode 100644 index 000000000..760890deb --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/StatusTypeExtensions.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.util + +import com.owncloud.android.lib.resources.users.StatusType +import it.niedermann.owncloud.notes.R + +val StatusType.imageResource: Int + get() = when (this) { + StatusType.ONLINE -> R.drawable.ic_user_status_online + StatusType.OFFLINE -> R.drawable.ic_user_status_busy + StatusType.DND -> R.drawable.ic_user_status_dnd + StatusType.AWAY -> R.drawable.ic_user_status_away + StatusType.INVISIBLE -> R.drawable.ic_user_status_invisible + StatusType.BUSY -> R.drawable.ic_user_status_busy + } diff --git a/app/src/main/res/layout/dialog_account_switcher.xml b/app/src/main/res/layout/dialog_account_switcher.xml index 4b3eaa341..beb9add50 100644 --- a/app/src/main/res/layout/dialog_account_switcher.xml +++ b/app/src/main/res/layout/dialog_account_switcher.xml @@ -41,11 +41,18 @@ android:layout_gravity="bottom|end" android:textSize="@dimen/emoji_size" android:visibility="gone" - android:translationX="2dp" - android:translationY="2dp" tools:visibility="visible" tools:text="๐Ÿ˜€" android:background="@android:color/transparent" /> + + 100dp 40dp + 16dp 196dip 48dp From 71492ac78e98fe361bb9b1c54e34957abda3cba9 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 29 Oct 2025 09:18:22 +0100 Subject: [PATCH 15/15] update license header Signed-off-by: alperozturk --- .../adapter/predefinedStatus/PredefinedStatusClickListener.kt | 2 +- .../adapter/predefinedStatus/PredefinedStatusListAdapter.kt | 2 +- .../adapter/predefinedStatus/PredefinedStatusViewHolder.kt | 2 +- .../accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt | 2 +- .../accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt | 2 +- app/src/main/res/drawable/borderless_btn.xml | 2 +- app/src/main/res/layout/predefined_status.xml | 2 +- app/src/main/res/layout/set_online_status_bottom_sheet.xml | 2 +- app/src/main/res/layout/set_status_message_bottom_sheet.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt index 891397457..2cba19c32 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusClickListener.kt @@ -1,5 +1,5 @@ /* - * Nextcloud Talk - Android Client + * Nextcloud Notes - Android Client * * SPDX-FileCopyrightText: 2022 Tim Krรผger * SPDX-FileCopyrightText: 2020 Nextcloud GmbH diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt index e587a2fea..c6774bf05 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/adapter/predefinedStatus/PredefinedStatusViewHolder.kt @@ -1,5 +1,5 @@ /* - * Nextcloud Talk - Android Client + * Nextcloud Notes - Android Client * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt index 7b1ec14ec..5e443a104 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetOnlineStatusBottomSheet.kt @@ -1,5 +1,5 @@ /* - * Nextcloud Talk - Android Client + * Nextcloud Notes - Android Client * * SPDX-FileCopyrightText: 2025 Sowjanya Kota * SPDX-License-Identifier: GPL-3.0-or-later diff --git a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt index 9a1d6e49b..2ef18c5d0 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/accountswitcher/bottomSheet/SetStatusMessageBottomSheet.kt @@ -1,5 +1,5 @@ /* - * Nextcloud Talk - Android Client + * Nextcloud Notes - Android Client * * SPDX-FileCopyrightText: 2022-2023 Marcel Hibbe * SPDX-FileCopyrightText: 2020 Nextcloud GmbH diff --git a/app/src/main/res/drawable/borderless_btn.xml b/app/src/main/res/drawable/borderless_btn.xml index e6384d893..9f2ccb856 100644 --- a/app/src/main/res/drawable/borderless_btn.xml +++ b/app/src/main/res/drawable/borderless_btn.xml @@ -1,6 +1,6 @@