Added account option to individually notify new messages

This commit is contained in:
M66B 2018-11-24 09:26:34 +01:00
parent 21f148955e
commit 7e9ab05aff
10 changed files with 1204 additions and 43 deletions

1
FAQ.md
View File

@ -48,7 +48,6 @@ Anything on this list is in random order and *might* be added in the near future
* Shortcut frequently contacted: Android [doesn't support this anymore](https://developer.android.com/guide/topics/providers/contacts-provider#ObsoleteData).
* Pull down to refresh: new messages are received in real-time, so manual refreshing is not needed, see also [this FAQ](#user-content-faq2).
* Switch language: although it is possible to change the language of an app, Android is not designed for this. Better fix the translation in your language if needed, see [this FAQ](#user-content-faq26) about how to.
* Notification per account: this would result in multiple icons in the status bar and most people don't like this. Note that the account colors are shown in the notifications too.
* Select identities to show in unified inbox: this would add complexity for something which would hardly be used.
* Better design: please let me know what you have in mind [in this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).
* Hide archived messages: hiding archived messages which exists in other folders too would have a performance impact.

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 5,
version = 6,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -155,6 +155,13 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `message` ADD COLUMN `last_attempt` INTEGER");
}
})
.addMigrations(new Migration(5, 6) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `account` ADD COLUMN `notify` INTEGER NOT NULL DEFAULT 0");
}
})
.build();
}

View File

@ -37,7 +37,7 @@ public interface DaoMessage {
// https://www.sqlite.org/lang_select.html
@Query("SELECT message.*" +
", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor" +
", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", COUNT(message.id) AS count" +
", SUM(CASE WHEN message.ui_seen" +
@ -69,7 +69,7 @@ public interface DaoMessage {
DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedInbox(String sort, boolean debug);
@Query("SELECT message.*" +
", account.name AS accountName, identity.color AS accountColor" +
", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", COUNT(message.id) AS count" +
", SUM(CASE WHEN message.ui_seen" +
@ -102,7 +102,7 @@ public interface DaoMessage {
DataSource.Factory<Integer, TupleMessageEx> pagedFolder(long folder, String sort, boolean found, boolean debug);
@Query("SELECT message.*" +
", account.name AS accountName, identity.color AS accountColor" +
", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" +
", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" +
@ -185,7 +185,7 @@ public interface DaoMessage {
List<Long> getMessageWithoutPreview();
@Query("SELECT message.*" +
", account.name AS accountName, identity.color AS accountColor" +
", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" +
", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" +
@ -200,7 +200,7 @@ public interface DaoMessage {
LiveData<TupleMessageEx> liveMessage(long id);
@Query("SELECT message.*" +
", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor" +
", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", 1 AS count" +
", 1 AS unseen" +

View File

@ -58,6 +58,8 @@ public class EntityAccount {
public Boolean primary;
public Integer color;
@NonNull
public Boolean notify;
@NonNull
public Integer poll_interval; // keep-alive interval
public Long created;
public String state;
@ -78,6 +80,7 @@ public class EntityAccount {
json.put("primary", primary);
if (color != null)
json.put("color", color);
json.put("notify", notify);
json.put("poll_interval", poll_interval);
// not created
// not state
@ -101,6 +104,8 @@ public class EntityAccount {
account.primary = json.getBoolean("primary");
if (json.has("color"))
account.color = json.getInt("color");
if (json.has("notify"))
account.notify = json.getBoolean("notify");
account.poll_interval = json.getInt("poll_interval");
return account;
}
@ -120,6 +125,7 @@ public class EntityAccount {
this.synchronize.equals(other.synchronize) &&
this.primary.equals(other.primary) &&
(this.color == null ? other.color == null : this.color.equals(other.color)) &&
this.notify.equals(other.notify) &&
this.poll_interval.equals(other.poll_interval) &&
(this.created == null ? other.created == null : this.created.equals(other.created)) &&
(this.state == null ? other.state == null : this.state.equals(other.state)) &&

View File

@ -111,6 +111,7 @@ public class FragmentAccount extends FragmentEx {
private Button btnColor;
private View vwColor;
private ImageView ibColorDefault;
private CheckBox cbNotify;
private CheckBox cbSynchronize;
private CheckBox cbPrimary;
@ -183,6 +184,7 @@ public class FragmentAccount extends FragmentEx {
btnColor = view.findViewById(R.id.btnColor);
vwColor = view.findViewById(R.id.vwColor);
ibColorDefault = view.findViewById(R.id.ibColorDefault);
cbNotify = view.findViewById(R.id.cbNotify);
cbSynchronize = view.findViewById(R.id.cbSynchronize);
cbPrimary = view.findViewById(R.id.cbPrimary);
@ -599,6 +601,7 @@ public class FragmentAccount extends FragmentEx {
args.putString("name", etName.getText().toString());
args.putInt("color", color);
args.putBoolean("notify", cbNotify.isChecked());
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
@ -624,6 +627,7 @@ public class FragmentAccount extends FragmentEx {
String name = args.getString("name");
Integer color = args.getInt("color");
boolean notify = args.getBoolean("notify");
boolean synchronize = args.getBoolean("synchronize");
boolean primary = args.getBoolean("primary");
@ -706,6 +710,7 @@ public class FragmentAccount extends FragmentEx {
account.name = name;
account.color = color;
account.notify = notify;
account.synchronize = synchronize;
account.primary = (account.synchronize && primary);
@ -954,6 +959,7 @@ public class FragmentAccount extends FragmentEx {
tilPassword.getEditText().setText(account == null ? null : account.password);
etName.setText(account == null ? null : account.name);
cbNotify.setChecked(account == null ? false : account.notify);
cbSynchronize.setChecked(account == null ? true : account.synchronize);
cbPrimary.setChecked(account == null ? true : account.primary);

View File

@ -48,6 +48,7 @@ import android.provider.ContactsContract;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import android.util.LongSparseArray;
import com.sun.mail.iap.ConnectionException;
import com.sun.mail.imap.AppendUID;
@ -173,40 +174,60 @@ public class ServiceSynchronize extends LifecycleService {
});
db.message().liveUnseenUnified().observe(this, new Observer<List<TupleMessageEx>>() {
private List<Integer> notifying = new ArrayList<>();
private LongSparseArray<List<Integer>> notifying = new LongSparseArray<>();
@Override
public void onChanged(List<TupleMessageEx> messages) {
NotificationManager nm = getSystemService(NotificationManager.class);
List<Notification> notifications = getNotificationUnseen(messages);
List<Integer> all = new ArrayList<>();
List<Integer> added = new ArrayList<>();
List<Integer> removed = new ArrayList<>(notifying);
for (Notification notification : notifications) {
Integer id = (int) notification.extras.getLong("id", 0);
if (id > 0) {
all.add(id);
if (removed.contains(id))
removed.remove(id);
else
added.add(id);
Widget.update(ServiceSynchronize.this, messages.size());
LongSparseArray<List<TupleMessageEx>> accountMessages = new LongSparseArray<>();
for (int i = 0; i < notifying.size(); i++)
accountMessages.put(notifying.keyAt(i), new ArrayList<TupleMessageEx>());
for (TupleMessageEx message : messages) {
long account = (message.accountNotify ? message.account : 0);
if (accountMessages.indexOfKey(account) < 0)
accountMessages.put(account, new ArrayList<TupleMessageEx>());
accountMessages.get(account).add(message);
if (notifying.indexOfKey(account) < 0)
notifying.put(account, new ArrayList<Integer>());
}
for (int i = 0; i < accountMessages.size(); i++) {
long account = accountMessages.keyAt(i);
List<Notification> notifications = getNotificationUnseen(account, accountMessages.get(account));
List<Integer> all = new ArrayList<>();
List<Integer> added = new ArrayList<>();
List<Integer> removed = notifying.get(account);
for (Notification notification : notifications) {
Integer id = (int) notification.extras.getLong("id", 0);
if (id > 0) {
all.add(id);
if (removed.contains(id))
removed.remove(id);
else
added.add(id);
}
}
if (notifications.size() == 0)
nm.cancel("unseen:" + account, 0);
for (Integer id : removed)
nm.cancel("unseen:" + account, id);
for (Notification notification : notifications) {
Integer id = (int) notification.extras.getLong("id", 0);
if ((id == 0 && added.size() + removed.size() > 0) || added.contains(id))
nm.notify("unseen:" + account, id, notification);
}
notifying.put(account, all);
}
if (notifications.size() == 0)
nm.cancel("unseen", 0);
for (Integer id : removed)
nm.cancel("unseen", id);
for (Notification notification : notifications) {
Integer id = (int) notification.extras.getLong("id", 0);
if ((id == 0 && added.size() + removed.size() > 0) || added.contains(id))
nm.notify("unseen", id, notification);
}
notifying = all;
}
});
}
@ -264,7 +285,7 @@ public class ServiceSynchronize extends LifecycleService {
else if ("clear".equals(action)) {
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) throws Throwable {
protected Void onLoad(Context context, Bundle args) {
DB.getInstance(context).message().ignoreAll();
return null;
}
@ -370,18 +391,18 @@ public class ServiceSynchronize extends LifecycleService {
return builder;
}
private List<Notification> getNotificationUnseen(List<TupleMessageEx> messages) {
// https://developer.android.com/training/notify-user/group
private List<Notification> getNotificationUnseen(long account, List<TupleMessageEx> messages) {
List<Notification> notifications = new ArrayList<>();
Widget.update(this, messages.size());
if (messages.size() == 0)
return notifications;
boolean pro = Helper.isPro(this);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
// https://developer.android.com/training/notify-user/group
String group = Long.toString(account);
// Build pending intent
Intent view = new Intent(this, ActivityView.class);
view.setAction("unified");
@ -429,7 +450,7 @@ public class ServiceSynchronize extends LifecycleService {
.setCategory(Notification.CATEGORY_STATUS)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPublicVersion(pbuilder.build())
.setGroup(BuildConfig.APPLICATION_ID)
.setGroup(group)
.setGroupSummary(true);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
@ -520,7 +541,7 @@ public class ServiceSynchronize extends LifecycleService {
.setPriority(Notification.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_MESSAGE)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setGroup(BuildConfig.APPLICATION_ID)
.setGroup(group)
.setGroupSummary(false)
.addAction(actionSeen.build())
.addAction(actionArchive.build())

View File

@ -22,6 +22,7 @@ package eu.faircode.email;
public class TupleMessageEx extends EntityMessage {
public String accountName;
public Integer accountColor;
public boolean accountNotify;
public String folderName;
public String folderDisplay;
public String folderType;
@ -38,6 +39,7 @@ public class TupleMessageEx extends EntityMessage {
return (super.equals(obj) &&
(this.accountName == null ? other.accountName == null : this.accountName.equals(other.accountName)) &&
(this.accountColor == null ? other.accountColor == null : this.accountColor.equals(other.accountColor)) &&
this.accountNotify == other.accountNotify &&
this.folderName.equals(other.folderName) &&
(this.folderDisplay == null ? other.folderDisplay == null : this.folderDisplay.equals(other.folderDisplay)) &&
this.folderType.equals(other.folderType) &&

View File

@ -289,6 +289,15 @@
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toBottomOf="@id/etName" />
<CheckBox
android:id="@+id/cbNotify"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_notify"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnColor" />
<CheckBox
android:id="@+id/cbSynchronize"
android:layout_width="wrap_content"
@ -296,7 +305,7 @@
android:layout_marginTop="12dp"
android:text="@string/title_synchronize_account"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnColor" />
app:layout_constraintTop_toBottomOf="@id/cbNotify" />
<CheckBox
android:id="@+id/cbPrimary"
@ -529,7 +538,7 @@
android:id="@+id/grpAdvanced"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="tvName,etName,btnColor,vwColor,ibColorDefault,cbSynchronize,cbPrimary,tvInterval,etInterval" />
app:constraint_referenced_ids="tvName,etName,btnColor,vwColor,ibColorDefault,cbNotify,cbSynchronize,cbPrimary,tvInterval,etInterval" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpFolders"

View File

@ -123,6 +123,7 @@
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_signature">Signature text</string>
<string name="title_account_color">Color</string>
<string name="title_account_notify">Separate notifications</string>
<string name="title_domain">Domain name</string>
<string name="title_autoconfig">Get settings</string>
<string name="title_no_settings">Settings not found</string>