Added snoozing messages

This commit is contained in:
M66B 2019-01-07 15:05:24 +00:00
parent 0bcb335203
commit e3e0f58197
17 changed files with 1622 additions and 20 deletions

2
FAQ.md
View File

@ -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.

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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) {

View File

@ -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();
}

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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();
}

View 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>

View 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>

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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>