mirror of
https://github.com/nextcloud/android.git
synced 2024-12-04 19:16:36 +01:00
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:
parent
423944137c
commit
efa886b455
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 |
139
app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt
Normal file
139
app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt
Normal 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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -40,9 +40,7 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
|
@ -151,6 +149,13 @@
|
|||
<activity
|
||||
android:name=".ui.activity.SyncedFoldersActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="com.nextcloud.client.widget.DashboardWidgetConfigurationActivity"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
|
||||
|
@ -158,6 +163,17 @@
|
|||
<receiver
|
||||
android:name="com.nextcloud.client.jobs.NotificationWork$NotificationReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name="com.nextcloud.client.widget.DashboardWidgetProvider"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/dashboard_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".ui.activity.UploadFilesActivity"
|
||||
|
@ -220,7 +236,6 @@
|
|||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".syncadapter.FileSyncService"
|
||||
android:exported="true"
|
||||
|
@ -233,6 +248,10 @@
|
|||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter_files" />
|
||||
</service>
|
||||
<service
|
||||
android:name="com.nextcloud.client.widget.DashboardWidgetService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
android:exported="true" />
|
||||
|
||||
<provider
|
||||
android:name=".providers.FileContentProvider"
|
||||
|
@ -304,16 +323,12 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/exposed_filepaths" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".providers.DiskLruImageCacheFileProvider"
|
||||
android:authorities="@string/image_cache_provider_authority"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:exported="true">
|
||||
</provider>
|
||||
|
||||
<!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"></provider> <!-- Disable WorkManager initialization. Whoever designed this, should pay closer attention -->
|
||||
<!-- to "best before" dates in his fridge. -->
|
||||
<!-- disable default provider -->
|
||||
<provider
|
||||
|
@ -327,8 +342,6 @@
|
|||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".authentication.AuthenticatorActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
|
@ -341,7 +354,6 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".authentication.DeepLinkLoginActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
|
@ -391,11 +403,9 @@
|
|||
<activity
|
||||
android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.nextcloud.client.logger.ui.LogsActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.nextcloud.client.errorhandling.ShowErrorActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
@ -465,7 +475,6 @@
|
|||
android:label="@string/manage_space_title"
|
||||
android:theme="@style/Theme.ownCloud" />
|
||||
|
||||
|
||||
<service
|
||||
android:name=".services.AccountManagerService"
|
||||
android:enabled="true"
|
||||
|
@ -476,12 +485,10 @@
|
|||
android:name=".ui.activity.SsoGrantPermissionActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
|
||||
|
||||
<activity
|
||||
android:name="com.nextcloud.client.etm.EtmActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.ownCloud.Toolbar" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.preview.PreviewBitmapActivity"
|
||||
android:exported="false"
|
||||
|
|
|
@ -29,6 +29,9 @@ import com.nextcloud.client.media.PlayerService;
|
|||
import com.nextcloud.client.migrations.Migrations;
|
||||
import com.nextcloud.client.onboarding.FirstRunActivity;
|
||||
import com.nextcloud.client.onboarding.WhatsNewActivity;
|
||||
import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity;
|
||||
import com.nextcloud.client.widget.DashboardWidgetProvider;
|
||||
import com.nextcloud.client.widget.DashboardWidgetService;
|
||||
import com.nextcloud.ui.ChooseAccountDialogFragment;
|
||||
import com.nextcloud.ui.SetStatusDialogFragment;
|
||||
import com.owncloud.android.MainApp;
|
||||
|
@ -102,8 +105,8 @@ import com.owncloud.android.ui.fragment.FileDetailFragment;
|
|||
import com.owncloud.android.ui.fragment.FileDetailSharingFragment;
|
||||
import com.owncloud.android.ui.fragment.GalleryFragment;
|
||||
import com.owncloud.android.ui.fragment.LocalFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialogFragment;
|
||||
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog;
|
||||
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialogFragment;
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment;
|
||||
import com.owncloud.android.ui.fragment.SharedListFragment;
|
||||
import com.owncloud.android.ui.fragment.UnifiedSearchFragment;
|
||||
|
@ -341,6 +344,9 @@ abstract class ComponentsModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract FileSyncService fileSyncService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetService dashboardWidgetService();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract PreviewPdfFragment previewPDFFragment();
|
||||
|
||||
|
@ -430,4 +436,10 @@ abstract class ComponentsModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract SyncFileNotEnoughSpaceDialogFragment syncFileNotEnoughSpaceDialogFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetConfigurationActivity dashboardWidgetConfigurationActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DashboardWidgetProvider dashboardWidgetProvider();
|
||||
}
|
||||
|
|
|
@ -37,11 +37,11 @@ import com.owncloud.android.lib.common.accounts.AccountUtils;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
class ClientFactoryImpl implements ClientFactory {
|
||||
public class ClientFactoryImpl implements ClientFactory {
|
||||
|
||||
private Context context;
|
||||
|
||||
ClientFactoryImpl(Context context) {
|
||||
public ClientFactoryImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@ class ClientFactoryImpl implements ClientFactory {
|
|||
public OwnCloudClient create(User user) throws CreationException {
|
||||
try {
|
||||
return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), context);
|
||||
} catch (OperationCanceledException|
|
||||
AuthenticatorException|
|
||||
} catch (OperationCanceledException |
|
||||
AuthenticatorException |
|
||||
IOException e) {
|
||||
throw new CreationException(e);
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.nextcloud.client.account.CurrentAccountProvider;
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
import com.nextcloud.client.account.UserAccountManagerImpl;
|
||||
import com.owncloud.android.datamodel.ArbitraryDataProvider;
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager;
|
||||
|
@ -45,15 +45,14 @@ import static com.owncloud.android.ui.fragment.OCFileListFragment.FOLDER_LAYOUT_
|
|||
|
||||
/**
|
||||
* Implementation of application-wide preferences using {@link SharedPreferences}.
|
||||
*
|
||||
* Users should not use this class directly. Please use {@link AppPreferences} interface
|
||||
* instead.
|
||||
* <p>
|
||||
* Users should not use this class directly. Please use {@link AppPreferences} interface instead.
|
||||
*/
|
||||
public final class AppPreferencesImpl implements AppPreferences {
|
||||
|
||||
/**
|
||||
* Constant to access value of last path selected by the user to upload a file shared from other app.
|
||||
* Value handled by the app without direct access in the UI.
|
||||
* Constant to access value of last path selected by the user to upload a file shared from other app. Value handled
|
||||
* by the app without direct access in the UI.
|
||||
*/
|
||||
public static final String AUTO_PREF__LAST_SEEN_VERSION_CODE = "lastSeenVersionCode";
|
||||
public static final String STORAGE_PATH = "storage_path";
|
||||
|
@ -101,7 +100,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
|
||||
private final Context context;
|
||||
private final SharedPreferences preferences;
|
||||
private final CurrentAccountProvider currentAccountProvider;
|
||||
private final UserAccountManager userAccountManager;
|
||||
private final ListenerRegistry listeners;
|
||||
|
||||
/**
|
||||
|
@ -123,7 +122,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
}
|
||||
}
|
||||
|
||||
void remove(@Nullable final Listener listener) {
|
||||
void remove(@Nullable final Listener listener) {
|
||||
if (listener != null) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
@ -133,7 +132,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PREF__DARK_THEME.equals(key)) {
|
||||
DarkMode mode = preferences.getDarkThemeMode();
|
||||
for(Listener l : listeners) {
|
||||
for (Listener l : listeners) {
|
||||
l.onDarkThemeModeChanged(mode);
|
||||
}
|
||||
}
|
||||
|
@ -141,9 +140,9 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
}
|
||||
|
||||
/**
|
||||
* This is a temporary workaround to access app preferences in places that cannot use
|
||||
* dependency injection yet. Use injected component via {@link AppPreferences} interface.
|
||||
*
|
||||
* This is a temporary workaround to access app preferences in places that cannot use dependency injection yet. Use
|
||||
* injected component via {@link AppPreferences} interface.
|
||||
* <p>
|
||||
* WARNING: this creates new instance! it does not return app-wide singleton
|
||||
*
|
||||
* @param context Context used to create shared preferences
|
||||
|
@ -151,15 +150,15 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
*/
|
||||
@Deprecated
|
||||
public static AppPreferences fromContext(Context context) {
|
||||
final CurrentAccountProvider currentAccountProvider = UserAccountManagerImpl.fromContext(context);
|
||||
final UserAccountManager userAccountManager = UserAccountManagerImpl.fromContext(context);
|
||||
final SharedPreferences prefs = android.preference.PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return new AppPreferencesImpl(context, prefs, currentAccountProvider);
|
||||
return new AppPreferencesImpl(context, prefs, userAccountManager);
|
||||
}
|
||||
|
||||
AppPreferencesImpl(Context appContext, SharedPreferences preferences, CurrentAccountProvider currentAccountProvider) {
|
||||
AppPreferencesImpl(Context appContext, SharedPreferences preferences, UserAccountManager userAccountManager) {
|
||||
this.context = appContext;
|
||||
this.preferences = preferences;
|
||||
this.currentAccountProvider = currentAccountProvider;
|
||||
this.userAccountManager = userAccountManager;
|
||||
this.listeners = new ListenerRegistry(this);
|
||||
this.preferences.registerOnSharedPreferenceChangeListener(listeners);
|
||||
}
|
||||
|
@ -277,7 +276,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
|
||||
@Override
|
||||
public String[] getPassCode() {
|
||||
return new String[] {
|
||||
return new String[]{
|
||||
preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D1, null),
|
||||
preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D2, null),
|
||||
preferences.getString(PassCodeActivity.PREFERENCE_PASSCODE_D3, null),
|
||||
|
@ -293,7 +292,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
@Override
|
||||
public String getFolderLayout(OCFile folder) {
|
||||
return getFolderPreference(context,
|
||||
currentAccountProvider.getUser(),
|
||||
userAccountManager.getUser(),
|
||||
PREF__FOLDER_LAYOUT,
|
||||
folder,
|
||||
FOLDER_LAYOUT_LIST);
|
||||
|
@ -302,7 +301,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
@Override
|
||||
public void setFolderLayout(@Nullable OCFile folder, String layoutName) {
|
||||
setFolderPreference(context,
|
||||
currentAccountProvider.getUser(),
|
||||
userAccountManager.getUser(),
|
||||
PREF__FOLDER_LAYOUT,
|
||||
folder,
|
||||
layoutName);
|
||||
|
@ -311,7 +310,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
@Override
|
||||
public FileSortOrder getSortOrderByFolder(OCFile folder) {
|
||||
return FileSortOrder.sortOrders.get(getFolderPreference(context,
|
||||
currentAccountProvider.getUser(),
|
||||
userAccountManager.getUser(),
|
||||
PREF__FOLDER_SORT_ORDER,
|
||||
folder,
|
||||
FileSortOrder.sort_a_to_z.name));
|
||||
|
@ -320,7 +319,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
@Override
|
||||
public void setSortOrder(@Nullable OCFile folder, FileSortOrder sortOrder) {
|
||||
setFolderPreference(context,
|
||||
currentAccountProvider.getUser(),
|
||||
userAccountManager.getUser(),
|
||||
PREF__FOLDER_SORT_ORDER,
|
||||
folder,
|
||||
sortOrder.name);
|
||||
|
@ -333,7 +332,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
|
||||
@Override
|
||||
public FileSortOrder getSortOrderByType(FileSortOrder.Type type, FileSortOrder defaultOrder) {
|
||||
User user = currentAccountProvider.getUser();
|
||||
User user = userAccountManager.getUser();
|
||||
if (user.isAnonymous()) {
|
||||
return defaultOrder;
|
||||
}
|
||||
|
@ -347,7 +346,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
|
||||
@Override
|
||||
public void setSortOrder(FileSortOrder.Type type, FileSortOrder sortOrder) {
|
||||
User user = currentAccountProvider.getUser();
|
||||
User user = userAccountManager.getUser();
|
||||
ArbitraryDataProvider dataProvider = new ArbitraryDataProvider(context.getContentResolver());
|
||||
dataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREF__FOLDER_SORT_ORDER + "_" + type, sortOrder.name);
|
||||
}
|
||||
|
@ -506,19 +505,19 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
@Override
|
||||
public void removeLegacyPreferences() {
|
||||
preferences.edit()
|
||||
.remove("instant_uploading")
|
||||
.remove("instant_video_uploading")
|
||||
.remove("instant_upload_path")
|
||||
.remove("instant_upload_path_use_subfolders")
|
||||
.remove("instant_upload_on_wifi")
|
||||
.remove("instant_upload_on_charging")
|
||||
.remove("instant_video_upload_path")
|
||||
.remove("instant_video_upload_path_use_subfolders")
|
||||
.remove("instant_video_upload_on_wifi")
|
||||
.remove("instant_video_uploading")
|
||||
.remove("instant_video_upload_on_charging")
|
||||
.remove("prefs_instant_behaviour")
|
||||
.apply();
|
||||
.remove("instant_uploading")
|
||||
.remove("instant_video_uploading")
|
||||
.remove("instant_upload_path")
|
||||
.remove("instant_upload_path_use_subfolders")
|
||||
.remove("instant_upload_on_wifi")
|
||||
.remove("instant_upload_on_charging")
|
||||
.remove("instant_video_upload_path")
|
||||
.remove("instant_video_upload_path_use_subfolders")
|
||||
.remove("instant_video_upload_on_wifi")
|
||||
.remove("instant_video_uploading")
|
||||
.remove("instant_video_upload_on_charging")
|
||||
.remove("prefs_instant_behaviour")
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -588,13 +587,12 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get preference value for a folder.
|
||||
* If folder is not set itself, it finds an ancestor that is set.
|
||||
* Get preference value for a folder. If folder is not set itself, it finds an ancestor that is set.
|
||||
*
|
||||
* @param context Context object.
|
||||
* @param context Context object.
|
||||
* @param preferenceName Name of the preference to lookup.
|
||||
* @param folder Folder.
|
||||
* @param defaultValue Fallback value in case no ancestor is set.
|
||||
* @param folder Folder.
|
||||
* @param defaultValue Fallback value in case no ancestor is set.
|
||||
* @return Preference value
|
||||
*/
|
||||
private static String getFolderPreference(final Context context,
|
||||
|
@ -621,10 +619,10 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
/**
|
||||
* Set preference value for a folder.
|
||||
*
|
||||
* @param context Context object.
|
||||
* @param context Context object.
|
||||
* @param preferenceName Name of the preference to set.
|
||||
* @param folder Folder.
|
||||
* @param value Preference value to set.
|
||||
* @param folder Folder.
|
||||
* @param value Preference value to set.
|
||||
*/
|
||||
private static void setFolderPreference(final Context context,
|
||||
final User user,
|
||||
|
@ -637,7 +635,7 @@ public final class AppPreferencesImpl implements AppPreferences {
|
|||
|
||||
private static String getKeyFromFolder(String preferenceName, @Nullable OCFile folder) {
|
||||
final String folderIdString = String.valueOf(folder != null ? folder.getFileId() :
|
||||
FileDataStorageManager.ROOT_PARENT_ID);
|
||||
FileDataStorageManager.ROOT_PARENT_ID);
|
||||
|
||||
return preferenceName + "_" + folderIdString;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.nextcloud.client.preferences;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.nextcloud.client.account.CurrentAccountProvider;
|
||||
import com.nextcloud.client.account.UserAccountManager;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
|
@ -23,7 +23,7 @@ public class PreferencesModule {
|
|||
@Singleton
|
||||
public AppPreferences appPreferences(Context context,
|
||||
SharedPreferences sharedPreferences,
|
||||
CurrentAccountProvider currentAccountProvider) {
|
||||
return new AppPreferencesImpl(context, sharedPreferences, currentAccountProvider);
|
||||
UserAccountManager userAccountManager) {
|
||||
return new AppPreferencesImpl(context, sharedPreferences, userAccountManager);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|