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 <uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<uses-permission <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
@ -151,6 +149,13 @@
<activity <activity
android:name=".ui.activity.SyncedFoldersActivity" android:name=".ui.activity.SyncedFoldersActivity"
android:exported="false" /> 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 <receiver
android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver" android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
@ -158,6 +163,17 @@
<receiver <receiver
android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver" android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver"
android:exported="false" /> 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 <activity
android:name=".ui.activity.UploadFilesActivity" android:name=".ui.activity.UploadFilesActivity"
@ -220,7 +236,6 @@
android:name="android.accounts.AccountAuthenticator" android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" /> android:resource="@xml/authenticator" />
</service> </service>
<service <service
android:name=".syncadapter.FileSyncService" android:name=".syncadapter.FileSyncService"
android:exported="true" android:exported="true"
@ -233,6 +248,10 @@
android:name="android.content.SyncAdapter" android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter_files" /> android:resource="@xml/syncadapter_files" />
</service> </service>
<service
android:name="com.nextcloud.client.widget.DashboardWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" />
<provider <provider
android:name=".providers.FileContentProvider" android:name=".providers.FileContentProvider"
@ -304,16 +323,12 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/exposed_filepaths" /> android:resource="@xml/exposed_filepaths" />
</provider> </provider>
<provider <provider
android:name=".providers.DiskLruImageCacheFileProvider" android:name=".providers.DiskLruImageCacheFileProvider"
android:authorities="@string/image_cache_provider_authority" android:authorities="@string/image_cache_provider_authority"
android:exported="true"
android:grantUriPermissions="true" android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS" android:permission="android.permission.MANAGE_DOCUMENTS"></provider> <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
android:exported="true">
</provider>
<!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
<!-- to "best before" dates in his fridge. --> <!-- to "best before" dates in his fridge. -->
<!-- disable default provider --> <!-- disable default provider -->
<provider <provider
@ -327,8 +342,6 @@
tools:node="remove" /> tools:node="remove" />
</provider> </provider>
<activity <activity
android:name=".authentication.AuthenticatorActivity" android:name=".authentication.AuthenticatorActivity"
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
@ -341,7 +354,6 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".authentication.DeepLinkLoginActivity" android:name=".authentication.DeepLinkLoginActivity"
android:clearTaskOnLaunch="true" android:clearTaskOnLaunch="true"
@ -391,11 +403,9 @@
<activity <activity
android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity" android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name="com.nextcloud.client.logger.ui.LogsActivity" android:name="com.nextcloud.client.logger.ui.LogsActivity"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name="com.nextcloud.client.errorhandling.ShowErrorActivity" android:name="com.nextcloud.client.errorhandling.ShowErrorActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
@ -465,7 +475,6 @@
android:label="@string/manage_space_title" android:label="@string/manage_space_title"
android:theme="@style/Theme.ownCloud" /> android:theme="@style/Theme.ownCloud" />
<service <service
android:name=".services.AccountManagerService" android:name=".services.AccountManagerService"
android:enabled="true" android:enabled="true"
@ -476,12 +485,10 @@
android:name=".ui.activity.SsoGrantPermissionActivity" android:name=".ui.activity.SsoGrantPermissionActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.ownCloud.Dialog.NoTitle" /> android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
<activity <activity
android:name="com.nextcloud.client.etm.EtmActivity" android:name="com.nextcloud.client.etm.EtmActivity"
android:exported="false" android:exported="false"
android:theme="@style/Theme.ownCloud.Toolbar" /> android:theme="@style/Theme.ownCloud.Toolbar" />
<activity <activity
android:name=".ui.preview.PreviewBitmapActivity" android:name=".ui.preview.PreviewBitmapActivity"
android:exported="false" 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.migrations.Migrations;
import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.onboarding.FirstRunActivity;
import com.nextcloud.client.onboarding.WhatsNewActivity; 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.ChooseAccountDialogFragment;
import com.nextcloud.ui.SetStatusDialogFragment; import com.nextcloud.ui.SetStatusDialogFragment;
import com.owncloud.android.MainApp; 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.FileDetailSharingFragment;
import com.owncloud.android.ui.fragment.GalleryFragment; import com.owncloud.android.ui.fragment.GalleryFragment;
import com.owncloud.android.ui.fragment.LocalFileListFragment; 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.OCFileListBottomSheetDialog;
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialogFragment;
import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.OCFileListFragment;
import com.owncloud.android.ui.fragment.SharedListFragment; import com.owncloud.android.ui.fragment.SharedListFragment;
import com.owncloud.android.ui.fragment.UnifiedSearchFragment; import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
@ -341,6 +344,9 @@ abstract class ComponentsModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract FileSyncService fileSyncService(); abstract FileSyncService fileSyncService();
@ContributesAndroidInjector
abstract DashboardWidgetService dashboardWidgetService();
@ContributesAndroidInjector @ContributesAndroidInjector
abstract PreviewPdfFragment previewPDFFragment(); abstract PreviewPdfFragment previewPDFFragment();
@ -430,4 +436,10 @@ abstract class ComponentsModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract SyncFileNotEnoughSpaceDialogFragment syncFileNotEnoughSpaceDialogFragment(); 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; import java.io.IOException;
class ClientFactoryImpl implements ClientFactory { public class ClientFactoryImpl implements ClientFactory {
private Context context; private Context context;
ClientFactoryImpl(Context context) { public ClientFactoryImpl(Context context) {
this.context = context; this.context = context;
} }
@ -49,8 +49,8 @@ class ClientFactoryImpl implements ClientFactory {
public OwnCloudClient create(User user) throws CreationException { public OwnCloudClient create(User user) throws CreationException {
try { try {
return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context); return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context);
} catch (OperationCanceledException| } catch (OperationCanceledException |
AuthenticatorException| AuthenticatorException |
IOException e) { IOException e) {
throw new CreationException(e); throw new CreationException(e);
} }

View file

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

View file

@ -3,7 +3,7 @@ package com.nextcloud.client.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.UserAccountManager;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -23,7 +23,7 @@ public class PreferencesModule {
@Singleton @Singleton
public AppPreferences appPreferences(Context context, public AppPreferences appPreferences(Context context,
SharedPreferences sharedPreferences, SharedPreferences sharedPreferences,
CurrentAccountProvider currentAccountProvider) { UserAccountManager userAccountManager) {
return new AppPreferencesImpl(context, sharedPreferences, currentAccountProvider); 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, this,
false, false,
false, false,
true,
themeColorUtils, themeColorUtils,
themeDrawableUtils themeDrawableUtils
) )

View file

@ -81,6 +81,24 @@ public class EmptyRecyclerView extends RecyclerView {
initEmptyView(); 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 @Override
public void onItemRangeInserted(int positionStart, int itemCount) { public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount); super.onItemRangeInserted(positionStart, itemCount);

View file

@ -769,9 +769,7 @@ public abstract class DrawerActivity extends ToolbarActivity
this, this,
firstQuota.getIconUrl(), firstQuota.getIconUrl(),
target, target,
R.drawable.ic_link, R.drawable.ic_link);
size,
size);
} else { } else {
mQuotaTextLink.setVisibility(View.GONE); 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)) { if (mNavigationView != null && getBaseContext().getResources().getBoolean(R.bool.show_external_links)) {
mNavigationView.getMenu().removeGroup(R.id.drawer_menu_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); int greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon);
for (final ExternalLink link : externalLinksProvider.getExternalLink(ExternalLinkType.LINK)) { for (final ExternalLink link : externalLinksProvider.getExternalLink(ExternalLinkType.LINK)) {
@ -911,9 +907,7 @@ public abstract class DrawerActivity extends ToolbarActivity
this, this,
link.getIconUrl(), link.getIconUrl(),
target, target,
R.drawable.ic_link, R.drawable.ic_link);
size,
size);
} }
setDrawerMenuItemChecked(mCheckedMenuItem); setDrawerMenuItemChecked(mCheckedMenuItem);

View file

@ -155,6 +155,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
this, this,
multipleAccountsSupported, multipleAccountsSupported,
true, true,
true,
themeColorUtils, themeColorUtils,
themeDrawableUtils); themeDrawableUtils);
@ -310,6 +311,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
this, this,
multipleAccountsSupported, multipleAccountsSupported,
false, false,
true,
themeColorUtils, themeColorUtils,
themeDrawableUtils); themeDrawableUtils);
recyclerView.setAdapter(userListAdapter); recyclerView.setAdapter(userListAdapter);
@ -364,6 +366,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
this, this,
multipleAccountsSupported, multipleAccountsSupported,
false, false,
true,
themeColorUtils, themeColorUtils,
themeDrawableUtils); themeDrawableUtils);
recyclerView.setAdapter(userListAdapter); 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.button.MaterialButton;
import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout; import com.google.android.material.textfield.TextInputLayout;
import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable; import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferences;
import com.owncloud.android.MainApp; 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.syncadapter.FileSyncAdapter;
import com.owncloud.android.ui.adapter.UploaderAdapter; import com.owncloud.android.ui.adapter.UploaderAdapter;
import com.owncloud.android.ui.asynctasks.CopyAndUploadContentUrisTask; 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.ConfirmationDialogFragment;
import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; import com.owncloud.android.ui.dialog.CreateFolderDialogFragment;
import com.owncloud.android.ui.dialog.MultipleAccountsDialog; 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.app.AlertDialog.Builder;
import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuItemCompat; import androidx.core.view.MenuItemCompat;
import androidx.core.widget.NestedScrollView;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -131,8 +132,8 @@ import static com.owncloud.android.utils.DisplayUtils.openSortingOrderDialogFrag
* This can be used to upload things to an ownCloud instance. * This can be used to upload things to an ownCloud instance.
*/ */
public class ReceiveExternalFilesActivity extends FileActivity public class ReceiveExternalFilesActivity extends FileActivity
implements OnItemClickListener, View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener, implements OnItemClickListener, View.OnClickListener, CopyAndUploadContentUrisTask.OnCopyTmpFilesTaskListener,
SortingOrderDialogFragment.OnSortingOrderListener, Injectable { SortingOrderDialogFragment.OnSortingOrderListener, Injectable, AccountChooserInterface {
private static final String TAG = ReceiveExternalFilesActivity.class.getSimpleName(); private static final String TAG = ReceiveExternalFilesActivity.class.getSimpleName();
@ -237,8 +238,9 @@ public class ReceiveExternalFilesActivity extends FileActivity
return this; return this;
} }
public void changeAccount(Account account) { @Override
setAccount(account, false); public void onAccountChosen(@NonNull User user) {
setAccount(user.toPlatformAccount(), false);
initTargetFolder(); initTargetFolder();
populateDirectoryList(); 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; package com.owncloud.android.ui.adapter;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.graphics.drawable.PictureDrawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
@ -147,7 +148,7 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
holder.binding.message.setText(notification.getMessage()); holder.binding.message.setText(notification.getMessage());
if (!TextUtils.isEmpty(notification.getIcon())) { if (!TextUtils.isEmpty(notification.getIcon())) {
downloadIcon(notification.getIcon(), holder.binding.icon); downloadIcon(notification.getIcon(), holder.binding.icon, notificationsActivity);
} }
int nightModeFlag = int nightModeFlag =
@ -360,12 +361,12 @@ public class NotificationListAdapter extends RecyclerView.Adapter<NotificationLi
} }
} }
private void downloadIcon(String icon, ImageView itemViewType) { private void downloadIcon(String icon, ImageView itemViewType, Context context) {
GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(notificationsActivity) GenericRequestBuilder<Uri, InputStream, SVG, Drawable> requestBuilder = Glide.with(notificationsActivity)
.using(Glide.buildStreamModelLoader(Uri.class, notificationsActivity), InputStream.class) .using(Glide.buildStreamModelLoader(Uri.class, notificationsActivity), InputStream.class)
.from(Uri.class) .from(Uri.class)
.as(SVG.class) .as(SVG.class)
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class) .transcode(new SvgDrawableTranscoder(context), Drawable.class)
.sourceEncoder(new StreamEncoder()) .sourceEncoder(new StreamEncoder())
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder())) .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
.decoder(new SvgDecoder()) .decoder(new SvgDecoder())

View file

@ -26,6 +26,7 @@
package com.owncloud.android.ui.adapter; package com.owncloud.android.ui.adapter;
import android.content.Context;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -40,7 +41,6 @@ import com.owncloud.android.databinding.AccountActionBinding;
import com.owncloud.android.databinding.AccountItemBinding; import com.owncloud.android.databinding.AccountItemBinding;
import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudAccount;
import com.owncloud.android.lib.common.utils.Log_OC; 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.DisplayUtils;
import com.owncloud.android.utils.theme.ThemeColorUtils; import com.owncloud.android.utils.theme.ThemeColorUtils;
import com.owncloud.android.utils.theme.ThemeDrawableUtils; 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 static final String TAG = UserListAdapter.class.getSimpleName();
private final float accountAvatarRadiusDimension; private final float accountAvatarRadiusDimension;
private final BaseActivity context; private final Context context;
private List<UserListItem> values; private List<UserListItem> values;
private Listener accountListAdapterListener; private Listener accountListAdapterListener;
private final UserAccountManager accountManager; private final UserAccountManager accountManager;
@ -69,15 +69,17 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
private final ClickListener clickListener; private final ClickListener clickListener;
private final boolean showAddAccount; private final boolean showAddAccount;
private final boolean showDotsMenu; private final boolean showDotsMenu;
private boolean highlightCurrentlyActiveAccount;
private final ThemeColorUtils themeColorUtils; private final ThemeColorUtils themeColorUtils;
private final ThemeDrawableUtils themeDrawableUtils; private final ThemeDrawableUtils themeDrawableUtils;
public UserListAdapter(BaseActivity context, public UserListAdapter(Context context,
UserAccountManager accountManager, UserAccountManager accountManager,
List<UserListItem> values, List<UserListItem> values,
ClickListener clickListener, ClickListener clickListener,
boolean showAddAccount, boolean showAddAccount,
boolean showDotsMenu, boolean showDotsMenu,
boolean highlightCurrentlyActiveAccount,
ThemeColorUtils themeColorUtils, ThemeColorUtils themeColorUtils,
ThemeDrawableUtils themeDrawableUtils) { ThemeDrawableUtils themeDrawableUtils) {
this.context = context; this.context = context;
@ -92,6 +94,7 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
this.showDotsMenu = showDotsMenu; this.showDotsMenu = showDotsMenu;
this.themeColorUtils = themeColorUtils; this.themeColorUtils = themeColorUtils;
this.themeDrawableUtils = themeDrawableUtils; this.themeDrawableUtils = themeDrawableUtils;
this.highlightCurrentlyActiveAccount = highlightCurrentlyActiveAccount;
} }
@Override @Override
@ -125,7 +128,7 @@ public class UserListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
if (UserListItem.TYPE_ACCOUNT == userListItem.getType()) { if (UserListItem.TYPE_ACCOUNT == userListItem.getType()) {
final User user = userListItem.getUser(); final User user = userListItem.getUser();
AccountViewHolderItem item = (AccountViewHolderItem) holder; AccountViewHolderItem item = (AccountViewHolderItem) holder;
item.bind(user, userListItem.isEnabled(), this); item.bind(user, userListItem.isEnabled(), highlightCurrentlyActiveAccount, this);
} // create add account action item } // create add account action item
else if (UserListItem.TYPE_ACTION_ADD == userListItem.getType() && accountListAdapterListener != null) { else if (UserListItem.TYPE_ACTION_ADD == userListItem.getType() && accountListAdapterListener != null) {
((AddAccountViewHolderItem) holder).bind(accountListAdapterListener); ((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); setData(user);
setUser(user); setUser(user);
setUsername(user); setUsername(user);
setAvatar(user, avatarGenerationListener); setAvatar(user, avatarGenerationListener);
setCurrentlyActiveState(user); if (highlightCurrentlyActiveAccount) {
setCurrentlyActiveState(user);
} else {
binding.ticker.setVisibility(View.INVISIBLE);
}
if (!userListItemEnabled) { if (!userListItemEnabled) {
binding.userName.setPaintFlags(binding.userName.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); 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.Activity;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -39,7 +40,6 @@ import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable; import com.nextcloud.client.di.Injectable;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.databinding.MultipleAccountsBinding; 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.UserListAdapter;
import com.owncloud.android.ui.adapter.UserListItem; import com.owncloud.android.ui.adapter.UserListItem;
import com.owncloud.android.utils.theme.ThemeColorUtils; import com.owncloud.android.utils.theme.ThemeColorUtils;
@ -60,6 +60,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
@Inject UserAccountManager accountManager; @Inject UserAccountManager accountManager;
@Inject ThemeColorUtils themeColorUtils; @Inject ThemeColorUtils themeColorUtils;
@Inject ThemeDrawableUtils themeDrawableUtils; @Inject ThemeDrawableUtils themeDrawableUtils;
public boolean highlightCurrentlyActiveAccount = true;
@NonNull @NonNull
@Override @Override
@ -73,7 +74,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
LayoutInflater inflater = activity.getLayoutInflater(); LayoutInflater inflater = activity.getLayoutInflater();
MultipleAccountsBinding binding = MultipleAccountsBinding.inflate(inflater, null, false); MultipleAccountsBinding binding = MultipleAccountsBinding.inflate(inflater, null, false);
final ReceiveExternalFilesActivity parent = (ReceiveExternalFilesActivity) getActivity(); final Context parent = getActivity();
AlertDialog.Builder builder = new AlertDialog.Builder(parent); AlertDialog.Builder builder = new AlertDialog.Builder(parent);
UserListAdapter adapter = new UserListAdapter(parent, UserListAdapter adapter = new UserListAdapter(parent,
@ -81,6 +82,7 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
getAccountListItems(), getAccountListItems(),
this, this,
false, false,
highlightCurrentlyActiveAccount,
false, false,
themeColorUtils, themeColorUtils,
themeDrawableUtils); themeDrawableUtils);
@ -125,9 +127,9 @@ public class MultipleAccountsDialog extends DialogFragment implements Injectable
@Override @Override
public void onAccountClicked(User user) { public void onAccountClicked(User user) {
final ReceiveExternalFilesActivity parentActivity = (ReceiveExternalFilesActivity) getActivity(); final AccountChooserInterface parentActivity = (AccountChooserInterface) getActivity();
if (parentActivity != null) { if (parentActivity != null) {
parentActivity.changeAccount(user.toPlatformAccount()); parentActivity.onAccountChosen(user);
} }
dismiss(); dismiss();
} }

View file

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

View file

@ -27,8 +27,10 @@ import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuffXfermode;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.widget.ImageView; import android.widget.ImageView;
@ -47,6 +49,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Locale; import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@ -429,7 +432,7 @@ public final class BitmapUtils {
imageView); 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); float avatarRadius = getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context); int width = DisplayUtils.convertDpToPixel(2 * avatarRadius, context);
@ -453,6 +456,42 @@ public final class BitmapUtils {
return output; 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 * from https://stackoverflow.com/a/12089127
*/ */

View file

@ -36,7 +36,6 @@ import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.text.Spannable; 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_YEAR_PATTERN = "MMMM yyyy";
public static final String MONTH_PATTERN = "MMMM"; public static final String MONTH_PATTERN = "MMMM";
public static final String YEAR_PATTERN = "yyyy"; public static final String YEAR_PATTERN = "yyyy";
public static final int SVG_SIZE = 512;
private static Map<String, String> mimeType2HumanReadable; private static Map<String, String> mimeType2HumanReadable;
@ -552,13 +552,10 @@ public final class DisplayUtils {
Context context, Context context,
String iconUrl, String iconUrl,
SimpleTarget imageView, SimpleTarget imageView,
int placeholder, int placeholder) {
int width,
int height) {
try { try {
if (iconUrl.endsWith(".svg")) { if (Uri.parse(iconUrl).getEncodedPath().endsWith(".svg")) {
downloadSVGIcon(currentAccountProvider, clientFactory, context, iconUrl, imageView, placeholder, width, downloadSVGIcon(currentAccountProvider, clientFactory, context, iconUrl, imageView, placeholder);
height);
} else { } else {
downloadPNGIcon(context, iconUrl, imageView, placeholder); downloadPNGIcon(context, iconUrl, imageView, placeholder);
} }
@ -583,17 +580,15 @@ public final class DisplayUtils {
Context context, Context context,
String iconUrl, String iconUrl,
SimpleTarget imageView, SimpleTarget imageView,
int placeholder, int placeholder) {
int width, GenericRequestBuilder<Uri, InputStream, SVG, Drawable> requestBuilder = Glide.with(context)
int height) {
GenericRequestBuilder<Uri, InputStream, SVG, PictureDrawable> requestBuilder = Glide.with(context)
.using(new CustomGlideUriLoader(currentAccountProvider.getUser(), clientFactory), InputStream.class) .using(new CustomGlideUriLoader(currentAccountProvider.getUser(), clientFactory), InputStream.class)
.from(Uri.class) .from(Uri.class)
.as(SVG.class) .as(SVG.class)
.transcode(new SvgDrawableTranscoder(), PictureDrawable.class) .transcode(new SvgDrawableTranscoder(context), Drawable.class)
.sourceEncoder(new StreamEncoder()) .sourceEncoder(new StreamEncoder())
.cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder(height, width))) .cacheDecoder(new FileToStreamDecoder<>(new SvgDecoder()))
.decoder(new SvgDecoder(height, width)) .decoder(new SvgDecoder())
.placeholder(placeholder) .placeholder(placeholder)
.error(placeholder) .error(placeholder)
.animate(android.R.anim.fade_in); .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}. * Decodes an SVG internal representation from an {@link InputStream}.
*/ */
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> { 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() { public SvgDecoder() {
// empty constructor // 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 { public Resource<SVG> decode(InputStream source, int w, int h) throws IOException {
try { try {
SVG svg = SVG.getFromInputStream(source); SVG svg = SVG.getFromInputStream(source);
svg.setDocumentViewBox(0, 0, svg.getDocumentWidth(), svg.getDocumentHeight());
if (width > 0) { svg.setDocumentWidth("100%");
svg.setDocumentWidth(width); svg.setDocumentHeight("100%");
}
if (height > 0) {
svg.setDocumentHeight(height);
}
svg.setDocumentPreserveAspectRatio(PreserveAspectRatio.LETTERBOX); svg.setDocumentPreserveAspectRatio(PreserveAspectRatio.LETTERBOX);
return new SimpleResource<>(svg); return new SimpleResource<>(svg);

View file

@ -10,7 +10,12 @@
*/ */
package com.owncloud.android.utils.svg; 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.Picture;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.PictureDrawable; import android.graphics.drawable.PictureDrawable;
import com.bumptech.glide.load.engine.Resource; 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}). * 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 @Override
public Resource<PictureDrawable> transcode(Resource<SVG> toTranscode) { public Resource<Drawable> transcode(Resource<SVG> toTranscode) {
SVG svg = toTranscode.get(); SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture(); Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture); 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 @Override

View file

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

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string-array name="pref_behaviour_entries" translatable="false"> <string-array name="pref_behaviour_entries" translatable="false">
<item>@string/pref_behaviour_entries_keep_file</item> <item>@string/pref_behaviour_entries_keep_file</item>
<item>@string/pref_behaviour_entries_move</item> <item>@string/pref_behaviour_entries_move</item>
<item>@string/pref_behaviour_entries_delete_file</item> <item>@string/pref_behaviour_entries_delete_file</item>
</string-array> </string-array>
<string-array name="pref_behaviour_entryValues" translatable="false"> <string-array name="pref_behaviour_entryValues" translatable="false">
<item>LOCAL_BEHAVIOUR_FORGET</item> <item>LOCAL_BEHAVIOUR_FORGET</item>
<item>LOCAL_BEHAVIOUR_MOVE</item> <item>LOCAL_BEHAVIOUR_MOVE</item>
<item>LOCAL_BEHAVIOUR_DELETE</item> <item>LOCAL_BEHAVIOUR_DELETE</item>
</string-array> </string-array>
<string-array name="pref_name_collision_policy_entries" translatable="false"> <string-array name="pref_name_collision_policy_entries" translatable="false">
<item>@string/pref_instant_name_collision_policy_entries_always_ask</item> <item>@string/pref_instant_name_collision_policy_entries_always_ask</item>
@ -29,4 +29,9 @@
<item>@string/link_share_view_only</item> <item>@string/link_share_view_only</item>
<item>@string/link_share_editing</item> <item>@string/link_share_editing</item>
</string-array> </string-array>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
</declare-styleable>
</resources> </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 Nextcloud Android client application
Copyright (C) 2012 Bartek Przybylski Copyright (C) 2012 Bartek Przybylski

View file

@ -955,8 +955,8 @@
<string name="dnd">Do not disturb</string> <string name="dnd">Do not disturb</string>
<string name="away">Away</string> <string name="away">Away</string>
<string name="invisible">Invisible</string> <string name="invisible">Invisible</string>
<string translatable="false" name="divider"></string> <string name="divider" translatable="false"></string>
<string translatable="false" name="default_emoji">😃</string> <string name="default_emoji" translatable="false">😃</string>
<string name="dontClear">Don\'t clear</string> <string name="dontClear">Don\'t clear</string>
<string name="today">Today</string> <string name="today">Today</string>
<string name="thirtyMinutes">30 minutes</string> <string name="thirtyMinutes">30 minutes</string>
@ -1041,4 +1041,15 @@
<string name="file_already_exists">Filename already exists</string> <string name="file_already_exists">Filename already exists</string>
<string name="filedetails_export">Export</string> <string name="filedetails_export">Export</string>
<string name="locate_folder">Locate folder</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> </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 ownCloud Android client application
Copyright (C) 2012 Bartek Przybylski Copyright (C) 2012 Bartek Przybylski
@ -101,8 +100,7 @@
<item name="android:buttonBarButtonStyle">@style/FallbackTheming.Dialog.ButtonStyle</item> <item name="android:buttonBarButtonStyle">@style/FallbackTheming.Dialog.ButtonStyle</item>
</style> </style>
<style name="FallbackTheming.Dialog.ButtonStyle" <style name="FallbackTheming.Dialog.ButtonStyle" parent="Widget.MaterialComponents.Button.TextButton.Dialog">
parent="Widget.MaterialComponents.Button.TextButton.Dialog">
<item name="android:textColor">@color/text_color</item> <item name="android:textColor">@color/text_color</item>
</style> </style>
@ -283,8 +281,7 @@
<item name="colorAccent">@color/color_accent</item> <item name="colorAccent">@color/color_accent</item>
</style> </style>
<style name="Theme.ownCloud.Widget.ActionBar" <style name="Theme.ownCloud.Widget.ActionBar" parent="@style/Theme.MaterialComponents.Light.DarkActionBar.Bridge">
parent="@style/Theme.MaterialComponents.Light.DarkActionBar.Bridge">
<item name="android:background">@color/primary</item> <item name="android:background">@color/primary</item>
<item name="background">@color/primary</item> <item name="background">@color/primary</item>
<item name="android:textColor">@color/text_color</item> <item name="android:textColor">@color/text_color</item>
@ -339,8 +336,8 @@
<item name="android:textSize">26sp</item> <item name="android:textSize">26sp</item>
<item name="android:textColor">@color/text_color</item> <item name="android:textColor">@color/text_color</item>
</style> </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"> <style name="Widget.App.Login.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="materialThemeOverlay">@style/ThemeOverlay.App.Login.TextInputLayout</item> <item name="materialThemeOverlay">@style/ThemeOverlay.App.Login.TextInputLayout</item>
@ -447,4 +444,8 @@
<item name="android:background">@drawable/ripple</item> <item name="android:background">@drawable/ripple</item>
</style> </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> </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.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.UserAccountManager;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -128,7 +128,7 @@ public class TestAppPreferences {
private SharedPreferences.Editor editor; private SharedPreferences.Editor editor;
@Mock @Mock
private CurrentAccountProvider accountProvider; private UserAccountManager userAccountManager;
private AppPreferencesImpl appPreferences; private AppPreferencesImpl appPreferences;
@ -137,7 +137,7 @@ public class TestAppPreferences {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
when(editor.remove(anyString())).thenReturn(editor); when(editor.remove(anyString())).thenReturn(editor);
when(sharedPreferences.edit()).thenReturn(editor); when(sharedPreferences.edit()).thenReturn(editor);
appPreferences = new AppPreferencesImpl(testContext, sharedPreferences, accountProvider); appPreferences = new AppPreferencesImpl(testContext, sharedPreferences, userAccountManager);
} }
@Test @Test

View file

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