浏览代码

wip

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
tobiasKaminsky 4 年之前
父节点
当前提交
c09fa8c944
共有 22 个文件被更改,包括 865 次插入163 次删除
  1. 二进制
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png
  2. 20 3
      src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt
  3. 19 1
      src/androidTest/java/com/owncloud/android/AbstractIT.java
  4. 35 8
      src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  5. 15 15
      src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt
  6. 0 24
      src/main/java/com/nextcloud/client/account/Status.kt
  7. 1 1
      src/main/java/com/nextcloud/client/core/Task.kt
  8. 56 20
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  9. 36 7
      src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
  10. 163 0
      src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt
  11. 1 0
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  12. 28 0
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  13. 4 4
      src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java
  14. 44 33
      src/main/java/com/owncloud/android/ui/StatusDrawable.java
  15. 72 0
      src/main/java/com/owncloud/android/ui/asynctasks/RetrieveStatusAsyncTask.java
  16. 7 46
      src/main/java/com/owncloud/android/utils/BitmapUtils.java
  17. 0 1
      src/main/res/drawable/ic_talk.xml
  18. 32 0
      src/main/res/drawable/ic_user_status_invisible.xml
  19. 23 0
      src/main/res/drawable/online_status.xml
  20. 58 0
      src/main/res/layout/custom_status.xml
  21. 239 0
      src/main/res/layout/dialog_set_status.xml
  22. 12 0
      src/main/res/values/strings.xml

二进制
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png


+ 20 - 3
src/main/java/com/nextcloud/client/account/StatusType.kt → src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt

@@ -20,8 +20,25 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-package com.nextcloud.client.account
+package com.nextcloud.ui
 
-enum class StatusType {
-    Online, Offline, Dnd, Away, Unknown
+import androidx.test.espresso.intent.rule.IntentsTestRule
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import org.junit.Rule
+import org.junit.Test
+
+class SetStatusDialogFragmentIT : AbstractIT() {
+    @get:Rule
+    var activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false)
+
+    @Test
+    fun open() {
+        val sut = SetStatusDialogFragment.newInstance(user)
+        val activity = activityRule.launchActivity(null)
+
+        sut.show(activity.supportFragmentManager, "")
+
+        longSleep()
+    }
 }

+ 19 - 1
src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -49,8 +49,10 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
+import java.util.Objects;
 
 import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
 import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -369,16 +371,32 @@ public abstract class AbstractIT {
     }
 
     protected void screenshot(View view) {
-        Screenshot.snap(view).setName(createName()).record();
+        screenshot(view, "");
+    }
+
+    protected void screenshot(View view, String prefix) {
+        Screenshot.snap(view).setName(createName(prefix)).record();
     }
 
     protected void screenshot(Activity sut) {
         Screenshot.snapActivity(sut).setName(createName()).record();
     }
 
+    protected void screenshot(DialogFragment dialogFragment, String prefix) {
+        screenshot(Objects.requireNonNull(dialogFragment.requireDialog().getWindow()).getDecorView(), prefix);
+    }
+
     private String createName() {
+        return createName("");
+    }
+
+    private String createName(String prefix) {
         String name = TestNameDetector.getTestClass() + "_" + TestNameDetector.getTestName();
 
+        if (!TextUtils.isEmpty(prefix)) {
+            name = name + "_" + prefix;
+        }
+
         if (!DARK_MODE.isEmpty()) {
             name = name + "_" + DARK_MODE;
         }

+ 35 - 8
src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java

@@ -22,7 +22,6 @@
 
 package com.owncloud.android.ui.dialog;
 
-import android.Manifest;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.Intent;
@@ -43,6 +42,8 @@ import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.lib.resources.status.OwnCloudVersion;
+import com.owncloud.android.lib.resources.users.Status;
+import com.owncloud.android.lib.resources.users.StatusType;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
 import com.owncloud.android.utils.ScreenshotTest;
 
@@ -55,7 +56,6 @@ import java.util.Objects;
 
 import androidx.fragment.app.DialogFragment;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -63,10 +63,6 @@ public class DialogFragmentIT extends AbstractIT {
     @Rule public IntentsTestRule<FileDisplayActivity> activityRule =
         new IntentsTestRule<>(FileDisplayActivity.class, true, false);
 
-    @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
-
     @Test
     @ScreenshotTest
     public void testRenameFileDialog() {
@@ -163,7 +159,36 @@ public class DialogFragmentIT extends AbstractIT {
                                                                        new OwnCloudAccount(newAccount, targetContext),
                                                                        new Server(URI.create("https://server.com"),
                                                                                   OwnCloudVersion.nextcloud_20)));
-        showDialog(sut);
+        FileDisplayActivity activity = showDialog(sut);
+
+        activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.dnd,
+                                                              "Busy fixing 🐛…",
+                                                              "",
+                                                              -1)));
+        shortSleep();
+        screenshot(sut, "dnd");
+
+        activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.online,
+                                                              "",
+                                                              "",
+                                                              -1)));
+        shortSleep();
+        screenshot(sut, "online");
+
+        activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.online,
+                                                              "Let's have some fun",
+                                                              "🎉",
+                                                              -1)));
+        shortSleep();
+        screenshot(sut, "fun");
+
+        activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.offline, "", "", -1)));
+        shortSleep();
+        screenshot(sut, "offline");
+
+        activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.away, "Vacation", "🌴", -1)));
+        shortSleep();
+        screenshot(sut, "away");
     }
 
     @Test
@@ -195,7 +220,7 @@ public class DialogFragmentIT extends AbstractIT {
         showDialog(sut);
     }
 
-    private void showDialog(DialogFragment dialog) {
+    private FileDisplayActivity showDialog(DialogFragment dialog) {
         Intent intent = new Intent(targetContext, FileDisplayActivity.class);
         FileDisplayActivity sut = activityRule.launchActivity(intent);
 
@@ -208,6 +233,8 @@ public class DialogFragmentIT extends AbstractIT {
         hideCursors(viewGroup);
 
         screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
+
+        return sut;
     }
 
     private void hideCursors(ViewGroup viewGroup) {

+ 15 - 15
src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt

@@ -25,9 +25,9 @@ import android.graphics.BitmapFactory
 import androidx.test.espresso.intent.rule.IntentsTestRule
 import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread
 import com.nextcloud.client.TestActivity
-import com.nextcloud.client.account.StatusType
 import com.owncloud.android.AbstractIT
 import com.owncloud.android.R
+import com.owncloud.android.lib.resources.users.StatusType
 import com.owncloud.android.ui.TextDrawable
 import com.owncloud.android.utils.BitmapUtils
 import com.owncloud.android.utils.DisplayUtils
@@ -80,98 +80,98 @@ class AvatarIT : AbstractIT() {
 
         runOnUiThread {
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(paulette, StatusType.Online, "😘", targetContext),
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.online, "😘", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(christine, StatusType.Online, "☁️", targetContext),
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "☁️", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(christine, StatusType.Online, "🌴️", targetContext),
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "🌴️", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(christine, StatusType.Online, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.online, "", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(paulette, StatusType.Dnd, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.dnd, "", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(christine, StatusType.Away, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.away, "", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(paulette, StatusType.Offline, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.offline, "", targetContext),
                 width * 2,
                 1,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Online, "😘", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "😘", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Online, "☁️", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "☁️", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Online, "🌴️", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "🌴️", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Online, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.online, "", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Dnd, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.dnd, "", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Away, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.away, "", targetContext),
                 width,
                 2,
                 targetContext
             )
 
             fragment.addBitmap(
-                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.Offline, "", targetContext),
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.offline, "", targetContext),
                 width,
                 2,
                 targetContext

+ 0 - 24
src/main/java/com/nextcloud/client/account/Status.kt

@@ -1,24 +0,0 @@
-/*
- *
- * Nextcloud Android client application
- *
- * @author Tobias Kaminsky
- * Copyright (C) 2020 Tobias Kaminsky
- * Copyright (C) 2020 Nextcloud GmbH
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-package com.nextcloud.client.account
-
-internal class Status(val status: Enum<StatusType>, val message: String, val icon: String, val clearAt: String)

+ 1 - 1
src/main/java/com/nextcloud/client/core/Task.kt

@@ -22,7 +22,7 @@ package com.nextcloud.client.core
 import java.util.concurrent.atomic.AtomicBoolean
 
 /**
- * This is a wrapper for a task function runing in background.
+ * This is a wrapper for a task function running in background.
  * It executes task function and handles result or error delivery.
  */
 @Suppress("LongParameterList")

+ 56 - 20
src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -27,6 +27,8 @@ import com.nextcloud.client.logger.ui.LogsActivity;
 import com.nextcloud.client.media.PlayerService;
 import com.nextcloud.client.onboarding.FirstRunActivity;
 import com.nextcloud.client.onboarding.WhatsNewActivity;
+import com.nextcloud.ui.ChooseAccountDialogFragment;
+import com.nextcloud.ui.SetStatusDialogFragment;
 import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.authentication.DeepLinkLoginActivity;
 import com.owncloud.android.files.BootupBroadcastReceiver;
@@ -138,26 +140,60 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract LocalFileListFragment localFileListFragment();
     @ContributesAndroidInjector abstract OCFileListFragment ocFileListFragment();
     @ContributesAndroidInjector abstract FileDetailActivitiesFragment fileDetailActivitiesFragment();
-    @ContributesAndroidInjector abstract FileDetailSharingFragment fileDetailSharingFragment();
-    @ContributesAndroidInjector abstract ChooseTemplateDialogFragment chooseTemplateDialogFragment();
-    @ContributesAndroidInjector abstract AccountRemovalConfirmationDialog accountRemovalConfirmationDialog();
-
-    @ContributesAndroidInjector abstract ChooseRichDocumentsTemplateDialogFragment chooseRichDocumentsTemplateDialogFragment();
-    @ContributesAndroidInjector abstract ContactsBackupFragment contactsBackupFragment();
-    @ContributesAndroidInjector abstract PreviewImageFragment previewImageFragment();
-    @ContributesAndroidInjector abstract ContactListFragment chooseContactListFragment();
-    @ContributesAndroidInjector abstract PreviewMediaFragment previewMediaFragment();
-    @ContributesAndroidInjector abstract PreviewTextFragment previewTextFragment();
-
-    @ContributesAndroidInjector abstract PreviewTextFileFragment previewTextFileFragment();
-    @ContributesAndroidInjector abstract PreviewTextStringFragment previewTextStringFragment();
-    @ContributesAndroidInjector abstract PhotoFragment photoFragment();
-
-    @ContributesAndroidInjector abstract MultipleAccountsDialog multipleAccountsDialog();
-    @ContributesAndroidInjector abstract ReceiveExternalFilesActivity.DialogInputUploadFilename dialogInputUploadFilename();
-
-    @ContributesAndroidInjector abstract FileUploader fileUploader();
-    @ContributesAndroidInjector abstract FileDownloader fileDownloader();
+
+    @ContributesAndroidInjector
+    abstract FileDetailSharingFragment fileDetailSharingFragment();
+
+    @ContributesAndroidInjector
+    abstract ChooseTemplateDialogFragment chooseTemplateDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract AccountRemovalConfirmationDialog accountRemovalConfirmationDialog();
+
+    @ContributesAndroidInjector
+    abstract ChooseRichDocumentsTemplateDialogFragment chooseRichDocumentsTemplateDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract ContactsBackupFragment contactsBackupFragment();
+
+    @ContributesAndroidInjector
+    abstract PreviewImageFragment previewImageFragment();
+
+    @ContributesAndroidInjector
+    abstract ContactListFragment chooseContactListFragment();
+
+    @ContributesAndroidInjector
+    abstract PreviewMediaFragment previewMediaFragment();
+
+    @ContributesAndroidInjector
+    abstract PreviewTextFragment previewTextFragment();
+
+    @ContributesAndroidInjector
+    abstract ChooseAccountDialogFragment chooseAccountDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract SetStatusDialogFragment setStatusDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract PreviewTextFileFragment previewTextFileFragment();
+
+    @ContributesAndroidInjector
+    abstract PreviewTextStringFragment previewTextStringFragment();
+
+    @ContributesAndroidInjector
+    abstract PhotoFragment photoFragment();
+
+    @ContributesAndroidInjector
+    abstract MultipleAccountsDialog multipleAccountsDialog();
+
+    @ContributesAndroidInjector
+    abstract ReceiveExternalFilesActivity.DialogInputUploadFilename dialogInputUploadFilename();
+
+    @ContributesAndroidInjector
+    abstract FileUploader fileUploader();
+
+    @ContributesAndroidInjector
+    abstract FileDownloader fileDownloader();
 
     @ContributesAndroidInjector abstract BootupBroadcastReceiver bootupBroadcastReceiver();
     @ContributesAndroidInjector abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver();

+ 36 - 7
src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt

@@ -33,29 +33,38 @@ import androidx.fragment.app.DialogFragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ClientFactory
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.lib.resources.users.Status
-import com.owncloud.android.lib.resources.users.StatusType
 import com.owncloud.android.ui.StatusDrawable
 import com.owncloud.android.ui.activity.BaseActivity
 import com.owncloud.android.ui.activity.DrawerActivity
 import com.owncloud.android.ui.adapter.UserListAdapter
 import com.owncloud.android.ui.adapter.UserListItem
+import com.owncloud.android.ui.asynctasks.RetrieveStatusAsyncTask
 import com.owncloud.android.utils.DisplayUtils
 import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
 import com.owncloud.android.utils.ThemeUtils
 import kotlinx.android.synthetic.main.account_item.*
 import kotlinx.android.synthetic.main.dialog_choose_account.*
 import java.util.ArrayList
+import javax.inject.Inject
 
 private const val ARG_CURRENT_USER_PARAM = "currentUser"
 
-class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener, UserListAdapter.ClickListener {
+class ChooseAccountDialogFragment : DialogFragment(),
+    AvatarGenerationListener,
+    UserListAdapter.ClickListener,
+    Injectable {
     private lateinit var dialogView: View
     private var currentUser: User? = null
     private lateinit var accountManager: UserAccountManager
 
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         arguments?.let {
@@ -121,6 +130,13 @@ class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener,
                 (activity as DrawerActivity).openManageAccounts()
             }
 
+            set_status.setOnClickListener {
+                val setStatusDialog = SetStatusDialogFragment.newInstance(accountManager.user)
+                setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status")
+
+                dismiss()
+            }
+
             val capability = FileDataStorageManager(user.toPlatformAccount(), context?.contentResolver)
                 .getCapability(user)
 
@@ -128,11 +144,7 @@ class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener,
                 statusView.visibility = View.VISIBLE
             }
 
-            val status = Status(StatusType.dnd, "Do not disturb", "", -1)
-
-//            if (status) {
-            ticker.setImageDrawable(StatusDrawable(R.drawable.ic_user_status_dnd, 18f, context))
-//            }
+            RetrieveStatusAsyncTask(user, this, clientFactory).execute()
         }
     }
 
@@ -182,4 +194,21 @@ class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener,
     override fun onOptionItemClicked(user: User?, view: View?) {
         // Un-needed for this context
     }
+
+    fun setStatus(newStatus: Status) {
+        val size = DisplayUtils.convertDpToPixel(9f, context)
+        ticker.background = null
+        ticker.setImageDrawable(StatusDrawable(newStatus, size.toFloat(), context))
+        ticker.visibility = View.VISIBLE
+
+        if (newStatus.message.isNullOrBlank()) {
+            status.text = ""
+            status.visibility = View.GONE
+        } else {
+            status.text = newStatus.message
+            status.visibility = View.VISIBLE
+        }
+
+        view?.invalidate()
+    }
 }

+ 163 - 0
src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt

@@ -0,0 +1,163 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Infomaniak Network SA
+ * Copyright (C) 2020 Infomaniak Network SA
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+import com.owncloud.android.lib.resources.users.Status
+import com.owncloud.android.ui.StatusDrawable
+import com.owncloud.android.ui.activity.BaseActivity
+import com.owncloud.android.ui.activity.DrawerActivity
+import com.owncloud.android.ui.adapter.UserListAdapter
+import com.owncloud.android.ui.adapter.UserListItem
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener
+import kotlinx.android.synthetic.main.account_item.*
+import java.util.ArrayList
+import javax.inject.Inject
+
+private const val ARG_CURRENT_USER_PARAM = "currentUser"
+
+class SetStatusDialogFragment : DialogFragment(),
+    AvatarGenerationListener,
+    UserListAdapter.ClickListener,
+    Injectable {
+    private lateinit var dialogView: View
+    private var currentUser: User? = null
+    private lateinit var accountManager: UserAccountManager
+    private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
+    @Inject
+    lateinit var arbitraryDataProvider: ArbitraryDataProvider
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        arguments?.let {
+            currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM)
+
+            val json = arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PREDEFINED_STATUS)
+
+            if (!json.isEmpty()) {
+                val myType = object : TypeToken<ArrayList<PredefinedStatus>>() {}.type
+                predefinedStatus = Gson().fromJson(json, myType)
+            }
+
+            val size = predefinedStatus.size
+        }
+    }
+
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_set_status, null)
+        return MaterialAlertDialogBuilder(requireContext())
+            .setView(dialogView)
+            .create()
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        accountManager = (activity as BaseActivity).userAccountManager
+
+    }
+
+    private fun getAccountListItems(): List<UserListItem>? {
+        val users = accountManager.allUsers
+        val adapterUserList: MutableList<UserListItem> = ArrayList(users.size)
+        // Remove the current account from the adapter to display only other accounts
+        for (user in users) {
+            if (user != currentUser) {
+                adapterUserList.add(UserListItem(user))
+            }
+        }
+        return adapterUserList
+    }
+
+    /**
+     * Fragment creator
+     */
+    companion object {
+        @JvmStatic
+        fun newInstance(user: User) =
+            SetStatusDialogFragment().apply {
+                arguments = Bundle().apply {
+                    putParcelable(ARG_CURRENT_USER_PARAM, user)
+                }
+            }
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        return dialogView
+    }
+
+    override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean {
+        return (callContext as ImageView).tag.toString() == tag
+    }
+
+    override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) {
+        if (user_icon != null) {
+            user_icon.setImageDrawable(avatarDrawable)
+        }
+    }
+
+    override fun onAccountClicked(user: User?) {
+        (activity as DrawerActivity).accountClicked(user.hashCode())
+    }
+
+    override fun onOptionItemClicked(user: User?, view: View?) {
+        // Un-needed for this context
+    }
+
+    fun setStatus(newStatus: Status) {
+        val size = DisplayUtils.convertDpToPixel(9f, context)
+        ticker.background = null
+        ticker.setImageDrawable(StatusDrawable(newStatus, size.toFloat(), context))
+        ticker.visibility = View.VISIBLE
+
+        if (newStatus.message.isNullOrBlank()) {
+            status.text = ""
+            status.visibility = View.GONE
+        } else {
+            status.text = newStatus.message
+            status.visibility = View.VISIBLE
+        }
+
+        view?.invalidate()
+    }
+}

+ 1 - 0
src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java

@@ -39,6 +39,7 @@ import androidx.annotation.Nullable;
 public class ArbitraryDataProvider {
     public static final String DIRECT_EDITING = "DIRECT_EDITING";
     public static final String DIRECT_EDITING_ETAG = "DIRECT_EDITING_ETAG";
+    public static final String PREDEFINED_STATUS = "PREDEFINED_STATUS";
 
     private static final String TAG = ArbitraryDataProvider.class.getSimpleName();
     private static final String TRUE = "true";

+ 28 - 0
src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java

@@ -26,12 +26,15 @@ import android.util.Log;
 
 import com.google.gson.Gson;
 import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation;
+import com.nextcloud.common.NextcloudClient;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.DirectEditing;
 import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientFactory;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@@ -42,6 +45,8 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile;
 import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation;
+import com.owncloud.android.lib.resources.users.PredefinedStatus;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.utils.DataHolderUtil;
 import com.owncloud.android.utils.EncryptionUtils;
@@ -295,6 +300,8 @@ public class RefreshFolderOperation extends RemoteOperation {
             if (!oldDirectEditingEtag.equalsIgnoreCase(newDirectEditingEtag)) {
                 updateDirectEditing(arbitraryDataProvider, newDirectEditingEtag);
             }
+
+            updatePredefinedStatus(arbitraryDataProvider);
         } else {
             Log_OC.w(TAG, "Update Capabilities unsuccessfully");
         }
@@ -316,6 +323,27 @@ public class RefreshFolderOperation extends RemoteOperation {
                                                     newDirectEditingEtag);
     }
 
+    private void updatePredefinedStatus(ArbitraryDataProvider arbitraryDataProvider) {
+        NextcloudClient client;
+
+        try {
+            client = OwnCloudClientFactory.createNextcloudClient(mAccount, mContext);
+        } catch (AccountUtils.AccountNotFoundException e) {
+            Log_OC.e(this, "Update of predefined status not possible!");
+            return;
+        }
+
+        RemoteOperationResult result = new GetPredefinedStatusesRemoteOperation().execute(client);
+
+        if (result.isSuccess()) {
+            ArrayList<PredefinedStatus> predefinedStatuses = (ArrayList<PredefinedStatus>) result.getSingleData();
+            String json = new Gson().toJson(predefinedStatuses);
+            arbitraryDataProvider.storeOrUpdateKeyValue(mAccount.name, ArbitraryDataProvider.PREDEFINED_STATUS, json);
+        } else {
+            arbitraryDataProvider.deleteKeyForAccount(mAccount.name, ArbitraryDataProvider.PREDEFINED_STATUS);
+        }
+    }
+
     private RemoteOperationResult checkForChanges(OwnCloudClient client) {
         mRemoteFolderChanged = true;
         RemoteOperationResult result;

+ 4 - 4
src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java

@@ -36,8 +36,6 @@ import android.provider.BaseColumns;
 import android.text.TextUtils;
 import android.widget.Toast;
 
-import com.nextcloud.client.account.Status;
-import com.nextcloud.client.account.StatusType;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.owncloud.android.R;
@@ -48,6 +46,8 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.shares.GetShareesRemoteOperation;
 import com.owncloud.android.lib.resources.shares.ShareType;
+import com.owncloud.android.lib.resources.users.Status;
+import com.owncloud.android.lib.resources.users.StatusType;
 import com.owncloud.android.ui.TextDrawable;
 import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.ErrorMessageAdapter;
@@ -264,9 +264,9 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                         status = new Status(StatusType.valueOf(statusObject.getString("status")),
                                             statusObject.isNull("message") ? "" : statusObject.getString("message"),
                                             statusObject.isNull("icon") ? "" : statusObject.getString("icon"),
-                                            statusObject.isNull("clearAt") ? "" : statusObject.getString("clearAt"));
+                                            statusObject.isNull("clearAt") ? -1 : statusObject.getLong("clearAt"));
                     } else {
-                        status = new Status(StatusType.Unknown, "", "", "");
+                        status = new Status(StatusType.offline, "", "", -1);
                     }
 
                     switch (type) {

+ 44 - 33
src/main/java/com/owncloud/android/ui/StatusDrawable.java

@@ -28,8 +28,10 @@ import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 
-import com.owncloud.android.utils.BitmapUtils;
+import com.owncloud.android.R;
+import com.owncloud.android.lib.resources.users.Status;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
@@ -42,44 +44,53 @@ public class StatusDrawable extends Drawable {
     private String text;
     private @DrawableRes int icon = -1;
     private Paint textPaint;
-    private final Paint backgroundPaint;
-    private final float radius;
+    private Paint backgroundPaint;
+    private float radius;
     private Context context;
+    private final static int whiteBackground = Color.argb(200, 255, 255, 255);
+    private final static int onlineStatus = Color.argb(255, 73, 179, 130);
 
-    public StatusDrawable(@DrawableRes int icon, float size, Context context) {
-        radius = size;
-        this.icon = icon;
-        this.context = context;
-
-        backgroundPaint = new Paint();
-        backgroundPaint.setStyle(Paint.Style.FILL);
-        backgroundPaint.setAntiAlias(true);
-        backgroundPaint.setColor(Color.argb(200, 255, 255, 255));
-    }
-
-    public StatusDrawable(BitmapUtils.Color color, float size) {
-        radius = size;
-
+    public StatusDrawable(Status status, float statusSize, Context context) {
         backgroundPaint = new Paint();
         backgroundPaint.setStyle(Paint.Style.FILL);
         backgroundPaint.setAntiAlias(true);
-        backgroundPaint.setColor(Color.argb(color.a, color.r, color.g, color.b));
-    }
-
-    public StatusDrawable(String icon, float size) {
-        text = icon;
-        radius = size;
 
-        backgroundPaint = new Paint();
-        backgroundPaint.setStyle(Paint.Style.FILL);
-        backgroundPaint.setAntiAlias(true);
-        backgroundPaint.setColor(Color.argb(200, 255, 255, 255));
-
-        textPaint = new Paint();
-        textPaint.setColor(Color.WHITE);
-        textPaint.setTextSize(size);
-        textPaint.setAntiAlias(true);
-        textPaint.setTextAlign(Paint.Align.CENTER);
+        radius = statusSize;
+
+        if (TextUtils.isEmpty(status.getIcon())) {
+            switch (status.getStatus()) {
+                case dnd:
+                    icon = R.drawable.ic_user_status_dnd;
+                    backgroundPaint.setColor(whiteBackground);
+                    this.context = context;
+                    break;
+
+                case online:
+                    backgroundPaint.setColor(onlineStatus);
+                    break;
+
+                case away:
+                    icon = R.drawable.ic_user_status_away;
+                    backgroundPaint.setColor(whiteBackground);
+                    this.context = context;
+                    break;
+
+                default:
+                    // do not show
+                    backgroundPaint = null;
+                    break;
+            }
+        } else {
+            text = status.getIcon();
+
+            backgroundPaint.setColor(whiteBackground);
+
+            textPaint = new Paint();
+            textPaint.setColor(Color.WHITE);
+            textPaint.setTextSize(statusSize);
+            textPaint.setAntiAlias(true);
+            textPaint.setTextAlign(Paint.Align.CENTER);
+        }
     }
 
     /**

+ 72 - 0
src/main/java/com/owncloud/android/ui/asynctasks/RetrieveStatusAsyncTask.java

@@ -0,0 +1,72 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.asynctasks;
+
+import android.os.AsyncTask;
+
+import com.nextcloud.client.account.User;
+import com.nextcloud.client.network.ClientFactory;
+import com.nextcloud.common.NextcloudClient;
+import com.nextcloud.ui.ChooseAccountDialogFragment;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.resources.users.GetStatusRemoteOperation;
+import com.owncloud.android.lib.resources.users.Status;
+import com.owncloud.android.lib.resources.users.StatusType;
+
+import java.lang.ref.WeakReference;
+
+public class RetrieveStatusAsyncTask extends AsyncTask<Void, Void, Status> {
+    private final User user;
+    private final WeakReference<ChooseAccountDialogFragment> chooseAccountDialogFragment;
+    private final ClientFactory clientFactory;
+
+    public RetrieveStatusAsyncTask(User user,
+                                   ChooseAccountDialogFragment chooseAccountDialogFragment,
+                                   ClientFactory clientFactory) {
+        this.user = user;
+        this.chooseAccountDialogFragment = new WeakReference<>(chooseAccountDialogFragment);
+        this.clientFactory = clientFactory;
+    }
+
+    @Override
+    protected com.owncloud.android.lib.resources.users.Status doInBackground(Void... voids) {
+        try {
+            NextcloudClient client = clientFactory.createNextcloudClient(user);
+            RemoteOperationResult result = new GetStatusRemoteOperation().execute(client);
+
+            return (com.owncloud.android.lib.resources.users.Status) result.getSingleData();
+
+        } catch (ClientFactory.CreationException e) {
+            return new com.owncloud.android.lib.resources.users.Status(StatusType.offline, "", "", -1);
+        }
+    }
+
+    @Override
+    protected void onPostExecute(com.owncloud.android.lib.resources.users.Status status) {
+        ChooseAccountDialogFragment fragment = chooseAccountDialogFragment.get();
+
+        if (fragment != null) {
+            fragment.setStatus(status);
+        }
+
+    }
+}

+ 7 - 46
src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -31,13 +31,13 @@ import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
-import android.text.TextUtils;
 import android.widget.ImageView;
 
-import com.nextcloud.client.account.StatusType;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 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.owncloud.android.ui.StatusDrawable;
 
 import org.apache.commons.codec.binary.Hex;
@@ -414,7 +414,7 @@ public final class BitmapUtils {
                                      imageView);
     }
 
-    public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType status, String icon, Context context) {
+    public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType statusType, String icon, Context context) {
         float avatarRadius = getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
         int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context);
 
@@ -429,50 +429,11 @@ public final class BitmapUtils {
         // status
         int statusSize = width / 4;
 
-        StatusDrawable statusDrawable;
-        if (TextUtils.isEmpty(icon)) {
-            switch (status) {
-                case Dnd:
-                    statusDrawable = new StatusDrawable(R.drawable.ic_user_status_dnd, statusSize, context);
-                    statusDrawable.setBounds(width / 2,
-                                             width / 2,
-                                             width,
-                                             width);
-                    break;
-
-                case Online:
-                    statusDrawable = new StatusDrawable(new Color(255, 73, 179, 130), statusSize);
-                    statusDrawable.setBounds(width,
-                                             width,
-                                             width,
-                                             width);
-                    break;
-
-                case Away:
-                    statusDrawable = new StatusDrawable(R.drawable.ic_user_status_away, statusSize, context);
-                    statusDrawable.setBounds(width / 2,
-                                             width / 2,
-                                             width,
-                                             width);
-                    break;
-
-                default:
-                    // do not show
-                    statusDrawable = null;
-                    break;
-            }
-        } else {
-            statusDrawable = new StatusDrawable(icon, statusSize);
-            statusDrawable.setBounds(width / 2,
-                                     width / 2,
-                                     width,
-                                     width);
-        }
+        Status status = new Status(statusType, "", icon, -1);
+        StatusDrawable statusDrawable = new StatusDrawable(status, statusSize, context);
 
-        if (statusDrawable != null) {
-            canvas.translate(width / 2f, width / 2f);
-            statusDrawable.draw(canvas);
-        }
+        canvas.translate(width / 2f, width / 2f);
+        statusDrawable.draw(canvas);
 
         return output;
     }

+ 0 - 1
src/main/res/drawable/ic_talk.xml

@@ -16,7 +16,6 @@
   You should have received a copy of the GNU Affero General Public
   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<?xml version="1.0" encoding="utf-8"?>
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="24dp"
     android:height="24dp"

+ 32 - 0
src/main/res/drawable/ic_user_status_invisible.xml

@@ -0,0 +1,32 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky
+  ~ Copyright (C) 2020 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<vector android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#000000"
+        android:pathData="m12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10 -4.48,-10 -10,-10zM12,6a6,6 0,0 1,6 6,6 6,0 0,1 -6,6 6,6 0,0 1,-6 -6,6 6,0 0,1 6,-6z" />
+</vector>

+ 23 - 0
src/main/res/drawable/online_status.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+    Nextcloud Android client application
+
+    @author Andy Scherzinger
+    Copyright (C) 2019 Andy Scherzinger
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#00ff00" />
+</shape>

+ 58 - 0
src/main/res/layout/custom_status.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky
+  ~ Copyright (C) 2020 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="48dp">
+
+    <TextView
+        android:id="@+id/imageView"
+        android:layout_width="48dp"
+        android:layout_height="match_parent"
+        android:textSize="25sp"
+        android:gravity="center"
+        tools:text="📆" />
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:textStyle="bold"
+        tools:text="In a meeting" />
+
+    <TextView
+        android:id="@+id/divider"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:layout_margin="@dimen/standard_half_margin"
+        android:text="@string/divider" />
+
+    <TextView
+        android:id="@+id/clearAt"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        tools:text="an hour" />
+</LinearLayout>

+ 239 - 0
src/main/res/layout/dialog_set_status.xml

@@ -0,0 +1,239 @@
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Tobias Kaminsky
+  Copyright (C) 2020 Nextcloud GmbH
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License version 2,
+  as published by the Free Software Foundation.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_margin="@dimen/standard_margin"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/onlineStatusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/online_status"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <LinearLayout
+        android:id="@+id/statusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:baselineAligned="false"
+        android:orientation="horizontal"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/onlineStatusView">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/onlineStatus"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/account_action_button_margin"
+                android:layout_marginEnd="@dimen/account_action_button_margin"
+                android:paddingStart="10dp"
+                android:paddingEnd="0dp"
+                android:text="@string/online"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                app:backgroundTint="@color/grey_200"
+                app:cornerRadius="5dp"
+                app:icon="@drawable/online_status"
+                app:iconGravity="start"
+                app:iconPadding="22dp"
+                app:iconTint="@color/hwSecurityGreen" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/dndStatus"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/account_action_button_margin"
+                android:layout_marginEnd="@dimen/account_action_button_margin"
+                android:paddingStart="10dp"
+                android:paddingEnd="0dp"
+                android:text="@string/dnd"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                app:backgroundTint="@color/grey_200"
+                app:cornerRadius="5dp"
+                app:icon="@drawable/ic_user_status_dnd"
+                app:iconGravity="start"
+                app:iconPadding="22dp" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/awayStatus"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/account_action_button_margin"
+                android:layout_marginEnd="@dimen/account_action_button_margin"
+                android:paddingStart="10dp"
+                android:paddingEnd="0dp"
+                android:text="@string/away"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                app:backgroundTint="@color/grey_200"
+                app:cornerRadius="5dp"
+                app:icon="@drawable/ic_user_status_away"
+                app:iconGravity="start"
+                app:iconPadding="22dp"
+                app:iconTint="#f4a331" />
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/invisibleStatus"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/account_action_button_margin"
+                android:layout_marginEnd="@dimen/account_action_button_margin"
+                android:paddingStart="10dp"
+                android:paddingEnd="0dp"
+                android:text="@string/invisible"
+                android:textAlignment="textStart"
+                android:textAllCaps="false"
+                android:textColor="@color/fontAppbar"
+                app:backgroundTint="@color/grey_200"
+                app:cornerRadius="5dp"
+                app:icon="@drawable/ic_user_status_invisible"
+                app:iconGravity="start"
+                app:iconPadding="22dp"
+                app:iconTint="@color/black" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+
+    <View
+        android:id="@+id/separator_line"
+        android:layout_width="0dp"
+        android:layout_height="1dp"
+        android:layout_marginTop="@dimen/standard_quarter_margin"
+        android:background="@color/list_divider_background"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/statusView" />
+
+    <TextView
+        android:id="@+id/statusMessage"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/status_message"
+        app:layout_constraintTop_toBottomOf="@+id/statusView"
+        tools:layout_editor_absoluteX="175dp" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:orientation="horizontal"
+        tools:layout_editor_absoluteX="201dp"
+        tools:layout_editor_absoluteY="141dp">
+
+        <ImageButton
+            android:id="@+id/imageButton"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:contentDescription="@string/set_status_icon"
+            android:src="@drawable/ic_cloud_sync_on" />
+
+        <EditText
+            android:id="@+id/customStatusInput"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:hint="@string/whats_your_status"
+            android:importantForAutofill="no"
+            android:inputType="textAutoCorrect" />
+    </LinearLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/customStatusList"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:itemCount="5"
+        tools:listitem="@layout/custom_status">
+
+    </androidx.recyclerview.widget.RecyclerView>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/textView3"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/clear_status_message_after" />
+
+        <Spinner
+            android:id="@+id/spinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/clearStatus"
+            style="@style/OutlinedButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_padding"
+            android:layout_weight="1"
+            android:text="@string/clear_status_message"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/setStatus"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/standard_padding"
+            android:layout_weight="1"
+            android:text="@string/set_status_message"
+            android:theme="@style/Button.Primary"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 12 - 0
src/main/res/values/strings.xml

@@ -939,4 +939,16 @@
     <string name="remote">(remote)</string>
     <string name="status_description">Your avatar with status</string>
     <string name="set_status">Set status</string>
+    <string name="online_status">Online status</string>
+    <string name="status_message">Status message</string>
+    <string name="set_status_icon">set status icon</string>
+    <string name="whats_your_status">What\'s your status?</string>
+    <string name="clear_status_message_after">Clear status message after</string>
+    <string name="clear_status_message">Clear status message</string>
+    <string name="set_status_message">Set status message</string>
+    <string name="online">Online</string>
+    <string name="dnd">Do not disturb</string>
+    <string name="away">Away</string>
+    <string name="invisible">Invisible</string>
+    <string translatable="false" name="divider">—</string>
 </resources>