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 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); diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.java b/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.java index 38cb2fb06d..77c97b4aac 100644 --- a/app/src/main/java/com/owncloud/android/utils/svg/SvgDrawableTranscoder.java +++ b/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 { +public class SvgDrawableTranscoder implements ResourceTranscoder { + private final Context context; + + public SvgDrawableTranscoder(Context context) { + this.context = context; + } + @Override - public Resource transcode(Resource toTranscode) { + public Resource transcode(Resource toTranscode) { SVG svg = toTranscode.get(); Picture picture = svg.renderToPicture(); PictureDrawable drawable = new PictureDrawable(picture); - return new SimpleResource(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 diff --git a/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.java b/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.java index 12bf2541a8..d7d56fdea5 100644 --- a/app/src/main/java/com/owncloud/android/utils/svg/SvgSoftwareLayerSetter.java +++ b/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 implements RequestListener { +public class SvgSoftwareLayerSetter implements RequestListener { @Override - public boolean onException(Exception e, T model, Target target, boolean isFirstResource) { + public boolean onException(Exception e, T model, Target 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 implements RequestListener target, + public boolean onResourceReady(Drawable resource, T model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { ImageView view = ((ImageViewTarget) target).getView(); if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) { diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000000..834940ea35 --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard.xml b/app/src/main/res/drawable/ic_dashboard.xml new file mode 100644 index 0000000000..ddff828159 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard.xml @@ -0,0 +1,35 @@ + + + + + diff --git a/app/src/main/res/layout/dashboard_widget.xml b/app/src/main/res/layout/dashboard_widget.xml new file mode 100644 index 0000000000..435a42e206 --- /dev/null +++ b/app/src/main/res/layout/dashboard_widget.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dashboard_widget_configuration_layout.xml b/app/src/main/res/layout/dashboard_widget_configuration_layout.xml new file mode 100644 index 0000000000..47ffc07000 --- /dev/null +++ b/app/src/main/res/layout/dashboard_widget_configuration_layout.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_item.xml b/app/src/main/res/layout/widget_item.xml new file mode 100644 index 0000000000..87c5fd3a4a --- /dev/null +++ b/app/src/main/res/layout/widget_item.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_item_load_more.xml b/app/src/main/res/layout/widget_item_load_more.xml new file mode 100644 index 0000000000..93ed31ae2d --- /dev/null +++ b/app/src/main/res/layout/widget_item_load_more.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/app/src/main/res/layout/widget_list_item.xml b/app/src/main/res/layout/widget_list_item.xml new file mode 100644 index 0000000000..632bf43e10 --- /dev/null +++ b/app/src/main/res/layout/widget_list_item.xml @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8378ef5bd5..f5d46ac6a7 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,16 +1,16 @@ - - @string/pref_behaviour_entries_keep_file - @string/pref_behaviour_entries_move - @string/pref_behaviour_entries_delete_file - + + @string/pref_behaviour_entries_keep_file + @string/pref_behaviour_entries_move + @string/pref_behaviour_entries_delete_file + - - LOCAL_BEHAVIOUR_FORGET - LOCAL_BEHAVIOUR_MOVE - LOCAL_BEHAVIOUR_DELETE - + + LOCAL_BEHAVIOUR_FORGET + LOCAL_BEHAVIOUR_MOVE + LOCAL_BEHAVIOUR_DELETE + @string/pref_instant_name_collision_policy_entries_always_ask @@ -29,4 +29,9 @@ @string/link_share_view_only @string/link_share_editing + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 20373da3ea..de115bc2ff 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,4 @@ - - + + + + + + + diff --git a/app/src/main/res/xml/dashboard_widget_info.xml b/app/src/main/res/xml/dashboard_widget_info.xml new file mode 100644 index 0000000000..9f74561b9f --- /dev/null +++ b/app/src/main/res/xml/dashboard_widget_info.xml @@ -0,0 +1,32 @@ + + + diff --git a/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java b/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java index 56f356861e..b13d3bf4dc 100644 --- a/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java +++ b/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 diff --git a/app/src/test/java/com/owncloud/android/ui/adapter/UserListAdapterTest.java b/app/src/test/java/com/owncloud/android/ui/adapter/UserListAdapterTest.java index 79546d1a2c..471b56167c 100644 --- a/app/src/test/java/com/owncloud/android/ui/adapter/UserListAdapterTest.java +++ b/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); diff --git a/drawable_resources/dashboard.svg b/drawable_resources/dashboard.svg new file mode 100644 index 0000000000..655c57ba3d --- /dev/null +++ b/drawable_resources/dashboard.svg @@ -0,0 +1,15 @@ + + + + + +