Explorar o código

Merge pull request #7149 from nextcloud/setStatus

Set status
Andy Scherzinger %!s(int64=4) %!d(string=hai) anos
pai
achega
fb7fd8c517
Modificáronse 71 ficheiros con 2587 adicións e 97 borrados
  1. 2 0
      build.gradle
  2. 7 0
      drawable_resources/user-status-away.svg
  3. 6 0
      drawable_resources/user-status-dnd.svg
  4. 6 0
      drawable_resources/user-status-invisible.svg
  5. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png
  6. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png
  7. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png
  8. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png
  9. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png
  10. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png
  11. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png
  12. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png
  13. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png
  14. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png
  15. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png
  16. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png
  17. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png
  18. BIN=BIN
      screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png
  19. 1 1
      scripts/analysis/lint-results.txt
  20. 1 1
      scripts/androidScreenshotTest
  21. BIN=BIN
      src/androidTest/assets/christine.jpg
  22. BIN=BIN
      src/androidTest/assets/paulette.jpg
  23. 60 0
      src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt
  24. 19 1
      src/androidTest/java/com/owncloud/android/AbstractIT.java
  25. 15 1
      src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerTest.java
  26. 49 0
      src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt
  27. 46 0
      src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt
  28. 77 10
      src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java
  29. 123 1
      src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt
  30. 23 3
      src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt
  31. 0 5
      src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt
  32. 17 2
      src/debug/res/layout/avatar_fragment.xml
  33. 1 1
      src/main/java/com/nextcloud/client/core/Task.kt
  34. 56 20
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  35. 50 1
      src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
  36. 36 0
      src/main/java/com/nextcloud/ui/ClearStatusTask.kt
  37. 39 0
      src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt
  38. 417 0
      src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt
  39. 39 0
      src/main/java/com/nextcloud/ui/SetStatusTask.kt
  40. 40 0
      src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt
  41. 1 0
      src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.java
  42. 12 0
      src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  43. 4 2
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  44. 28 0
      src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java
  45. 22 0
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  46. 108 9
      src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java
  47. 142 0
      src/main/java/com/owncloud/android/ui/StatusDrawable.java
  48. 22 18
      src/main/java/com/owncloud/android/ui/TextDrawable.java
  49. 29 0
      src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusClickListener.kt
  50. 48 0
      src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusListAdapter.kt
  51. 55 0
      src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusViewHolder.kt
  52. 1 3
      src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java
  53. 72 0
      src/main/java/com/owncloud/android/ui/asynctasks/RetrieveStatusAsyncTask.java
  54. 56 0
      src/main/java/com/owncloud/android/ui/components/AvatarWithStatus.kt
  55. 7 0
      src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java
  56. 79 4
      src/main/java/com/owncloud/android/utils/BitmapUtils.java
  57. 21 2
      src/main/java/com/owncloud/android/utils/DisplayUtils.java
  58. 4 2
      src/main/res/drawable/ic_circles.xml
  59. 4 2
      src/main/res/drawable/ic_edit.xml
  60. 4 2
      src/main/res/drawable/ic_post_add.xml
  61. 28 0
      src/main/res/drawable/ic_talk.xml
  62. 32 0
      src/main/res/drawable/ic_user_status_away.xml
  63. 38 0
      src/main/res/drawable/ic_user_status_dnd.xml
  64. 34 0
      src/main/res/drawable/ic_user_status_invisible.xml
  65. 23 0
      src/main/res/drawable/online_status.xml
  66. 21 3
      src/main/res/layout/account_item.xml
  67. 37 1
      src/main/res/layout/dialog_choose_account.xml
  68. 441 0
      src/main/res/layout/dialog_set_status.xml
  69. 62 0
      src/main/res/layout/predefined_status.xml
  70. 1 0
      src/main/res/values/dims.xml
  71. 21 2
      src/main/res/values/strings.xml

+ 2 - 0
build.gradle

@@ -321,6 +321,8 @@ dependencies {
     implementation 'com.caverock:androidsvg:1.4'
     implementation 'androidx.annotation:annotation:1.1.0'
     implementation 'com.google.code.gson:gson:2.8.6'
+    implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.0.0'
+    implementation 'com.github.vanniktech:Emoji:0.7.0'
 
     implementation 'com.github.cotechde.hwsecurity:hwsecurity-fido:4.1.0'
     implementation 'com.github.cotechde.hwsecurity:hwsecurity-fido2:4.1.0'

+ 7 - 0
drawable_resources/user-status-away.svg

@@ -0,0 +1,7 @@
+<svg width="24" height="24" enable-background="new 0 0 24 24" version="1.1" viewBox="0 0 24 24"
+    xmlns="http://www.w3.org/2000/svg">
+    <rect width="24" height="24" fill="none" />
+    <path
+        d="m10.615 2.1094c-4.8491 0.68106-8.6152 4.8615-8.6152 9.8906 0 5.5 4.5 10 10 10 5.0292 0 9.2096-3.7661 9.8906-8.6152-1.4654 1.601-3.5625 2.6152-5.8906 2.6152-4.4 0-8-3.6-8-8 0-2.3281 1.0143-4.4252 2.6152-5.8906z"
+        fill="#f4a331" />
+</svg>

+ 6 - 0
drawable_resources/user-status-dnd.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none" />
+    <path d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10z" fill="#ed484c" />
+    <path d="m8 10h8c1.108 0 2 0.892 2 2s-0.892 2-2 2h-8c-1.108 0-2-0.892-2-2s0.892-2 2-2z" fill="#fdffff"
+        stroke-linecap="round" stroke-linejoin="round" stroke-width="2" style="paint-order:stroke markers fill" />
+</svg>

+ 6 - 0
drawable_resources/user-status-invisible.svg

@@ -0,0 +1,6 @@
+<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none" />
+    <path
+        d="m12 2c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 4a6 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"
+        fill="#ffffff" />
+</svg>

BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png


BIN=BIN
screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png


+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 337 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 334 warnings</span>

+ 1 - 1
scripts/androidScreenshotTest

@@ -58,7 +58,7 @@ fi
 if [[ $4 = "all" ]]; then
     scripts/runAllScreenshotCombinations "noCI" "$1" "-Pandroid.testInstrumentationRunnerArguments.class=$class$method"
 else
-    ./gradlew gplayDebugExecuteScreenshotTests $record \
+    ./gradlew --offline gplayDebugExecuteScreenshotTests $record \
     -Pandroid.testInstrumentationRunnerArguments.annotation=com.owncloud.android.utils.ScreenshotTest \
     -Pandroid.testInstrumentationRunnerArguments.class=$class$method \
     $darkMode \

BIN=BIN
src/androidTest/assets/christine.jpg


BIN=BIN
src/androidTest/assets/paulette.jpg


+ 60 - 0
src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt

@@ -0,0 +1,60 @@
+/*
+ *
+ * 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.ui
+
+import androidx.test.espresso.intent.rule.IntentsTestRule
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.lib.resources.users.ClearAt
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+import com.owncloud.android.lib.resources.users.Status
+import com.owncloud.android.lib.resources.users.StatusType
+import com.owncloud.android.ui.activity.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, Status(StatusType.DND, "Working hard…", "🤖", -1))
+        val activity = activityRule.launchActivity(null)
+
+        sut.show(activity.supportFragmentManager, "")
+
+        val predefinedStatus: ArrayList<PredefinedStatus> = arrayListOf(
+            PredefinedStatus("meeting", "📅", "In a meeting", ClearAt("period", "3600")),
+            PredefinedStatus("commuting", "🚌", "Commuting", ClearAt("period", "1800")),
+            PredefinedStatus("remote-work", "🏡", "Working remotely", ClearAt("end-of", "day")),
+            PredefinedStatus("sick-leave", "🤒", "Out sick", ClearAt("end-of", "day")),
+            PredefinedStatus("vacationing", "🌴", "Vacationing", null)
+        )
+
+        shortSleep()
+
+        activity.runOnUiThread { sut.setPredefinedStatus(predefinedStatus) }
+
+        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;
         }

+ 15 - 1
src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerTest.java

@@ -31,6 +31,8 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation;
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
 import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
 import com.owncloud.android.lib.resources.files.model.RemoteFile;
+import com.owncloud.android.lib.resources.status.CapabilityBooleanType;
+import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.utils.FileStorageUtils;
 
@@ -240,11 +242,23 @@ abstract public class FileDataStorageManagerTest extends AbstractOnServerIT {
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void testSaveNewFile_NonexistingParent() {
+    public void testSaveNewFile_NonExistingParent() {
         assertTrue(new CreateFolderRemoteOperation("/1/1/", true).execute(client).isSuccess());
 
         OCFile newFile = new OCFile("/1/1/1.txt");
 
         sut.saveNewFile(newFile);
     }
+
+    @Test
+    public void testOCCapability() {
+        OCCapability capability = new OCCapability();
+        capability.setUserStatus(CapabilityBooleanType.TRUE);
+
+        sut.saveCapabilities(capability);
+
+        OCCapability newCapability = sut.getCapability(user);
+
+        assertEquals(capability.getUserStatus(), newCapability.getUserStatus());
+    }
 }

+ 49 - 0
src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt

@@ -0,0 +1,49 @@
+/*
+ *
+ * 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.datamodel
+
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.lib.resources.status.CapabilityBooleanType
+import com.owncloud.android.lib.resources.status.OCCapability
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class OCCapabilityIT : AbstractIT() {
+    @Test
+    fun saveCapability() {
+        val fileDataStorageManager = FileDataStorageManager(account, targetContext.contentResolver)
+
+        val capability = OCCapability()
+        capability.etag = "123"
+        capability.userStatus = CapabilityBooleanType.TRUE
+        capability.userStatusSupportsEmoji = CapabilityBooleanType.TRUE
+
+        fileDataStorageManager.saveCapabilities(capability)
+
+        val newCapability = fileDataStorageManager.getCapability(account.name)
+
+        assertEquals(capability.etag, newCapability.etag)
+        assertEquals(capability.userStatus, newCapability.userStatus)
+        assertEquals(capability.userStatusSupportsEmoji, newCapability.userStatusSupportsEmoji)
+    }
+}

+ 46 - 0
src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt

@@ -0,0 +1,46 @@
+/*
+ *
+ * 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.providers
+
+import androidx.test.espresso.intent.rule.IntentsTestRule
+import com.nextcloud.client.TestActivity
+import com.owncloud.android.AbstractOnServerIT
+import org.junit.Rule
+import org.junit.Test
+
+class UsersAndGroupsSearchProviderIT : AbstractOnServerIT() {
+    @get:Rule
+    val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
+
+    @Test
+    fun searchUser() {
+        val activity = testActivityRule.launchActivity(null)
+
+        shortSleep()
+
+        activity.runOnUiThread {
+            // fragment.search("Admin")
+        }
+
+        longSleep()
+    }
+}

+ 77 - 10
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;
@@ -36,10 +35,15 @@ import com.nextcloud.client.account.Server;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.MainApp;
+import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 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;
 
@@ -52,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;
 
@@ -60,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() {
@@ -131,7 +130,7 @@ public class DialogFragmentIT extends AbstractIT {
     @ScreenshotTest
     public void testAccountChooserDialog() throws AccountUtils.AccountNotFoundException {
         AccountManager accountManager = AccountManager.get(targetContext);
-        for (Account account : accountManager.getAccounts()) {
+        for (Account account : accountManager.getAccountsByType(MainApp.getAccountType(targetContext))) {
             accountManager.removeAccountExplicitly(account);
         }
 
@@ -145,17 +144,83 @@ public class DialogFragmentIT extends AbstractIT {
         accountManager.addAccountExplicitly(newAccount2, "password", null);
         accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_BASE_URL, "https://server.com");
         accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_USER_ID, "user1");
-        accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_VERSION, "19.0.0.0");
+        accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_VERSION, "20.0.0");
+
+        FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(newAccount,
+                                                                                   targetContext.getContentResolver());
+
+        OCCapability capability = new OCCapability();
+        capability.setUserStatus(CapabilityBooleanType.TRUE);
+        capability.setUserStatusSupportsEmoji(CapabilityBooleanType.TRUE);
+        fileDataStorageManager.saveCapabilities(capability);
+
+        ChooseAccountDialogFragment sut =
+            ChooseAccountDialogFragment.newInstance(new RegisteredUser(newAccount,
+                                                                       new OwnCloudAccount(newAccount, targetContext),
+                                                                       new Server(URI.create("https://server.com"),
+                                                                                  OwnCloudVersion.nextcloud_20)));
+        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
+    @ScreenshotTest
+    public void testAccountChooserDialogWithStatusDisabled() throws AccountUtils.AccountNotFoundException {
+        AccountManager accountManager = AccountManager.get(targetContext);
+        for (Account account : accountManager.getAccounts()) {
+            accountManager.removeAccountExplicitly(account);
+        }
+
+        Account newAccount = new Account("test@https://server.com", MainApp.getAccountType(targetContext));
+        accountManager.addAccountExplicitly(newAccount, "password", null);
+        accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_OC_BASE_URL, "https://server.com");
+        accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_USER_ID, "test");
+
+        FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(newAccount,
+                                                                                   targetContext.getContentResolver());
+
+        OCCapability capability = new OCCapability();
+        capability.setUserStatus(CapabilityBooleanType.FALSE);
+
+        fileDataStorageManager.saveCapabilities(capability);
 
         ChooseAccountDialogFragment sut =
             ChooseAccountDialogFragment.newInstance(new RegisteredUser(newAccount,
                                                                        new OwnCloudAccount(newAccount, targetContext),
                                                                        new Server(URI.create("https://server.com"),
-                                                                                  OwnCloudVersion.nextcloud_19)));
+                                                                                  OwnCloudVersion.nextcloud_20)));
         showDialog(sut);
     }
 
-    private void showDialog(DialogFragment dialog) {
+    private FileDisplayActivity showDialog(DialogFragment dialog) {
         Intent intent = new Intent(targetContext, FileDisplayActivity.class);
         FileDisplayActivity sut = activityRule.launchActivity(intent);
 
@@ -168,6 +233,8 @@ public class DialogFragmentIT extends AbstractIT {
         hideCursors(viewGroup);
 
         screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView());
+
+        return sut;
     }
 
     private void hideCursors(ViewGroup viewGroup) {

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

@@ -1,5 +1,4 @@
 /*
- *
  * Nextcloud Android client application
  *
  * @author Tobias Kaminsky
@@ -22,11 +21,15 @@
 
 package com.owncloud.android.ui.fragment
 
+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.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
 import com.owncloud.android.utils.ScreenshotTest
 import org.junit.Rule
@@ -60,4 +63,123 @@ class AvatarIT : AbstractIT() {
         waitForIdleSync()
         screenshot(sut)
     }
+
+    @Test
+    @ScreenshotTest
+    fun showAvatarsWithStatus() {
+        val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius)
+        val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext)
+        val sut = testActivityRule.launchActivity(null)
+        val fragment = AvatarTestFragment()
+
+        val paulette = BitmapFactory.decodeFile(getFile("paulette.jpg").absolutePath)
+        val christine = BitmapFactory.decodeFile(getFile("christine.jpg").absolutePath)
+        val textBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar("Admin", avatarRadius))
+
+        sut.addFragment(fragment)
+
+        runOnUiThread {
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.ONLINE, "😘", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "☁️", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "🌴️", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.DND, "", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(christine, StatusType.AWAY, "", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(paulette, StatusType.OFFLINE, "", targetContext),
+                width * 2,
+                1,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "😘", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "☁️", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "🌴️", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.DND, "", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.AWAY, "", targetContext),
+                width,
+                2,
+                targetContext
+            )
+
+            fragment.addBitmap(
+                BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.OFFLINE, "", targetContext),
+                width,
+                2,
+                targetContext
+            )
+        }
+
+        shortSleep()
+        waitForIdleSync()
+        screenshot(sut)
+    }
 }

+ 23 - 3
src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt

@@ -22,6 +22,7 @@
 package com.owncloud.android.ui.fragment
 
 import android.content.Context
+import android.graphics.Bitmap
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
@@ -34,12 +35,14 @@ import com.owncloud.android.R
 import com.owncloud.android.ui.TextDrawable
 
 internal class AvatarTestFragment : Fragment() {
-    lateinit var list: LinearLayout
+    lateinit var list1: LinearLayout
+    lateinit var list2: LinearLayout
 
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
         val view: View = inflater.inflate(R.layout.avatar_fragment, null)
 
-        list = view.findViewById(R.id.avatar_list)
+        list1 = view.findViewById(R.id.avatar_list1)
+        list2 = view.findViewById(R.id.avatar_list2)
 
         return view
     }
@@ -54,7 +57,24 @@ internal class AvatarTestFragment : Fragment() {
         layoutParams.setMargins(margin, margin, margin, margin)
         imageView.layoutParams = layoutParams
 
-        list.addView(imageView)
+        list1.addView(imageView)
+    }
+
+    fun addBitmap(bitmap: Bitmap, width: Int, list: Int, targetContext: Context) {
+        val margin = padding
+        val imageView = ImageView(targetContext)
+        imageView.setImageBitmap(bitmap)
+
+        val layoutParams: RelativeLayout.LayoutParams = RelativeLayout.LayoutParams(width, width)
+        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
+        layoutParams.setMargins(margin, margin, margin, margin)
+        imageView.layoutParams = layoutParams
+
+        if (list == 1) {
+            list1.addView(imageView)
+        } else {
+            list2.addView(imageView)
+        }
     }
 
     companion object {

+ 0 - 5
src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt

@@ -21,11 +21,9 @@
  */
 package com.owncloud.android.ui.fragment
 
-import android.Manifest
 import android.widget.ImageView
 import androidx.appcompat.widget.PopupMenu
 import androidx.test.espresso.intent.rule.IntentsTestRule
-import androidx.test.rule.GrantPermissionRule
 import com.nextcloud.client.TestActivity
 import com.owncloud.android.AbstractIT
 import com.owncloud.android.R
@@ -51,9 +49,6 @@ class FileDetailSharingFragmentIT : AbstractIT() {
     @get:Rule
     val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
 
-    @get:Rule
-    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
-
     lateinit var file: OCFile
     lateinit var folder: OCFile
     lateinit var activity: TestActivity

+ 17 - 2
src/debug/res/layout/avatar_fragment.xml

@@ -18,10 +18,25 @@
   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"
-    android:id="@+id/avatar_list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:orientation="horizontal"
+    android:baselineAligned="false">
+
+    <LinearLayout
+        android:id="@+id/avatar_list1"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:layout_weight="1" />
+
+    <LinearLayout
+        android:id="@+id/avatar_list2"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:orientation="vertical" />
 
 </LinearLayout>

+ 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();

+ 50 - 1
src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt

@@ -33,24 +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.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
+    private var currentStatus: Status? = null
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -116,6 +130,22 @@ class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener,
             manage_accounts.setOnClickListener {
                 (activity as DrawerActivity).openManageAccounts()
             }
+
+            set_status.setOnClickListener {
+                val setStatusDialog = SetStatusDialogFragment.newInstance(accountManager.user, currentStatus)
+                setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status")
+
+                dismiss()
+            }
+
+            val capability = FileDataStorageManager(user.toPlatformAccount(), context?.contentResolver)
+                .getCapability(user)
+
+            if (capability.userStatus.isTrue) {
+                statusView.visibility = View.VISIBLE
+            }
+
+            RetrieveStatusAsyncTask(user, this, clientFactory).execute()
         }
     }
 
@@ -165,4 +195,23 @@ class ChooseAccountDialogFragment : DialogFragment(), AvatarGenerationListener,
     override fun onOptionItemClicked(user: User?, view: View?) {
         // Un-needed for this context
     }
+
+    fun setStatus(newStatus: Status) {
+        currentStatus = newStatus
+
+        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()
+    }
 }

+ 36 - 0
src/main/java/com/nextcloud/ui/ClearStatusTask.kt

@@ -0,0 +1,36 @@
+/*
+ *
+ * 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.ui
+
+import android.accounts.Account
+import android.content.Context
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+import com.owncloud.android.lib.resources.users.ClearStatusMessageRemoteOperation
+
+public class ClearStatusTask(val account: Account?, val context: Context?) : Function0<Boolean> {
+    override fun invoke(): Boolean {
+        val client = OwnCloudClientFactory.createNextcloudClient(account, context)
+
+        return ClearStatusMessageRemoteOperation().execute(client).isSuccess
+    }
+}

+ 39 - 0
src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt

@@ -0,0 +1,39 @@
+/*
+ *
+ * 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.ui
+
+import android.accounts.Account
+import android.content.Context
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+import com.owncloud.android.lib.resources.users.SetPredefinedCustomStatusMessageRemoteOperation
+
+public class SetPredefinedCustomStatusTask(val messageId: String,
+                                           val clearAt: Long?,
+                                           val account: Account?,
+                                           val context: Context?) : Function0<Boolean> {
+    override fun invoke(): Boolean {
+        val client = OwnCloudClientFactory.createNextcloudClient(account, context)
+
+        return SetPredefinedCustomStatusMessageRemoteOperation(messageId, clearAt).execute(client).isSuccess
+    }
+}

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

@@ -0,0 +1,417 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.ui
+
+import android.annotation.SuppressLint
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+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.appcompat.app.AlertDialog
+import androidx.emoji.bundled.BundledEmojiCompatConfig
+import androidx.emoji.text.EmojiCompat
+import androidx.fragment.app.DialogFragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.core.AsyncRunner
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.databinding.DialogSetStatusBinding
+import com.owncloud.android.datamodel.ArbitraryDataProvider
+import com.owncloud.android.lib.resources.users.ClearAt
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+import com.owncloud.android.lib.resources.users.Status
+import com.owncloud.android.lib.resources.users.StatusType
+import com.owncloud.android.ui.activity.BaseActivity
+import com.owncloud.android.ui.adapter.PredefinedStatusClickListener
+import com.owncloud.android.ui.adapter.PredefinedStatusListAdapter
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.ThemeUtils
+import com.vanniktech.emoji.EmojiManager
+import com.vanniktech.emoji.EmojiPopup
+import com.vanniktech.emoji.googlecompat.GoogleCompatEmojiProvider
+import kotlinx.android.synthetic.main.dialog_set_status.*
+import java.util.ArrayList
+import java.util.Calendar
+import java.util.Locale
+import javax.inject.Inject
+
+private const val ARG_CURRENT_USER_PARAM = "currentUser"
+private const val ARG_CURRENT_STATUS_PARAM = "currentStatus"
+
+class SetStatusDialogFragment : DialogFragment(),
+    PredefinedStatusClickListener,
+    Injectable {
+
+    private lateinit var binding: DialogSetStatusBinding
+
+    private var currentUser: User? = null
+    private var currentStatus: Status? = null
+    private lateinit var accountManager: UserAccountManager
+    private lateinit var predefinedStatus: ArrayList<PredefinedStatus>
+    private lateinit var adapter: PredefinedStatusListAdapter
+    private var selectedPredefinedMessageId: String? = null
+    private var clearAt: Long? = -1
+    private lateinit var popup: EmojiPopup
+
+    @Inject
+    lateinit var arbitraryDataProvider: ArbitraryDataProvider
+
+    @Inject
+    lateinit var asyncRunner: AsyncRunner
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        arguments?.let {
+            currentUser = it.getParcelable(ARG_CURRENT_USER_PARAM)
+            currentStatus = it.getParcelable(ARG_CURRENT_STATUS_PARAM)
+
+            val json = arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PREDEFINED_STATUS)
+
+            if (json.isNotEmpty()) {
+                val myType = object : TypeToken<ArrayList<PredefinedStatus>>() {}.type
+                predefinedStatus = Gson().fromJson(json, myType)
+            }
+        }
+
+        val config = BundledEmojiCompatConfig(requireContext())
+        config.setReplaceAll(true)
+        val emojiCompat = EmojiCompat.init(config)
+
+        EmojiManager.install(GoogleCompatEmojiProvider(emojiCompat))
+    }
+
+    @SuppressLint("InflateParams")
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = DialogSetStatusBinding.inflate(LayoutInflater.from(context))
+
+        return AlertDialog.Builder(requireContext())
+            .setView(binding.root)
+            .create()
+    }
+
+    @SuppressLint("DefaultLocale")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        accountManager = (activity as BaseActivity).userAccountManager
+
+        currentStatus?.let {
+            emoji.setText(it.icon)
+            customStatusInput.text.clear()
+            customStatusInput.setText(it.message)
+            visualizeStatus(it.status)
+
+            if (it.clearAt > 0) {
+                clearStatusAfterSpinner.visibility = View.GONE
+                remainingClearTime.apply {
+                    clearStatusMessageTextView.text = getString(R.string.clear_status_message)
+                    visibility = View.VISIBLE
+                    text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * 1000, true)
+                        .toString()
+                        .decapitalize(Locale.getDefault())
+                    setOnClickListener {
+                        visibility = View.GONE
+                        clearStatusAfterSpinner.visibility = View.VISIBLE
+                        clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
+                    }
+                }
+            }
+        }
+
+        adapter = PredefinedStatusListAdapter(this, requireContext())
+        if (this::predefinedStatus.isInitialized) {
+            adapter.list = predefinedStatus
+        }
+        predefinedStatusList.adapter = adapter
+        predefinedStatusList.layoutManager = LinearLayoutManager(context)
+
+        onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) }
+        dndStatus.setOnClickListener { setStatus(StatusType.DND) }
+        awayStatus.setOnClickListener { setStatus(StatusType.AWAY) }
+        invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) }
+
+        clearStatus.setOnClickListener { clearStatus() }
+        setStatus.setOnClickListener { setStatusMessage() }
+        emoji.setOnClickListener { openEmojiPopup() }
+
+        popup = EmojiPopup.Builder
+            .fromRootView(view)
+            .setOnEmojiClickListener { _, _ ->
+                popup.dismiss()
+                emoji.clearFocus()
+                val imm: InputMethodManager = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as
+                    InputMethodManager
+                imm.hideSoftInputFromWindow(emoji.windowToken, 0)
+            }
+            .build(emoji)
+        emoji.disableKeyboardInput(popup)
+        emoji.forceSingleEmoji()
+
+        val adapter = ArrayAdapter<String>(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))
+
+        clearStatusAfterSpinner.apply {
+            this.adapter = adapter
+            onItemSelectedListener = object : OnItemSelectedListener {
+                override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
+                    setClearStatusAfterValue(position)
+                }
+
+                override fun onNothingSelected(parent: AdapterView<*>?) {
+                    // nothing to do
+                }
+            }
+        }
+    }
+
+    private fun setClearStatusAfterValue(item: Int) {
+        when (item) {
+            0 -> {
+                // don't clear
+                clearAt = null
+            }
+
+            1 -> {
+                // 30 minutes
+                clearAt = System.currentTimeMillis() / 1000 + 30 * 60
+            }
+
+            2 -> {
+                // one hour
+                clearAt = System.currentTimeMillis() / 1000 + 60 * 60
+            }
+
+            3 -> {
+                // four hours
+                clearAt = System.currentTimeMillis() / 1000 + 4 * 60 * 60
+            }
+
+            4 -> {
+                // today
+                val date = Calendar.getInstance().apply {
+                    set(Calendar.HOUR_OF_DAY, 23)
+                    set(Calendar.MINUTE, 59)
+                    set(Calendar.SECOND, 59)
+                }
+                clearAt = date.timeInMillis / 1000
+            }
+
+            5 -> {
+                // end of week
+                val date = Calendar.getInstance().apply {
+                    set(Calendar.HOUR_OF_DAY, 23)
+                    set(Calendar.MINUTE, 59)
+                    set(Calendar.SECOND, 59)
+                }
+
+                while (date.get(Calendar.DAY_OF_WEEK) != Calendar.SUNDAY) {
+                    date.add(Calendar.DAY_OF_YEAR, 1)
+                }
+
+                clearAt = date.timeInMillis / 1000
+            }
+        }
+    }
+
+    private fun clearAtToUnixTime(clearAt: ClearAt?): Long {
+        if (clearAt != null) {
+            if (clearAt.type.equals("period")) {
+                return System.currentTimeMillis() / 1000 + clearAt.time.toLong()
+            } else if (clearAt.type.equals("end-of")) {
+                if (clearAt.time.equals("day")) {
+                    val date = Calendar.getInstance().apply {
+                        set(Calendar.HOUR_OF_DAY, 23)
+                        set(Calendar.MINUTE, 59)
+                        set(Calendar.SECOND, 59)
+                    }
+                    return date.timeInMillis / 1000
+                }
+            }
+        }
+
+        return -1
+    }
+
+    private fun openEmojiPopup() {
+        popup.show()
+    }
+
+    private fun clearStatus() {
+        asyncRunner.postQuickTask(ClearStatusTask(accountManager.currentOwnCloudAccount?.savedAccount, context),
+            { dismiss(it) })
+    }
+
+    private fun setStatus(statusType: StatusType) {
+        visualizeStatus(statusType)
+
+        asyncRunner.postQuickTask(
+            SetStatusTask(
+                statusType,
+                accountManager.currentOwnCloudAccount?.savedAccount,
+                context),
+            {
+                if (!it) {
+                    clearTopStatus()
+                }
+            },
+            { clearTopStatus() }
+        )
+    }
+
+    private fun visualizeStatus(statusType: StatusType) {
+        when (statusType) {
+            StatusType.ONLINE -> {
+                clearTopStatus()
+                onlineStatus.setBackgroundColor(ThemeUtils.primaryColor(context))
+            }
+            StatusType.AWAY -> {
+                clearTopStatus()
+                awayStatus.setBackgroundColor(ThemeUtils.primaryColor(context))
+            }
+            StatusType.DND -> {
+                clearTopStatus()
+                dndStatus.setBackgroundColor(ThemeUtils.primaryColor(context))
+            }
+            StatusType.INVISIBLE -> {
+                clearTopStatus()
+                invisibleStatus.setBackgroundColor(ThemeUtils.primaryColor(context))
+            }
+            else -> clearTopStatus()
+        }
+    }
+
+    private fun clearTopStatus() {
+        val grey = resources.getColor(R.color.grey_200)
+
+        onlineStatus.setBackgroundColor(grey)
+        awayStatus.setBackgroundColor(grey)
+        dndStatus.setBackgroundColor(grey)
+        invisibleStatus.setBackgroundColor(grey)
+    }
+
+    private fun setStatusMessage() {
+        if (selectedPredefinedMessageId != null) {
+            asyncRunner.postQuickTask(
+                SetPredefinedCustomStatusTask(
+                    selectedPredefinedMessageId!!,
+                    clearAt,
+                    accountManager.currentOwnCloudAccount?.savedAccount,
+                    context),
+                { dismiss(it) }
+            )
+        } else {
+            asyncRunner.postQuickTask(
+                SetUserDefinedCustomStatusTask(
+                    customStatusInput.text.toString(),
+                    emoji.text.toString(),
+                    clearAt,
+                    accountManager.currentOwnCloudAccount?.savedAccount,
+                    context),
+                { dismiss(it) }
+            )
+        }
+    }
+
+    private fun dismiss(boolean: Boolean) {
+        if (boolean) {
+            dismiss()
+        }
+    }
+
+    /**
+     * Fragment creator
+     */
+    companion object {
+        @JvmStatic
+        fun newInstance(user: User, status: Status?): SetStatusDialogFragment {
+            val args = Bundle()
+            args.putParcelable(ARG_CURRENT_USER_PARAM, user)
+            args.putParcelable(ARG_CURRENT_STATUS_PARAM, status)
+            val dialogFragment = SetStatusDialogFragment()
+            dialogFragment.arguments = args
+            dialogFragment.setStyle(STYLE_NORMAL, R.style.Theme_ownCloud_Dialog)
+            return dialogFragment
+        }
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        return binding.root
+    }
+
+    override fun onClick(predefinedStatus: PredefinedStatus) {
+        selectedPredefinedMessageId = predefinedStatus.id
+        clearAt = clearAtToUnixTime(predefinedStatus.clearAt)
+        emoji.setText(predefinedStatus.icon)
+        customStatusInput.text.clear()
+        customStatusInput.text.append(predefinedStatus.message)
+
+        remainingClearTime.visibility = View.GONE
+        clearStatusAfterSpinner.visibility = View.VISIBLE
+        clearStatusMessageTextView.text = getString(R.string.clear_status_message_after)
+
+        if (predefinedStatus.clearAt == null) {
+            clearStatusAfterSpinner.setSelection(0)
+        } else {
+            val clearAt = predefinedStatus.clearAt!!
+            if (clearAt.type.equals("period")) {
+                when (clearAt.time) {
+                    "1800" -> clearStatusAfterSpinner.setSelection(1)
+                    "3600" -> clearStatusAfterSpinner.setSelection(2)
+                    "14400" -> clearStatusAfterSpinner.setSelection(3)
+                    else -> clearStatusAfterSpinner.setSelection(0)
+                }
+            } else if (clearAt.type.equals("end-of")) {
+                when (clearAt.time) {
+                    "day" -> clearStatusAfterSpinner.setSelection(4)
+                    "week" -> clearStatusAfterSpinner.setSelection(5)
+                    else -> clearStatusAfterSpinner.setSelection(0)
+                }
+            }
+        }
+        setClearStatusAfterValue(clearStatusAfterSpinner.selectedItemPosition)
+    }
+
+    @VisibleForTesting
+    fun setPredefinedStatus(predefinedStatus: ArrayList<PredefinedStatus>) {
+        adapter.list = predefinedStatus
+        predefinedStatusList.adapter?.notifyDataSetChanged()
+    }
+}

+ 39 - 0
src/main/java/com/nextcloud/ui/SetStatusTask.kt

@@ -0,0 +1,39 @@
+/*
+ *
+ * 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.ui
+
+import android.accounts.Account
+import android.content.Context
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+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<Boolean> {
+    override fun invoke(): Boolean {
+        val client = OwnCloudClientFactory.createNextcloudClient(account, context)
+
+        return SetStatusRemoteOperation(statusType).execute(client).isSuccess
+    }
+}

+ 40 - 0
src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt

@@ -0,0 +1,40 @@
+/*
+ *
+ * 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.ui
+
+import android.accounts.Account
+import android.content.Context
+import com.owncloud.android.lib.common.OwnCloudClientFactory
+import com.owncloud.android.lib.resources.users.SetUserDefinedCustomStatusMessageRemoteOperation
+
+public class SetUserDefinedCustomStatusTask(val message: String,
+                                            val icon: String,
+                                            val clearAt: Long?,
+                                            val account: Account?,
+                                            val context: Context?) : Function0<Boolean> {
+    override fun invoke(): Boolean {
+        val client = OwnCloudClientFactory.createNextcloudClient(account, context)
+
+        return SetUserDefinedCustomStatusMessageRemoteOperation(message, icon, clearAt).execute(client).isSuccess
+    }
+}

+ 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";

+ 12 - 0
src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -38,6 +38,7 @@ import android.text.TextUtils;
 
 import com.google.gson.Gson;
 import com.google.gson.JsonSyntaxException;
+import com.nextcloud.client.account.User;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import com.owncloud.android.lib.common.network.WebdavEntry;
@@ -2061,6 +2062,9 @@ public class FileDataStorageManager {
         contentValues.put(ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG,
                           capability.getDirectEditingEtag());
         contentValues.put(ProviderTableMeta.CAPABILITIES_ETAG, capability.getEtag());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_USER_STATUS, capability.getUserStatus().getValue());
+        contentValues.put(ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI,
+                          capability.getUserStatusSupportsEmoji().getValue());
 
         return contentValues;
     }
@@ -2100,6 +2104,11 @@ public class FileDataStorageManager {
         return cursor;
     }
 
+    @NonNull
+    public OCCapability getCapability(User user) {
+        return getCapability(user.getAccountName());
+    }
+
     @NonNull
     public OCCapability getCapability(String accountName) {
         OCCapability capability;
@@ -2192,6 +2201,9 @@ public class FileDataStorageManager {
                                                              ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME));
             capability.setDirectEditingEtag(getString(cursor, ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG));
             capability.setEtag(getString(cursor, ProviderTableMeta.CAPABILITIES_ETAG));
+            capability.setUserStatus(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS));
+            capability.setUserStatusSupportsEmoji(
+                getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI));
         }
         return capability;
     }

+ 4 - 2
src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -35,7 +35,7 @@ import java.util.List;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 59;
+    public static final int DB_VERSION = 60;
 
     private ProviderMeta() {
         // No instance
@@ -223,9 +223,11 @@ public class ProviderMeta {
         public static final String CAPABILITIES_RICHDOCUMENT_TEMPLATES = "richdocument_direct_templates";
         public static final String CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME = "richdocument_product_name";
         public static final String CAPABILITIES_DEFAULT_SORT_ORDER = CAPABILITIES_ACCOUNT_NAME
-                + " collate nocase asc";
+            + " collate nocase asc";
         public static final String CAPABILITIES_DIRECT_EDITING_ETAG = "direct_editing_etag";
         public static final String CAPABILITIES_ETAG = "etag";
+        public static final String CAPABILITIES_USER_STATUS = "user_status";
+        public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji";
 
         //Columns of Uploads table
         public static final String UPLOADS_LOCAL_PATH = "local_path";

+ 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;

+ 22 - 0
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -796,6 +796,8 @@ public class FileContentProvider extends ContentProvider {
                        + ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD + INTEGER
                        + ProviderTableMeta.CAPABILITIES_RICHDOCUMENT_PRODUCT_NAME + TEXT
                        + ProviderTableMeta.CAPABILITIES_DIRECT_EDITING_ETAG + TEXT
+                       + ProviderTableMeta.CAPABILITIES_USER_STATUS + INTEGER
+                       + ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI + INTEGER
                        + ProviderTableMeta.CAPABILITIES_ETAG + " TEXT );");
     }
 
@@ -2266,6 +2268,26 @@ public class FileContentProvider extends ContentProvider {
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
+            if (oldVersion < 60 && newVersion >= 60) {
+                Log_OC.i(SQL, "Entering in the #60 add user status to capability table");
+                db.beginTransaction();
+                try {
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
+                                   ADD_COLUMN + ProviderTableMeta.CAPABILITIES_USER_STATUS + " INTEGER ");
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.CAPABILITIES_TABLE_NAME +
+                                   ADD_COLUMN + ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI + " INTEGER ");
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
         }
     }
 }

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

@@ -27,25 +27,38 @@ import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.ParcelFileDescriptor;
 import android.provider.BaseColumns;
+import android.text.TextUtils;
 import android.widget.Toast;
 
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.owncloud.android.R;
+import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 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;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -72,6 +85,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
     private static final String[] COLUMNS = {
         BaseColumns._ID,
         SearchManager.SUGGEST_COLUMN_TEXT_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_2,
         SearchManager.SUGGEST_COLUMN_ICON_1,
         SearchManager.SUGGEST_COLUMN_INTENT_DATA
     };
@@ -229,7 +243,8 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                 Iterator<JSONObject> namesIt = names.iterator();
                 JSONObject item;
                 String displayName;
-                int icon = 0;
+                String subline = null;
+                Object icon = 0;
                 Uri dataUri;
                 int count = 0;
                 while (namesIt.hasNext()) {
@@ -237,13 +252,26 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                     dataUri = null;
                     displayName = null;
                     String userName = item.getString(GetShareesRemoteOperation.PROPERTY_LABEL);
+                    String name = item.isNull("name") ? "" : item.getString("name");
                     JSONObject value = item.getJSONObject(GetShareesRemoteOperation.NODE_VALUE);
                     ShareType type = ShareType.fromValue(value.getInt(GetShareesRemoteOperation.PROPERTY_SHARE_TYPE));
                     String shareWith = value.getString(GetShareesRemoteOperation.PROPERTY_SHARE_WITH);
 
+                    Status status;
+                    JSONObject statusObject = item.optJSONObject("status");
+
+                    if (statusObject != null) {
+                        status = new Status(StatusType.valueOf(statusObject.getString("status")),
+                                            statusObject.isNull("message") ? "" : statusObject.getString("message"),
+                                            statusObject.isNull("icon") ? "" : statusObject.getString("icon"),
+                                            statusObject.isNull("clearAt") ? -1 : statusObject.getLong("clearAt"));
+                    } else {
+                        status = new Status(StatusType.OFFLINE, "", "", -1);
+                    }
+
                     switch (type) {
                         case GROUP:
-                            displayName = getContext().getString(R.string.share_group_clarification, userName);
+                            displayName = userName;
                             icon = R.drawable.ic_group;
                             dataUri = Uri.withAppendedPath(groupBaseUri, shareWith);
                             break;
@@ -254,30 +282,47 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                                 dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith);
 
                                 if (userName.equals(shareWith)) {
-                                    displayName = getContext().getString(R.string.share_remote_clarification, userName);
+                                    displayName = name;
+                                    subline = getContext().getString(R.string.remote);
                                 } else {
                                     String[] uriSplitted = shareWith.split("@");
-                                    displayName = getContext().getString(R.string.share_known_remote_clarification,
-                                                                         userName, uriSplitted[uriSplitted.length - 1]);
+                                    displayName = name;
+                                    subline = getContext().getString(R.string.share_known_remote_on_clarification,
+                                                                     uriSplitted[uriSplitted.length - 1]);
                                 }
                             }
                             break;
 
                         case USER:
                             displayName = userName;
-                            icon = R.drawable.ic_user;
+                            subline = status.getMessage().isEmpty() ? null : status.getMessage();
+                            Uri.Builder builder =
+                                Uri.parse("content://com.nextcloud.android.providers.UsersAndGroupsSearchProvider/icon")
+                                    .buildUpon();
+
+                            builder.appendQueryParameter("shareWith", shareWith);
+                            builder.appendQueryParameter("displayName", displayName);
+                            builder.appendQueryParameter("status", status.getStatus().toString());
+
+                            if (!TextUtils.isEmpty(status.getIcon()) && !"null".equals(status.getIcon())) {
+                                builder.appendQueryParameter("icon", status.getIcon());
+                            }
+
+                            icon = builder.build();
+
                             dataUri = Uri.withAppendedPath(userBaseUri, shareWith);
                             break;
 
                         case EMAIL:
                             icon = R.drawable.ic_email;
-                            displayName = getContext().getString(R.string.share_email_clarification, userName);
+                            displayName = name;
+                            subline = shareWith;
                             dataUri = Uri.withAppendedPath(emailBaseUri, shareWith);
                             break;
 
                         case ROOM:
-                            icon = R.drawable.ic_chat_bubble;
-                            displayName = getContext().getString(R.string.share_room_clarification, userName);
+                            icon = R.drawable.ic_talk;
+                            displayName = userName;
                             dataUri = Uri.withAppendedPath(roomBaseUri, shareWith);
                             break;
 
@@ -295,6 +340,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
                         response.newRow()
                             .add(count++)             // BaseColumns._ID
                             .add(displayName)         // SearchManager.SUGGEST_COLUMN_TEXT_1
+                            .add(subline)             // SearchManager.SUGGEST_COLUMN_TEXT_2
                             .add(icon)                // SearchManager.SUGGEST_COLUMN_ICON_1
                             .add(dataUri);
                     }
@@ -324,6 +370,59 @@ public class UsersAndGroupsSearchProvider extends ContentProvider {
         return 0;
     }
 
+    @Nullable
+    @Override
+    @SuppressFBWarnings("IOI_USE_OF_FILE_STREAM_CONSTRUCTORS") // TODO remove with API26
+    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+        ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProvider(getContext().getContentResolver());
+
+        String userId = uri.getQueryParameter("shareWith");
+        String displayName = uri.getQueryParameter("displayName");
+        String accountName = accountManager.getUser().getAccountName();
+        String serverName = accountName.substring(accountName.lastIndexOf('@') + 1);
+
+        String eTag = arbitraryDataProvider.getValue(userId + "@" + serverName, ThumbnailsCacheManager.AVATAR);
+        String avatarKey = "a_" + userId + "_" + serverName + "_" + eTag;
+
+        StatusType status = StatusType.valueOf(uri.getQueryParameter("status"));
+        String icon = uri.getQueryParameter("icon");
+
+        Bitmap avatarBitmap = ThumbnailsCacheManager.getBitmapFromDiskCache(avatarKey);
+
+        if (avatarBitmap == null) {
+            float avatarRadius = getContext().getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
+            avatarBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar(displayName, avatarRadius));
+        }
+
+        Bitmap avatar = BitmapUtils.createAvatarWithStatus(avatarBitmap, status, icon, getContext());
+
+        // create a file to write bitmap data
+        File f = new File(getContext().getCacheDir(), "test");
+        try {
+            if (!f.createNewFile()) {
+                throw new IllegalStateException("File could not be created!");
+            }
+
+            //Convert bitmap to byte array
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+
+            avatar.compress(Bitmap.CompressFormat.PNG, 90, bos);
+            byte[] bitmapData = bos.toByteArray();
+
+            //write the bytes in file
+            try (FileOutputStream fos = new FileOutputStream(f)) {
+                fos.write(bitmapData);
+            } catch (FileNotFoundException e) {
+                Log_OC.e(TAG, "File not found: " + e.getMessage());
+            }
+
+        } catch (Exception e) {
+            Log_OC.e(TAG, "Error opening file: " + e.getMessage());
+        }
+
+        return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+
     /**
      * Show error message
      *

+ 142 - 0
src/main/java/com/owncloud/android/ui/StatusDrawable.java

@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+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.R;
+import com.owncloud.android.lib.resources.users.Status;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.core.content.res.ResourcesCompat;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * A Drawable object that draws a status
+ */
+@SuppressFBWarnings("PME_POOR_MANS_ENUM")
+public class StatusDrawable extends Drawable {
+    private String text;
+    private @DrawableRes int icon = -1;
+    private Paint textPaint;
+    private Paint backgroundPaint;
+    private final 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(Status status, float statusSize, Context context) {
+        backgroundPaint = new Paint();
+        backgroundPaint.setStyle(Paint.Style.FILL);
+        backgroundPaint.setAntiAlias(true);
+
+        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);
+        }
+    }
+
+    /**
+     * Draw in its bounds (set via setBounds) respecting optional effects such as alpha (set via setAlpha) and color
+     * filter (set via setColorFilter) a circular background with a user's first character.
+     *
+     * @param canvas The canvas to draw into
+     */
+    @Override
+    public void draw(@NonNull Canvas canvas) {
+        if (backgroundPaint != null) {
+            canvas.drawCircle(radius, radius, radius, backgroundPaint);
+        }
+
+        if (text != null) {
+            textPaint.setTextSize(1.6f * radius);
+            canvas.drawText(text, radius, radius - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
+        }
+
+        if (icon != -1) {
+            Drawable drawable = ResourcesCompat.getDrawable(context.getResources(), icon, null);
+
+            if (drawable != null) {
+                drawable.setBounds(0,
+                                   0,
+                                   (int) (2 * radius),
+                                   (int) (2 * radius));
+                drawable.draw(canvas);
+            }
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        textPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        textPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+}

+ 22 - 18
src/main/java/com/owncloud/android/ui/TextDrawable.java

@@ -2,7 +2,7 @@
  * ownCloud Android client application
  *
  * @author Andy Scherzinger
- * @author Tobias Kaminsiky
+ * @author Tobias Kaminsky
  * @author Chris Narkiewicz
  * Copyright (C) 2016 ownCloud Inc.
  * Copyright (C) 2018 Andy Scherzinger
@@ -35,7 +35,6 @@ import com.nextcloud.client.account.UserAccountManager;
 import com.owncloud.android.utils.BitmapUtils;
 import com.owncloud.android.utils.NextcloudServer;
 
-import java.security.NoSuchAlgorithmException;
 import java.util.Locale;
 
 import androidx.annotation.NonNull;
@@ -65,6 +64,8 @@ public class TextDrawable extends Drawable {
      */
     private float mRadius;
 
+    private boolean bigText = false;
+
     /**
      * Create a TextDrawable with the given radius.
      *
@@ -79,44 +80,43 @@ public class TextDrawable extends Drawable {
         mBackground = new Paint();
         mBackground.setStyle(Paint.Style.FILL);
         mBackground.setAntiAlias(true);
-        mBackground.setColor(Color.rgb(color.r, color.g, color.b));
+        mBackground.setColor(Color.argb(color.a, color.r, color.g, color.b));
 
         mTextPaint = new Paint();
         mTextPaint.setColor(Color.WHITE);
         mTextPaint.setTextSize(radius);
         mTextPaint.setAntiAlias(true);
         mTextPaint.setTextAlign(Paint.Align.CENTER);
+
+        setBounds(0, 0, (int) radius * 2, (int) radius * 2);
     }
 
     /**
-     * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the
-     * given radius.
+     * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the given
+     * radius.
      *
-     * @param account user account
-     * @param radiusInDp  the circle's radius
+     * @param account    user account
+     * @param radiusInDp the circle's radius
      * @return the avatar as a TextDrawable
-     * @throws NoSuchAlgorithmException     if the specified algorithm is not available when calculating the color values
      */
     @NonNull
     @NextcloudServer(max = 12)
-    public static TextDrawable createAvatar(Account account, float radiusInDp) throws
-            NoSuchAlgorithmException {
+    public static TextDrawable createAvatar(Account account, float radiusInDp) {
         String username = UserAccountManager.getDisplayName(account);
         return createNamedAvatar(username, radiusInDp);
     }
 
     /**
-     * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the
-     * given radius.
+     * creates an avatar in form of a TextDrawable with the first letter of the account name in a circle with the given
+     * radius.
      *
-     * @param userId      userId to use
-     * @param radiusInDp  the circle's radius
+     * @param userId     userId to use
+     * @param radiusInDp the circle's radius
      * @return the avatar as a TextDrawable
-     * @throws NoSuchAlgorithmException     if the specified algorithm is not available when calculating the color values
      */
     @NonNull
     @NextcloudServer(max = 12)
-    public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) throws NoSuchAlgorithmException {
+    public static TextDrawable createAvatarByUserId(String userId, float radiusInDp) {
         return createNamedAvatar(userId, radiusInDp);
     }
 
@@ -127,10 +127,9 @@ public class TextDrawable extends Drawable {
      * @param name       the name
      * @param radiusInDp the circle's radius
      * @return the avatar as a TextDrawable
-     * @throws NoSuchAlgorithmException     if the specified algorithm is not available when calculating the color values
      */
     @NonNull
-    public static TextDrawable createNamedAvatar(String name, float radiusInDp) throws NoSuchAlgorithmException {
+    public static TextDrawable createNamedAvatar(String name, float radiusInDp) {
         BitmapUtils.Color color = BitmapUtils.usernameToColor(name);
         return new TextDrawable(extractCharsFromDisplayName(name), color, radiusInDp);
     }
@@ -160,6 +159,11 @@ public class TextDrawable extends Drawable {
     @Override
     public void draw(@NonNull Canvas canvas) {
         canvas.drawCircle(mRadius, mRadius, mRadius, mBackground);
+
+        if (bigText) {
+            mTextPaint.setTextSize(1.8f * mRadius);
+        }
+
         canvas.drawText(mText, mRadius, mRadius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
     }
 

+ 29 - 0
src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusClickListener.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * 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.adapter
+
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+
+interface PredefinedStatusClickListener {
+    fun onClick(predefinedStatus: PredefinedStatus)
+}

+ 48 - 0
src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusListAdapter.kt

@@ -0,0 +1,48 @@
+/*
+ *
+ * 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.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.databinding.PredefinedStatusBinding
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+
+class PredefinedStatusListAdapter(private val clickListener: PredefinedStatusClickListener,
+                                  val context: Context) : RecyclerView.Adapter<PredefinedStatusViewHolder>() {
+    internal var list: List<PredefinedStatus> = 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 {
+        return list.size
+    }
+}

+ 55 - 0
src/main/java/com/owncloud/android/ui/adapter/PredefinedStatusViewHolder.kt

@@ -0,0 +1,55 @@
+/*
+ *
+ * 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.adapter
+
+import android.content.Context
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.R
+import com.owncloud.android.databinding.PredefinedStatusBinding
+import com.owncloud.android.lib.resources.users.PredefinedStatus
+import com.owncloud.android.utils.DisplayUtils
+
+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.equals("period")) {
+                binding.clearAt.text = DisplayUtils.getRelativeTimestamp(
+                    context,
+                    System.currentTimeMillis() + clearAt.time.toInt() * 1000,
+                    true)
+            } else {
+                // end-of
+                if (clearAt.time.equals("day")) {
+                    binding.clearAt.text = context.getString(R.string.today)
+                }
+            }
+        }
+    }
+}

+ 1 - 3
src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java

@@ -31,8 +31,6 @@ import com.owncloud.android.databinding.FileDetailsShareShareItemBinding;
 import com.owncloud.android.lib.resources.shares.OCShare;
 import com.owncloud.android.ui.TextDrawable;
 
-import java.security.NoSuchAlgorithmException;
-
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
@@ -95,7 +93,7 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
     private void setImage(ImageView avatar, String name, @DrawableRes int fallback) {
         try {
             avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension));
-        } catch (NoSuchAlgorithmException | StringIndexOutOfBoundsException e) {
+        } catch (StringIndexOutOfBoundsException e) {
             avatar.setImageResource(fallback);
         }
     }

+ 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);
+        }
+
+    }
+}

+ 56 - 0
src/main/java/com/owncloud/android/ui/components/AvatarWithStatus.kt

@@ -0,0 +1,56 @@
+/*
+ *
+ * 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.components
+
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.drawable.Drawable
+import androidx.core.graphics.drawable.RoundedBitmapDrawable
+
+class AvatarWithStatus(val roundedBitmapDrawable: RoundedBitmapDrawable) : Drawable() {
+    private val redPaint: Paint = Paint().apply { setARGB(255, 255, 0, 0) }
+
+    private val avatarSize = 100
+
+    override fun draw(canvas: Canvas) {
+        val width = avatarSize
+        val height = avatarSize
+        val radius: Float = Math.min(width, height).toFloat() / 2f
+
+        // Draw a red circle in the center
+        // canvas.drawBitmap(roundedBitmapDrawable.bitmap!!, 0f, 0f, null)
+        canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, redPaint)
+    }
+
+    override fun setAlpha(alpha: Int) {
+        TODO("Not yet implemented")
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getOpacity(): Int = PixelFormat.OPAQUE
+}

+ 7 - 0
src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java

@@ -76,6 +76,7 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.appcompat.widget.PopupMenu;
+import androidx.appcompat.widget.SearchView;
 import androidx.fragment.app.Fragment;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
@@ -713,4 +714,10 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
     private boolean canReshare(OCShare share) {
         return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0;
     }
+
+    @VisibleForTesting
+    public void search(String query) {
+        SearchView searchView = getView().findViewById(R.id.searchView);
+        searchView.setQuery(query, true);
+    }
 }

+ 79 - 4
src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -18,12 +18,17 @@
  */
 package com.owncloud.android.utils;
 
+import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory.Options;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.widget.ImageView;
@@ -31,6 +36,9 @@ import android.widget.ImageView;
 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;
 
@@ -194,12 +202,19 @@ public final class BitmapUtils {
         return resultBitmap;
     }
 
-    public static Color usernameToColor(String name) throws NoSuchAlgorithmException {
+    public static Color usernameToColor(String name) {
         String hash = name.toLowerCase(Locale.ROOT);
 
         // already a md5 hash?
         if (!hash.matches("([0-9a-f]{4}-?){8}$")) {
-            hash = md5(hash);
+            try {
+                hash = md5(hash);
+            } catch (NoSuchAlgorithmException e) {
+                int color = getResources().getColor(R.color.primary_dark);
+                return new Color(android.graphics.Color.red(color),
+                                 android.graphics.Color.green(color),
+                                 android.graphics.Color.blue(color));
+            }
         }
 
         hash = hash.replaceAll("[^0-9a-f]", "");
@@ -279,6 +294,7 @@ public final class BitmapUtils {
     }
 
     public static class Color {
+        public int a = 255;
         public int r;
         public int g;
         public int b;
@@ -289,6 +305,13 @@ public final class BitmapUtils {
             this.b = b;
         }
 
+        public Color(int a, int r, int g, int b) {
+            this.a = a;
+            this.r = r;
+            this.g = g;
+            this.b = b;
+        }
+
         @Override
         public boolean equals(@Nullable Object obj) {
             if (!(obj instanceof Color)) {
@@ -358,9 +381,16 @@ public final class BitmapUtils {
 
         Bitmap bitmap;
         if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
-            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+            if (drawable.getBounds().width() > 0 && drawable.getBounds().height() > 0) {
+                bitmap = Bitmap.createBitmap(drawable.getBounds().width(),
+                                             drawable.getBounds().height(),
+                                             Bitmap.Config.ARGB_8888);
+            } else {
+                bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+            }
         } else {
-            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
+            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                                         drawable.getIntrinsicHeight(),
                                          Bitmap.Config.ARGB_8888);
         }
 
@@ -384,6 +414,51 @@ public final class BitmapUtils {
                                      imageView);
     }
 
+    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);
+
+        Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+
+        // avatar
+        Bitmap croppedBitmap = getCroppedBitmap(avatar, width);
+
+        canvas.drawBitmap(croppedBitmap, 0f, 0f, null);
+
+        // status
+        int statusSize = width / 4;
+
+        Status status = new Status(statusType, "", icon, -1);
+        StatusDrawable statusDrawable = new StatusDrawable(status, statusSize, context);
+
+        canvas.translate(width / 2f, width / 2f);
+        statusDrawable.draw(canvas);
+
+        return output;
+    }
+
+    /**
+     * from https://stackoverflow.com/a/12089127
+     */
+    private static Bitmap getCroppedBitmap(Bitmap bitmap, int width) {
+        Bitmap output = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+        int color = -0xbdbdbe;
+        Paint paint = new Paint();
+        Rect rect = new Rect(0, 0, width, width);
+        paint.setAntiAlias(true);
+        canvas.drawARGB(0, 0, 0, 0);
+        paint.setColor(color);
+
+        canvas.drawCircle(width / 2f, width / 2f, width / 2f, paint);
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+
+        canvas.drawBitmap(Bitmap.createScaledBitmap(bitmap, width, width, false), rect, rect, paint);
+
+        return output;
+    }
+
     private static Resources getResources() {
         return MainApp.getAppContext().getResources();
     }

+ 21 - 2
src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -313,6 +313,15 @@ public final class DisplayUtils {
                                          DateUtils.WEEK_IN_MILLIS, 0);
     }
 
+    public static CharSequence getRelativeTimestamp(Context context, long modificationTimestamp, boolean showFuture) {
+        return getRelativeDateTimeString(context,
+                                         modificationTimestamp,
+                                         DateUtils.SECOND_IN_MILLIS,
+                                         DateUtils.WEEK_IN_MILLIS,
+                                         0,
+                                         showFuture);
+    }
+
 
     /**
      * determines the info level color based on {@link #RELATIVE_THRESHOLD_WARNING}.
@@ -331,15 +340,25 @@ public final class DisplayUtils {
 
     public static CharSequence getRelativeDateTimeString(Context c, long time, long minResolution,
                                                          long transitionResolution, int flags) {
+        return getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags, false);
+    }
+
+    public static CharSequence getRelativeDateTimeString(Context c,
+                                                         long time,
+                                                         long minResolution,
+                                                         long transitionResolution,
+                                                         int flags,
+                                                         boolean showFuture) {
 
         CharSequence dateString = "";
 
         // in Future
-        if (time > System.currentTimeMillis()) {
+        if (!showFuture && time > System.currentTimeMillis()) {
             return DisplayUtils.unixTimeToHumanReadable(time);
         }
         // < 60 seconds -> seconds ago
-        else if ((System.currentTimeMillis() - time) < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) {
+        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 {
             dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags);

+ 4 - 2
src/main/res/drawable/ic_circles.xml

@@ -1,9 +1,11 @@
-<vector android:autoMirrored="true"
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
     android:height="64dp"
     android:viewportHeight="57"
     android:viewportWidth="57"
     android:width="64dp"
-    xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
     <path
         android:fillAlpha="1"
         android:fillColor="#00000000"

+ 4 - 2
src/main/res/drawable/ic_edit.xml

@@ -20,12 +20,14 @@
   ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<vector android:autoMirrored="true"
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
     android:height="24dp"
     android:viewportHeight="24"
     android:viewportWidth="24"
     android:width="24dp"
-    xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
     <path
         android:fillColor="#FF000000"
         android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />

+ 4 - 2
src/main/res/drawable/ic_post_add.xml

@@ -20,12 +20,14 @@
   ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<vector android:autoMirrored="true"
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
     android:height="24dp"
     android:viewportHeight="24"
     android:viewportWidth="24"
     android:width="24dp"
-    xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
     <path
         android:fillColor="#FF000000"
         android:pathData="M17,19.22H5V7h7V5H5C3.9,5 3,5.9 3,7v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-7h-2V19.22z" />

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

@@ -0,0 +1,28 @@
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Nextcloud GmbH
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="128"
+    android:viewportHeight="128">
+    <path
+        android:fillColor="#757575"
+        android:pathData="M63.992,0.689C29.031,0.689 0.691,29.031 0.692,63.992c0,34.96 28.34,63.301 63.3,63.302 6.982,-0.014 13.881,-1.183 20.426,-3.43 4.317,-1.482 8.48,-3.433 12.411,-5.831 3.383,1.344 8.59,3.838 13.736,5.902 6.688,2.683 13.274,4.639 15.618,2.399 2.317,-2.212 0.703,-8.809 -1.647,-15.575 -2.046,-5.892 -4.649,-11.913 -5.701,-15.282 2.544,-4.415 4.535,-9.101 5.945,-13.954 1.648,-5.674 2.5,-11.574 2.512,-17.532C127.291,29.032 98.952,0.692 63.992,0.691ZM63.999,24.756l0.001,0c21.677,0 39.25,17.573 39.25,39.251 -0.001,21.677 -17.574,39.249 -39.251,39.249 -21.676,0 -39.249,-17.572 -39.25,-39.249 0,-21.678 17.573,-39.251 39.25,-39.251z"
+        android:strokeWidth="4.78543139" />
+</vector>

+ 32 - 0
src/main/res/drawable/ic_user_status_away.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
+  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/>.
+-->
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#f4a331"
+        android:pathData="m10.615,2.1094c-4.8491,0.6811 -8.6152,4.8615 -8.6152,9.8906 0,5.5 4.5,10 10,10 5.0292,0 9.2096,-3.7661 9.8906,-8.6152 -1.4654,1.601 -3.5625,2.6152 -5.8906,2.6152 -4.4,0 -8,-3.6 -8,-8 0,-2.3281 1.0143,-4.4252 2.6152,-5.8906z" />
+</vector>

+ 38 - 0
src/main/res/drawable/ic_user_status_dnd.xml

@@ -0,0 +1,38 @@
+<!--
+  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
+  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/>.
+-->
+<vector xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <path
+        android:fillColor="#ed484c"
+        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,-10z" />
+    <path
+        android:fillColor="#fdffff"
+        android:pathData="m8,10h8c1.108,0 2,0.892 2,2s-0.892,2 -2,2h-8c-1.108,0 -2,-0.892 -2,-2s0.892,-2 2,-2z"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round"
+        android:strokeWidth="2" />
+</vector>

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

@@ -0,0 +1,34 @@
+<!--
+  ~
+  ~ 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 xmlns:tools="http://schemas.android.com/tools"
+    android:autoMirrored="true"
+    android:height="24dp"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    tools:ignore="VectorRaster">
+    <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>

+ 21 - 3
src/main/res/layout/account_item.xml

@@ -61,7 +61,8 @@
                 android:layout_gravity="bottom|end"
                 android:background="@drawable/round_bgnd"
                 android:contentDescription="@string/active_user"
-                android:src="@drawable/ic_check_circle" />
+                android:src="@drawable/ic_check_circle"
+                tools:visibility="gone" />
         </FrameLayout>
 
 
@@ -86,7 +87,23 @@
                 android:gravity="bottom"
                 android:maxLines="1"
                 android:text="@string/placeholder_filename"
-                android:textAppearance="?android:attr/textAppearanceListItem" />
+                android:textAppearance="?android:attr/textAppearanceListItem"
+                tools:text="Firstname Lastname" />
+
+            <TextView
+                android:id="@+id/status"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/standard_half_margin"
+                android:layout_marginEnd="@dimen/standard_double_margin"
+                android:layout_marginBottom="@dimen/standard_quarter_margin"
+                android:ellipsize="end"
+                android:gravity="top"
+                android:maxLines="1"
+                android:text="@string/placeholder_sentence"
+                android:textColor="?android:attr/textColorSecondary"
+                android:visibility="gone"
+                tools:text="☁️ My custom status" />
 
             <TextView
                 android:id="@+id/account"
@@ -99,7 +116,8 @@
                 android:gravity="top"
                 android:maxLines="1"
                 android:text="@string/placeholder_sentence"
-                android:textColor="?android:attr/textColorSecondary" />
+                android:textColor="?android:attr/textColorSecondary"
+                tools:text="https://server.com/nextcloud" />
 
         </LinearLayout>
 

+ 37 - 1
src/main/res/layout/dialog_choose_account.xml

@@ -33,6 +33,42 @@
         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:orientation="vertical"
+        android:visibility="gone"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/current_account"
+        tools:visibility="visible">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:layout_marginTop="@dimen/standard_quarter_margin"
+            android:background="@color/list_divider_background" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/set_status"
+            style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/account_action_button_height"
+            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/set_status"
+            android:textAlignment="textStart"
+            android:textAllCaps="false"
+            android:textColor="@color/fontAppbar"
+            app:icon="@drawable/ic_edit"
+            app:iconGravity="start"
+            app:iconPadding="22dp"
+            app:iconTint="@color/fontAppbar" />
+    </LinearLayout>
+
     <View
         android:id="@+id/separator_line"
         android:layout_width="0dp"
@@ -41,7 +77,7 @@
         android:background="@color/list_divider_background"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/current_account" />
+        app:layout_constraintTop_toBottomOf="@id/statusView" />
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/accounts_list"

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

@@ -0,0 +1,441 @@
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Andy Scherzinger
+  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:orientation="vertical"
+    android:padding="@dimen/standard_padding">
+
+    <TextView
+        android:id="@+id/onlineStatusView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/standard_half_margin"
+        android:text="@string/online_status"
+        android:textColor="@color/text_color"
+        android:textSize="@dimen/activity_list_item_title_header_text_size"
+        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="vertical"
+        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_marginBottom="@dimen/standard_margin"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/onlineStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/online_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/online_status"
+                        app:tint="@color/hwSecurityGreen" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/online_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/online_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/online"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                        <TextView
+                            android:id="@+id/online_text"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="@dimen/standard_half_margin"
+                            android:layout_marginBottom="@dimen/standard_quarter_margin"
+                            android:ellipsize="end"
+                            android:gravity="top"
+                            android:maxLines="1"
+                            android:textColor="?android:attr/textColorSecondary"
+                            android:visibility="gone" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/awayStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/away_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_away"
+                        app:tint="#f4a331" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/away_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/away_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/away"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                        <TextView
+                            android:id="@+id/away_text"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_marginStart="@dimen/standard_half_margin"
+                            android:layout_marginBottom="@dimen/standard_quarter_margin"
+                            android:ellipsize="end"
+                            android:gravity="top"
+                            android:maxLines="1"
+                            android:textColor="?android:attr/textColorSecondary"
+                            android:visibility="gone" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_margin"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/dndStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/dnd_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_dnd" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/dnd_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/dnd_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/dnd"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/invisibleStatus"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/online_status_item_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginStart="@dimen/standard_half_margin"
+
+                android:layout_weight="1"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp">
+
+                <RelativeLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginStart="@dimen/standard_margin"
+                    android:layout_marginEnd="@dimen/standard_margin"
+                    android:orientation="horizontal"
+                    tools:ignore="UnusedAttribute">
+
+                    <ImageView
+                        android:id="@+id/invisible_icon"
+                        android:layout_width="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_height="@dimen/iconized_single_line_item_icon_size"
+                        android:layout_alignParentStart="true"
+                        android:layout_centerVertical="true"
+                        android:layout_gravity="top|start"
+                        android:layout_marginEnd="@dimen/standard_half_margin"
+                        android:contentDescription="@null"
+                        android:src="@drawable/ic_user_status_invisible" />
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_centerInParent="true"
+                        android:layout_toEndOf="@id/invisible_icon"
+                        android:orientation="vertical">
+
+                        <TextView
+                            android:id="@+id/invisible_headline"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:ellipsize="end"
+                            android:gravity="bottom"
+                            android:maxLines="1"
+                            android:text="@string/invisible"
+                            android:textAppearance="?android:attr/textAppearanceListItem" />
+
+                    </LinearLayout>
+
+                </RelativeLayout>
+
+            </com.google.android.material.card.MaterialCardView>
+
+        </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" />
+
+    <LinearLayout
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/statusMessage"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/standard_half_margin"
+            android:text="@string/status_message"
+            android:textColor="@color/text_color"
+            android:textSize="@dimen/activity_list_item_title_header_text_size"
+            app:layout_constraintTop_toBottomOf="@+id/statusView" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/activity_row_layout_height"
+            android:orientation="horizontal">
+
+            <com.google.android.material.card.MaterialCardView
+                android:id="@+id/emojiCard"
+                android:layout_width="@dimen/activity_row_layout_height"
+                android:layout_height="@dimen/activity_row_layout_height"
+                android:layout_gravity="center_vertical"
+                android:layout_marginEnd="@dimen/standard_half_margin"
+                android:orientation="horizontal"
+                app:cardBackgroundColor="@color/grey_200"
+                app:cardElevation="0dp">
+
+                <com.vanniktech.emoji.EmojiEditText
+                    android:id="@+id/emoji"
+                    android:layout_width="@dimen/activity_row_layout_height"
+                    android:layout_height="@dimen/activity_row_layout_height"
+                    android:background="@color/grey_200"
+                    android:cursorVisible="false"
+                    android:gravity="center"
+                    android:text="@string/default_emoji"
+                    android:textSize="25sp"
+                    app:maxEmojiCount="1" />
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <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/predefinedStatusList"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            tools:itemCount="5"
+            tools:listitem="@layout/predefined_status" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/clearStatusMessageTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/clear_status_message_after"
+            android:textColor="@color/text_color" />
+
+        <Spinner
+            android:id="@+id/clearStatusAfterSpinner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/remainingClearTime"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textColor="@color/text_color"
+            android:layout_marginStart="4dp"
+            android:visibility="gone" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/standard_half_margin"
+        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_marginEnd="@dimen/standard_half_margin"
+            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_weight="1"
+            android:text="@string/set_status_message"
+            android:theme="@style/Button.Primary"
+            app:cornerRadius="@dimen/button_corner_radius" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 62 - 0
src/main/res/layout/predefined_status.xml

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+
+  Nextcloud Android client application
+
+  Copyright (C) 2020 Andy Scherzinger
+  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/icon"
+        android:layout_width="48dp"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:textSize="25sp"
+        tools:text="📆" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        tools:text="In a meeting" />
+
+    <TextView
+        android:id="@+id/divider"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_margin="@dimen/standard_half_margin"
+        android:gravity="center_vertical"
+        android:text="@string/divider"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary" />
+
+    <TextView
+        android:id="@+id/clearAt"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:textAppearance="?android:attr/textAppearanceListItem"
+        android:textColor="?android:attr/textColorSecondary"
+        tools:text="an hour" />
+</LinearLayout>

+ 1 - 0
src/main/res/values/dims.xml

@@ -138,4 +138,5 @@
     <dimen name="account_action_button_margin">12dp</dimen>
     <dimen name="account_action_button_height">50dp</dimen>
     <dimen name="account_action_button_vertical_margin">10dp</dimen>
+    <dimen name="online_status_item_height">52dp</dimen>
 </resources>

+ 21 - 2
src/main/res/values/strings.xml

@@ -477,9 +477,8 @@
 
     <string name="share_group_clarification">%1$s (group)</string>
     <string name="share_remote_clarification">%1$s (remote)</string>
-    <string name="share_email_clarification">%1$s (email)</string>
     <string name="share_room_clarification">%1$s (conversation)</string>
-    <string name="share_known_remote_clarification">%1$s ( at %2$s )</string>
+    <string name="share_known_remote_on_clarification">on %1$s</string>
 
     <string name="share_privilege_unshare">Unshare</string>
 
@@ -936,4 +935,24 @@
     <string name="link_share_file_drop">File drop (upload only)</string>
     <string name="could_not_retrieve_shares">Could not retrieve shares</string>
     <string name="failed_update_ui">Failed to update UI</string>
+    <string name="remote">(remote)</string>
+    <string name="set_status">Set status</string>
+    <string name="online_status">Online status</string>
+    <string name="status_message">Status message</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>
+    <string translatable="false" name="default_emoji">😃</string>
+    <string name="dontClear">Don\'t clear</string>
+    <string name="today">Today</string>
+    <string name="thirtyMinutes">30 minutes</string>
+    <string name="oneHour">1 hour</string>
+    <string name="fourHours">4 hour</string>
+    <string name="thisWeek">This week</string>
 </resources>