Ver Fonte

Merge pull request #10719 from nextcloud/dashboard

Dashboard
Álvaro Brey há 2 anos atrás
pai
commit
2cbd6d9659
48 ficheiros alterados com 2033 adições e 164 exclusões
  1. BIN
      app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png
  2. BIN
      app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png
  3. 139 0
      app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt
  4. 25 18
      app/src/main/AndroidManifest.xml
  5. 13 1
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  6. 4 4
      app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java
  7. 43 45
      app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java
  8. 3 3
      app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java
  9. 236 0
      app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt
  10. 29 0
      app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt
  11. 80 0
      app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt
  12. 243 0
      app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt
  13. 166 0
      app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt
  14. 37 0
      app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt
  15. 150 0
      app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt
  16. 1 0
      app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
  17. 18 0
      app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java
  18. 2 8
      app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
  19. 3 0
      app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
  20. 7 5
      app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  21. 72 0
      app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt
  22. 6 5
      app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java
  23. 16 6
      app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java
  24. 77 0
      app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt
  25. 29 0
      app/src/main/java/com/owncloud/android/ui/dialog/AccountChooserInterface.kt
  26. 6 4
      app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java
  27. 1 3
      app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt
  28. 40 1
      app/src/main/java/com/owncloud/android/utils/BitmapUtils.java
  29. 9 14
      app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
  30. 3 15
      app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.java
  31. 22 3
      app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.java
  32. 4 4
      app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.java
  33. 11 0
      app/src/main/res/drawable/ic_check.xml
  34. 35 0
      app/src/main/res/drawable/ic_dashboard.xml
  35. 142 0
      app/src/main/res/layout/dashboard_widget.xml
  36. 79 0
      app/src/main/res/layout/dashboard_widget_configuration_layout.xml
  37. 68 0
      app/src/main/res/layout/widget_item.xml
  38. 37 0
      app/src/main/res/layout/widget_item_load_more.xml
  39. 47 0
      app/src/main/res/layout/widget_list_item.xml
  40. 15 10
      app/src/main/res/values/attrs.xml
  41. 1 2
      app/src/main/res/values/colors.xml
  42. 13 2
      app/src/main/res/values/strings.xml
  43. 9 8
      app/src/main/res/values/styles.xml
  44. 39 0
      app/src/main/res/values/themes.xml
  45. 32 0
      app/src/main/res/xml/dashboard_widget_info.xml
  46. 3 3
      app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java
  47. 3 0
      app/src/test/java/com/owncloud/android/ui/adapter/UserListAdapterTest.java
  48. 15 0
      drawable_resources/dashboard.svg

BIN
app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png


BIN
app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png


+ 139 - 0
app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt

@@ -0,0 +1,139 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 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.graphics.BitmapFactory
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.espresso.intent.rule.IntentsTestRule
+import com.nextcloud.client.TestActivity
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.R
+import com.owncloud.android.utils.BitmapUtils
+import com.owncloud.android.utils.ScreenshotTest
+import org.junit.Rule
+import org.junit.Test
+
+class BitmapIT : AbstractIT() {
+    @get:Rule
+    val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
+
+    @Test
+    @ScreenshotTest
+    fun roundBitmap() {
+        val file = getFile("christine.jpg")
+        val bitmap = BitmapFactory.decodeFile(file.absolutePath)
+
+        val activity = testActivityRule.launchActivity(null)
+        val imageView = ImageView(activity).apply {
+            setImageBitmap(bitmap)
+        }
+
+        val bitmap2 = BitmapFactory.decodeFile(file.absolutePath)
+        val imageView2 = ImageView(activity).apply {
+            setImageBitmap(BitmapUtils.roundBitmap(bitmap2))
+        }
+
+        val linearLayout = LinearLayout(activity).apply {
+            orientation = LinearLayout.VERTICAL
+            setBackgroundColor(context.getColor(R.color.grey_200))
+        }
+        linearLayout.addView(imageView, 200, 200)
+        linearLayout.addView(imageView2, 200, 200)
+        activity.addView(linearLayout)
+
+        screenshot(activity)
+    }
+
+    // @Test
+    // @ScreenshotTest
+    // fun glideSVG() {
+    //     val activity = testActivityRule.launchActivity(null)
+    //     val accountProvider = UserAccountManagerImpl.fromContext(activity)
+    //     val clientFactory = ClientFactoryImpl(activity)
+    //
+    //     val linearLayout = LinearLayout(activity).apply {
+    //         orientation = LinearLayout.VERTICAL
+    //         setBackgroundColor(context.getColor(R.color.grey_200))
+    //     }
+    //
+    //     val file = getFile("christine.jpg")
+    //     val bitmap = BitmapFactory.decodeFile(file.absolutePath)
+    //
+    //     ImageView(activity).apply {
+    //         setImageBitmap(bitmap)
+    //         linearLayout.addView(this, 50, 50)
+    //     }
+    //
+    //     downloadIcon(
+    //         client.baseUri.toString() + "/apps/files/img/app.svg",
+    //         activity,
+    //         linearLayout,
+    //         accountProvider,
+    //         clientFactory
+    //     )
+    //
+    //     downloadIcon(
+    //         client.baseUri.toString() + "/core/img/actions/group.svg",
+    //         activity,
+    //         linearLayout,
+    //         accountProvider,
+    //         clientFactory
+    //     )
+    //
+    //     activity.addView(linearLayout)
+    //
+    //     longSleep()
+    //
+    //     screenshot(activity)
+    // }
+    //
+    // private fun downloadIcon(
+    //     url: String,
+    //     activity: TestActivity,
+    //     linearLayout: LinearLayout,
+    //     accountProvider: UserAccountManager,
+    //     clientFactory: ClientFactory
+    // ) {
+    //     val view = ImageView(activity).apply {
+    //         linearLayout.addView(this, 50, 50)
+    //     }
+    //     val target = object : SimpleTarget<Drawable>() {
+    //         override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation<in Drawable>?) {
+    //             view.setColorFilter(targetContext.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
+    //             view.setImageDrawable(resource)
+    //         }
+    //     }
+    //
+    //     testActivityRule.runOnUiThread {
+    //         DisplayUtils.downloadIcon(
+    //             accountProvider,
+    //             clientFactory,
+    //             activity,
+    //             url,
+    //             target,
+    //             R.drawable.ic_user
+    //         )
+    //     }
+    // }
+}

+ 25 - 18
app/src/main/AndroidManifest.xml

@@ -40,9 +40,7 @@
     <uses-permission
         android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
         tools:ignore="ScopedStorage" />
-    <uses-permission
-        android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.VIBRATE" />
 
@@ -151,6 +149,13 @@
         <activity
             android:name=".ui.activity.SyncedFoldersActivity"
             android:exported="false" />
+        <activity
+            android:name="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
+            </intent-filter>
+        </activity>
 
         <receiver
             android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
@@ -158,6 +163,17 @@
         <receiver
             android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver"
             android:exported="false" />
+        <receiver
+            android:name="com.nextcloud.client.widget.DashboardWidgetProvider"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.appwidget.provider"
+                android:resource="@xml/dashboard_widget_info" />
+        </receiver>
 
         <activity
             android:name=".ui.activity.UploadFilesActivity"
@@ -220,7 +236,6 @@
                 android:name="android.accounts.AccountAuthenticator"
                 android:resource="@xml/authenticator" />
         </service>
-
         <service
             android:name=".syncadapter.FileSyncService"
             android:exported="true"
@@ -233,6 +248,10 @@
                 android:name="android.content.SyncAdapter"
                 android:resource="@xml/syncadapter_files" />
         </service>
+        <service
+            android:name="com.nextcloud.client.widget.DashboardWidgetService"
+            android:permission="android.permission.BIND_REMOTEVIEWS"
+            android:exported="true" />
 
         <provider
             android:name=".providers.FileContentProvider"
@@ -304,16 +323,12 @@
                 android:name="android.support.FILE_PROVIDER_PATHS"
                 android:resource="@xml/exposed_filepaths" />
         </provider>
-
         <provider
             android:name=".providers.DiskLruImageCacheFileProvider"
             android:authorities="@string/image_cache_provider_authority"
+            android:exported="true"
             android:grantUriPermissions="true"
-            android:permission="android.permission.MANAGE_DOCUMENTS"
-            android:exported="true">
-        </provider>
-
-        <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
+            android:permission="android.permission.MANAGE_DOCUMENTS"></provider> <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
         <!-- to "best before" dates in his fridge. -->
         <!-- disable default provider -->
         <provider
@@ -327,8 +342,6 @@
                 tools:node="remove" />
         </provider>
 
-
-
         <activity
             android:name=".authentication.AuthenticatorActivity"
             android:configChanges="orientation|screenSize|keyboardHidden"
@@ -341,7 +354,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
         <activity
             android:name=".authentication.DeepLinkLoginActivity"
             android:clearTaskOnLaunch="true"
@@ -391,11 +403,9 @@
         <activity
             android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"
             android:exported="false" />
-
         <activity
             android:name="com.nextcloud.client.logger.ui.LogsActivity"
             android:exported="false" />
-
         <activity
             android:name="com.nextcloud.client.errorhandling.ShowErrorActivity"
             android:excludeFromRecents="true"
@@ -465,7 +475,6 @@
             android:label="@string/manage_space_title"
             android:theme="@style/Theme.ownCloud" />
 
-
         <service
             android:name=".services.AccountManagerService"
             android:enabled="true"
@@ -476,12 +485,10 @@
             android:name=".ui.activity.SsoGrantPermissionActivity"
             android:exported="true"
             android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
-
         <activity
             android:name="com.nextcloud.client.etm.EtmActivity"
             android:exported="false"
             android:theme="@style/Theme.ownCloud.Toolbar" />
-
         <activity
             android:name=".ui.preview.PreviewBitmapActivity"
             android:exported="false"

+ 13 - 1
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -29,6 +29,9 @@ import com.nextcloud.client.media.PlayerService;
 import com.nextcloud.client.migrations.Migrations;
 import com.nextcloud.client.onboarding.FirstRunActivity;
 import com.nextcloud.client.onboarding.WhatsNewActivity;
+import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
+import com.nextcloud.client.widget.DashboardWidgetProvider;
+import com.nextcloud.client.widget.DashboardWidgetService;
 import com.nextcloud.ui.ChooseAccountDialogFragment;
 import com.nextcloud.ui.SetStatusDialogFragment;
 import com.owncloud.android.MainApp;
@@ -102,8 +105,8 @@ import com.owncloud.android.ui.fragment.FileDetailFragment;
 import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
 import com.owncloud.android.ui.fragment.GalleryFragment;
 import com.owncloud.android.ui.fragment.LocalFileListFragment;
-import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialogFragment;
 import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog;
+import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialogFragment;
 import com.owncloud.android.ui.fragment.OCFileListFragment;
 import com.owncloud.android.ui.fragment.SharedListFragment;
 import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
@@ -341,6 +344,9 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract FileSyncService fileSyncService();
 
+    @ContributesAndroidInjector
+    abstract DashboardWidgetService dashboardWidgetService();
+
     @ContributesAndroidInjector
     abstract PreviewPdfFragment previewPDFFragment();
 
@@ -430,4 +436,10 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract SyncFileNotEnoughSpaceDialogFragment syncFileNotEnoughSpaceDialogFragment();
+
+    @ContributesAndroidInjector
+    abstract DashboardWidgetConfigurationActivity dashboardWidgetConfigurationActivity();
+
+    @ContributesAndroidInjector
+    abstract DashboardWidgetProvider dashboardWidgetProvider();
 }

+ 4 - 4
app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java

@@ -37,11 +37,11 @@ import com.owncloud.android.lib.common.accounts.AccountUtils;
 
 import java.io.IOException;
 
-class ClientFactoryImpl implements ClientFactory {
+public class ClientFactoryImpl implements ClientFactory {
 
     private Context context;
 
-    ClientFactoryImpl(Context context) {
+    public ClientFactoryImpl(Context context) {
         this.context = context;
     }
 
@@ -49,8 +49,8 @@ class ClientFactoryImpl implements ClientFactory {
     public OwnCloudClient create(User user) throws CreationException {
         try {
             return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context);
-        } catch (OperationCanceledException|
-                 AuthenticatorException|
+        } catch (OperationCanceledException |
+            AuthenticatorException |
             IOException e) {
             throw new CreationException(e);
         }

+ 43 - 45
app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -25,8 +25,8 @@ import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 
-import com.nextcloud.client.account.CurrentAccountProvider;
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -45,15 +45,14 @@ import static com.owncloud.android.ui.fragment.OCFileListFragment.FOLDER_LAYOUT_
 
 /**
  * Implementation of application-wide preferences using {@link SharedPreferences}.
- *
- * Users should not use this class directly. Please use {@link AppPreferences} interface
- * instead.
+ * <p>
+ * Users should not use this class directly. Please use {@link AppPreferences} interface instead.
  */
 public final class AppPreferencesImpl implements AppPreferences {
 
     /**
-     * Constant to access value of last path selected by the user to upload a file shared from other app.
-     * Value handled by the app without direct access in the UI.
+     * Constant to access value of last path selected by the user to upload a file shared from other app. Value handled
+     * by the app without direct access in the UI.
      */
     public static final String AUTO_PREF__LAST_SEEN_VERSION_CODE = "lastSeenVersionCode";
     public static final String STORAGE_PATH = "storage_path";
@@ -101,7 +100,7 @@ public final class AppPreferencesImpl implements AppPreferences {
 
     private final Context context;
     private final SharedPreferences preferences;
-    private final CurrentAccountProvider currentAccountProvider;
+    private final UserAccountManager userAccountManager;
     private final ListenerRegistry listeners;
 
     /**
@@ -123,7 +122,7 @@ public final class AppPreferencesImpl implements AppPreferences {
             }
         }
 
-        void remove(@Nullable  final Listener listener) {
+        void remove(@Nullable final Listener listener) {
             if (listener != null) {
                 listeners.remove(listener);
             }
@@ -133,7 +132,7 @@ public final class AppPreferencesImpl implements AppPreferences {
         public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
             if (PREF__DARK_THEME.equals(key)) {
                 DarkMode mode = preferences.getDarkThemeMode();
-                for(Listener l : listeners) {
+                for (Listener l : listeners) {
                     l.onDarkThemeModeChanged(mode);
                 }
             }
@@ -141,9 +140,9 @@ public final class AppPreferencesImpl implements AppPreferences {
     }
 
     /**
-     * This is a temporary workaround to access app preferences in places that cannot use
-     * dependency injection yet. Use injected component via {@link AppPreferences} interface.
-     *
+     * This is a temporary workaround to access app preferences in places that cannot use dependency injection yet. Use
+     * injected component via {@link AppPreferences} interface.
+     * <p>
      * WARNING: this creates new instance! it does not return app-wide singleton
      *
      * @param context Context used to create shared preferences
@@ -151,15 +150,15 @@ public final class AppPreferencesImpl implements AppPreferences {
      */
     @Deprecated
     public static AppPreferences fromContext(Context context) {
-        final CurrentAccountProvider currentAccountProvider = UserAccountManagerImpl.fromContext(context);
+        final UserAccountManager userAccountManager = UserAccountManagerImpl.fromContext(context);
         final SharedPreferences prefs = android.preference.PreferenceManager.getDefaultSharedPreferences(context);
-        return new AppPreferencesImpl(context, prefs, currentAccountProvider);
+        return new AppPreferencesImpl(context, prefs, userAccountManager);
     }
 
-    AppPreferencesImpl(Context appContext, SharedPreferences preferences, CurrentAccountProvider currentAccountProvider) {
+    AppPreferencesImpl(Context appContext, SharedPreferences preferences, UserAccountManager userAccountManager) {
         this.context = appContext;
         this.preferences = preferences;
-        this.currentAccountProvider = currentAccountProvider;
+        this.userAccountManager = userAccountManager;
         this.listeners = new ListenerRegistry(this);
         this.preferences.registerOnSharedPreferenceChangeListener(listeners);
     }
@@ -277,7 +276,7 @@ public final class AppPreferencesImpl implements AppPreferences {
 
     @Override
     public String[] getPassCode() {
-        return new String[] {
+        return new String[]{
             preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D1, null),
             preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D2, null),
             preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D3, null),
@@ -293,7 +292,7 @@ public final class AppPreferencesImpl implements AppPreferences {
     @Override
     public String getFolderLayout(OCFile folder) {
         return getFolderPreference(context,
-                                   currentAccountProvider.getUser(),
+                                   userAccountManager.getUser(),
                                    PREF__FOLDER_LAYOUT,
                                    folder,
                                    FOLDER_LAYOUT_LIST);
@@ -302,7 +301,7 @@ public final class AppPreferencesImpl implements AppPreferences {
     @Override
     public void setFolderLayout(@Nullable OCFile folder, String layoutName) {
         setFolderPreference(context,
-                            currentAccountProvider.getUser(),
+                            userAccountManager.getUser(),
                             PREF__FOLDER_LAYOUT,
                             folder,
                             layoutName);
@@ -311,7 +310,7 @@ public final class AppPreferencesImpl implements AppPreferences {
     @Override
     public FileSortOrder getSortOrderByFolder(OCFile folder) {
         return FileSortOrder.sortOrders.get(getFolderPreference(context,
-                                                                currentAccountProvider.getUser(),
+                                                                userAccountManager.getUser(),
                                                                 PREF__FOLDER_SORT_ORDER,
                                                                 folder,
                                                                 FileSortOrder.sort_a_to_z.name));
@@ -320,7 +319,7 @@ public final class AppPreferencesImpl implements AppPreferences {
     @Override
     public void setSortOrder(@Nullable OCFile folder, FileSortOrder sortOrder) {
         setFolderPreference(context,
-                            currentAccountProvider.getUser(),
+                            userAccountManager.getUser(),
                             PREF__FOLDER_SORT_ORDER,
                             folder,
                             sortOrder.name);
@@ -333,7 +332,7 @@ public final class AppPreferencesImpl implements AppPreferences {
 
     @Override
     public FileSortOrder getSortOrderByType(FileSortOrder.Type type, FileSortOrder defaultOrder) {
-        User user = currentAccountProvider.getUser();
+        User user = userAccountManager.getUser();
         if (user.isAnonymous()) {
             return defaultOrder;
         }
@@ -347,7 +346,7 @@ public final class AppPreferencesImpl implements AppPreferences {
 
     @Override
     public void setSortOrder(FileSortOrder.Type type, FileSortOrder sortOrder) {
-        User user = currentAccountProvider.getUser();
+        User user = userAccountManager.getUser();
         ArbitraryDataProvider dataProvider = new ArbitraryDataProvider(context.getContentResolver());
         dataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREF__FOLDER_SORT_ORDER + "_" + type, sortOrder.name);
     }
@@ -506,19 +505,19 @@ public final class AppPreferencesImpl implements AppPreferences {
     @Override
     public void removeLegacyPreferences() {
         preferences.edit()
-                .remove("instant_uploading")
-                .remove("instant_video_uploading")
-                .remove("instant_upload_path")
-                .remove("instant_upload_path_use_subfolders")
-                .remove("instant_upload_on_wifi")
-                .remove("instant_upload_on_charging")
-                .remove("instant_video_upload_path")
-                .remove("instant_video_upload_path_use_subfolders")
-                .remove("instant_video_upload_on_wifi")
-                .remove("instant_video_uploading")
-                .remove("instant_video_upload_on_charging")
-                .remove("prefs_instant_behaviour")
-                .apply();
+            .remove("instant_uploading")
+            .remove("instant_video_uploading")
+            .remove("instant_upload_path")
+            .remove("instant_upload_path_use_subfolders")
+            .remove("instant_upload_on_wifi")
+            .remove("instant_upload_on_charging")
+            .remove("instant_video_upload_path")
+            .remove("instant_video_upload_path_use_subfolders")
+            .remove("instant_video_upload_on_wifi")
+            .remove("instant_video_uploading")
+            .remove("instant_video_upload_on_charging")
+            .remove("prefs_instant_behaviour")
+            .apply();
     }
 
     @Override
@@ -588,13 +587,12 @@ public final class AppPreferencesImpl implements AppPreferences {
     }
 
     /**
-     * Get preference value for a folder.
-     * If folder is not set itself, it finds an ancestor that is set.
+     * Get preference value for a folder. If folder is not set itself, it finds an ancestor that is set.
      *
-     * @param context Context object.
+     * @param context        Context object.
      * @param preferenceName Name of the preference to lookup.
-     * @param folder Folder.
-     * @param defaultValue Fallback value in case no ancestor is set.
+     * @param folder         Folder.
+     * @param defaultValue   Fallback value in case no ancestor is set.
      * @return Preference value
      */
     private static String getFolderPreference(final Context context,
@@ -621,10 +619,10 @@ public final class AppPreferencesImpl implements AppPreferences {
     /**
      * Set preference value for a folder.
      *
-     * @param context Context object.
+     * @param context        Context object.
      * @param preferenceName Name of the preference to set.
-     * @param folder Folder.
-     * @param value Preference value to set.
+     * @param folder         Folder.
+     * @param value          Preference value to set.
      */
     private static void setFolderPreference(final Context context,
                                             final User user,
@@ -637,7 +635,7 @@ public final class AppPreferencesImpl implements AppPreferences {
 
     private static String getKeyFromFolder(String preferenceName, @Nullable OCFile folder) {
         final String folderIdString = String.valueOf(folder != null ? folder.getFileId() :
-            FileDataStorageManager.ROOT_PARENT_ID);
+                                                         FileDataStorageManager.ROOT_PARENT_ID);
 
         return preferenceName + "_" + folderIdString;
     }

+ 3 - 3
app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java

@@ -3,7 +3,7 @@ package com.nextcloud.client.preferences;
 import android.content.Context;
 import android.content.SharedPreferences;
 
-import com.nextcloud.client.account.CurrentAccountProvider;
+import com.nextcloud.client.account.UserAccountManager;
 
 import javax.inject.Singleton;
 
@@ -23,7 +23,7 @@ public class PreferencesModule {
     @Singleton
     public AppPreferences appPreferences(Context context,
                                          SharedPreferences sharedPreferences,
-                                         CurrentAccountProvider currentAccountProvider) {
-        return new AppPreferencesImpl(context, sharedPreferences, currentAccountProvider);
+                                         UserAccountManager userAccountManager) {
+        return new AppPreferencesImpl(context, sharedPreferences, userAccountManager);
     }
 }

+ 236 - 0
app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt

@@ -0,0 +1,236 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.widget
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.nextcloud.android.lib.resources.dashboard.DashBoardButtonType
+import com.nextcloud.android.lib.resources.dashboard.DashboardListWidgetsRemoteOperation
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+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.nextcloud.client.network.ClientFactory.CreationException
+import com.owncloud.android.R
+import com.owncloud.android.databinding.DashboardWidgetConfigurationLayoutBinding
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.ui.adapter.DashboardWidgetListAdapter
+import com.owncloud.android.ui.dialog.AccountChooserInterface
+import com.owncloud.android.ui.dialog.MultipleAccountsDialog
+import com.owncloud.android.utils.theme.ThemeDrawableUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class DashboardWidgetConfigurationActivity :
+    AppCompatActivity(),
+    DashboardWidgetConfigurationInterface,
+    Injectable,
+    AccountChooserInterface {
+    private lateinit var mAdapter: DashboardWidgetListAdapter
+    private lateinit var binding: DashboardWidgetConfigurationLayoutBinding
+    private lateinit var currentUser: User
+
+    @Inject
+    lateinit var themeDrawableUtils: ThemeDrawableUtils
+
+    @Inject
+    lateinit var accountManager: UserAccountManager
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    @Inject
+    lateinit var widgetRepository: WidgetRepository
+
+    @Inject
+    lateinit var widgetUpdater: DashboardWidgetUpdater
+
+    var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
+
+    public override fun onCreate(bundle: Bundle?) {
+        super.onCreate(bundle)
+
+        // Set the result to CANCELED.  This will cause the widget host to cancel
+        // out of the widget placement if the user presses the back button.
+        setResult(RESULT_CANCELED)
+
+        binding = DashboardWidgetConfigurationLayoutBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        themeDrawableUtils.tintDrawable(binding.icon.drawable, getColor(R.color.dark))
+
+        val layoutManager = LinearLayoutManager(this)
+        // TODO follow our new architecture
+        mAdapter = DashboardWidgetListAdapter(themeDrawableUtils, accountManager, clientFactory, this, this)
+        binding.list.apply {
+            setHasFooter(false)
+            setAdapter(mAdapter)
+            setLayoutManager(layoutManager)
+            setEmptyView(binding.emptyView.emptyListView)
+        }
+
+        currentUser = accountManager.user
+        if (accountManager.allUsers.size > 1) {
+            binding.chooseWidget.visibility = View.GONE
+
+            binding.accountName.apply {
+                setCompoundDrawablesWithIntrinsicBounds(
+                    null,
+                    null,
+                    themeDrawableUtils.tintDrawable(
+                        AppCompatResources.getDrawable(
+                            context,
+                            R.drawable.ic_baseline_arrow_drop_down_24
+                        ),
+                        R.color.black
+                    ),
+                    null
+                )
+                visibility = View.VISIBLE
+                text = currentUser.accountName
+                setOnClickListener {
+                    val dialog = MultipleAccountsDialog()
+                    dialog.highlightCurrentlyActiveAccount = false
+                    dialog.show(supportFragmentManager, null)
+                }
+            }
+        }
+        loadWidgets(currentUser)
+
+        binding.close.setOnClickListener { finish() }
+
+        // Find the widget id from the intent.
+        appWidgetId = intent?.extras?.getInt(
+            AppWidgetManager.EXTRA_APPWIDGET_ID,
+            AppWidgetManager.INVALID_APPWIDGET_ID
+        ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
+
+        // If this activity was started with an intent without an app widget ID, finish with an error.
+        if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+            finish()
+            return
+        }
+    }
+
+    private fun loadWidgets(user: User) {
+        CoroutineScope(Dispatchers.IO).launch {
+            withContext(Dispatchers.Main) {
+                binding.emptyView.root.visibility = View.GONE
+                if (accountManager.allUsers.size > 1) {
+                    binding.accountName.text = user.accountName
+                }
+            }
+
+            try {
+                val client = clientFactory.createNextcloudClient(user)
+                val result = DashboardListWidgetsRemoteOperation().execute(client)
+
+                withContext(Dispatchers.Main) {
+                    if (result.code == RemoteOperationResult.ResultCode.FILE_NOT_FOUND) {
+                        withContext(Dispatchers.Main) {
+                            mAdapter.setWidgetList(null)
+                            binding.emptyView.root.visibility = View.VISIBLE
+                            binding.emptyView.emptyListViewHeadline.setText(R.string.widgets_not_available_title)
+
+                            binding.emptyView.emptyListIcon.apply {
+                                setImageResource(R.drawable.ic_list_empty_error)
+                                visibility = View.VISIBLE
+                            }
+                            binding.emptyView.emptyListViewText.apply {
+                                setText(
+                                    String.format(
+                                        getString(R.string.widgets_not_available),
+                                        getString(R.string.app_name)
+                                    )
+                                )
+                                visibility = View.VISIBLE
+                            }
+                        }
+                    } else {
+                        mAdapter.setWidgetList(result.resultData)
+                    }
+                }
+            } catch (e: CreationException) {
+                Log_OC.e(this, "Error loading widgets for user $user", e)
+
+                withContext(Dispatchers.Main) {
+                    mAdapter.setWidgetList(null)
+                    binding.emptyView.root.visibility = View.VISIBLE
+
+                    binding.emptyView.emptyListIcon.apply {
+                        setImageResource(R.drawable.ic_list_empty_error)
+                        visibility = View.VISIBLE
+                    }
+                    binding.emptyView.emptyListViewText.apply {
+                        setText(R.string.common_error)
+                        visibility = View.VISIBLE
+                    }
+                    binding.emptyView.emptyListViewAction.apply {
+                        visibility = View.VISIBLE
+                        setText(R.string.reload)
+                        setOnClickListener {
+                            loadWidgets(user)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    override fun onItemClicked(dashboardWidget: DashboardWidget) {
+        widgetRepository.saveWidget(appWidgetId, dashboardWidget, currentUser)
+
+        // update widget
+        val appWidgetManager = AppWidgetManager.getInstance(this)
+
+        widgetUpdater.updateAppWidget(
+            appWidgetManager,
+            appWidgetId,
+            dashboardWidget.title,
+            dashboardWidget.iconUrl,
+            dashboardWidget.buttons?.find { it.type == DashBoardButtonType.NEW }
+        )
+
+        val resultValue = Intent().apply {
+            putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
+        }
+
+        setResult(RESULT_OK, resultValue)
+        finish()
+    }
+
+    override fun onAccountChosen(user: User) {
+        currentUser = user
+        loadWidgets(user)
+    }
+}

+ 29 - 0
app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+
+interface DashboardWidgetConfigurationInterface {
+    fun onItemClicked(dashboardWidget: DashboardWidget)
+}

+ 80 - 0
app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt

@@ -0,0 +1,80 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.Context
+import android.content.Intent
+import dagger.android.AndroidInjection
+import javax.inject.Inject
+
+/**
+ * Manages widgets
+ */
+class DashboardWidgetProvider : AppWidgetProvider() {
+    @Inject
+    lateinit var widgetRepository: WidgetRepository
+
+    @Inject
+    lateinit var widgetUpdater: DashboardWidgetUpdater
+
+    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+        AndroidInjection.inject(this, context)
+
+        for (appWidgetId in appWidgetIds) {
+            val widgetConfiguration = widgetRepository.getWidget(appWidgetId)
+
+            widgetUpdater.updateAppWidget(
+                appWidgetManager,
+                appWidgetId,
+                widgetConfiguration.title,
+                widgetConfiguration.iconUrl,
+                widgetConfiguration.addButton
+            )
+        }
+    }
+
+    override fun onReceive(context: Context?, intent: Intent?) {
+        super.onReceive(context, intent)
+        AndroidInjection.inject(this, context)
+
+        if (intent?.action == OPEN_INTENT) {
+            val clickIntent = Intent(Intent.ACTION_VIEW, intent.data)
+            clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            context?.startActivity(clickIntent)
+        }
+    }
+
+    override fun onDeleted(context: Context?, appWidgetIds: IntArray) {
+        AndroidInjection.inject(this, context)
+
+        for (appWidgetId in appWidgetIds) {
+            widgetRepository.deleteWidget(appWidgetId)
+        }
+    }
+
+    companion object {
+        const val OPEN_INTENT = "open"
+    }
+}

+ 243 - 0
app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt

@@ -0,0 +1,243 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.model.StreamEncoder
+import com.bumptech.glide.load.resource.file.FileToStreamDecoder
+import com.bumptech.glide.request.FutureTarget
+import com.nextcloud.android.lib.resources.dashboard.DashboardGetWidgetItemsRemoteOperation
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidgetItem
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.utils.BitmapUtils
+import com.owncloud.android.utils.DisplayUtils.SVG_SIZE
+import com.owncloud.android.utils.glide.CustomGlideStreamLoader
+import com.owncloud.android.utils.glide.CustomGlideUriLoader
+import com.owncloud.android.utils.svg.SVGorImage
+import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder
+import com.owncloud.android.utils.svg.SvgOrImageDecoder
+import dagger.android.AndroidInjection
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.InputStream
+import javax.inject.Inject
+
+class DashboardWidgetService : RemoteViewsService() {
+    @Inject
+    lateinit var userAccountManager: UserAccountManager
+
+    @Inject
+    lateinit var clientFactory: ClientFactory
+
+    @Inject
+    lateinit var widgetRepository: WidgetRepository
+
+    override fun onCreate() {
+        super.onCreate()
+        AndroidInjection.inject(this)
+    }
+
+    override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
+        return StackRemoteViewsFactory(
+            this.applicationContext,
+            userAccountManager,
+            clientFactory,
+            intent,
+            widgetRepository
+        )
+    }
+}
+
+class StackRemoteViewsFactory(
+    private val context: Context,
+    val userAccountManager: UserAccountManager,
+    val clientFactory: ClientFactory,
+    val intent: Intent,
+    val widgetRepository: WidgetRepository
+) : RemoteViewsService.RemoteViewsFactory {
+
+    private lateinit var widgetConfiguration: WidgetConfiguration
+    private var widgetItems: List<DashboardWidgetItem> = emptyList()
+    private var hasLoadMore = false
+
+    override fun onCreate() {
+        Log_OC.d(this, "onCreate")
+        val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
+
+        widgetConfiguration = widgetRepository.getWidget(appWidgetId)
+
+        if (!widgetConfiguration.user.isPresent) {
+            // TODO show error
+            Log_OC.e(this, "No user found!")
+        }
+
+        onDataSetChanged()
+    }
+
+    override fun onDataSetChanged() {
+        CoroutineScope(Dispatchers.IO).launch {
+            try {
+                val client = clientFactory.createNextcloudClient(widgetConfiguration.user.get())
+                val result =
+                    DashboardGetWidgetItemsRemoteOperation(widgetConfiguration.widgetId, LIMIT_SIZE).execute(client)
+                widgetItems = result.resultData[widgetConfiguration.widgetId] ?: emptyList()
+
+                hasLoadMore = widgetConfiguration.moreButton != null &&
+                    widgetItems.size == LIMIT_SIZE
+            } catch (e: ClientFactory.CreationException) {
+                Log_OC.e(this, "Error updating widget", e)
+            }
+        }
+
+        Log_OC.d("WidgetService", "onDataSetChanged")
+    }
+
+    override fun onDestroy() {
+        Log_OC.d("WidgetService", "onDestroy")
+
+        widgetItems = emptyList()
+    }
+
+    override fun getCount(): Int {
+        return if (hasLoadMore && widgetItems.isNotEmpty()) {
+            widgetItems.size + 1
+        } else {
+            widgetItems.size
+        }
+    }
+
+    override fun getViewAt(position: Int): RemoteViews {
+        return if (position == widgetItems.size) {
+            createLoadMoreView()
+        } else {
+            createItemView(position)
+        }
+    }
+
+    private fun createLoadMoreView(): RemoteViews {
+        return RemoteViews(context.packageName, R.layout.widget_item_load_more).apply {
+            val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetConfiguration.moreButton?.link))
+            setTextViewText(R.id.load_more, widgetConfiguration.moreButton?.text)
+            setOnClickFillInIntent(R.id.load_more_container, clickIntent)
+        }
+    }
+
+    // we will switch soon to coil and then streamline all of this
+    // Kotlin cannot catch multiple exception types at same time
+    @Suppress("NestedBlockDepth", "TooGenericExceptionCaught")
+    private fun createItemView(position: Int): RemoteViews {
+        return RemoteViews(context.packageName, R.layout.widget_item).apply {
+            val widgetItem = widgetItems[position]
+
+            // icon bitmap/svg
+            if (widgetItem.iconUrl.isNotEmpty()) {
+                val glide: FutureTarget<Bitmap>
+                if (Uri.parse(widgetItem.iconUrl).encodedPath!!.endsWith(".svg")) {
+                    glide = Glide.with(context)
+                        .using(
+                            CustomGlideUriLoader(userAccountManager.user, clientFactory),
+                            InputStream::class.java
+                        )
+                        .from(Uri::class.java)
+                        .`as`(SVGorImage::class.java)
+                        .transcode(SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE), Bitmap::class.java)
+                        .sourceEncoder(StreamEncoder())
+                        .cacheDecoder(FileToStreamDecoder(SvgOrImageDecoder()))
+                        .decoder(SvgOrImageDecoder())
+                        .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+                        .load(Uri.parse(widgetItem.iconUrl))
+                        .into(SVG_SIZE, SVG_SIZE)
+                } else {
+                    glide = Glide.with(context)
+                        .using(CustomGlideStreamLoader(widgetConfiguration.user.get(), clientFactory))
+                        .load(widgetItem.iconUrl)
+                        .asBitmap()
+                        .into(SVG_SIZE, SVG_SIZE)
+                }
+
+                try {
+                    if (widgetConfiguration.roundIcon) {
+                        setImageViewBitmap(R.id.icon, BitmapUtils.roundBitmap(glide.get()))
+                    } else {
+                        setImageViewBitmap(R.id.icon, glide.get())
+                    }
+                } catch (e: Exception) {
+                    Log_OC.d(this, "Error setting icon", e)
+                    setImageViewResource(R.id.icon, R.drawable.ic_dashboard)
+                }
+            }
+
+            // text
+            setTextViewText(R.id.title, widgetItem.title)
+
+            if (widgetItem.subtitle.isNotEmpty()) {
+                setViewVisibility(R.id.subtitle, View.VISIBLE)
+                setTextViewText(R.id.subtitle, widgetItem.subtitle)
+            } else {
+                setViewVisibility(R.id.subtitle, View.GONE)
+            }
+
+            if (widgetItem.link.isNotEmpty()) {
+                val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetItem.link))
+                setOnClickFillInIntent(R.id.text_container, clickIntent)
+            }
+        }
+    }
+
+    override fun getLoadingView(): RemoteViews? {
+        return null
+    }
+
+    override fun getViewTypeCount(): Int {
+        return if (hasLoadMore) {
+            2
+        } else {
+            1
+        }
+    }
+
+    override fun getItemId(position: Int): Long {
+        return position.toLong()
+    }
+
+    override fun hasStableIds(): Boolean {
+        return true
+    }
+
+    companion object {
+        const val LIMIT_SIZE = 14
+    }
+}

+ 166 - 0
app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt

@@ -0,0 +1,166 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.view.View
+import android.widget.RemoteViews
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.model.StreamEncoder
+import com.bumptech.glide.load.resource.file.FileToStreamDecoder
+import com.bumptech.glide.request.animation.GlideAnimation
+import com.bumptech.glide.request.target.AppWidgetTarget
+import com.nextcloud.android.lib.resources.dashboard.DashboardButton
+import com.nextcloud.client.account.CurrentAccountProvider
+import com.nextcloud.client.network.ClientFactory
+import com.owncloud.android.R
+import com.owncloud.android.utils.BitmapUtils
+import com.owncloud.android.utils.DisplayUtils.SVG_SIZE
+import com.owncloud.android.utils.glide.CustomGlideUriLoader
+import com.owncloud.android.utils.svg.SVGorImage
+import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder
+import com.owncloud.android.utils.svg.SvgOrImageDecoder
+import java.io.InputStream
+import javax.inject.Inject
+
+class DashboardWidgetUpdater @Inject constructor(
+    private val context: Context,
+    private val clientFactory: ClientFactory,
+    private val accountProvider: CurrentAccountProvider
+) {
+
+    fun updateAppWidget(
+        appWidgetManager: AppWidgetManager,
+        appWidgetId: Int,
+        title: String,
+        iconUrl: String,
+        addButton: DashboardButton?
+    ) {
+        val intent = Intent(context, DashboardWidgetService::class.java).apply {
+            putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+            data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
+        }
+
+        val views = RemoteViews(context.packageName, R.layout.dashboard_widget).apply {
+            setRemoteAdapter(R.id.list, intent)
+            setEmptyView(R.id.list, R.id.empty_view)
+            setTextViewText(R.id.title, title)
+
+            setAddButton(addButton, appWidgetId, this)
+            setPendingReload(this, appWidgetId)
+            setPendingClick(this)
+            loadIcon(appWidgetId, iconUrl, this)
+        }
+
+        appWidgetManager.updateAppWidget(appWidgetId, views)
+        appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.list)
+    }
+
+    private fun setPendingReload(remoteViews: RemoteViews, appWidgetId: Int) {
+        val intentUpdate = Intent(context, DashboardWidgetProvider::class.java)
+        intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+
+        val idArray = intArrayOf(appWidgetId)
+        intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
+
+        remoteViews.setOnClickPendingIntent(
+            R.id.reload,
+            PendingIntent.getBroadcast(
+                context,
+                appWidgetId,
+                intentUpdate,
+                PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+            )
+        )
+    }
+
+    // clickPI needs to me mutable, as it is re-used. PendingIntent.FLAG_IMMUTABLE requires S (API 31)
+    @SuppressLint("UnspecifiedImmutableFlag")
+    private fun setPendingClick(remoteViews: RemoteViews) {
+        val clickPI = PendingIntent.getActivity(
+            context,
+            0,
+            Intent(),
+            PendingIntent.FLAG_UPDATE_CURRENT
+        )
+
+        remoteViews.setPendingIntentTemplate(R.id.list, clickPI)
+    }
+
+    private fun setAddButton(addButton: DashboardButton?, appWidgetId: Int, remoteViews: RemoteViews) {
+        // create add button
+        if (addButton == null) {
+            remoteViews.setViewVisibility(R.id.create, View.GONE)
+        } else {
+            remoteViews.setViewVisibility(R.id.create, View.VISIBLE)
+            remoteViews.setContentDescription(R.id.create, addButton.text)
+
+            val clickIntent = Intent(context, DashboardWidgetProvider::class.java)
+            clickIntent.action = DashboardWidgetProvider.OPEN_INTENT
+            clickIntent.data = Uri.parse(addButton.link)
+
+            remoteViews.setOnClickPendingIntent(
+                R.id.create,
+                PendingIntent.getBroadcast(
+                    context,
+                    appWidgetId,
+                    clickIntent,
+                    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+                )
+            )
+        }
+    }
+
+    private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) {
+        val iconTarget = object : AppWidgetTarget(context, remoteViews, R.id.icon, appWidgetId) {
+            override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
+                if (resource != null) {
+                    val tintedBitmap = BitmapUtils.tintImage(resource, R.color.black)
+                    super.onResourceReady(tintedBitmap, glideAnimation)
+                }
+            }
+        }
+
+        Glide.with(context)
+            .using(
+                CustomGlideUriLoader(accountProvider.user, clientFactory),
+                InputStream::class.java
+            )
+            .from(Uri::class.java)
+            .`as`(SVGorImage::class.java)
+            .transcode(SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE), Bitmap::class.java)
+            .sourceEncoder(StreamEncoder())
+            .cacheDecoder(FileToStreamDecoder(SvgOrImageDecoder()))
+            .decoder(SvgOrImageDecoder())
+            .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+            .load(Uri.parse(iconUrl))
+            .into(iconTarget)
+    }
+}

+ 37 - 0
app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt

@@ -0,0 +1,37 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import com.nextcloud.android.lib.resources.dashboard.DashboardButton
+import com.nextcloud.client.account.User
+import com.nextcloud.java.util.Optional
+
+data class WidgetConfiguration(
+    val widgetId: String,
+    val title: String,
+    val iconUrl: String,
+    val roundIcon: Boolean,
+    val user: Optional<User>,
+    val addButton: DashboardButton?,
+    val moreButton: DashboardButton?
+)

+ 150 - 0
app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt

@@ -0,0 +1,150 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.widget
+
+import android.content.SharedPreferences
+import com.nextcloud.android.lib.resources.dashboard.DashBoardButtonType
+import com.nextcloud.android.lib.resources.dashboard.DashboardButton
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.java.util.Optional
+import javax.inject.Inject
+
+class WidgetRepository @Inject constructor(
+    private val userAccountManager: UserAccountManager,
+    val preferences: SharedPreferences
+) {
+    fun saveWidget(widgetId: Int, widget: DashboardWidget, user: User) {
+        val editor: SharedPreferences.Editor = preferences
+            .edit()
+            .putString(PREF__WIDGET_ID + widgetId, widget.id)
+            .putString(PREF__WIDGET_TITLE + widgetId, widget.title)
+            .putString(PREF__WIDGET_ICON + widgetId, widget.iconUrl)
+            .putBoolean(PREF__WIDGET_ROUND_ICON + widgetId, widget.roundIcons)
+            .putString(PREF__WIDGET_USER + widgetId, user.accountName)
+        val buttonList = widget.buttons
+        if (buttonList != null && buttonList.isNotEmpty()) {
+            for (button in buttonList) {
+                if (button.type == DashBoardButtonType.NEW) {
+                    editor
+                        .putString(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId, button.type.toString())
+                        .putString(PREF__WIDGET_ADD_BUTTON_URL + widgetId, button.link)
+                        .putString(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId, button.text)
+                }
+                if (button.type == DashBoardButtonType.MORE) {
+                    editor
+                        .putString(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId, button.type.toString())
+                        .putString(PREF__WIDGET_MORE_BUTTON_URL + widgetId, button.link)
+                        .putString(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId, button.text)
+                }
+            }
+        }
+        editor.apply()
+    }
+
+    fun deleteWidget(widgetId: Int) {
+        preferences
+            .edit()
+            .remove(PREF__WIDGET_ID + widgetId)
+            .remove(PREF__WIDGET_TITLE + widgetId)
+            .remove(PREF__WIDGET_ICON + widgetId)
+            .remove(PREF__WIDGET_ROUND_ICON + widgetId)
+            .remove(PREF__WIDGET_USER + widgetId)
+            .remove(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId)
+            .remove(PREF__WIDGET_ADD_BUTTON_URL + widgetId)
+            .remove(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId)
+            .remove(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId)
+            .remove(PREF__WIDGET_MORE_BUTTON_URL + widgetId)
+            .remove(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId)
+            .apply()
+    }
+
+    fun getWidget(widgetId: Int): WidgetConfiguration {
+        val userOptional: Optional<User> =
+            userAccountManager.getUser(preferences.getString(PREF__WIDGET_USER + widgetId, ""))
+
+        val addButton = createAddButton(widgetId)
+        val moreButton = createMoreButton(widgetId)
+
+        return WidgetConfiguration(
+            preferences.getString(PREF__WIDGET_ID + widgetId, "") ?: "",
+            preferences.getString(PREF__WIDGET_TITLE + widgetId, "") ?: "",
+            preferences.getString(PREF__WIDGET_ICON + widgetId, "") ?: "",
+            preferences.getBoolean(PREF__WIDGET_ROUND_ICON + widgetId, false),
+            userOptional,
+            addButton,
+            moreButton
+        )
+    }
+
+    private fun createAddButton(widgetId: Int): DashboardButton? {
+        var addButton: DashboardButton? = null
+        if (preferences.contains(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId)) {
+            addButton = DashboardButton(
+                DashBoardButtonType.valueOf(
+                    preferences.getString(
+                        PREF__WIDGET_ADD_BUTTON_TYPE + widgetId,
+                        ""
+                    ) ?: ""
+                ),
+                preferences.getString(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId, "") ?: "",
+                preferences.getString(PREF__WIDGET_ADD_BUTTON_URL + widgetId, "") ?: ""
+            )
+        }
+
+        return addButton
+    }
+
+    private fun createMoreButton(widgetId: Int): DashboardButton? {
+        var moreButton: DashboardButton? = null
+        if (preferences.contains(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId)) {
+            moreButton = DashboardButton(
+                DashBoardButtonType.valueOf(
+                    preferences.getString(
+                        PREF__WIDGET_MORE_BUTTON_TYPE + widgetId,
+                        ""
+                    ) ?: ""
+                ),
+                preferences.getString(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId, "") ?: "",
+                preferences.getString(PREF__WIDGET_MORE_BUTTON_URL + widgetId, "") ?: ""
+            )
+        }
+
+        return moreButton
+    }
+
+    companion object {
+        const val PREF__WIDGET_TITLE = "widget_title_"
+        private const val PREF__WIDGET_ID = "widget_id_"
+        private const val PREF__WIDGET_ICON = "widget_icon_"
+        private const val PREF__WIDGET_ROUND_ICON = "widget_round_icon_"
+        private const val PREF__WIDGET_USER = "widget_user_"
+        private const val PREF__WIDGET_ADD_BUTTON_TEXT = "widget_add_button_text_"
+        private const val PREF__WIDGET_ADD_BUTTON_URL = "widget_add_button_url_"
+        private const val PREF__WIDGET_ADD_BUTTON_TYPE = "widget_add_button_type_"
+        private const val PREF__WIDGET_MORE_BUTTON_TEXT = "widget_more_button_text_"
+        private const val PREF__WIDGET_MORE_BUTTON_URL = "widget_more_button_url_"
+        private const val PREF__WIDGET_MORE_BUTTON_TYPE = "widget_more_button_type_"
+    }
+}

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

@@ -131,6 +131,7 @@ class ChooseAccountDialogFragment :
                 this,
                 false,
                 false,
+                true,
                 themeColorUtils,
                 themeDrawableUtils
             )

+ 18 - 0
app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java

@@ -81,6 +81,24 @@ public class EmptyRecyclerView extends RecyclerView {
             initEmptyView();
         }
 
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            super.onItemRangeChanged(positionStart, itemCount);
+            initEmptyView();
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
+            super.onItemRangeChanged(positionStart, itemCount, payload);
+            initEmptyView();
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            super.onItemRangeMoved(fromPosition, toPosition, itemCount);
+            initEmptyView();
+        }
+
         @Override
         public void onItemRangeInserted(int positionStart, int itemCount) {
             super.onItemRangeInserted(positionStart, itemCount);

+ 2 - 8
app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -769,9 +769,7 @@ public abstract class DrawerActivity extends ToolbarActivity
                                               this,
                                               firstQuota.getIconUrl(),
                                               target,
-                                              R.drawable.ic_link,
-                                              size,
-                                              size);
+                                              R.drawable.ic_link);
 
                 } else {
                     mQuotaTextLink.setVisibility(View.GONE);
@@ -884,8 +882,6 @@ public abstract class DrawerActivity extends ToolbarActivity
         if (mNavigationView != null && getBaseContext().getResources().getBoolean(R.bool.show_external_links)) {
             mNavigationView.getMenu().removeGroup(R.id.drawer_menu_external_links);
 
-            float density = getResources().getDisplayMetrics().density;
-            final int size = Math.round(24 * density);
             int greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon);
 
             for (final ExternalLink link : externalLinksProvider.getExternalLink(ExternalLinkType.LINK)) {
@@ -911,9 +907,7 @@ public abstract class DrawerActivity extends ToolbarActivity
                                           this,
                                           link.getIconUrl(),
                                           target,
-                                          R.drawable.ic_link,
-                                          size,
-                                          size);
+                                          R.drawable.ic_link);
             }
 
             setDrawerMenuItemChecked(mCheckedMenuItem);

+ 3 - 0
app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java

@@ -155,6 +155,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
                                               this,
                                               multipleAccountsSupported,
                                               true,
+                                              true,
                                               themeColorUtils,
                                               themeDrawableUtils);
 
@@ -310,6 +311,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
                                       this,
                                       multipleAccountsSupported,
                                       false,
+                                      true,
                                       themeColorUtils,
                                       themeDrawableUtils);
                                   recyclerView.setAdapter(userListAdapter);
@@ -364,6 +366,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
                                                       this,
                                                       multipleAccountsSupported,
                                                       false,
+                                                      true,
                                                       themeColorUtils,
                                                       themeDrawableUtils);
                 recyclerView.setAdapter(userListAdapter);

+ 7 - 5
app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

@@ -62,6 +62,7 @@ import android.widget.Toast;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.textfield.TextInputEditText;
 import com.google.android.material.textfield.TextInputLayout;
+import com.nextcloud.client.account.User;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.MainApp;
@@ -80,6 +81,7 @@ import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
 import com.owncloud.android.ui.adapter.UploaderAdapter;
 import com.owncloud.android.ui.asynctasks.CopyAndUploadContentUrisTask;
+import com.owncloud.android.ui.dialog.AccountChooserInterface;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
 import com.owncloud.android.ui.dialog.MultipleAccountsDialog;
@@ -120,7 +122,6 @@ import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AlertDialog.Builder;
 import androidx.appcompat.widget.SearchView;
 import androidx.core.view.MenuItemCompat;
-import androidx.core.widget.NestedScrollView;
 import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -131,8 +132,8 @@ import static com.owncloud.android.utils.DisplayUtils.openSortingOrderDialogFrag
  * This can be used to upload things to an ownCloud instance.
  */
 public class ReceiveExternalFilesActivity extends FileActivity
-        implements OnItemClickListener, View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener,
-        SortingOrderDialogFragment.OnSortingOrderListener, Injectable {
+    implements OnItemClickListener, View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener,
+    SortingOrderDialogFragment.OnSortingOrderListener, Injectable, AccountChooserInterface {
 
     private static final String TAG = ReceiveExternalFilesActivity.class.getSimpleName();
 
@@ -237,8 +238,9 @@ public class ReceiveExternalFilesActivity extends FileActivity
         return this;
     }
 
-    public void changeAccount(Account account) {
-        setAccount(account, false);
+    @Override
+    public void onAccountChosen(@NonNull User user) {
+        setAccount(user.toPlatformAccount(), false);
         initTargetFolder();
         populateDirectoryList();
     }

+ 72 - 0
app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt

@@ -0,0 +1,72 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 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.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.widget.DashboardWidgetConfigurationInterface
+import com.owncloud.android.databinding.WidgetListItemBinding
+import com.owncloud.android.utils.theme.ThemeDrawableUtils
+
+class DashboardWidgetListAdapter(
+    val themeDrawableUtils: ThemeDrawableUtils,
+    val accountManager: UserAccountManager,
+    val clientFactory: ClientFactory,
+    val context: Context,
+    private val dashboardWidgetConfigurationInterface: DashboardWidgetConfigurationInterface
+) :
+    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
+    private var widgets = emptyList<DashboardWidget>()
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+        return WidgetListItemViewHolder(
+            WidgetListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
+            themeDrawableUtils,
+            accountManager,
+            clientFactory,
+            context
+        )
+    }
+
+    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+        val widgetListItemViewHolder = holder as WidgetListItemViewHolder
+
+        widgetListItemViewHolder.bind(widgets[position], dashboardWidgetConfigurationInterface)
+    }
+
+    override fun getItemCount(): Int {
+        return widgets.size
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    fun setWidgetList(list: Map<String, DashboardWidget>?) {
+        widgets = list?.map { (_, value) -> value }?.sortedBy { it.order } ?: emptyList()
+        notifyDataSetChanged()
+    }
+}

+ 6 - 5
app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java

@@ -19,13 +19,14 @@
 
 package com.owncloud.android.ui.adapter;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.PorterDuff;
 import android.graphics.Typeface;
-import android.graphics.drawable.PictureDrawable;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.text.Spannable;
 import android.text.SpannableStringBuilder;
@@ -147,7 +148,7 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
         holder.binding.message.setText(notification.getMessage());
 
         if (!TextUtils.isEmpty(notification.getIcon())) {
-            downloadIcon(notification.getIcon(), holder.binding.icon);
+            downloadIcon(notification.getIcon(), holder.binding.icon, notificationsActivity);
         }
 
         int nightModeFlag =
@@ -360,12 +361,12 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
         }
     }
 
-    private void downloadIcon(String icon, ImageView itemViewType) {
-        GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(notificationsActivity)
+    private void downloadIcon(String icon, ImageView itemViewType, Context context) {
+        GenericRequestBuilder<Uri, InputStream, SVG, Drawable> requestBuilder = Glide.with(notificationsActivity)
             .using(Glide.buildStreamModelLoader(Uri.class, notificationsActivity), InputStream.class)
             .from(Uri.class)
             .as(SVG.class)
-            .transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
+            .transcode(new SvgDrawableTranscoder(context), Drawable.class)
             .sourceEncoder(new StreamEncoder())
             .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
             .decoder(new SvgDecoder())

+ 16 - 6
app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java

@@ -26,6 +26,7 @@
 
 package com.owncloud.android.ui.adapter;
 
+import android.content.Context;
 import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
@@ -40,7 +41,6 @@ import com.owncloud.android.databinding.AccountActionBinding;
 import com.owncloud.android.databinding.AccountItemBinding;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.ui.activity.BaseActivity;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.theme.ThemeColorUtils;
 import com.owncloud.android.utils.theme.ThemeDrawableUtils;
@@ -59,7 +59,7 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
     private static final String TAG = UserListAdapter.class.getSimpleName();
 
     private final float accountAvatarRadiusDimension;
-    private final BaseActivity context;
+    private final Context context;
     private List<UserListItem> values;
     private Listener accountListAdapterListener;
     private final UserAccountManager accountManager;
@@ -69,15 +69,17 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
     private final ClickListener clickListener;
     private final boolean showAddAccount;
     private final boolean showDotsMenu;
+    private boolean highlightCurrentlyActiveAccount;
     private final ThemeColorUtils themeColorUtils;
     private final ThemeDrawableUtils themeDrawableUtils;
 
-    public UserListAdapter(BaseActivity context,
+    public UserListAdapter(Context context,
                            UserAccountManager accountManager,
                            List<UserListItem> values,
                            ClickListener clickListener,
                            boolean showAddAccount,
                            boolean showDotsMenu,
+                           boolean highlightCurrentlyActiveAccount,
                            ThemeColorUtils themeColorUtils,
                            ThemeDrawableUtils themeDrawableUtils) {
         this.context = context;
@@ -92,6 +94,7 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
         this.showDotsMenu = showDotsMenu;
         this.themeColorUtils = themeColorUtils;
         this.themeDrawableUtils = themeDrawableUtils;
+        this.highlightCurrentlyActiveAccount = highlightCurrentlyActiveAccount;
     }
 
     @Override
@@ -125,7 +128,7 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
             if (UserListItem.TYPE_ACCOUNT == userListItem.getType()) {
                 final User user = userListItem.getUser();
                 AccountViewHolderItem item = (AccountViewHolderItem) holder;
-                item.bind(user, userListItem.isEnabled(), this);
+                item.bind(user, userListItem.isEnabled(), highlightCurrentlyActiveAccount, this);
             } // create add account action item
             else if (UserListItem.TYPE_ACTION_ADD == userListItem.getType() && accountListAdapterListener != null) {
                 ((AddAccountViewHolderItem) holder).bind(accountListAdapterListener);
@@ -228,12 +231,19 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
             }
         }
 
-        public void bind(User user, boolean userListItemEnabled, DisplayUtils.AvatarGenerationListener avatarGenerationListener) {
+        public void bind(User user,
+                         boolean userListItemEnabled,
+                         boolean highlightCurrentlyActiveAccount,
+                         DisplayUtils.AvatarGenerationListener avatarGenerationListener) {
             setData(user);
             setUser(user);
             setUsername(user);
             setAvatar(user, avatarGenerationListener);
-            setCurrentlyActiveState(user);
+            if (highlightCurrentlyActiveAccount) {
+                setCurrentlyActiveState(user);
+            } else {
+                binding.ticker.setVisibility(View.INVISIBLE);
+            }
 
             if (!userListItemEnabled) {
                 binding.userName.setPaintFlags(binding.userName.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);

+ 77 - 0
app/src/main/java/com/owncloud/android/ui/adapter/WidgetListItemViewHolder.kt

@@ -0,0 +1,77 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 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.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import androidx.recyclerview.widget.RecyclerView
+import com.bumptech.glide.request.animation.GlideAnimation
+import com.bumptech.glide.request.target.SimpleTarget
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.widget.DashboardWidgetConfigurationInterface
+import com.owncloud.android.R
+import com.owncloud.android.databinding.WidgetListItemBinding
+import com.owncloud.android.utils.DisplayUtils
+import com.owncloud.android.utils.theme.ThemeDrawableUtils
+
+class WidgetListItemViewHolder(
+    val binding: WidgetListItemBinding,
+    val themeDrawableUtils: ThemeDrawableUtils,
+    val accountManager: UserAccountManager,
+    val clientFactory: ClientFactory,
+    val context: Context
+) :
+    RecyclerView.ViewHolder(binding.root) {
+    fun bind(
+        dashboardWidget: DashboardWidget,
+        dashboardWidgetConfigurationInterface: DashboardWidgetConfigurationInterface
+    ) {
+        binding.layout.setOnClickListener { dashboardWidgetConfigurationInterface.onItemClicked(dashboardWidget) }
+
+        val target = object : SimpleTarget<Drawable>() {
+            override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation<in Drawable>?) {
+                binding.icon.setImageDrawable(resource)
+                binding.icon.setColorFilter(context.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
+            }
+
+            override fun onLoadFailed(e: java.lang.Exception?, errorDrawable: Drawable?) {
+                super.onLoadFailed(e, errorDrawable)
+                binding.icon.setImageDrawable(errorDrawable)
+                binding.icon.setColorFilter(context.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
+            }
+        }
+
+        DisplayUtils.downloadIcon(
+            accountManager,
+            clientFactory,
+            context,
+            dashboardWidget.iconUrl,
+            target,
+            R.drawable.ic_dashboard
+        )
+        binding.name.text = dashboardWidget.title
+    }
+}

+ 29 - 0
app/src/main/java/com/owncloud/android/ui/dialog/AccountChooserInterface.kt

@@ -0,0 +1,29 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 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.dialog
+
+import com.nextcloud.client.account.User
+
+interface AccountChooserInterface {
+    fun onAccountChosen(user: User)
+}

+ 6 - 4
app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java

@@ -28,6 +28,7 @@ package com.owncloud.android.ui.dialog;
 
 import android.app.Activity;
 import android.app.Dialog;
+import android.content.Context;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -39,7 +40,6 @@ import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.owncloud.android.R;
 import com.owncloud.android.databinding.MultipleAccountsBinding;
-import com.owncloud.android.ui.activity.ReceiveExternalFilesActivity;
 import com.owncloud.android.ui.adapter.UserListAdapter;
 import com.owncloud.android.ui.adapter.UserListItem;
 import com.owncloud.android.utils.theme.ThemeColorUtils;
@@ -60,6 +60,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
     @Inject UserAccountManager accountManager;
     @Inject ThemeColorUtils themeColorUtils;
     @Inject ThemeDrawableUtils themeDrawableUtils;
+    public boolean highlightCurrentlyActiveAccount = true;
 
     @NonNull
     @Override
@@ -73,7 +74,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
         LayoutInflater inflater = activity.getLayoutInflater();
         MultipleAccountsBinding binding = MultipleAccountsBinding.inflate(inflater, null, false);
 
-        final ReceiveExternalFilesActivity parent = (ReceiveExternalFilesActivity) getActivity();
+        final Context parent = getActivity();
         AlertDialog.Builder builder = new AlertDialog.Builder(parent);
 
         UserListAdapter adapter = new UserListAdapter(parent,
@@ -81,6 +82,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
                                                       getAccountListItems(),
                                                       this,
                                                       false,
+                                                      highlightCurrentlyActiveAccount,
                                                       false,
                                                       themeColorUtils,
                                                       themeDrawableUtils);
@@ -125,9 +127,9 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
 
     @Override
     public void onAccountClicked(User user) {
-        final ReceiveExternalFilesActivity parentActivity = (ReceiveExternalFilesActivity) getActivity();
+        final AccountChooserInterface parentActivity = (AccountChooserInterface) getActivity();
         if (parentActivity != null) {
-            parentActivity.changeAccount(user.toPlatformAccount());
+            parentActivity.onAccountChosen(user);
         }
         dismiss();
     }

+ 1 - 3
app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt

@@ -282,9 +282,7 @@ class BackupListAdapter(
                 context,
                 url,
                 target,
-                R.drawable.ic_user,
-                imageView.width,
-                imageView.height
+                R.drawable.ic_user
             )
         }
     }

+ 40 - 1
app/src/main/java/com/owncloud/android/utils/BitmapUtils.java

@@ -27,8 +27,10 @@ import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.widget.ImageView;
@@ -47,6 +49,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Locale;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawable;
 import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -429,7 +432,7 @@ public final class BitmapUtils {
                                      imageView);
     }
 
-    public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType statusType, String icon, Context context) {
+    public static Bitmap createAvatarWithStatus(Bitmap avatar, StatusType statusType, @NonNull String icon, Context context) {
         float avatarRadius = getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
         int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context);
 
@@ -453,6 +456,42 @@ public final class BitmapUtils {
         return output;
     }
 
+    /**
+     * Inspired from https://www.demo2s.com/android/android-bitmap-get-a-round-version-of-the-bitmap.html
+     */
+    public static Bitmap roundBitmap(Bitmap bitmap) {
+        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+
+        final Canvas canvas = new Canvas(output);
+
+        final int color = R.color.white;
+        final Paint paint = new Paint();
+        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+        final RectF rectF = new RectF(rect);
+
+        paint.setAntiAlias(true);
+        canvas.drawARGB(0, 0, 0, 0);
+        paint.setColor(getResources().getColor(color, null));
+        canvas.drawOval(rectF, paint);
+
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+        canvas.drawBitmap(bitmap, rect, rect, paint);
+
+        return output;
+    }
+
+    /**
+     * from https://stackoverflow.com/a/38249623
+     **/
+    public static Bitmap tintImage(Bitmap bitmap, int color) {
+        Paint paint = new Paint();
+        paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
+        Bitmap bitmapResult = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmapResult);
+        canvas.drawBitmap(bitmap, 0, 0, paint);
+        return bitmapResult;
+    }
+
     /**
      * from https://stackoverflow.com/a/12089127
      */

+ 9 - 14
app/src/main/java/com/owncloud/android/utils/DisplayUtils.java

@@ -36,7 +36,6 @@ import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.PictureDrawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.text.Spannable;
@@ -142,6 +141,7 @@ public final class DisplayUtils {
     public static final String MONTH_YEAR_PATTERN = "MMMM yyyy";
     public static final String MONTH_PATTERN = "MMMM";
     public static final String YEAR_PATTERN = "yyyy";
+    public static final int SVG_SIZE = 512;
 
     private static Map<String, String> mimeType2HumanReadable;
 
@@ -552,13 +552,10 @@ public final class DisplayUtils {
                                     Context context,
                                     String iconUrl,
                                     SimpleTarget imageView,
-                                    int placeholder,
-                                    int width,
-                                    int height) {
+                                    int placeholder) {
         try {
-            if (iconUrl.endsWith(".svg")) {
-                downloadSVGIcon(currentAccountProvider, clientFactory, context, iconUrl, imageView, placeholder, width,
-                                height);
+            if (Uri.parse(iconUrl).getEncodedPath().endsWith(".svg")) {
+                downloadSVGIcon(currentAccountProvider, clientFactory, context, iconUrl, imageView, placeholder);
             } else {
                 downloadPNGIcon(context, iconUrl, imageView, placeholder);
             }
@@ -583,17 +580,15 @@ public final class DisplayUtils {
                                         Context context,
                                         String iconUrl,
                                         SimpleTarget imageView,
-                                        int placeholder,
-                                        int width,
-                                        int height) {
-        GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(context)
+                                        int placeholder) {
+        GenericRequestBuilder<Uri, InputStream, SVG, Drawable> requestBuilder = Glide.with(context)
             .using(new CustomGlideUriLoader(currentAccountProvider.getUser(), clientFactory), InputStream.class)
             .from(Uri.class)
             .as(SVG.class)
-            .transcode(new SvgDrawableTranscoder(), PictureDrawable.class)
+            .transcode(new SvgDrawableTranscoder(context), Drawable.class)
             .sourceEncoder(new StreamEncoder())
-            .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder(height, width)))
-            .decoder(new SvgDecoder(height, width))
+            .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
+            .decoder(new SvgDecoder())
             .placeholder(placeholder)
             .error(placeholder)
             .animate(android.R.anim.fade_in);

+ 3 - 15
app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.java

@@ -25,14 +25,6 @@ import java.io.InputStream;
  * Decodes an SVG internal representation from an {@link InputStream}.
  */
 public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
-    private int height = -1;
-    private int width = -1;
-
-    public SvgDecoder(int height, int width) {
-        this.height = height;
-        this.width = width;
-    }
-
     public SvgDecoder() {
         // empty constructor
     }
@@ -40,13 +32,9 @@ public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
     public Resource<SVG> decode(InputStream source, int w, int h) throws IOException {
         try {
             SVG svg = SVG.getFromInputStream(source);
-
-            if (width > 0) {
-                svg.setDocumentWidth(width);
-            }
-            if (height > 0) {
-                svg.setDocumentHeight(height);
-            }
+            svg.setDocumentViewBox(0, 0, svg.getDocumentWidth(), svg.getDocumentHeight());
+            svg.setDocumentWidth("100%");
+            svg.setDocumentHeight("100%");
             svg.setDocumentPreserveAspectRatio(PreserveAspectRatio.LETTERBOX);
 
             return new SimpleResource<>(svg);

+ 22 - 3
app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.java

@@ -10,7 +10,12 @@
  */
 package com.owncloud.android.utils.svg;
 
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.Picture;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PictureDrawable;
 
 import com.bumptech.glide.load.engine.Resource;
@@ -21,13 +26,27 @@ import com.caverock.androidsvg.SVG;
 /**
  * Convert the {@link SVG}'s internal representation to an Android-compatible one ({@link Picture}).
  */
-public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
+public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, Drawable> {
+    private final Context context;
+
+    public SvgDrawableTranscoder(Context context) {
+        this.context = context;
+    }
+
     @Override
-    public Resource<PictureDrawable> transcode(Resource<SVG> toTranscode) {
+    public Resource<Drawable> transcode(Resource<SVG> toTranscode) {
         SVG svg = toTranscode.get();
         Picture picture = svg.renderToPicture();
         PictureDrawable drawable = new PictureDrawable(picture);
-        return new SimpleResource<PictureDrawable>(drawable);
+
+        Bitmap returnedBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                                                    drawable.getIntrinsicHeight(),
+                                                    Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(returnedBitmap);
+        canvas.drawPicture(drawable.getPicture());
+        Drawable d = new BitmapDrawable(context.getResources(), returnedBitmap);
+
+        return new SimpleResource<>(d);
     }
 
     @Override

+ 4 - 4
app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.java

@@ -10,7 +10,7 @@
  */
 package com.owncloud.android.utils.svg;
 
-import android.graphics.drawable.PictureDrawable;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.widget.ImageView;
 
@@ -18,10 +18,10 @@ import com.bumptech.glide.request.RequestListener;
 import com.bumptech.glide.request.target.ImageViewTarget;
 import com.bumptech.glide.request.target.Target;
 
-public class SvgSoftwareLayerSetter<T> implements RequestListener<T, PictureDrawable> {
+public class SvgSoftwareLayerSetter<T> implements RequestListener<T, Drawable> {
 
     @Override
-    public boolean onException(Exception e, T model, Target<PictureDrawable> target, boolean isFirstResource) {
+    public boolean onException(Exception e, T model, Target<Drawable> target, boolean isFirstResource) {
         ImageView view = ((ImageViewTarget<?>) target).getView();
         if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {
             view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
@@ -30,7 +30,7 @@ public class SvgSoftwareLayerSetter<T> implements RequestListener<T, PictureDraw
     }
 
     @Override
-    public boolean onResourceReady(PictureDrawable resource, T model, Target<PictureDrawable> target,
+    public boolean onResourceReady(Drawable resource, T model, Target<Drawable> target,
                                    boolean isFromMemoryCache, boolean isFirstResource) {
         ImageView view = ((ImageViewTarget<?>) target).getView();
         if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {

+ 11 - 0
app/src/main/res/drawable/ic_check.xml

@@ -0,0 +1,11 @@
+<vector android:autoMirrored="true"
+    android:height="24dp"
+    android:tint="#000000"
+    android:viewportHeight="24"
+    android:viewportWidth="24"
+    android:width="24dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
+</vector>

+ 35 - 0
app/src/main/res/drawable/ic_dashboard.xml

@@ -0,0 +1,35 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+
+<vector android:autoMirrored="true"
+    android:height="16dp"
+    android:viewportHeight="16"
+    android:viewportWidth="16"
+    android:width="16dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path
+        android:fillColor="#000000"
+        android:pathData="m7.906,1a7,7 0,0 0,-6.906 7,7 7,0 0,0 7,7 7,7 0,0 0,7 -7,7 7,0 0,0 -7,-7 7,7 0,0 0,-0.094 0zM8,3.699a4.3,4.3 0,0 1,4.301 4.301,4.3 4.3,0 0,1 -4.301,4.301 4.3,4.3 0,0 1,-4.301 -4.301,4.3 4.3,0 0,1 4.301,-4.301z"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round"
+        android:strokeWidth=".5" />
+</vector>

+ 142 - 0
app/src/main/res/layout/dashboard_widget.xml

@@ -0,0 +1,142 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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:id="@android:id/background"
+    style="@style/Widget.Nextcloud.AppWidget.Container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:backgroundTint="@color/grey_200"
+    android:orientation="vertical"
+    android:theme="@style/Theme.ownCloud.Toolbar.AppWidgetContainer">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginTop="@dimen/standard_margin"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:layout_marginBottom="@dimen/standard_margin"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="32dp"
+            android:layout_height="32dp"
+            android:layout_gravity="center_vertical"
+            android:layout_marginEnd="@dimen/standard_half_margin"
+            android:contentDescription="@string/icon_of_dashboard_widget"
+            android:src="@drawable/ic_dashboard" />
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="1"
+            android:textColor="@color/black"
+            android:textStyle="bold" />
+
+        <ImageView
+            android:id="@+id/create"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="0"
+            android:contentDescription="@string/create_new"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:src="@drawable/ic_plus" />
+
+        <ImageView
+            android:id="@+id/reload"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="0"
+            android:contentDescription="@string/refresh_content"
+            android:src="@drawable/ic_sync" />
+    </LinearLayout>
+
+    <ListView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:divider="@null"
+        android:layout_marginStart="@dimen/standard_margin"
+        android:layout_marginEnd="@dimen/zero" />
+
+    <LinearLayout
+        android:id="@+id/empty_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="top"
+        android:layout_margin="@dimen/standard_half_margin"
+        android:gravity="center_horizontal"
+        android:orientation="vertical"
+        android:visibility="gone"
+        android:paddingTop="@dimen/standard_margin">
+
+        <ImageView
+            android:id="@+id/empty_list_icon"
+            android:layout_width="64dp"
+            android:layout_height="64dp"
+            android:src="@drawable/ic_check"
+            android:contentDescription="@string/icon_for_empty_list" />
+
+        <TextView
+            android:id="@+id/empty_list_view_headline"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:ellipsize="end"
+            android:gravity="center"
+            android:maxLines="2"
+            android:textColor="@color/black"
+            android:paddingTop="@dimen/standard_half_padding"
+            android:paddingBottom="@dimen/standard_half_padding"
+            android:text="@string/no_items"
+            android:textSize="20sp" />
+
+        <TextView
+            android:id="@+id/empty_list_view_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:ellipsize="end"
+            android:gravity="center"
+            android:textColor="@color/black"
+            android:paddingTop="@dimen/standard_half_padding"
+            android:paddingBottom="@dimen/standard_half_padding"
+            android:text="@string/check_back_later_or_reload" />
+    </LinearLayout>
+
+    <Button
+        android:id="@+id/button"
+
+        android:layout_width="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_height="wrap_content"
+        android:visibility="gone" />
+</LinearLayout>

+ 79 - 0
app/src/main/res/layout/dashboard_widget_configuration_layout.xml

@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@id/layout"
+        android:layout_width="match_parent"
+        android:layout_height="32dp"
+        android:layout_margin="@dimen/standard_margin">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="32dp"
+            android:layout_height="32dp"
+            android:layout_marginEnd="@dimen/standard_margin"
+            android:contentDescription="@string/icon_of_dashboard_widget"
+            app:srcCompat="@drawable/ic_dashboard" />
+
+        <TextView
+            android:id="@+id/chooseWidget"
+            android:layout_width="0dp"
+            android:layout_height="32dp"
+            android:layout_weight="1"
+            android:gravity="center_vertical"
+            android:text="@string/choose_widget"
+            android:textStyle="bold" />
+
+        <TextView
+            android:id="@+id/accountName"
+            android:layout_width="wrap_content"
+            android:layout_height="32dp"
+            android:gravity="center_vertical"
+            android:visibility="gone" />
+    </LinearLayout>
+
+    <com.owncloud.android.ui.EmptyRecyclerView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <include
+        android:id="@+id/empty_view"
+        layout="@layout/empty_list" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/close"
+        style="@style/OutlinedButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start"
+        android:layout_margin="@dimen/standard_margin"
+        android:text="@string/common_cancel"
+        app:cornerRadius="@dimen/button_corner_radius" />
+</LinearLayout>

+ 68 - 0
app/src/main/res/layout/widget_item.xml

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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:paddingEnd="@dimen/standard_half_margin"
+    android:paddingStart="@dimen/zero"
+    android:layout_height="48dp">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginEnd="@dimen/standard_half_margin"
+        android:src="@drawable/ic_user"
+        android:contentDescription="@string/icon_of_widget_entry" />
+
+    <LinearLayout
+        android:id="@+id/text_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:ellipsize="end"
+            android:gravity="start"
+            android:textColor="@color/black"
+            tools:text="First line" />
+
+        <TextView
+            android:id="@+id/subtitle"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="start"
+            android:textColor="@color/standard_grey"
+            android:maxLines="1"
+            android:ellipsize="end"
+            tools:text="Subline" />
+    </LinearLayout>
+
+</LinearLayout>
+    

+ 37 - 0
app/src/main/res/layout/widget_item_load_more.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/standard_margin"
+    android:id="@+id/load_more_container">
+
+    <TextView
+        android:id="@+id/load_more"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textColor="@color/standard_grey" />
+
+</LinearLayout>
+    

+ 47 - 0
app/src/main/res/layout/widget_list_item.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@id/layout"
+    android:layout_width="match_parent"
+    android:layout_height="32dp"
+    android:layout_margin="@dimen/standard_margin"
+    tools:ignore="UseCompoundDrawables">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:contentDescription="@string/icon_of_dashboard_widget"
+        app:srcCompat="@drawable/ic_dashboard" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="0dp"
+        android:layout_height="32dp"
+        android:layout_weight="1"
+        android:gravity="center_vertical"
+        tools:text="Widget name" />
+</LinearLayout>

+ 15 - 10
app/src/main/res/values/attrs.xml

@@ -1,16 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-	<string-array name="pref_behaviour_entries" translatable="false">
-		<item>@string/pref_behaviour_entries_keep_file</item>
-		<item>@string/pref_behaviour_entries_move</item>
-		<item>@string/pref_behaviour_entries_delete_file</item>
-	</string-array>
+    <string-array name="pref_behaviour_entries" translatable="false">
+        <item>@string/pref_behaviour_entries_keep_file</item>
+        <item>@string/pref_behaviour_entries_move</item>
+        <item>@string/pref_behaviour_entries_delete_file</item>
+    </string-array>
 
-	<string-array name="pref_behaviour_entryValues" translatable="false">
-		<item>LOCAL_BEHAVIOUR_FORGET</item>
-		<item>LOCAL_BEHAVIOUR_MOVE</item>
-		<item>LOCAL_BEHAVIOUR_DELETE</item>
-	</string-array>
+    <string-array name="pref_behaviour_entryValues" translatable="false">
+        <item>LOCAL_BEHAVIOUR_FORGET</item>
+        <item>LOCAL_BEHAVIOUR_MOVE</item>
+        <item>LOCAL_BEHAVIOUR_DELETE</item>
+    </string-array>
 
     <string-array name="pref_name_collision_policy_entries" translatable="false">
         <item>@string/pref_instant_name_collision_policy_entries_always_ask</item>
@@ -29,4 +29,9 @@
         <item>@string/link_share_view_only</item>
         <item>@string/link_share_editing</item>
     </string-array>
+    <declare-styleable name="AppWidgetAttrs">
+        <attr name="appWidgetPadding" format="dimension" />
+        <attr name="appWidgetInnerRadius" format="dimension" />
+        <attr name="appWidgetRadius" format="dimension" />
+    </declare-styleable>
 </resources>

+ 1 - 2
app/src/main/res/values/colors.xml

@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   Nextcloud Android client application
 
   Copyright (C) 2012 Bartek Przybylski

+ 13 - 2
app/src/main/res/values/strings.xml

@@ -955,8 +955,8 @@
     <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="divider" translatable="false">—</string>
+    <string name="default_emoji" translatable="false">😃</string>
     <string name="dontClear">Don\'t clear</string>
     <string name="today">Today</string>
     <string name="thirtyMinutes">30 minutes</string>
@@ -1041,4 +1041,15 @@
     <string name="file_already_exists">Filename already exists</string>
     <string name="filedetails_export">Export</string>
     <string name="locate_folder">Locate folder</string>
+    <string name="app_widget_description">Shows one widget from dashboard</string>
+    <string name="icon_of_dashboard_widget">Icon of dashboard widget</string>
+    <string name="refresh_content">Refresh content</string>
+    <string name="icon_of_widget_entry">Icon of widget entry</string>
+    <string name="choose_widget">Choose widget</string>
+    <string name="reload">Reload</string>
+    <string name="widgets_not_available">Widgets are only available on %1$s 25 or later</string>
+    <string name="widgets_not_available_title">Not available</string>
+    <string name="icon_for_empty_list">icon for empty list</string>
+    <string name="no_items">No items</string>
+    <string name="check_back_later_or_reload">Check back later or reload.</string>
 </resources>

+ 9 - 8
app/src/main/res/values/styles.xml

@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
   ownCloud Android client application
 
   Copyright (C) 2012  Bartek Przybylski
@@ -101,8 +100,7 @@
         <item name="android:buttonBarButtonStyle">@style/FallbackTheming.Dialog.ButtonStyle</item>
     </style>
 
-    <style name="FallbackTheming.Dialog.ButtonStyle"
-        parent="Widget.MaterialComponents.Button.TextButton.Dialog">
+    <style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
         <item name="android:textColor">@color/text_color</item>
     </style>
 
@@ -283,8 +281,7 @@
         <item name="colorAccent">@color/color_accent</item>
     </style>
 
-    <style name="Theme.ownCloud.Widget.ActionBar"
-        parent="@style/Theme.MaterialComponents.Light.DarkActionBar.Bridge">
+    <style name="Theme.ownCloud.Widget.ActionBar" parent="@style/Theme.MaterialComponents.Light.DarkActionBar.Bridge">
         <item name="android:background">@color/primary</item>
         <item name="background">@color/primary</item>
         <item name="android:textColor">@color/text_color</item>
@@ -339,8 +336,8 @@
         <item name="android:textSize">26sp</item>
         <item name="android:textColor">@color/text_color</item>
     </style>
-    <style name="NextcloudTextAppearanceMedium" parent="@style/TextAppearance.AppCompat.Medium">
-    </style>
+
+    <style name="NextcloudTextAppearanceMedium" parent="@style/TextAppearance.AppCompat.Medium"></style>
 
     <style name="Widget.App.Login.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
         <item name="materialThemeOverlay">@style/ThemeOverlay.App.Login.TextInputLayout</item>
@@ -447,4 +444,8 @@
         <item name="android:background">@drawable/ripple</item>
     </style>
 
+    <style name="Widget.Nextcloud.AppWidget.Container" parent="android:Widget">
+        <item name="android:id">@android:id/background</item>
+        <item name="android:background">?android:attr/colorBackground</item>
+    </style>
 </resources>

+ 39 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,39 @@
+<!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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/>.
+  -->
+
+<resources>
+
+    <style name="Theme.ownCloud.Toolbar.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
+        <!-- Radius of the outer bound of widgets to make the rounded corners -->
+        <item name="appWidgetRadius">16dp</item>
+        <!--
+        Radius of the inner view's bound of widgets to make the rounded corners.
+        It needs to be 8dp or less than the value of appWidgetRadius
+        -->
+        <item name="appWidgetInnerRadius">8dp</item>
+    </style>
+
+    <style name="Theme.ownCloud.Toolbar.AppWidgetContainer" parent="Theme.ownCloud.Toolbar.AppWidgetContainerParent">
+        <!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
+        <item name="appWidgetPadding">0dp</item>
+    </style>
+</resources>

+ 32 - 0
app/src/main/res/xml/dashboard_widget_info.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2022 Tobias Kaminsky
+  ~ Copyright (C) 2022 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/>.
+  -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+    android:description="@string/app_widget_description"
+    android:initialKeyguardLayout="@layout/dashboard_widget"
+    android:initialLayout="@layout/dashboard_widget"
+    android:minWidth="270dp"
+    android:minHeight="180dp"
+    android:resizeMode="horizontal|vertical"
+    android:updatePeriodMillis="1800000"
+    android:widgetCategory="home_screen"
+    android:configure="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity" />

+ 3 - 3
app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java

@@ -3,7 +3,7 @@ package com.nextcloud.client.preferences;
 import android.content.Context;
 import android.content.SharedPreferences;
 
-import com.nextcloud.client.account.CurrentAccountProvider;
+import com.nextcloud.client.account.UserAccountManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -128,7 +128,7 @@ public class TestAppPreferences {
         private SharedPreferences.Editor editor;
 
         @Mock
-        private CurrentAccountProvider accountProvider;
+        private UserAccountManager userAccountManager;
 
         private AppPreferencesImpl appPreferences;
 
@@ -137,7 +137,7 @@ public class TestAppPreferences {
             MockitoAnnotations.initMocks(this);
             when(editor.remove(anyString())).thenReturn(editor);
             when(sharedPreferences.edit()).thenReturn(editor);
-            appPreferences = new AppPreferencesImpl(testContext, sharedPreferences, accountProvider);
+            appPreferences = new AppPreferencesImpl(testContext, sharedPreferences, userAccountManager);
         }
 
         @Test

+ 3 - 0
app/src/test/java/com/owncloud/android/ui/adapter/UserListAdapterTest.java

@@ -73,6 +73,7 @@ public class UserListAdapterTest {
                                               null,
                                               true,
                                               true,
+                                              true,
                                               themeColorUtils,
                                               themeDrawableUtils);
         assertEquals(0, userListAdapter.getItemCount());
@@ -93,6 +94,7 @@ public class UserListAdapterTest {
                                               null,
                                               true,
                                               true,
+                                              true,
                                               themeColorUtils,
                                               themeDrawableUtils);
 
@@ -115,6 +117,7 @@ public class UserListAdapterTest {
                                               null,
                                               true,
                                               true,
+                                              true,
                                               themeColorUtils,
                                               themeDrawableUtils);
 

+ 15 - 0
drawable_resources/dashboard.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16" height="16" version="1.1" id="svg4" sodipodi:docname="dashboard.svg"
+    inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg">
+    <defs id="defs8" />
+    <sodipodi:namedview id="namedview6" pagecolor="#505050" bordercolor="#eeeeee" borderopacity="1"
+        inkscape:showpageshadow="0" inkscape:pageopacity="0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#505050"
+        showgrid="false" inkscape:zoom="46.4375" inkscape:cx="8" inkscape:cy="7.9892328" inkscape:window-width="1920"
+        inkscape:window-height="1141" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1"
+        inkscape:current-layer="svg4" />
+    <path
+        d="m7.9062 1a7 7 0 0 0-6.9062 7 7 7 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-0.09375 0zm0.09375 2.6992a4.3 4.3 0 0 1 4.3008 4.3008 4.3 4.3 0 0 1-4.3008 4.3008 4.3 4.3 0 0 1-4.3008-4.3008 4.3 4.3 0 0 1 4.3008-4.3008z"
+        fill="#fff" stroke-dashoffset="10" stroke-linecap="round" stroke-linejoin="round" stroke-width=".5"
+        style="paint-order:markers stroke fill;fill:#000000" id="path2" />
+</svg>