Add dashboard widgets

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2022-08-16 16:57:58 +02:00
parent 423944137c
commit efa886b455
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
48 changed files with 2033 additions and 164 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

View file

@ -0,0 +1,139 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.nextcloud.ui
import android.graphics.BitmapFactory
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.test.espresso.intent.rule.IntentsTestRule
import com.nextcloud.client.TestActivity
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import com.owncloud.android.utils.BitmapUtils
import com.owncloud.android.utils.ScreenshotTest
import org.junit.Rule
import org.junit.Test
class BitmapIT : AbstractIT() {
@get:Rule
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
@Test
@ScreenshotTest
fun roundBitmap() {
val file = getFile("christine.jpg")
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
val activity = testActivityRule.launchActivity(null)
val imageView = ImageView(activity).apply {
setImageBitmap(bitmap)
}
val bitmap2 = BitmapFactory.decodeFile(file.absolutePath)
val imageView2 = ImageView(activity).apply {
setImageBitmap(BitmapUtils.roundBitmap(bitmap2))
}
val linearLayout = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setBackgroundColor(context.getColor(R.color.grey_200))
}
linearLayout.addView(imageView, 200, 200)
linearLayout.addView(imageView2, 200, 200)
activity.addView(linearLayout)
screenshot(activity)
}
// @Test
// @ScreenshotTest
// fun glideSVG() {
// val activity = testActivityRule.launchActivity(null)
// val accountProvider = UserAccountManagerImpl.fromContext(activity)
// val clientFactory = ClientFactoryImpl(activity)
//
// val linearLayout = LinearLayout(activity).apply {
// orientation = LinearLayout.VERTICAL
// setBackgroundColor(context.getColor(R.color.grey_200))
// }
//
// val file = getFile("christine.jpg")
// val bitmap = BitmapFactory.decodeFile(file.absolutePath)
//
// ImageView(activity).apply {
// setImageBitmap(bitmap)
// linearLayout.addView(this, 50, 50)
// }
//
// downloadIcon(
// client.baseUri.toString() + "/apps/files/img/app.svg",
// activity,
// linearLayout,
// accountProvider,
// clientFactory
// )
//
// downloadIcon(
// client.baseUri.toString() + "/core/img/actions/group.svg",
// activity,
// linearLayout,
// accountProvider,
// clientFactory
// )
//
// activity.addView(linearLayout)
//
// longSleep()
//
// screenshot(activity)
// }
//
// private fun downloadIcon(
// url: String,
// activity: TestActivity,
// linearLayout: LinearLayout,
// accountProvider: UserAccountManager,
// clientFactory: ClientFactory
// ) {
// val view = ImageView(activity).apply {
// linearLayout.addView(this, 50, 50)
// }
// val target = object : SimpleTarget<Drawable>() {
// override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation<in Drawable>?) {
// view.setColorFilter(targetContext.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
// view.setImageDrawable(resource)
// }
// }
//
// testActivityRule.runOnUiThread {
// DisplayUtils.downloadIcon(
// accountProvider,
// clientFactory,
// activity,
// url,
// target,
// R.drawable.ic_user
// )
// }
// }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;
@ -132,7 +133,7 @@ import static com.owncloud.android.utils.DisplayUtils.openSortingOrderDialogFrag
*/
public class ReceiveExternalFilesActivity extends FileActivity
implements OnItemClickListener, View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener,
SortingOrderDialogFragment.OnSortingOrderListener, Injectable {
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();
}

View file

@ -0,0 +1,72 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.widget.DashboardWidgetConfigurationInterface
import com.owncloud.android.databinding.WidgetListItemBinding
import com.owncloud.android.utils.theme.ThemeDrawableUtils
class DashboardWidgetListAdapter(
val themeDrawableUtils: ThemeDrawableUtils,
val accountManager: UserAccountManager,
val clientFactory: ClientFactory,
val context: Context,
private val dashboardWidgetConfigurationInterface: DashboardWidgetConfigurationInterface
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var widgets = emptyList<DashboardWidget>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return WidgetListItemViewHolder(
WidgetListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
themeDrawableUtils,
accountManager,
clientFactory,
context
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val widgetListItemViewHolder = holder as WidgetListItemViewHolder
widgetListItemViewHolder.bind(widgets[position], dashboardWidgetConfigurationInterface)
}
override fun getItemCount(): Int {
return widgets.size
}
@SuppressLint("NotifyDataSetChanged")
fun setWidgetList(list: Map<String, DashboardWidget>?) {
widgets = list?.map { (_, value) -> value }?.sortedBy { it.order } ?: emptyList()
notifyDataSetChanged()
}
}

View file

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

View file

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

View file

@ -0,0 +1,77 @@
/*
*
* Nextcloud Android client application
*
* @author Tobias Kaminsky
* Copyright (C) 2022 Tobias Kaminsky
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.adapter
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import com.nextcloud.android.lib.resources.dashboard.DashboardWidget
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.network.ClientFactory
import com.nextcloud.client.widget.DashboardWidgetConfigurationInterface
import com.owncloud.android.R
import com.owncloud.android.databinding.WidgetListItemBinding
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.theme.ThemeDrawableUtils
class WidgetListItemViewHolder(
val binding: WidgetListItemBinding,
val themeDrawableUtils: ThemeDrawableUtils,
val accountManager: UserAccountManager,
val clientFactory: ClientFactory,
val context: Context
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
dashboardWidget: DashboardWidget,
dashboardWidgetConfigurationInterface: DashboardWidgetConfigurationInterface
) {
binding.layout.setOnClickListener { dashboardWidgetConfigurationInterface.onItemClicked(dashboardWidget) }
val target = object : SimpleTarget<Drawable>() {
override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation<in Drawable>?) {
binding.icon.setImageDrawable(resource)
binding.icon.setColorFilter(context.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
}
override fun onLoadFailed(e: java.lang.Exception?, errorDrawable: Drawable?) {
super.onLoadFailed(e, errorDrawable)
binding.icon.setImageDrawable(errorDrawable)
binding.icon.setColorFilter(context.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP)
}
}
DisplayUtils.downloadIcon(
accountManager,
clientFactory,
context,
dashboardWidget.iconUrl,
target,
R.drawable.ic_dashboard
)
binding.name.text = dashboardWidget.title
}
}

View file

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

View file

@ -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();
}

View file

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

View file

@ -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
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,142 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
style="@style/Widget.Nextcloud.AppWidget.Container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/grey_200"
android:orientation="vertical"
android:theme="@style/Theme.ownCloud.Toolbar.AppWidgetContainer">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginTop="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_margin"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/standard_half_margin"
android:contentDescription="@string/icon_of_dashboard_widget"
android:src="@drawable/ic_dashboard" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:textColor="@color/black"
android:textStyle="bold" />
<ImageView
android:id="@+id/create"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_weight="0"
android:contentDescription="@string/create_new"
android:layout_marginEnd="@dimen/standard_margin"
android:src="@drawable/ic_plus" />
<ImageView
android:id="@+id/reload"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_weight="0"
android:contentDescription="@string/refresh_content"
android:src="@drawable/ic_sync" />
</LinearLayout>
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:divider="@null"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/zero" />
<LinearLayout
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_margin="@dimen/standard_half_margin"
android:gravity="center_horizontal"
android:orientation="vertical"
android:visibility="gone"
android:paddingTop="@dimen/standard_margin">
<ImageView
android:id="@+id/empty_list_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_check"
android:contentDescription="@string/icon_for_empty_list" />
<TextView
android:id="@+id/empty_list_view_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:textColor="@color/black"
android:paddingTop="@dimen/standard_half_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:text="@string/no_items"
android:textSize="20sp" />
<TextView
android:id="@+id/empty_list_view_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:ellipsize="end"
android:gravity="center"
android:textColor="@color/black"
android:paddingTop="@dimen/standard_half_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:text="@string/check_back_later_or_reload" />
</LinearLayout>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@id/layout"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_margin="@dimen/standard_margin">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@string/icon_of_dashboard_widget"
app:srcCompat="@drawable/ic_dashboard" />
<TextView
android:id="@+id/chooseWidget"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="@string/choose_widget"
android:textStyle="bold" />
<TextView
android:id="@+id/accountName"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:gravity="center_vertical"
android:visibility="gone" />
</LinearLayout>
<com.owncloud.android.ui.EmptyRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<include
android:id="@+id/empty_view"
layout="@layout/empty_list" />
<com.google.android.material.button.MaterialButton
android:id="@+id/close"
style="@style/OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_margin="@dimen/standard_margin"
android:text="@string/common_cancel"
app:cornerRadius="@dimen/button_corner_radius" />
</LinearLayout>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:paddingEnd="@dimen/standard_half_margin"
android:paddingStart="@dimen/zero"
android:layout_height="48dp">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/standard_half_margin"
android:src="@drawable/ic_user"
android:contentDescription="@string/icon_of_widget_entry" />
<LinearLayout
android:id="@+id/text_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:ellipsize="end"
android:gravity="start"
android:textColor="@color/black"
tools:text="First line" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:textColor="@color/standard_grey"
android:maxLines="1"
android:ellipsize="end"
tools:text="Subline" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_margin"
android:id="@+id/load_more_container">
<TextView
android:id="@+id/load_more"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="@color/standard_grey" />
</LinearLayout>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@id/layout"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_margin="@dimen/standard_margin"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="@dimen/standard_margin"
android:contentDescription="@string/icon_of_dashboard_widget"
app:srcCompat="@drawable/ic_dashboard" />
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:gravity="center_vertical"
tools:text="Widget name" />
</LinearLayout>

View file

@ -29,4 +29,9 @@
<item>@string/link_share_view_only</item>
<item>@string/link_share_editing</item>
</string-array>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
</declare-styleable>
</resources>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,39 @@
<!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<style name="Theme.ownCloud.Toolbar.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- Radius of the outer bound of widgets to make the rounded corners -->
<item name="appWidgetRadius">16dp</item>
<!--
Radius of the inner view's bound of widgets to make the rounded corners.
It needs to be 8dp or less than the value of appWidgetRadius
-->
<item name="appWidgetInnerRadius">8dp</item>
</style>
<style name="Theme.ownCloud.Toolbar.AppWidgetContainer" parent="Theme.ownCloud.Toolbar.AppWidgetContainerParent">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">0dp</item>
</style>
</resources>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~
~ Nextcloud Android client application
~
~ @author Tobias Kaminsky
~ Copyright (C) 2022 Tobias Kaminsky
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU Affero General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU Affero General Public License for more details.
~
~ You should have received a copy of the GNU Affero General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_widget_description"
android:initialKeyguardLayout="@layout/dashboard_widget"
android:initialLayout="@layout/dashboard_widget"
android:minWidth="270dp"
android:minHeight="180dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen"
android:configure="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity" />

View file

@ -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

View file

@ -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);

View file

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

After

Width:  |  Height:  |  Size: 1.3 KiB