Added unified inbox widget

pull/159/head
M66B 5 years ago
parent e0d35792b2
commit e046e0f7cf

@ -54,7 +54,6 @@ Anything on this list is in random order and *might* be added in the near future
## Frequently requested features ## 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. * *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. * *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). The goal of the design is to be minimalistic (no unnecessary menus, buttons, etc) and non distracting (no fancy colors, animations, etc).

@ -255,6 +255,23 @@
android:resource="@xml/widget" /> android:resource="@xml/widget" />
</receiver> </receiver>
<receiver
android:name=".WidgetUnified"
android:label="@string/title_folder_unified">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_unified" />
</receiver>
<service
android:name="WidgetUnifiedService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver android:name=".ReceiverAutoStart"> <receiver android:name=".ReceiverAutoStart">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />

@ -102,6 +102,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
static final int REQUEST_OUTBOX = 4; static final int REQUEST_OUTBOX = 4;
static final int REQUEST_ERROR = 5; static final int REQUEST_ERROR = 5;
static final int REQUEST_UPDATE = 6; 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_FOLDERS = BuildConfig.APPLICATION_ID + ".VIEW_FOLDERS";
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES"; 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")) { else if (action.startsWith("thread")) {
intent.putExtra("thread", action.split(":", 2)[1]); intent.putExtra("thread", action.split(":", 2)[1]);
onViewThread(intent); onViewThread(intent);
} } else if (action.equals("widget"))
onViewThread(intent);
} }
if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) { if (intent.hasExtra(Intent.EXTRA_PROCESS_TEXT)) {

@ -273,6 +273,17 @@ public interface DaoMessage {
" ORDER BY message.received") " ORDER BY message.received")
LiveData<List<TupleMessageEx>> liveUnseenNotify(); LiveData<List<TupleMessageEx>> 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<List<EntityMessage>> liveWidgetUnified();
@Query("SELECT COUNT(message.id) FROM message" + @Query("SELECT COUNT(message.id) FROM message" +
" JOIN account ON account.id = message.account" + " JOIN account ON account.id = message.account" +
" JOIN folder ON folder.id = message.folder" + " JOIN folder ON folder.id = message.folder" +

@ -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 <http://www.gnu.org/licenses/>.
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);
}
}

@ -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 <http://www.gnu.org/licenses/>.
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<EntityMessage> 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<List<EntityMessage>>() {
@Override
public void onChanged(List<EntityMessage> 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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
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());
}
}

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llMessage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="6dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvFrom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:text="From"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/colorWidgetForeground" />
<TextView
android:id="@+id/tvTime"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="start"
android:gravity="end"
android:singleLine="true"
android:text="To"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/colorWidgetForeground" />
</LinearLayout>
<TextView
android:id="@+id/tvSubject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="Subject"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/colorWidgetForeground" />
</LinearLayout>

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="6dp"
android:text="@string/title_folder_unified"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/colorWidgetForeground"
android:textStyle="bold" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorWidgetForeground" />
<ListView
android:id="@+id/lv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:divider="@color/colorWidgetForeground"
android:dividerHeight="1dp" />
</LinearLayout>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_unified"
android:minWidth="110dp"
android:minHeight="110dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"
android:widgetCategory="home_screen" />
Loading…
Cancel
Save