diff --git a/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png b/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png
new file mode 100644
index 0000000000..668c4cd0e8
Binary files /dev/null and b/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png differ
diff --git a/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png b/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png
new file mode 100644
index 0000000000..0a5549bba0
Binary files /dev/null and b/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png differ
diff --git a/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt b/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt
new file mode 100644
index 0000000000..23bfb671c8
--- /dev/null
+++ b/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 .
+ */
+
+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() {
+ // override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation?) {
+ // 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
+ // )
+ // }
+ // }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e389fb86cb..40520d51cb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,9 +40,7 @@
-
-
+
@@ -151,6 +149,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+ android:permission="android.permission.MANAGE_DOCUMENTS">
-
-
-
-
-
-
-
-
+ * 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.
+ *
* 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;
}
diff --git a/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java b/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java
index 6d3338fb37..d0b1d37103 100644
--- a/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java
+++ b/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);
}
}
diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt
new file mode 100644
index 0000000000..ce1aeaeee0
--- /dev/null
+++ b/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 .
+ */
+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)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt
new file mode 100644
index 0000000000..1901349f26
--- /dev/null
+++ b/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 .
+ */
+
+package com.nextcloud.client.widget
+
+import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
+
+interface DashboardWidgetConfigurationInterface {
+ fun onItemClicked(dashboardWidget: DashboardWidget)
+}
diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt
new file mode 100644
index 0000000000..220298dd3e
--- /dev/null
+++ b/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 .
+ */
+
+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"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt
new file mode 100644
index 0000000000..aed8a70fff
--- /dev/null
+++ b/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 .
+ */
+
+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 = 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
+ 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
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt
new file mode 100644
index 0000000000..d6db1eed18
--- /dev/null
+++ b/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 .
+ */
+
+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?) {
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt b/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt
new file mode 100644
index 0000000000..01b5147c63
--- /dev/null
+++ b/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 .
+ */
+
+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,
+ val addButton: DashboardButton?,
+ val moreButton: DashboardButton?
+)
diff --git a/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt b/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt
new file mode 100644
index 0000000000..2f52e158b8
--- /dev/null
+++ b/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 .
+ */
+
+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 =
+ 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_"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
index 77f91dc9ea..9fb5183e36 100644
--- a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
+++ b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt
@@ -131,6 +131,7 @@ class ChooseAccountDialogFragment :
this,
false,
false,
+ true,
themeColorUtils,
themeDrawableUtils
)
diff --git a/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java b/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java
index f77c9da404..4b90cae5aa 100644
--- a/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java
+++ b/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);
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
index 526ac55c05..313efc0911 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java
+++ b/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);
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
index 63b110baa5..59f6764d7a 100644
--- a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
+++ b/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);
diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
index 7cf07a8137..be4246003c 100755
--- a/app/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
+++ b/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();
}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/DashboardWidgetListAdapter.kt
new file mode 100644
index 0000000000..5e048d36ea
--- /dev/null
+++ b/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 .
+ */
+
+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() {
+ private var widgets = emptyList()
+
+ 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?) {
+ widgets = list?.map { (_, value) -> value }?.sortedBy { it.order } ?: emptyList()
+ notifyDataSetChanged()
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java
index a9abcd3b1b..21ca7d9655 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/NotificationListAdapter.java
+++ b/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 requestBuilder = Glide.with(notificationsActivity)
+ private void downloadIcon(String icon, ImageView itemViewType, Context context) {
+ GenericRequestBuilder 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())
diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java
index 4c08d891d8..e484433af7 100644
--- a/app/src/main/java/com/owncloud/android/ui/adapter/UserListAdapter.java
+++ b/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 values;
private Listener accountListAdapterListener;
private final UserAccountManager accountManager;
@@ -69,15 +69,17 @@ public class UserListAdapter extends RecyclerView.Adapter 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.
+ */
+
+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() {
+ override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation?) {
+ 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
+ }
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/AccountChooserInterface.kt b/app/src/main/java/com/owncloud/android/ui/dialog/AccountChooserInterface.kt
new file mode 100644
index 0000000000..f38f37d7a6
--- /dev/null
+++ b/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 .
+ */
+
+package com.owncloud.android.ui.dialog
+
+import com.nextcloud.client.account.User
+
+interface AccountChooserInterface {
+ fun onAccountChosen(user: User)
+}
diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java
index 109b355198..063cdbfe99 100644
--- a/app/src/main/java/com/owncloud/android/ui/dialog/MultipleAccountsDialog.java
+++ b/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();
}
diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt
index 7c7de75c2e..d0cb7bb956 100644
--- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListAdapter.kt
+++ b/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
)
}
}
diff --git a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java b/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java
index 231d4f5787..70c3debcde 100644
--- a/app/src/main/java/com/owncloud/android/utils/BitmapUtils.java
+++ b/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
*/
diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
index 6d4876e1ef..3b1187f6e2 100644
--- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java
+++ b/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 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 requestBuilder = Glide.with(context)
+ int placeholder) {
+ GenericRequestBuilder 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);
diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.java b/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.java
index 788d314565..e314786264 100644
--- a/app/src/main/java/com/owncloud/android/utils/svg/SvgDecoder.java
+++ b/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 {
- 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 {
public Resource