diff --git a/FAQ.md b/FAQ.md index fd64dec087..4301e97ace 100644 --- a/FAQ.md +++ b/FAQ.md @@ -54,7 +54,6 @@ Anything on this list is in random order and *might* be added in the near future ## Frequently requested features * *Design*: the design is based on many discussions and if you like you can discuss about it [in this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168) too. See below for the design goals. -* *Widget to read messages*: widgets can have limited user interaction only, so a widget to read conversations would not be very convenient. Moreover, it would be not very useful to duplicate functions which are already available in the app. * *ActiveSync*: using the Exchange ActiveSync protocol requires [a license](https://en.wikipedia.org/wiki/Exchange_ActiveSync#Licensing), so this cannot be added. The goal of the design is to be minimalistic (no unnecessary menus, buttons, etc) and non distracting (no fancy colors, animations, etc). diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ba4be76b01..ca45260399 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -255,6 +255,23 @@ android:resource="@xml/widget" /> + + + + + + + + + + + diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index f0f443a683..c46f18bed0 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -102,6 +102,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB static final int REQUEST_OUTBOX = 4; static final int REQUEST_ERROR = 5; static final int REQUEST_UPDATE = 6; + static final int REQUEST_WIDGET = 7; static final String ACTION_VIEW_FOLDERS = BuildConfig.APPLICATION_ID + ".VIEW_FOLDERS"; static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES"; @@ -506,7 +507,8 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB else if (action.startsWith("thread")) { intent.putExtra("thread", action.split(":", 2)[1]); onViewThread(intent); - } + } else if (action.equals("widget")) + onViewThread(intent); } if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) { diff --git a/app/src/main/java/eu/faircode/email/DaoMessage.java b/app/src/main/java/eu/faircode/email/DaoMessage.java index 86dd6207fe..5e783b28b7 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -273,6 +273,17 @@ public interface DaoMessage { " ORDER BY message.received") LiveData> liveUnseenNotify(); + @Query("SELECT message.*" + + " FROM message" + + " JOIN account ON account.id = message.account" + + " JOIN folder ON folder.id = message.folder" + + " WHERE account.`synchronize`" + + " AND folder.unified" + + " AND message.ui_hide = 0" + + " AND message.ui_snoozed IS NULL" + + " ORDER BY message.received DESC") + LiveData> liveWidgetUnified(); + @Query("SELECT COUNT(message.id) FROM message" + " JOIN account ON account.id = message.account" + " JOIN folder ON folder.id = message.folder" + diff --git a/app/src/main/java/eu/faircode/email/WidgetUnified.java b/app/src/main/java/eu/faircode/email/WidgetUnified.java new file mode 100644 index 0000000000..d79996ec67 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/WidgetUnified.java @@ -0,0 +1,67 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2019 by Marcel Bokhorst (M66B) +*/ + +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.widget.RemoteViews; + +public class WidgetUnified extends AppWidgetProvider { + @Override + public void onUpdate(final Context context, final AppWidgetManager appWidgetManager, final int[] appWidgetIds) { + Intent view = new Intent(context, ActivityView.class); + view.setAction("unified"); + view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent pi = PendingIntent.getActivity(context, ActivityView.REQUEST_UNIFIED, view, PendingIntent.FLAG_UPDATE_CURRENT); + + for (int id : appWidgetIds) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_unified); + + views.setOnClickPendingIntent(R.id.title, pi); + + Intent service = new Intent(context, WidgetUnifiedService.class); + service.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id); + service.setData(Uri.parse(service.toUri(Intent.URI_INTENT_SCHEME))); + + views.setRemoteAdapter(R.id.lv, service); + + Intent thread = new Intent(context, ActivityView.class); + thread.setAction("widget"); + thread.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent piItem = PendingIntent.getActivity( + context, ActivityView.REQUEST_WIDGET, thread, PendingIntent.FLAG_UPDATE_CURRENT); + + views.setPendingIntentTemplate(R.id.lv, piItem); + + appWidgetManager.updateAppWidget(id, views); + } + } + + static void update(Context context) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetUnified.class)); + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.lv); + } +} diff --git a/app/src/main/java/eu/faircode/email/WidgetUnifiedRemoteViewsFactory.java b/app/src/main/java/eu/faircode/email/WidgetUnifiedRemoteViewsFactory.java new file mode 100644 index 0000000000..542634a907 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/WidgetUnifiedRemoteViewsFactory.java @@ -0,0 +1,173 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.os.Handler; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.StyleSpan; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.lifecycle.Observer; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static android.os.Looper.getMainLooper; + +public class WidgetUnifiedRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory { + private Context context; + private DateFormat DTF; + + private Handler handler; + private TwoStateOwner owner; + private List messages = new ArrayList<>(); + + WidgetUnifiedRemoteViewsFactory(final Context context) { + this.context = context; + this.DTF = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT); + + this.handler = new Handler(getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + DB db = DB.getInstance(context); + owner = new TwoStateOwner("WidgetUnified"); + db.message().liveWidgetUnified().observe(owner, new Observer>() { + @Override + public void onChanged(List messages) { + if (messages == null) + messages = new ArrayList<>(); + + boolean changed = false; + if (WidgetUnifiedRemoteViewsFactory.this.messages.size() == messages.size()) { + for (int i = 0; i < messages.size(); i++) { + EntityMessage m1 = messages.get(i); + EntityMessage m2 = WidgetUnifiedRemoteViewsFactory.this.messages.get(i); + if (!m1.id.equals(m2.id) || + !MessageHelper.equal(m1.from, m2.from) || + !m1.received.equals(m2.received) || + !Objects.equals(m1.subject, m2.subject) || + m1.ui_seen != m2.ui_seen) { + changed = true; + break; + } + } + } else + changed = true; + + WidgetUnifiedRemoteViewsFactory.this.messages = messages; + + if (changed) + WidgetUnified.update(context); + } + }); + } + }); + } + + @Override + public void onCreate() { + Log.i("Widget factory create"); + handler.post(new Runnable() { + @Override + public void run() { + owner.start(); + } + }); + } + + @Override + public void onDataSetChanged() { + } + + @Override + public void onDestroy() { + Log.i("Widget factory destroy"); + handler.post(new Runnable() { + @Override + public void run() { + owner.destroy(); + } + }); + } + + @Override + public int getCount() { + return messages.size(); + } + + @Override + public RemoteViews getViewAt(int position) { + EntityMessage message = messages.get(position); + + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.item_widget_unified); + + Intent thread = new Intent(context, ActivityView.class); + thread.putExtra("account", message.account); + thread.putExtra("thread", message.thread); + thread.putExtra("id", message.id); + views.setOnClickFillInIntent(R.id.llMessage, thread); + + SpannableString from = new SpannableString(MessageHelper.formatAddressesShort(message.from)); + SpannableString time = new SpannableString(DTF.format(message.received)); + SpannableString subject = new SpannableString(TextUtils.isEmpty(message.subject) ? "" : message.subject); + + if (!message.ui_seen) { + from.setSpan(new StyleSpan(Typeface.BOLD), 0, from.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + time.setSpan(new StyleSpan(Typeface.BOLD), 0, time.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + subject.setSpan(new StyleSpan(Typeface.BOLD), 0, subject.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + views.setTextViewText(R.id.tvFrom, from); + views.setTextViewText(R.id.tvTime, time); + views.setTextViewText(R.id.tvSubject, subject); + + return views; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int position) { + return messages.get(position).id; + } + + @Override + public boolean hasStableIds() { + return true; + } +} diff --git a/app/src/main/java/eu/faircode/email/WidgetUnifiedService.java b/app/src/main/java/eu/faircode/email/WidgetUnifiedService.java new file mode 100644 index 0000000000..6e3ed7fcce --- /dev/null +++ b/app/src/main/java/eu/faircode/email/WidgetUnifiedService.java @@ -0,0 +1,30 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class WidgetUnifiedService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new WidgetUnifiedRemoteViewsFactory(this.getApplicationContext()); + } +} diff --git a/app/src/main/res/layout/item_widget_unified.xml b/app/src/main/res/layout/item_widget_unified.xml new file mode 100644 index 0000000000..f627701bcd --- /dev/null +++ b/app/src/main/res/layout/item_widget_unified.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_unified.xml b/app/src/main/res/layout/widget_unified.xml new file mode 100644 index 0000000000..70feb2af92 --- /dev/null +++ b/app/src/main/res/layout/widget_unified.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/widget_unified.xml b/app/src/main/res/xml/widget_unified.xml new file mode 100644 index 0000000000..f53a376d9a --- /dev/null +++ b/app/src/main/res/xml/widget_unified.xml @@ -0,0 +1,8 @@ + +