Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 3c99532d authored by Andy Scherzinger's avatar Andy Scherzinger Committed by GitHub
Browse files

Merge pull request #2741 from nextcloud/backport/2737/stable-4.4

[stable-4.4] BugFix - Widget Crash
parents e6ecb5dd 58c9dfb8
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -174,11 +174,13 @@

        <service
            android:name=".widget.singlenote.SingleNoteWidgetService"
            android:permission="android.permission.BIND_REMOTEVIEWS" />
            android:permission="android.permission.BIND_REMOTEVIEWS"
            android:exported="false"/>

        <service
            android:name=".widget.notelist.NoteListWidgetService"
            android:permission="android.permission.BIND_REMOTEVIEWS" />
            android:permission="android.permission.BIND_REMOTEVIEWS"
            android:exported="false"/>

        <service
            android:name=".quicksettings.NewNoteTileService"
+0 −118
Original line number Diff line number Diff line
/*
 * Nextcloud Notes - Android Client
 *
 * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.niedermann.owncloud.notes.widget.notelist;

import static it.niedermann.owncloud.notes.shared.util.WidgetUtil.pendingIntentFlagCompat;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;

import java.util.NoSuchElementException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.persistence.NotesRepository;

public class NoteListWidget extends AppWidgetProvider {
    private static final String TAG = NoteListWidget.class.getSimpleName();
    private final ExecutorService executor = Executors.newCachedThreadPool();

    static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds) {
        final var repo = NotesRepository.getInstance(context);

        RemoteViews views;

        for (int appWidgetId : appWidgetIds) {
            try {
                final var data = repo.getNoteListWidgetData(appWidgetId);

                final var serviceIntent = new Intent(context, NoteListWidgetService.class);
                serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
                serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));

                Log.v(TAG, "-- data - " + data);

                Intent editNoteIntent = new Intent(context, EditNoteActivity.class);
                editNoteIntent.setPackage(context.getPackageName());

                int pendingIntentFlags = pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT | Intent.FILL_IN_COMPONENT);
                PendingIntent editNotePendingIntent = PendingIntent.getActivity(context, 0, editNoteIntent, pendingIntentFlags);

                views = new RemoteViews(context.getPackageName(), R.layout.widget_note_list);
                views.setRemoteAdapter(R.id.note_list_widget_lv, serviceIntent);
                views.setPendingIntentTemplate(R.id.note_list_widget_lv, editNotePendingIntent);
                views.setEmptyView(R.id.note_list_widget_lv, R.id.widget_note_list_placeholder_tv);

                awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv);
                awm.updateAppWidget(appWidgetId, views);
            } catch (NoSuchElementException e) {
                Log.i(TAG, "onUpdate has been triggered before the user finished configuring the widget");
            }
        }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        updateAppWidget(context, appWidgetManager, appWidgetIds);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        final var awm = AppWidgetManager.getInstance(context);

        if (intent.getAction() == null) {
            Log.w(TAG, "Intent action is null");
            return;
        }

        if (!intent.getAction().equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE)) {
            Log.w(TAG, "Intent action is not ACTION_APPWIDGET_UPDATE");
            return;
        }

        if (!intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
            Log.w(TAG,"Update widget via default appWidgetIds");
            updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, NoteListWidget.class)));
        }

        if (intent.getExtras() == null) {
            Log.w(TAG, "Intent doesn't have bundle");
            return;
        }

        Log.w(TAG,"Update widget via given appWidgetIds");
        updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)});
    }

    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
        final var repo = NotesRepository.getInstance(context);

        for (final int appWidgetId : appWidgetIds) {
            executor.submit(() -> repo.removeNoteListWidget(appWidgetId));
        }
    }

    /**
     * Update note list widgets, if the note data was changed.
     */
    public static void updateNoteListWidgets(Context context) {
        context.sendBroadcast(new Intent(context, NoteListWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
    }
}
+135 −0
Original line number Diff line number Diff line
/*
 * Nextcloud Notes - Android Client
 *
 * SPDX-FileCopyrightText: 2017-2025 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.niedermann.owncloud.notes.widget.notelist

import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.RemoteViews
import com.owncloud.android.lib.common.utils.Log_OC
import it.niedermann.owncloud.notes.R
import it.niedermann.owncloud.notes.edit.EditNoteActivity
import it.niedermann.owncloud.notes.persistence.NotesRepository
import it.niedermann.owncloud.notes.shared.util.WidgetUtil
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import androidx.core.net.toUri

class NoteListWidget : AppWidgetProvider() {
    private val executor: ExecutorService = Executors.newCachedThreadPool()

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        updateAppWidget(context, appWidgetManager, appWidgetIds)
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)
        val awm = AppWidgetManager.getInstance(context)

        if (intent.action == null) {
            Log.w(TAG, "Intent action is null")
            return
        }

        if (intent.action != AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
            Log.w(TAG, "Intent action is not ACTION_APPWIDGET_UPDATE")
            return
        }

        if (!intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
            Log.w(TAG, "Update widget via default appWidgetIds")
            updateAppWidget(
                context,
                awm,
                awm.getAppWidgetIds(ComponentName(context, NoteListWidget::class.java))
            )
        }

        if (intent.extras == null) {
            Log.w(TAG, "Intent doesn't have bundle")
            return
        }

        Log.w(TAG, "Update widget via given appWidgetIds")

        val appWidgetIds = intArrayOf(intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) ?: -1)

        updateAppWidget(
            context,
            awm,
            appWidgetIds
        )
    }

    override fun onDeleted(context: Context, appWidgetIds: IntArray) {
        super.onDeleted(context, appWidgetIds)
        val repo = NotesRepository.getInstance(context)

        for (appWidgetId in appWidgetIds) {
            executor.submit(Runnable { repo.removeNoteListWidget(appWidgetId) })
        }
    }

    companion object {
        private val TAG: String = NoteListWidget::class.java.getSimpleName()
        fun updateAppWidget(context: Context, awm: AppWidgetManager, appWidgetIds: IntArray) {
            val repo = NotesRepository.getInstance(context)
            appWidgetIds.forEach { appWidgetId ->
                repo.getNoteListWidgetData(appWidgetId)?.let { data ->
                    val serviceIntent = Intent(context, NoteListWidgetService::class.java).apply {
                        putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
                        setData(toUri(Intent.URI_INTENT_SCHEME).toUri())
                    }


                    Log.v(TAG, "-- data - $data")

                    val editNoteIntent = Intent(context, EditNoteActivity::class.java).apply {
                        setPackage(context.packageName)
                    }

                    val pendingIntentFlags =
                        WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT or Intent.FILL_IN_COMPONENT)
                    val editNotePendingIntent =
                        PendingIntent.getActivity(context, 0, editNoteIntent, pendingIntentFlags)

                    val views = RemoteViews(context.packageName, R.layout.widget_note_list).apply {
                        setRemoteAdapter(R.id.note_list_widget_lv, serviceIntent)
                        setPendingIntentTemplate(R.id.note_list_widget_lv, editNotePendingIntent)
                        setEmptyView(
                            R.id.note_list_widget_lv,
                            R.id.widget_note_list_placeholder_tv
                        )
                    }

                    awm.run {
                        updateAppWidget(appWidgetId, views)
                        notifyAppWidgetViewDataChanged(appWidgetId, R.id.note_list_widget_lv)
                    }
                }
            }
        }

        @JvmStatic
        fun updateNoteListWidgets(context: Context) {
            val intent = Intent(context, NoteListWidget::class.java).apply {
                setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
            }
            context.sendBroadcast(intent)
        }
    }
}
+0 −207
Original line number Diff line number Diff line
/*
 * Nextcloud Notes - Android Client
 *
 * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.niedermann.owncloud.notes.widget.notelist;

import static it.niedermann.owncloud.notes.edit.EditNoteActivity.PARAM_CATEGORY;
import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_ALL;
import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_CATEGORY;
import static it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData.MODE_DISPLAY_STARRED;

import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;

import java.util.ArrayList;
import java.util.List;

import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.edit.EditNoteActivity;
import it.niedermann.owncloud.notes.main.MainActivity;
import it.niedermann.owncloud.notes.persistence.NotesRepository;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import it.niedermann.owncloud.notes.shared.util.NotesColorUtil;

public class NoteListWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
    private static final String TAG = NoteListWidgetFactory.class.getSimpleName();

    private final Context context;
    private final int appWidgetId;
    private final NotesRepository repo;
    @NonNull
    private final List<Note> dbNotes = new ArrayList<>();
    private NotesListWidgetData data;

    NoteListWidgetFactory(Context context, Intent intent) {
        this.context = context;
        this.appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        repo = NotesRepository.getInstance(context);
    }

    @Override
    public void onCreate() {
        // Nothing to do here…
    }

    @Override
    public void onDataSetChanged() {
        dbNotes.clear();
        try {
            data = repo.getNoteListWidgetData(appWidgetId);
            if (data == null) {
                Log.w(TAG, "Widget data is null");
                return;
            }

            Log.v(TAG, "--- data - " + data);
            switch (data.getMode()) {
                case MODE_DISPLAY_ALL ->
                        dbNotes.addAll(repo.searchRecentByModified(data.getAccountId(), "%"));
                case MODE_DISPLAY_STARRED ->
                        dbNotes.addAll(repo.searchFavoritesByModified(data.getAccountId(), "%"));
                default -> {
                    if (data.getCategory() != null) {
                        dbNotes.addAll(repo.searchCategoryByModified(data.getAccountId(), "%", data.getCategory()));
                    } else {
                        dbNotes.addAll(repo.searchUncategorizedByModified(data.getAccountId(), "%"));
                    }
                }
            }
        } catch (Exception e) {
            Log.w(TAG, "Error caught at onDataSetChanged: " + e);
        }
    }

    @Override
    public void onDestroy() {
        //NoOp
    }

    @Override
    public int getCount() {
        return dbNotes.size() + 1;
    }

    private Intent getEditNoteIntent(Bundle bundle) {
        final Intent intent = new Intent(context, EditNoteActivity.class);
        intent.setPackage(context.getPackageName());
        intent.putExtras(bundle);
        intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));

        return intent;
    }

    private Intent getCreateNoteIntent(Account localAccount ) {
        final Bundle bundle = new Bundle();
        bundle.putSerializable(PARAM_CATEGORY, data.getMode() == MODE_DISPLAY_STARRED ? new NavigationCategory(ENavigationCategoryType.FAVORITES) : new NavigationCategory(localAccount.getId(), data.getCategory()));
        bundle.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, data.getAccountId());

        return getEditNoteIntent(bundle);
    }

    private Intent getOpenNoteIntent(Note note) {
        final Bundle bundle = new Bundle();
        bundle.putLong(EditNoteActivity.PARAM_NOTE_ID, note.getId());
        bundle.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.getAccountId());

        return getEditNoteIntent(bundle);
    }

    @Override
    public RemoteViews getViewAt(int position) {
        final RemoteViews note_content;

        if (position == 0) {
            final Account localAccount = repo.getAccountById(data.getAccountId());

            final Intent createNoteIntent = getCreateNoteIntent(localAccount);
            final Intent openIntent = new Intent(Intent.ACTION_MAIN).setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName()));

            note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry_add);
            note_content.setOnClickFillInIntent(R.id.widget_entry_content_tv, openIntent);
            note_content.setOnClickFillInIntent(R.id.widget_entry_fav_icon, createNoteIntent);
            note_content.setTextViewText(R.id.widget_entry_content_tv, getCategoryTitle(context, data.getMode(), data.getCategory()));
            note_content.setImageViewResource(R.id.widget_entry_fav_icon, R.drawable.ic_add_blue_24dp);
            note_content.setInt(R.id.widget_entry_fav_icon, "setColorFilter", NotesColorUtil.contrastRatioIsSufficient(ContextCompat.getColor(context, R.color.widget_background), localAccount.getColor())
                    ? localAccount.getColor()
                    : ContextCompat.getColor(context, R.color.widget_foreground));
        } else {
            position--;
            if (position > dbNotes.size() - 1 || dbNotes.get(position) == null) {
                Log.e(TAG, "Could not find position \"" + position + "\" in dbNotes list.");
                return null;
            }

            final Note note = dbNotes.get(position);
            final Intent openNoteIntent = getOpenNoteIntent(note);

            note_content = new RemoteViews(context.getPackageName(), R.layout.widget_entry);
            note_content.setOnClickFillInIntent(R.id.widget_note_list_entry, openNoteIntent);
            note_content.setTextViewText(R.id.widget_entry_content_tv, note.getTitle());
            note_content.setImageViewResource(R.id.widget_entry_fav_icon, note.getFavorite()
                    ? R.drawable.ic_star_yellow_24dp
                    : R.drawable.ic_star_grey_ccc_24dp);
        }

        return note_content;

    }

    @NonNull
    private static String getCategoryTitle(@NonNull Context context, int displayMode, String category) {
        return switch (displayMode) {
            case MODE_DISPLAY_STARRED ->
                    context.getString(R.string.label_favorites);
            case MODE_DISPLAY_CATEGORY ->
                    "".equals(category)
                        ? context.getString(R.string.action_uncategorized)
                        : category;
            default -> context.getString(R.string.app_name);
        };
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public long getItemId(int position) {
        if (position == 0) {
            return -1;
        } else {
            position--;
            if (position > dbNotes.size() - 1 || dbNotes.get(position) == null) {
                Log.e(TAG, "Could not find position \"" + position + "\" in dbNotes list.");
                return -2;
            }
            return dbNotes.get(position).getId();
        }
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }
}
+181 −0
Original line number Diff line number Diff line
/*
 * Nextcloud Notes - Android Client
 *
 * SPDX-FileCopyrightText: 2017-2025 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: GPL-3.0-or-later
 */
package it.niedermann.owncloud.notes.widget.notelist

import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import androidx.core.net.toUri
import it.niedermann.owncloud.notes.R
import it.niedermann.owncloud.notes.edit.EditNoteActivity
import it.niedermann.owncloud.notes.persistence.NotesRepository
import it.niedermann.owncloud.notes.persistence.entity.Account
import it.niedermann.owncloud.notes.persistence.entity.Note
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType
import it.niedermann.owncloud.notes.shared.model.NavigationCategory

class NoteListWidgetFactory internal constructor(private val context: Context, intent: Intent) :
    RemoteViewsFactory {
    private val appWidgetId: Int = intent.getIntExtra(
        AppWidgetManager.EXTRA_APPWIDGET_ID,
        AppWidgetManager.INVALID_APPWIDGET_ID
    )
    private val repo: NotesRepository = NotesRepository.getInstance(context)
    private val dbNotes: MutableList<Note> = ArrayList()
    private var data: NotesListWidgetData? = null

    override fun onCreate() = Unit

    override fun onDataSetChanged() {
        dbNotes.clear()
        try {
            data = repo.getNoteListWidgetData(appWidgetId)
            if (data == null) {
                Log.w(TAG, "Widget data is null")
                return
            }
            val widgetData = data ?: return

            Log.v(TAG, "--- data - $widgetData")

            when (widgetData.mode) {
                NotesListWidgetData.MODE_DISPLAY_ALL -> dbNotes.addAll(
                    repo.searchRecentByModified(
                        widgetData.accountId, "%"
                    )
                )

                NotesListWidgetData.MODE_DISPLAY_STARRED -> dbNotes.addAll(
                    repo.searchFavoritesByModified(
                        widgetData.accountId, "%"
                    )
                )

                else -> {
                    if (widgetData.category != null) {
                        dbNotes.addAll(
                            repo.searchCategoryByModified(
                                widgetData.accountId,
                                "%",
                                widgetData.category
                            )
                        )
                    } else {
                        dbNotes.addAll(
                            repo.searchUncategorizedByModified(
                                widgetData.accountId,
                                "%"
                            )
                        )
                    }
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "Error caught at onDataSetChanged: $e")
        }
    }

    override fun onDestroy() = Unit

    override fun getCount(): Int {
        return dbNotes.size
    }

    private fun getEditNoteIntent(bundle: Bundle): Intent {
        return Intent(context, EditNoteActivity::class.java).apply {
            setPackage(context.packageName)
            putExtras(bundle)
            setData(toUri(Intent.URI_INTENT_SCHEME).toUri())
        }
    }

    private fun getCreateNoteIntent(localAccount: Account): Intent {
        val bundle = Bundle()

        data?.let {
            val navigationCategory = if (it.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) NavigationCategory(
                ENavigationCategoryType.FAVORITES
            ) else NavigationCategory(localAccount.id, it.category)

            bundle.putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
            bundle.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, it.accountId)
        }

        return getEditNoteIntent(bundle)
    }

    private fun getOpenNoteIntent(note: Note): Intent {
        val bundle = Bundle().apply {
            putLong(EditNoteActivity.PARAM_NOTE_ID, note.id)
            putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.accountId)
        }

        return getEditNoteIntent(bundle)
    }

    override fun getViewAt(position: Int): RemoteViews? {
        val note = dbNotes.getOrNull(position) ?: return null

        val openNoteIntent = getOpenNoteIntent(note)

        var createNoteIntent: Intent? = null
        data?.let {
            val localAccount =  repo.getAccountById(it.accountId)
            createNoteIntent = getCreateNoteIntent(localAccount)
        }

        return RemoteViews(context.packageName, R.layout.widget_entry).apply {
            setOnClickFillInIntent(R.id.widget_note_list_entry, openNoteIntent)

            createNoteIntent?.let {
                setOnClickFillInIntent(R.id.widget_entry_fav_icon, createNoteIntent)
            }

            setTextViewText(R.id.widget_entry_title, note.title)

            if (note.category.isEmpty()) {
                setViewVisibility(R.id.widget_entry_category, View.GONE)
            } else {
                setViewVisibility(R.id.widget_entry_category, View.VISIBLE)
                setTextViewText(R.id.widget_entry_category, note.category)
            }

            val starIconId = if (note.favorite) {
                R.drawable.ic_star_yellow_24dp
            } else {
                R.drawable.ic_star_grey_ccc_24dp
            }
            setImageViewResource(R.id.widget_entry_fav_icon, starIconId)
        }
    }

    override fun getLoadingView(): RemoteViews? {
        return null
    }

    override fun getViewTypeCount(): Int {
        return 2
    }

    override fun getItemId(position: Int): Long {
        return dbNotes[position].id
    }

    override fun hasStableIds(): Boolean {
        return true
    }

    companion object {
        private val TAG: String = NoteListWidgetFactory::class.java.getSimpleName()
    }
}
Loading