Added snoozing messages

pull/147/head
M66B 6 years ago
parent 0bcb335203
commit e3e0f58197

@ -41,7 +41,7 @@ None at this moment.
* Resize images: this is not a feature directly related to email and there are plenty of apps that can do this for you. * Resize images: this is not a feature directly related to email and there are plenty of apps that can do this for you.
* Calendar events: opening the attached calendar file should open the related calendar app. * Calendar events: opening the attached calendar file should open the related calendar app.
* Executing filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for this. * Executing filter rules: filter rules should be executed on the server because a battery powered device with possibly an unstable internet connection is not suitable for this.
* Snooze/send timer: basically the same as executing filter rules. Snoozing and delayed sending is not supported by [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol). You could move messages to a "to do" folder instead. * Send timer: basically the same as executing filter rules. Delayed sending is not supported by [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol). You could move messages to a "to do" folder instead.
* Badge count: there is no standard Android API for this and third party solutions might stop working anytime. For example *ShortcutBadger* [has lots of problems](https://github.com/leolin310148/ShortcutBadger/issues). You can use the provided widget instead. * Badge count: there is no standard Android API for this and third party solutions might stop working anytime. For example *ShortcutBadger* [has lots of problems](https://github.com/leolin310148/ShortcutBadger/issues). You can use the provided widget instead.
* 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. * 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.
* Select identities to show in unified inbox: this would add complexity for something which would hardly be used. * Select identities to show in unified inbox: this would add complexity for something which would hardly be used.

@ -37,6 +37,7 @@ This app starts a foreground service with a low priority status bar notification
* Account/identity colors * Account/identity colors
* Notifications per account * Notifications per account
* Notifications with message preview (requires Android 7 Nougat or later) * Notifications with message preview (requires Android 7 Nougat or later)
* Snoozing messages
* Reply templates * Reply templates
* Search on server * Search on server
* Keyword management * Keyword management

File diff suppressed because it is too large Load Diff

@ -156,6 +156,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private TextView tvSize; private TextView tvSize;
private TextView tvTime; private TextView tvTime;
private ImageView ivDraft; private ImageView ivDraft;
private ImageView ivSnoozed;
private ImageView ivAnswered; private ImageView ivAnswered;
private ImageView ivAttachments; private ImageView ivAttachments;
private TextView tvSubject; private TextView tvSubject;
@ -220,6 +221,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvSize = itemView.findViewById(R.id.tvSize); tvSize = itemView.findViewById(R.id.tvSize);
tvTime = itemView.findViewById(R.id.tvTime); tvTime = itemView.findViewById(R.id.tvTime);
ivDraft = itemView.findViewById(R.id.ivDraft); ivDraft = itemView.findViewById(R.id.ivDraft);
ivSnoozed = itemView.findViewById(R.id.ivSnoozed);
ivAnswered = itemView.findViewById(R.id.ivAnswered); ivAnswered = itemView.findViewById(R.id.ivAnswered);
ivAttachments = itemView.findViewById(R.id.ivAttachments); ivAttachments = itemView.findViewById(R.id.ivAttachments);
tvSubject = itemView.findViewById(R.id.tvSubject); tvSubject = itemView.findViewById(R.id.tvSubject);
@ -284,6 +286,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private void wire() { private void wire() {
itemView.setOnClickListener(this); itemView.setOnClickListener(this);
ivSnoozed.setOnClickListener(this);
ivFlagged.setOnClickListener(this); ivFlagged.setOnClickListener(this);
ivExpanderAddress.setOnClickListener(this); ivExpanderAddress.setOnClickListener(this);
ivAddContact.setOnClickListener(this); ivAddContact.setOnClickListener(this);
@ -298,6 +301,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
private void unwire() { private void unwire() {
itemView.setOnClickListener(null); itemView.setOnClickListener(null);
ivSnoozed.setOnClickListener(null);
ivFlagged.setOnClickListener(null); ivFlagged.setOnClickListener(null);
ivExpanderAddress.setOnClickListener(null); ivExpanderAddress.setOnClickListener(null);
ivAddContact.setOnClickListener(null); ivAddContact.setOnClickListener(null);
@ -319,6 +323,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvSize.setText(null); tvSize.setText(null);
tvTime.setText(null); tvTime.setText(null);
ivDraft.setVisibility(View.GONE); ivDraft.setVisibility(View.GONE);
ivSnoozed.setVisibility(View.GONE);
ivAnswered.setVisibility(View.GONE); ivAnswered.setVisibility(View.GONE);
ivAttachments.setVisibility(View.GONE); ivAttachments.setVisibility(View.GONE);
tvSubject.setText(null); tvSubject.setText(null);
@ -371,6 +376,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvSize.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); tvSize.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
tvTime.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); tvTime.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
ivDraft.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); ivDraft.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
ivSnoozed.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
ivAnswered.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); ivAnswered.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
ivAttachments.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); ivAttachments.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
tvSubject.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f); tvSubject.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
@ -457,6 +463,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received)); tvTime.setText(DateUtils.getRelativeTimeSpanString(context, message.received));
ivDraft.setVisibility(message.drafts > 0 ? View.VISIBLE : View.GONE); ivDraft.setVisibility(message.drafts > 0 ? View.VISIBLE : View.GONE);
ivSnoozed.setVisibility(message.ui_snoozed == null ? View.GONE : View.VISIBLE);
ivAnswered.setVisibility(message.ui_answered ? View.VISIBLE : View.GONE); ivAnswered.setVisibility(message.ui_answered ? View.VISIBLE : View.GONE);
ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE); ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
btnDownloadAttachments.setVisibility(View.GONE); btnDownloadAttachments.setVisibility(View.GONE);
@ -729,7 +736,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
TupleMessageEx message = differ.getItem(pos); TupleMessageEx message = differ.getItem(pos);
if (view.getId() == R.id.ivFlagged) if (view.getId() == R.id.ivSnoozed)
onShowSnoozed(message);
else if (view.getId() == R.id.ivFlagged)
onToggleFlag(message); onToggleFlag(message);
else if (view.getId() == R.id.ivAddContact) else if (view.getId() == R.id.ivAddContact)
onAddContact(message); onAddContact(message);
@ -1344,6 +1353,11 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}.execute(context, owner, args, "message:unseen"); }.execute(context, owner, args, "message:unseen");
} }
private void onShowSnoozed(TupleMessageEx message) {
if (message.ui_snoozed != null)
Toast.makeText(context, new Date(message.ui_snoozed).toString(), Toast.LENGTH_LONG).show();
}
private void onToggleFlag(TupleMessageEx message) { private void onToggleFlag(TupleMessageEx message) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("id", message.id); args.putLong("id", message.id);
@ -1436,7 +1450,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
Helper.unexpectedError(context, owner, ex); Helper.unexpectedError(context, owner, ex);
} }
}.execute(context, owner, args, "message:share"); }.execute(context, owner, args, "message:share");
} }
private void onShowHeaders(ActionData data) { private void onShowHeaders(ActionData data) {

@ -49,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 31, version = 32,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -396,6 +396,14 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `attachment` ADD COLUMN `disposition` TEXT"); db.execSQL("ALTER TABLE `attachment` ADD COLUMN `disposition` TEXT");
} }
}) })
.addMigrations(new Migration(31, 32) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `message` ADD COLUMN `ui_snoozed` INTEGER");
db.execSQL("CREATE INDEX `index_message_ui_snoozed` ON `message` (`ui_snoozed`)");
}
})
.build(); .build();
} }

@ -63,6 +63,7 @@ public interface DaoMessage {
" JOIN folder ON folder.id = message.folder" + " JOIN folder ON folder.id = message.folder" +
" WHERE account.`synchronize`" + " WHERE account.`synchronize`" +
" AND (NOT message.ui_hide OR :debug)" + " AND (NOT message.ui_hide OR :debug)" +
" AND (:snoozed OR ui_snoozed IS NULL)" +
" GROUP BY account.id, CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" + " GROUP BY account.id, CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
" HAVING SUM(unified) > 0" + " HAVING SUM(unified) > 0" +
" ORDER BY" + " ORDER BY" +
@ -73,7 +74,7 @@ public interface DaoMessage {
" ELSE 0" + " ELSE 0" +
" END DESC, message.received DESC") " END DESC, message.received DESC")
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedInbox(boolean threading, String sort, boolean debug); DataSource.Factory<Integer, TupleMessageEx> pagedUnifiedInbox(boolean threading, String sort, boolean snoozed, boolean debug);
String unseen_folder = "SUM(CASE WHEN message.ui_seen" + String unseen_folder = "SUM(CASE WHEN message.ui_seen" +
" OR (folder.id <> :folder AND folder.type = '" + EntityFolder.ARCHIVE + "')" + " OR (folder.id <> :folder AND folder.type = '" + EntityFolder.ARCHIVE + "')" +
@ -102,6 +103,7 @@ public interface DaoMessage {
" JOIN folder f ON f.id = :folder" + " JOIN folder f ON f.id = :folder" +
" WHERE (message.account = f.account OR folder.type = '" + EntityFolder.OUTBOX + "')" + " WHERE (message.account = f.account OR folder.type = '" + EntityFolder.OUTBOX + "')" +
" AND (NOT message.ui_hide OR :debug)" + " AND (NOT message.ui_hide OR :debug)" +
" AND (:snoozed OR :found OR ui_snoozed IS NULL)" +
" AND (NOT :found OR ui_found = :found)" + " AND (NOT :found OR ui_found = :found)" +
" GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" + " GROUP BY CASE WHEN message.thread IS NULL OR NOT :threading THEN message.id ELSE message.thread END" +
" HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" + " HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" +
@ -113,7 +115,7 @@ public interface DaoMessage {
" ELSE 0" + " ELSE 0" +
" END DESC, message.received DESC") " END DESC, message.received DESC")
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
DataSource.Factory<Integer, TupleMessageEx> pagedFolder(long folder, boolean threading, String sort, boolean found, boolean debug); DataSource.Factory<Integer, TupleMessageEx> pagedFolder(long folder, boolean threading, String sort, boolean snoozed, boolean found, boolean debug);
@Query("SELECT message.*" + @Query("SELECT message.*" +
", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" +
@ -258,6 +260,9 @@ public interface DaoMessage {
" AND NOT uid IS NULL") " AND NOT uid IS NULL")
List<Long> getUids(long folder, Long received); List<Long> getUids(long folder, Long received);
@Query("SELECT * FROM message WHERE NOT ui_snoozed IS NULL")
List<EntityMessage> getSnoozed();
@Insert @Insert
long insertMessage(EntityMessage message); long insertMessage(EntityMessage message);
@ -319,6 +324,10 @@ public interface DaoMessage {
@Query("UPDATE message SET ui_found = 0") @Query("UPDATE message SET ui_found = 0")
int resetSearch(); int resetSearch();
@Query("UPDATE message SET ui_snoozed = :wakeup" +
" WHERE id = :id")
int setMessageSnoozed(long id, Long wakeup);
@Query("DELETE FROM message WHERE id = :id") @Query("DELETE FROM message WHERE id = :id")
int deleteMessage(long id); int deleteMessage(long id);

@ -20,8 +20,11 @@ package eu.faircode.email;
*/ */
import android.Manifest; import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
@ -81,7 +84,8 @@ import static androidx.room.ForeignKey.SET_NULL;
@Index(value = {"ui_hide"}), @Index(value = {"ui_hide"}),
@Index(value = {"ui_found"}), @Index(value = {"ui_found"}),
@Index(value = {"ui_ignored"}), @Index(value = {"ui_ignored"}),
@Index(value = {"ui_browsed"}) @Index(value = {"ui_browsed"}),
@Index(value = {"ui_snoozed"})
} }
) )
public class EntityMessage implements Serializable { public class EntityMessage implements Serializable {
@ -142,6 +146,7 @@ public class EntityMessage implements Serializable {
public Boolean ui_ignored = false; public Boolean ui_ignored = false;
@NonNull @NonNull
public Boolean ui_browsed = false; public Boolean ui_browsed = false;
public Long ui_snoozed;
public String error; public String error;
public Long last_attempt; // send public Long last_attempt; // send
@ -276,6 +281,21 @@ public class EntityMessage implements Serializable {
return false; return false;
} }
static void snooze(Context context, long id, Long wakeup) {
Intent snoozed = new Intent(context, ServiceSynchronize.class);
snoozed.setAction("snooze:" + id);
PendingIntent pi = PendingIntent.getService(context, ServiceSynchronize.PI_SNOOZED, snoozed, PendingIntent.FLAG_UPDATE_CURRENT);
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (wakeup == null) {
Log.i("Cancel snooze id=" + id);
am.cancel(pi);
} else {
Log.i("Set snooze id=" + id + " wakeup=" + new Date(wakeup));
am.set(AlarmManager.RTC_WAKEUP, wakeup, pi);
}
}
public boolean uiEquals(Object obj) { public boolean uiEquals(Object obj) {
if (obj instanceof EntityMessage) { if (obj instanceof EntityMessage) {
EntityMessage other = (EntityMessage) obj; EntityMessage other = (EntityMessage) obj;
@ -315,6 +335,8 @@ public class EntityMessage implements Serializable {
this.ui_hide.equals(other.ui_hide) && this.ui_hide.equals(other.ui_hide) &&
this.ui_found.equals(other.ui_found) && this.ui_found.equals(other.ui_found) &&
this.ui_ignored.equals(other.ui_ignored) && this.ui_ignored.equals(other.ui_ignored) &&
//this.ui_browsed.equals(other.ui_browsed) &&
(this.ui_snoozed == null ? other.ui_snoozed == null : this.ui_snoozed.equals(other.ui_snoozed)) &&
(this.error == null ? other.error == null : this.error.equals(other.error))); (this.error == null ? other.error == null : this.error.equals(other.error)));
} }
return false; return false;
@ -359,6 +381,8 @@ public class EntityMessage implements Serializable {
this.ui_hide.equals(other.ui_hide) && this.ui_hide.equals(other.ui_hide) &&
this.ui_found.equals(other.ui_found) && this.ui_found.equals(other.ui_found) &&
this.ui_ignored.equals(other.ui_ignored) && this.ui_ignored.equals(other.ui_ignored) &&
this.ui_browsed.equals(other.ui_browsed) &&
(this.ui_snoozed == null ? other.ui_snoozed == null : this.ui_snoozed.equals(other.ui_snoozed)) &&
(this.error == null ? other.error == null : this.error.equals(other.error))); (this.error == null ? other.error == null : this.error.equals(other.error)));
} }
return false; return false;

@ -44,6 +44,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.NumberPicker;
import android.widget.TextView; import android.widget.TextView;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
@ -52,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -744,6 +746,7 @@ public class FragmentMessages extends FragmentEx {
private final int action_delete = 7; private final int action_delete = 7;
private final int action_junk = 8; private final int action_junk = 8;
private final int action_move = 9; private final int action_move = 9;
private final int action_snooze = 10;
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -814,10 +817,12 @@ public class FragmentMessages extends FragmentEx {
popupMenu.getMenu().add(Menu.NONE, action_trash, 6, R.string.title_trash); popupMenu.getMenu().add(Menu.NONE, action_trash, 6, R.string.title_trash);
if (!result[8] && !result[9]) if (!result[8] && !result[9])
popupMenu.getMenu().add(Menu.NONE, action_junk, 6, R.string.title_spam); popupMenu.getMenu().add(Menu.NONE, action_junk, 7, R.string.title_spam);
if (!result[9]) if (!result[9])
popupMenu.getMenu().add(Menu.NONE, action_move, 7, R.string.title_move); popupMenu.getMenu().add(Menu.NONE, action_move, 8, R.string.title_move);
popupMenu.getMenu().add(Menu.NONE, action_snooze, 9, R.string.title_snooze);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override @Override
@ -850,6 +855,9 @@ public class FragmentMessages extends FragmentEx {
case action_move: case action_move:
onActionMove(); onActionMove();
return true; return true;
case action_snooze:
onActionSnooze();
return true;
default: default:
return false; return false;
} }
@ -1191,6 +1199,85 @@ public class FragmentMessages extends FragmentEx {
} }
}.execute(FragmentMessages.this, args, "messages:move"); }.execute(FragmentMessages.this, args, "messages:move");
} }
private void onActionSnooze() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
final View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_duration, null);
final NumberPicker npHours = dview.findViewById(R.id.npHours);
final NumberPicker npDays = dview.findViewById(R.id.npDays);
npHours.setMinValue(0);
npHours.setMaxValue(24);
npDays.setMinValue(0);
npDays.setMaxValue(30);
npHours.setValue(prefs.getInt("snooze_hours", 1));
npDays.setValue(prefs.getInt("snooze_days", 0));
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
.setTitle(R.string.title_snooze)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
int hours = npHours.getValue();
int days = npDays.getValue();
long duration = (hours + days * 24) * 3600L * 1000L;
if (duration > 0) {
prefs.edit().putInt("snooze_hours", hours).apply();
prefs.edit().putInt("snooze_days", days).apply();
}
Bundle args = new Bundle();
args.putLongArray("ids", getSelection());
args.putLong("wakeup", duration == 0 ? 0 : new Date().getTime() + duration);
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) {
long[] ids = args.getLongArray("ids");
Long wakeup = args.getLong("wakeup");
if (wakeup == 0)
wakeup = null;
DB db = DB.getInstance(context);
for (long id : ids) {
EntityMessage message = db.message().getMessage(id);
if (message != null) {
List<EntityMessage> messages = db.message().getMessageByThread(
message.account, message.thread, threading ? null : id, null);
for (EntityMessage threaded : messages) {
db.message().setMessageSnoozed(threaded.id, wakeup);
EntityMessage.snooze(context, threaded.id, wakeup);
}
}
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentMessages.this, args, "messages:snooze");
} catch (Throwable ex) {
Log.e(ex);
}
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
selectionTracker.clearSelection();
}
})
.show();
}
}); });
((ActivityBase) getActivity()).addBackPressedListener(onBackPressedListener); ((ActivityBase) getActivity()).addBackPressedListener(onBackPressedListener);
@ -1498,15 +1585,14 @@ public class FragmentMessages extends FragmentEx {
@Override @Override
public void onPrepareOptionsMenu(Menu menu) { public void onPrepareOptionsMenu(Menu menu) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
menu.findItem(R.id.menu_search).setVisible( menu.findItem(R.id.menu_search).setVisible(
folder >= 0 && viewType != AdapterMessage.ViewType.SEARCH); folder >= 0 && viewType != AdapterMessage.ViewType.SEARCH);
menu.findItem(R.id.menu_sort_on).setVisible( menu.findItem(R.id.menu_sort_on).setVisible(
viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER); viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER);
menu.findItem(R.id.menu_folders).setVisible(primary >= 0);
menu.findItem(R.id.menu_folders).setIcon(connected ? R.drawable.baseline_folder_24 : R.drawable.baseline_folder_open_24);
menu.findItem(R.id.menu_move_sent).setVisible(outbox);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
String sort = prefs.getString("sort", "time"); String sort = prefs.getString("sort", "time");
if ("time".equals(sort)) if ("time".equals(sort))
menu.findItem(R.id.menu_sort_on_time).setChecked(true); menu.findItem(R.id.menu_sort_on_time).setChecked(true);
@ -1517,6 +1603,15 @@ public class FragmentMessages extends FragmentEx {
else if ("sender".equals(sort)) else if ("sender".equals(sort))
menu.findItem(R.id.menu_sort_on_sender).setChecked(true); menu.findItem(R.id.menu_sort_on_sender).setChecked(true);
menu.findItem(R.id.menu_folders).setVisible(primary >= 0);
menu.findItem(R.id.menu_folders).setIcon(connected ? R.drawable.baseline_folder_24 : R.drawable.baseline_folder_open_24);
menu.findItem(R.id.menu_snoozed).setVisible(!outbox &&
(viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER));
menu.findItem(R.id.menu_snoozed).setChecked(prefs.getBoolean("snoozed", false));
menu.findItem(R.id.menu_move_sent).setVisible(outbox);
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
} }
@ -1543,6 +1638,10 @@ public class FragmentMessages extends FragmentEx {
onMenuSort("sender"); onMenuSort("sender");
return true; return true;
case R.id.menu_snoozed:
onMenuSnoozed();
return true;
case R.id.menu_zoom: case R.id.menu_zoom:
onMenuZoom(); onMenuZoom();
return true; return true;
@ -1567,6 +1666,13 @@ public class FragmentMessages extends FragmentEx {
loadMessages(); loadMessages();
} }
private void onMenuSnoozed() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean snoozed = prefs.getBoolean("snoozed", false);
prefs.edit().putBoolean("snoozed", !snoozed).apply();
loadMessages();
}
private void onMenuZoom() { private void onMenuZoom() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
int zoom = prefs.getInt("zoom", compact ? 0 : 1); int zoom = prefs.getInt("zoom", compact ? 0 : 1);
@ -1640,6 +1746,7 @@ public class FragmentMessages extends FragmentEx {
// Observe folder/messages/search // Observe folder/messages/search
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
String sort = prefs.getString("sort", "time"); String sort = prefs.getString("sort", "time");
boolean snoozed = prefs.getBoolean("snoozed", false);
boolean debug = prefs.getBoolean("debug", false); boolean debug = prefs.getBoolean("debug", false);
Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug); Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug);
@ -1651,7 +1758,7 @@ public class FragmentMessages extends FragmentEx {
switch (viewType) { switch (viewType) {
case UNIFIED: case UNIFIED:
builder = new LivePagedListBuilder<>( builder = new LivePagedListBuilder<>(
db.message().pagedUnifiedInbox(threading, sort, debug), LOCAL_PAGE_SIZE); db.message().pagedUnifiedInbox(threading, sort, snoozed, debug), LOCAL_PAGE_SIZE);
break; break;
case FOLDER: case FOLDER:
@ -1684,7 +1791,7 @@ public class FragmentMessages extends FragmentEx {
.setPrefetchDistance(REMOTE_PAGE_SIZE) .setPrefetchDistance(REMOTE_PAGE_SIZE)
.build(); .build();
builder = new LivePagedListBuilder<>( builder = new LivePagedListBuilder<>(
db.message().pagedFolder(folder, threading, sort, false, debug), configFolder); db.message().pagedFolder(folder, threading, sort, snoozed, false, debug), configFolder);
builder.setBoundaryCallback(searchCallback); builder.setBoundaryCallback(searchCallback);
break; break;
@ -1726,7 +1833,7 @@ public class FragmentMessages extends FragmentEx {
.setPrefetchDistance(REMOTE_PAGE_SIZE) .setPrefetchDistance(REMOTE_PAGE_SIZE)
.build(); .build();
builder = new LivePagedListBuilder<>( builder = new LivePagedListBuilder<>(
db.message().pagedFolder(folder, threading, "time", true, false), configSearch); db.message().pagedFolder(folder, threading, "time", snoozed, true, false), configSearch);
builder.setBoundaryCallback(searchCallback); builder.setBoundaryCallback(searchCallback);
break; break;
} }

@ -23,6 +23,10 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import java.util.List;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
public class ReceiverAutostart extends BroadcastReceiver { public class ReceiverAutostart extends BroadcastReceiver {
@Override @Override
public void onReceive(final Context context, Intent intent) { public void onReceive(final Context context, Intent intent) {
@ -30,6 +34,22 @@ public class ReceiverAutostart extends BroadcastReceiver {
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
EntityLog.log(context, intent.getAction()); EntityLog.log(context, intent.getAction());
ServiceSynchronize.init(context); ServiceSynchronize.init(context);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
DB db = DB.getInstance(context);
List<EntityMessage> messages = db.message().getSnoozed();
for (EntityMessage message : messages)
EntityMessage.snooze(context, message.id, message.ui_snoozed);
} catch (Throwable ex) {
Log.e(ex);
}
}
});
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
thread.start();
} }
} }
} }

@ -157,6 +157,7 @@ public class ServiceSynchronize extends LifecycleService {
static final int PI_ARCHIVE = 4; static final int PI_ARCHIVE = 4;
static final int PI_TRASH = 5; static final int PI_TRASH = 5;
static final int PI_IGNORED = 6; static final int PI_IGNORED = 6;
static final int PI_SNOOZED = 7;
@Override @Override
public void onCreate() { public void onCreate() {
@ -355,6 +356,7 @@ public class ServiceSynchronize extends LifecycleService {
case "archive": case "archive":
case "trash": case "trash":
case "ignore": case "ignore":
case "snooze":
executor.submit(new Runnable() { executor.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -388,6 +390,10 @@ public class ServiceSynchronize extends LifecycleService {
db.message().setMessageUiIgnored(message.id, true); db.message().setMessageUiIgnored(message.id, true);
break; break;
case "snooze":
db.message().setMessageSnoozed(message.id, null);
break;
default: default:
Log.w("Unknown action: " + parts[0]); Log.w("Unknown action: " + parts[0]);
} }
@ -1153,6 +1159,7 @@ public class ServiceSynchronize extends LifecycleService {
} }
} }
}, "idler." + folder.id); }, "idler." + folder.id);
idler.setPriority(THREAD_PRIORITY_BACKGROUND);
idler.start(); idler.start();
idlers.add(idler); idlers.add(idler);
@ -2953,6 +2960,7 @@ public class ServiceSynchronize extends LifecycleService {
void runnable(Runnable runnable, String name) { void runnable(Runnable runnable, String name) {
thread = new Thread(runnable, name); thread = new Thread(runnable, name);
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
} }
void release() { void release() {
@ -2982,7 +2990,6 @@ public class ServiceSynchronize extends LifecycleService {
} }
void start() { void start() {
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
thread.start(); thread.start();
yield(); yield();
} }

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16.24,7.76C15.07,6.59 13.54,6 12,6v6l-4.24,4.24c2.34,2.34 6.14,2.34 8.49,0 2.34,-2.34 2.34,-6.14 -0.01,-8.48zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<TextView
android:id="@+id/tvHours"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/title_hours"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toStartOf="@+id/tvDays"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvDays"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/title_days"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?android:attr/textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvHours"
app:layout_constraintTop_toTopOf="parent" />
<NumberPicker
android:id="@+id/npHours"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="@id/tvHours"
app:layout_constraintStart_toStartOf="@id/tvHours"
app:layout_constraintTop_toBottomOf="@id/tvHours" />
<NumberPicker
android:id="@+id/npDays"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="@id/tvDays"
app:layout_constraintStart_toStartOf="@id/tvDays"
app:layout_constraintTop_toBottomOf="@id/tvDays" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -197,6 +197,27 @@
app:layout_constraintStart_toEndOf="@id/ivCC" app:layout_constraintStart_toEndOf="@id/ivCC"
app:layout_constraintTop_toTopOf="@id/ivCC" /> app:layout_constraintTop_toTopOf="@id/ivCC" />
<ImageView
android:id="@+id/ivSnoozed"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="18dp"
android:src="@drawable/baseline_timelapse_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivCC" />
<TextView
android:id="@+id/tvSnoozed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:text="@string/title_legend_snoozed"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintBottom_toBottomOf="@id/ivSnoozed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ivSnoozed"
app:layout_constraintTop_toTopOf="@id/ivSnoozed" />
<ImageView <ImageView
android:id="@+id/ivDraft" android:id="@+id/ivDraft"
android:layout_width="24dp" android:layout_width="24dp"
@ -204,7 +225,7 @@
android:layout_marginTop="18dp" android:layout_marginTop="18dp"
android:src="@drawable/baseline_edit_24" android:src="@drawable/baseline_edit_24"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ivCC" /> app:layout_constraintTop_toBottomOf="@id/ivSnoozed" />
<TextView <TextView
android:id="@+id/tvDraft" android:id="@+id/tvDraft"

@ -112,6 +112,16 @@
app:layout_constraintStart_toEndOf="@id/paddingStart" app:layout_constraintStart_toEndOf="@id/paddingStart"
app:layout_constraintTop_toTopOf="@+id/tvSubject" /> app:layout_constraintTop_toTopOf="@+id/tvSubject" />
<ImageView
android:id="@+id/ivSnoozed"
android:layout_width="21dp"
android:layout_height="21dp"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_timelapse_24"
app:layout_constraintBottom_toBottomOf="@+id/tvSubject"
app:layout_constraintStart_toEndOf="@id/ivDraft"
app:layout_constraintTop_toTopOf="@+id/tvSubject" />
<ImageView <ImageView
android:id="@+id/ivAnswered" android:id="@+id/ivAnswered"
android:layout_width="21dp" android:layout_width="21dp"
@ -119,7 +129,7 @@
android:layout_marginStart="6dp" android:layout_marginStart="6dp"
android:src="@drawable/baseline_reply_24" android:src="@drawable/baseline_reply_24"
app:layout_constraintBottom_toBottomOf="@+id/tvSubject" app:layout_constraintBottom_toBottomOf="@+id/tvSubject"
app:layout_constraintStart_toEndOf="@id/ivDraft" app:layout_constraintStart_toEndOf="@id/ivSnoozed"
app:layout_constraintTop_toTopOf="@+id/tvSubject" /> app:layout_constraintTop_toTopOf="@+id/tvSubject" />
<ImageView <ImageView

@ -135,6 +135,16 @@
app:layout_constraintStart_toEndOf="@id/ivAvatar" app:layout_constraintStart_toEndOf="@id/ivAvatar"
app:layout_constraintTop_toTopOf="@+id/tvFolder" /> app:layout_constraintTop_toTopOf="@+id/tvFolder" />
<ImageView
android:id="@+id/ivSnoozed"
android:layout_width="21dp"
android:layout_height="21dp"
android:layout_marginStart="6dp"
android:src="@drawable/baseline_timelapse_24"
app:layout_constraintBottom_toBottomOf="@+id/tvFolder"
app:layout_constraintStart_toEndOf="@id/ivDraft"
app:layout_constraintTop_toTopOf="@+id/tvFolder" />
<ImageView <ImageView
android:id="@+id/ivAnswered" android:id="@+id/ivAnswered"
android:layout_width="21dp" android:layout_width="21dp"
@ -142,7 +152,7 @@
android:layout_marginStart="6dp" android:layout_marginStart="6dp"
android:src="@drawable/baseline_reply_24" android:src="@drawable/baseline_reply_24"
app:layout_constraintBottom_toBottomOf="@+id/tvFolder" app:layout_constraintBottom_toBottomOf="@+id/tvFolder"
app:layout_constraintStart_toEndOf="@id/ivDraft" app:layout_constraintStart_toEndOf="@id/ivSnoozed"
app:layout_constraintTop_toTopOf="@+id/tvFolder" /> app:layout_constraintTop_toTopOf="@+id/tvFolder" />
<ImageView <ImageView

@ -44,6 +44,12 @@
android:title="@string/title_folder_primary" android:title="@string/title_folder_primary"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/menu_snoozed"
android:checkable="true"
android:title="@string/title_snoozed"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_move_sent" android:id="@+id/menu_move_sent"
android:title="@string/title_move_sent" android:title="@string/title_move_sent"

@ -274,6 +274,7 @@
<string name="title_more">More</string> <string name="title_more">More</string>
<string name="title_spam">Spam</string> <string name="title_spam">Spam</string>
<string name="title_move">Move</string> <string name="title_move">Move</string>
<string name="title_snooze">Snooze</string>
<string name="title_archive">Archive</string> <string name="title_archive">Archive</string>
<string name="title_reply">Reply</string> <string name="title_reply">Reply</string>
<string name="title_moving">Moving to %1$s</string> <string name="title_moving">Moving to %1$s</string>
@ -352,6 +353,7 @@
<string name="title_address_sent">Sent:</string> <string name="title_address_sent">Sent:</string>
<string name="title_address_unsent">Unsent:</string> <string name="title_address_unsent">Unsent:</string>
<string name="title_address_invalid">Invalid:</string> <string name="title_address_invalid">Invalid:</string>
<string name="title_snoozed">Snoozed</string>
<string name="title_move_sent">Move to sent</string> <string name="title_move_sent">Move to sent</string>
<string name="title_previous">Previous</string> <string name="title_previous">Previous</string>
@ -377,6 +379,7 @@
<string name="title_legend_thread">Conversation</string> <string name="title_legend_thread">Conversation</string>
<string name="title_legend_cc">CC/BCC</string> <string name="title_legend_cc">CC/BCC</string>
<string name="title_legend_attachment">Attachment</string> <string name="title_legend_attachment">Attachment</string>
<string name="title_legend_snoozed">Snoozed</string>
<string name="title_legend_draft">Draft/edit</string> <string name="title_legend_draft">Draft/edit</string>
<string name="title_legend_answered">Answered</string> <string name="title_legend_answered">Answered</string>
<string name="title_legend_contacts">Contacts</string> <string name="title_legend_contacts">Contacts</string>
@ -418,6 +421,8 @@
<string name="title_undo">Undo</string> <string name="title_undo">Undo</string>
<string name="title_add">Add</string> <string name="title_add">Add</string>
<string name="title_browse">Browse</string> <string name="title_browse">Browse</string>
<string name="title_hours">Hours</string>
<string name="title_days">Days</string>
<string name="title_report">Report</string> <string name="title_report">Report</string>
<string name="title_no_ask_again">Do not ask this again</string> <string name="title_no_ask_again">Do not ask this again</string>

Loading…
Cancel
Save