mirror of
https://github.com/M66B/FairEmail.git
synced 2025-03-15 08:29:24 +00:00
Added snoozing messages
This commit is contained in:
parent
0bcb335203
commit
e3e0f58197
17 changed files with 1622 additions and 20 deletions
2
FAQ.md
2
FAQ.md
|
@ -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.
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
|
|
|
@ -37,6 +37,7 @@ This app starts a foreground service with a low priority status bar notification
|
|||
* Account/identity colors
|
||||
* Notifications per account
|
||||
* Notifications with message preview (requires Android 7 Nougat or later)
|
||||
* Snoozing messages
|
||||
* Reply templates
|
||||
* Search on server
|
||||
* Keyword management
|
||||
|
|
1302
app/schemas/eu.faircode.email.DB/32.json
Normal file
1302
app/schemas/eu.faircode.email.DB/32.json
Normal file
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 tvTime;
|
||||
private ImageView ivDraft;
|
||||
private ImageView ivSnoozed;
|
||||
private ImageView ivAnswered;
|
||||
private ImageView ivAttachments;
|
||||
private TextView tvSubject;
|
||||
|
@ -220,6 +221,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
tvSize = itemView.findViewById(R.id.tvSize);
|
||||
tvTime = itemView.findViewById(R.id.tvTime);
|
||||
ivDraft = itemView.findViewById(R.id.ivDraft);
|
||||
ivSnoozed = itemView.findViewById(R.id.ivSnoozed);
|
||||
ivAnswered = itemView.findViewById(R.id.ivAnswered);
|
||||
ivAttachments = itemView.findViewById(R.id.ivAttachments);
|
||||
tvSubject = itemView.findViewById(R.id.tvSubject);
|
||||
|
@ -284,6 +286,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
private void wire() {
|
||||
itemView.setOnClickListener(this);
|
||||
ivSnoozed.setOnClickListener(this);
|
||||
ivFlagged.setOnClickListener(this);
|
||||
ivExpanderAddress.setOnClickListener(this);
|
||||
ivAddContact.setOnClickListener(this);
|
||||
|
@ -298,6 +301,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
private void unwire() {
|
||||
itemView.setOnClickListener(null);
|
||||
ivSnoozed.setOnClickListener(null);
|
||||
ivFlagged.setOnClickListener(null);
|
||||
ivExpanderAddress.setOnClickListener(null);
|
||||
ivAddContact.setOnClickListener(null);
|
||||
|
@ -319,6 +323,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
tvSize.setText(null);
|
||||
tvTime.setText(null);
|
||||
ivDraft.setVisibility(View.GONE);
|
||||
ivSnoozed.setVisibility(View.GONE);
|
||||
ivAnswered.setVisibility(View.GONE);
|
||||
ivAttachments.setVisibility(View.GONE);
|
||||
tvSubject.setText(null);
|
||||
|
@ -371,6 +376,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
tvSize.setAlpha(message.duplicate ? LOW_LIGHT : 1.0f);
|
||||
tvTime.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);
|
||||
ivAttachments.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));
|
||||
|
||||
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);
|
||||
ivAttachments.setVisibility(message.attachments > 0 ? View.VISIBLE : View.GONE);
|
||||
btnDownloadAttachments.setVisibility(View.GONE);
|
||||
|
@ -729,7 +736,9 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
|
||||
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);
|
||||
else if (view.getId() == R.id.ivAddContact)
|
||||
onAddContact(message);
|
||||
|
@ -1344,6 +1353,11 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
}.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) {
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", message.id);
|
||||
|
@ -1436,7 +1450,6 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
Helper.unexpectedError(context, owner, ex);
|
||||
}
|
||||
}.execute(context, owner, args, "message:share");
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@Database(
|
||||
version = 31,
|
||||
version = 32,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
|
@ -396,6 +396,14 @@ public abstract class DB extends RoomDatabase {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ public interface DaoMessage {
|
|||
" JOIN folder ON folder.id = message.folder" +
|
||||
" WHERE account.`synchronize`" +
|
||||
" 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" +
|
||||
" HAVING SUM(unified) > 0" +
|
||||
" ORDER BY" +
|
||||
|
@ -73,7 +74,7 @@ public interface DaoMessage {
|
|||
" ELSE 0" +
|
||||
" END DESC, message.received DESC")
|
||||
@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" +
|
||||
" OR (folder.id <> :folder AND folder.type = '" + EntityFolder.ARCHIVE + "')" +
|
||||
|
@ -102,6 +103,7 @@ public interface DaoMessage {
|
|||
" JOIN folder f ON f.id = :folder" +
|
||||
" WHERE (message.account = f.account OR folder.type = '" + EntityFolder.OUTBOX + "')" +
|
||||
" AND (NOT message.ui_hide OR :debug)" +
|
||||
" AND (:snoozed OR :found OR ui_snoozed IS NULL)" +
|
||||
" 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" +
|
||||
" HAVING SUM(CASE WHEN folder.id = :folder THEN 1 ELSE 0 END) > 0" +
|
||||
|
@ -113,7 +115,7 @@ public interface DaoMessage {
|
|||
" ELSE 0" +
|
||||
" END DESC, message.received DESC")
|
||||
@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.*" +
|
||||
", 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")
|
||||
List<Long> getUids(long folder, Long received);
|
||||
|
||||
@Query("SELECT * FROM message WHERE NOT ui_snoozed IS NULL")
|
||||
List<EntityMessage> getSnoozed();
|
||||
|
||||
@Insert
|
||||
long insertMessage(EntityMessage message);
|
||||
|
||||
|
@ -319,6 +324,10 @@ public interface DaoMessage {
|
|||
@Query("UPDATE message SET ui_found = 0")
|
||||
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")
|
||||
int deleteMessage(long id);
|
||||
|
||||
|
|
|
@ -20,8 +20,11 @@ package eu.faircode.email;
|
|||
*/
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
@ -81,7 +84,8 @@ import static androidx.room.ForeignKey.SET_NULL;
|
|||
@Index(value = {"ui_hide"}),
|
||||
@Index(value = {"ui_found"}),
|
||||
@Index(value = {"ui_ignored"}),
|
||||
@Index(value = {"ui_browsed"})
|
||||
@Index(value = {"ui_browsed"}),
|
||||
@Index(value = {"ui_snoozed"})
|
||||
}
|
||||
)
|
||||
public class EntityMessage implements Serializable {
|
||||
|
@ -142,6 +146,7 @@ public class EntityMessage implements Serializable {
|
|||
public Boolean ui_ignored = false;
|
||||
@NonNull
|
||||
public Boolean ui_browsed = false;
|
||||
public Long ui_snoozed;
|
||||
public String error;
|
||||
public Long last_attempt; // send
|
||||
|
||||
|
@ -276,6 +281,21 @@ public class EntityMessage implements Serializable {
|
|||
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) {
|
||||
if (obj instanceof EntityMessage) {
|
||||
EntityMessage other = (EntityMessage) obj;
|
||||
|
@ -315,6 +335,8 @@ public class EntityMessage implements Serializable {
|
|||
this.ui_hide.equals(other.ui_hide) &&
|
||||
this.ui_found.equals(other.ui_found) &&
|
||||
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)));
|
||||
}
|
||||
return false;
|
||||
|
@ -359,6 +381,8 @@ public class EntityMessage implements Serializable {
|
|||
this.ui_hide.equals(other.ui_hide) &&
|
||||
this.ui_found.equals(other.ui_found) &&
|
||||
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)));
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -44,6 +44,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
@ -52,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -744,6 +746,7 @@ public class FragmentMessages extends FragmentEx {
|
|||
private final int action_delete = 7;
|
||||
private final int action_junk = 8;
|
||||
private final int action_move = 9;
|
||||
private final int action_snooze = 10;
|
||||
|
||||
@Override
|
||||
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);
|
||||
|
||||
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])
|
||||
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() {
|
||||
@Override
|
||||
|
@ -850,6 +855,9 @@ public class FragmentMessages extends FragmentEx {
|
|||
case action_move:
|
||||
onActionMove();
|
||||
return true;
|
||||
case action_snooze:
|
||||
onActionSnooze();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -1191,6 +1199,85 @@ public class FragmentMessages extends FragmentEx {
|
|||
}
|
||||
}.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);
|
||||
|
@ -1498,15 +1585,14 @@ public class FragmentMessages extends FragmentEx {
|
|||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
menu.findItem(R.id.menu_search).setVisible(
|
||||
folder >= 0 && viewType != AdapterMessage.ViewType.SEARCH);
|
||||
|
||||
menu.findItem(R.id.menu_sort_on).setVisible(
|
||||
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");
|
||||
if ("time".equals(sort))
|
||||
menu.findItem(R.id.menu_sort_on_time).setChecked(true);
|
||||
|
@ -1517,6 +1603,15 @@ public class FragmentMessages extends FragmentEx {
|
|||
else if ("sender".equals(sort))
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1543,6 +1638,10 @@ public class FragmentMessages extends FragmentEx {
|
|||
onMenuSort("sender");
|
||||
return true;
|
||||
|
||||
case R.id.menu_snoozed:
|
||||
onMenuSnoozed();
|
||||
return true;
|
||||
|
||||
case R.id.menu_zoom:
|
||||
onMenuZoom();
|
||||
return true;
|
||||
|
@ -1567,6 +1666,13 @@ public class FragmentMessages extends FragmentEx {
|
|||
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() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
int zoom = prefs.getInt("zoom", compact ? 0 : 1);
|
||||
|
@ -1640,6 +1746,7 @@ public class FragmentMessages extends FragmentEx {
|
|||
// Observe folder/messages/search
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
String sort = prefs.getString("sort", "time");
|
||||
boolean snoozed = prefs.getBoolean("snoozed", false);
|
||||
boolean debug = prefs.getBoolean("debug", false);
|
||||
Log.i("Load messages type=" + viewType + " sort=" + sort + " debug=" + debug);
|
||||
|
||||
|
@ -1651,7 +1758,7 @@ public class FragmentMessages extends FragmentEx {
|
|||
switch (viewType) {
|
||||
case UNIFIED:
|
||||
builder = new LivePagedListBuilder<>(
|
||||
db.message().pagedUnifiedInbox(threading, sort, debug), LOCAL_PAGE_SIZE);
|
||||
db.message().pagedUnifiedInbox(threading, sort, snoozed, debug), LOCAL_PAGE_SIZE);
|
||||
break;
|
||||
|
||||
case FOLDER:
|
||||
|
@ -1684,7 +1791,7 @@ public class FragmentMessages extends FragmentEx {
|
|||
.setPrefetchDistance(REMOTE_PAGE_SIZE)
|
||||
.build();
|
||||
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);
|
||||
break;
|
||||
|
||||
|
@ -1726,7 +1833,7 @@ public class FragmentMessages extends FragmentEx {
|
|||
.setPrefetchDistance(REMOTE_PAGE_SIZE)
|
||||
.build();
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,10 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
|
||||
|
||||
public class ReceiverAutostart extends BroadcastReceiver {
|
||||
@Override
|
||||
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())) {
|
||||
EntityLog.log(context, intent.getAction());
|
||||
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_TRASH = 5;
|
||||
static final int PI_IGNORED = 6;
|
||||
static final int PI_SNOOZED = 7;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
|
@ -355,6 +356,7 @@ public class ServiceSynchronize extends LifecycleService {
|
|||
case "archive":
|
||||
case "trash":
|
||||
case "ignore":
|
||||
case "snooze":
|
||||
executor.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -388,6 +390,10 @@ public class ServiceSynchronize extends LifecycleService {
|
|||
db.message().setMessageUiIgnored(message.id, true);
|
||||
break;
|
||||
|
||||
case "snooze":
|
||||
db.message().setMessageSnoozed(message.id, null);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.w("Unknown action: " + parts[0]);
|
||||
}
|
||||
|
@ -1153,6 +1159,7 @@ public class ServiceSynchronize extends LifecycleService {
|
|||
}
|
||||
}
|
||||
}, "idler." + folder.id);
|
||||
idler.setPriority(THREAD_PRIORITY_BACKGROUND);
|
||||
idler.start();
|
||||
idlers.add(idler);
|
||||
|
||||
|
@ -2953,6 +2960,7 @@ public class ServiceSynchronize extends LifecycleService {
|
|||
|
||||
void runnable(Runnable runnable, String name) {
|
||||
thread = new Thread(runnable, name);
|
||||
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
|
||||
}
|
||||
|
||||
void release() {
|
||||
|
@ -2982,7 +2990,6 @@ public class ServiceSynchronize extends LifecycleService {
|
|||
}
|
||||
|
||||
void start() {
|
||||
thread.setPriority(THREAD_PRIORITY_BACKGROUND);
|
||||
thread.start();
|
||||
yield();
|
||||
}
|
||||
|
|
10
app/src/main/res/drawable/baseline_timelapse_24.xml
Normal file
10
app/src/main/res/drawable/baseline_timelapse_24.xml
Normal file
|
@ -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>
|
49
app/src/main/res/layout/dialog_duration.xml
Normal file
49
app/src/main/res/layout/dialog_duration.xml
Normal file
|
@ -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_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
|
||||
android:id="@+id/ivDraft"
|
||||
android:layout_width="24dp"
|
||||
|
@ -204,7 +225,7 @@
|
|||
android:layout_marginTop="18dp"
|
||||
android:src="@drawable/baseline_edit_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ivCC" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ivSnoozed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvDraft"
|
||||
|
|
|
@ -112,6 +112,16 @@
|
|||
app:layout_constraintStart_toEndOf="@id/paddingStart"
|
||||
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
|
||||
android:id="@+id/ivAnswered"
|
||||
android:layout_width="21dp"
|
||||
|
@ -119,7 +129,7 @@
|
|||
android:layout_marginStart="6dp"
|
||||
android:src="@drawable/baseline_reply_24"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/tvSubject"
|
||||
app:layout_constraintStart_toEndOf="@id/ivDraft"
|
||||
app:layout_constraintStart_toEndOf="@id/ivSnoozed"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvSubject" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -135,6 +135,16 @@
|
|||
app:layout_constraintStart_toEndOf="@id/ivAvatar"
|
||||
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
|
||||
android:id="@+id/ivAnswered"
|
||||
android:layout_width="21dp"
|
||||
|
@ -142,7 +152,7 @@
|
|||
android:layout_marginStart="6dp"
|
||||
android:src="@drawable/baseline_reply_24"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/tvFolder"
|
||||
app:layout_constraintStart_toEndOf="@id/ivDraft"
|
||||
app:layout_constraintStart_toEndOf="@id/ivSnoozed"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvFolder" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -44,6 +44,12 @@
|
|||
android:title="@string/title_folder_primary"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_snoozed"
|
||||
android:checkable="true"
|
||||
android:title="@string/title_snoozed"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_move_sent"
|
||||
android:title="@string/title_move_sent"
|
||||
|
|
|
@ -274,6 +274,7 @@
|
|||
<string name="title_more">More</string>
|
||||
<string name="title_spam">Spam</string>
|
||||
<string name="title_move">Move</string>
|
||||
<string name="title_snooze">Snooze</string>
|
||||
<string name="title_archive">Archive</string>
|
||||
<string name="title_reply">Reply</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_unsent">Unsent:</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_previous">Previous</string>
|
||||
|
@ -377,6 +379,7 @@
|
|||
<string name="title_legend_thread">Conversation</string>
|
||||
<string name="title_legend_cc">CC/BCC</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_answered">Answered</string>
|
||||
<string name="title_legend_contacts">Contacts</string>
|
||||
|
@ -418,6 +421,8 @@
|
|||
<string name="title_undo">Undo</string>
|
||||
<string name="title_add">Add</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_no_ask_again">Do not ask this again</string>
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue