Expandable messages

This commit is contained in:
M66B
2018-10-15 10:05:42 +00:00
parent 36a36ae62d
commit 9f753a014e
20 changed files with 1568 additions and 333 deletions

View File

@@ -82,23 +82,24 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
private ListView drawerList;
private ActionBarDrawerToggle drawerToggle;
private boolean newMessages = false;
private long attachment = -1;
private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
static final int REQUEST_SERVICE = 1;
static final int REQUEST_UNSEEN = 2;
static final int REQUEST_UNIFIED = 1;
static final int REQUEST_THREAD = 2;
static final int REQUEST_ERROR = 3;
static final int REQUEST_ATTACHMENT = 1;
static final int REQUEST_INVITE = 2;
static final String ACTION_VIEW_MESSAGES = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGES";
static final String ACTION_VIEW_MESSAGE = BuildConfig.APPLICATION_ID + ".VIEW_MESSAGE";
static final String ACTION_VIEW_THREAD = BuildConfig.APPLICATION_ID + ".VIEW_THREAD";
static final String ACTION_VIEW_FULL = BuildConfig.APPLICATION_ID + ".VIEW_FULL";
static final String ACTION_EDIT_FOLDER = BuildConfig.APPLICATION_ID + ".EDIT_FOLDER";
static final String ACTION_EDIT_ANSWER = BuildConfig.APPLICATION_ID + ".EDIT_ANSWER";
static final String ACTION_STORE_ATTACHMENT = BuildConfig.APPLICATION_ID + ".STORE_ATTACHMENT";
static final String ACTION_SHOW_PRO = BuildConfig.APPLICATION_ID + ".SHOW_PRO";
static final String UPDATE_LATEST_API = "https://api.github.com/repos/M66B/open-source-email/releases/latest";
static final long UPDATE_INTERVAL = 12 * 3600 * 1000L; // milliseconds
@@ -286,19 +287,13 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_VIEW_MESSAGES);
iff.addAction(ACTION_VIEW_MESSAGE);
iff.addAction(ACTION_VIEW_THREAD);
iff.addAction(ACTION_VIEW_FULL);
iff.addAction(ACTION_EDIT_FOLDER);
iff.addAction(ACTION_EDIT_ANSWER);
iff.addAction(ACTION_STORE_ATTACHMENT);
iff.addAction(ACTION_SHOW_PRO);
lbm.registerReceiver(receiver, iff);
if (newMessages) {
newMessages = false;
FragmentManager fm = getSupportFragmentManager();
fm.popBackStackImmediate("unified", 0);
FragmentMessages fragment = (FragmentMessages) fm.findFragmentById(R.id.content_frame);
fragment.onNewMessages();
}
}
@Override
@@ -463,12 +458,13 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
}
private void checkIntent(Intent intent) {
Log.i(Helper.TAG, "View intent=" + intent + " action=" + intent.getAction());
String action = intent.getAction();
if ("notification".equals(action)) {
Log.i(Helper.TAG, "View intent=" + intent + " action=" + action);
if (action != null && action.startsWith("thread")) {
intent.setAction(null);
setIntent(intent);
newMessages = true;
intent.putExtra("id", Long.parseLong(action.split(":")[1]));
onViewThread(intent);
}
}
@@ -739,14 +735,18 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
public void onReceive(Context context, Intent intent) {
if (ACTION_VIEW_MESSAGES.equals(intent.getAction()))
onViewMessages(intent);
else if (ACTION_VIEW_MESSAGE.equals(intent.getAction()))
onViewMessage(intent);
else if (ACTION_VIEW_THREAD.equals(intent.getAction()))
onViewThread(intent);
else if (ACTION_VIEW_FULL.equals(intent.getAction()))
onViewFull(intent);
else if (ACTION_EDIT_FOLDER.equals(intent.getAction()))
onEditFolder(intent);
else if (ACTION_EDIT_ANSWER.equals(intent.getAction()))
onEditAnswer(intent);
else if (ACTION_STORE_ATTACHMENT.equals(intent.getAction()))
onStoreAttachment(intent);
else if (ACTION_SHOW_PRO.equals(intent.getAction()))
onShowPro(intent);
}
};
@@ -758,65 +758,25 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
fragmentTransaction.commit();
}
private void onViewMessage(Intent intent) {
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
TupleMessageEx message = (TupleMessageEx) args.getSerializable("message");
private void onViewThread(Intent intent) {
Bundle args = new Bundle();
args.putLong("thread", intent.getLongExtra("id", -1));
DB db = DB.getInstance(context);
try {
db.beginTransaction();
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(args);
if (!EntityFolder.OUTBOX.equals(message.folderType)) {
if (!message.content)
EntityOperation.queue(db, message, EntityOperation.BODY);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("thread");
fragmentTransaction.commit();
}
if (!message.threaded) {
db.message().setMessageUiSeen(message.id, true);
EntityOperation.queue(db, message, EntityOperation.SEEN, true);
}
}
private void onViewFull(Intent intent) {
FragmentWebView fragment = new FragmentWebView();
fragment.setArguments(intent.getExtras());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
EntityOperation.process(context);
return null;
}
@Override
protected void onLoaded(Bundle args, Void result) {
TupleMessageEx message = (TupleMessageEx) args.getSerializable("message");
if (message.threaded) {
Bundle targs = new Bundle();
targs.putLong("thread", message.id);
FragmentMessages fragment = new FragmentMessages();
fragment.setArguments(targs);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("thread");
fragmentTransaction.commit();
} else {
FragmentMessage fragment = new FragmentMessage();
fragment.setArguments(args);
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("message");
fragmentTransaction.commit();
}
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(ActivityView.this, ex);
}
}.load(ActivityView.this, intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
fragmentTransaction.commit();
}
private void onEditFolder(Intent intent) {
@@ -844,6 +804,12 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB
startActivityForResult(create, REQUEST_ATTACHMENT);
}
private void onShowPro(Intent intent) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
fragmentTransaction.commit();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.i(Helper.TAG, "View onActivityResult request=" + requestCode + " result=" + resultCode + " data=" + data);

File diff suppressed because it is too large Load Diff

View File

@@ -75,9 +75,6 @@ public interface DaoAccount {
@Update
void updateAccount(EntityAccount account);
@Query("UPDATE account SET seen_until = :time WHERE id = :id")
int setAccountSeenUntil(long id, long time);
@Query("UPDATE account SET state = :state WHERE id = :id")
int setAccountState(long id, String state);

View File

@@ -38,7 +38,6 @@ public interface DaoMessage {
@Query("SELECT message.*" +
", account.name AS accountName, account.color AS accountColor" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", SUM(CASE WHEN folder.type = '" + EntityFolder.ARCHIVE + "' THEN 0 ELSE 1 END) > 1 AS threaded" +
", COUNT(message.id) AS count" +
", SUM(CASE WHEN message.ui_seen" +
" OR folder.type = '" + EntityFolder.ARCHIVE + "'" +
@@ -67,7 +66,6 @@ public interface DaoMessage {
@Query("SELECT message.*" +
", account.name AS accountName, account.color AS accountColor" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", SUM(CASE WHEN folder.type = '" + EntityFolder.ARCHIVE + "' THEN 0 ELSE 1 END) > 1 AS threaded" +
", COUNT(message.id) AS count" +
", SUM(CASE WHEN message.ui_seen" +
" OR (folder.id <> :folder AND folder.type = '" + EntityFolder.ARCHIVE + "')" +
@@ -98,7 +96,6 @@ public interface DaoMessage {
@Query("SELECT message.*" +
", account.name AS accountName, account.color AS accountColor" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", 0 AS threaded" +
", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" +
", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" +
", CASE WHEN message.ui_flagged THEN 0 ELSE 1 END AS unflagged" +
@@ -147,7 +144,6 @@ public interface DaoMessage {
@Query("SELECT message.*" +
", account.name AS accountName, account.color AS accountColor" +
", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType" +
", 0 AS threaded" +
", (SELECT COUNT(m1.id) FROM message m1 WHERE m1.account = message.account AND m1.thread = message.thread AND NOT m1.ui_hide) AS count" +
", CASE WHEN message.ui_seen THEN 0 ELSE 1 END AS unseen" +
", CASE WHEN message.ui_flagged THEN 0 ELSE 1 END AS unflagged" +
@@ -164,7 +160,6 @@ public interface DaoMessage {
" WHERE account.`synchronize`" +
" AND folder.unified" +
" AND NOT message.ui_seen AND NOT message.ui_hide" +
" AND (account.seen_until IS NULL OR message.stored > account.seen_until)" +
" ORDER BY message.received")
LiveData<List<EntityMessage>> liveUnseenUnified();

View File

@@ -57,7 +57,7 @@ public class EntityAccount {
public Boolean store_sent; // obsolete
@NonNull
public Integer poll_interval; // keep-alive interval
public Long seen_until;
public Long seen_until; // obsolete
public String state;
public String error;
@@ -76,7 +76,6 @@ public class EntityAccount {
this.primary.equals(other.primary) &&
(this.color == null ? other.color == null : this.color.equals(other.color)) &&
this.poll_interval.equals(other.poll_interval) &&
(this.seen_until == null ? other.seen_until == null : this.seen_until.equals(other.seen_until)) &&
(this.state == null ? other.state == null : this.state.equals(other.state)) &&
(this.error == null ? other.error == null : this.error.equals(other.error)));
} else

View File

@@ -189,6 +189,7 @@ public class EntityMessage implements Serializable {
(this.uid == null ? other.uid == null : this.uid.equals(other.uid)) &&
(this.msgid == null ? other.msgid == null : this.msgid.equals(other.msgid)) &&
(this.references == null ? other.references == null : this.references.equals(other.references)) &&
(this.deliveredto == null ? other.deliveredto == null : this.deliveredto.equals(other.deliveredto)) &&
(this.inreplyto == null ? other.inreplyto == null : this.inreplyto.equals(other.inreplyto)) &&
(this.thread == null ? other.thread == null : this.thread.equals(other.thread)) &&
(this.avatar == null ? other.avatar == null : this.avatar.equals(other.avatar)) &&
@@ -199,12 +200,14 @@ public class EntityMessage implements Serializable {
equal(this.reply, other.reply) &&
(this.headers == null ? other.headers == null : this.headers.equals(other.headers)) &&
(this.subject == null ? other.subject == null : this.subject.equals(other.subject)) &&
(this.size == null ? other.size == null : this.size.equals(other.size)) &&
this.content == other.content &&
(this.sent == null ? other.sent == null : this.sent.equals(other.sent)) &&
this.received.equals(other.received) &&
this.stored.equals(other.stored) &&
this.seen.equals(other.seen) &&
this.ui_seen.equals(other.ui_seen) &&
this.flagged.equals(other.flagged) &&
this.ui_seen.equals(other.ui_seen) &&
this.ui_flagged.equals(other.ui_flagged) &&
this.ui_hide.equals(other.ui_hide) &&
this.ui_found.equals(other.ui_found) &&

View File

@@ -71,7 +71,6 @@ import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
@@ -683,10 +682,8 @@ public class FragmentAccount extends FragmentEx {
account.primary = (account.synchronize && primary);
account.poll_interval = Integer.parseInt(interval);
if (!update)
account.seen_until = new Date().getTime();
account.store_sent = false;
account.store_sent = false; // obsolete
account.seen_until = null; // obsolete
if (!synchronize)
account.error = null;

View File

@@ -167,7 +167,7 @@ public class FragmentMessage extends FragmentEx {
ivFlagged = view.findViewById(R.id.ivFlagged);
ivAvatar = view.findViewById(R.id.ivAvatar);
tvFrom = view.findViewById(R.id.tvFrom);
ivContactAdd = view.findViewById(R.id.ivContactAdd);
ivContactAdd = view.findViewById(R.id.ivAddContact);
tvTime = view.findViewById(R.id.tvTime);
tvCount = view.findViewById(R.id.tvCount);
tvTo = view.findViewById(R.id.tvTo);

View File

@@ -42,7 +42,6 @@ import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -185,7 +184,7 @@ public class FragmentMessages extends FragmentEx {
return 0;
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
if (message == null || message.threaded || EntityFolder.OUTBOX.equals(message.folderType))
if (message == null || viewType != AdapterMessage.ViewType.THREAD || EntityFolder.OUTBOX.equals(message.folderType))
return 0;
return makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
@@ -403,7 +402,7 @@ public class FragmentMessages extends FragmentEx {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
grpHintSupport.setVisibility(prefs.getBoolean("app_support", false) ? View.GONE : View.VISIBLE);
grpHintActions.setVisibility(prefs.getBoolean("message_actions", false) ? View.GONE : View.VISIBLE);
grpHintActions.setVisibility(prefs.getBoolean("message_actions", false) || viewType != AdapterMessage.ViewType.THREAD ? View.GONE : View.VISIBLE);
final DB db = DB.getInstance(getContext());
@@ -522,37 +521,6 @@ public class FragmentMessages extends FragmentEx {
public void onResume() {
super.onResume();
grpSupport.setVisibility(Helper.isPro(getContext()) ? View.GONE : View.VISIBLE);
if (viewType == AdapterMessage.ViewType.UNIFIED) {
Bundle args = new Bundle();
args.putLong("time", new Date().getTime());
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
long time = args.getLong("time");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
for (EntityAccount account : db.account().getAccounts(true))
db.account().setAccountSeenUntil(account.id, time);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), ex);
}
}.load(this, args);
}
}
@Override
@@ -793,8 +761,4 @@ public class FragmentMessages extends FragmentEx {
});
}
void onNewMessages() {
rvMessage.scrollToPosition(0);
}
}

View File

@@ -122,7 +122,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
public class ServiceSynchronize extends LifecycleService {
private TupleAccountStats stats = null;
private final Object lock = new Object();
private ServiceManager serviceManager = new ServiceManager();
@@ -188,7 +187,6 @@ public class ServiceSynchronize extends LifecycleService {
db.account().liveStats().observe(this, new Observer<TupleAccountStats>() {
@Override
public void onChanged(@Nullable TupleAccountStats stats) {
ServiceSynchronize.this.stats = stats;
NotificationManager nm = getSystemService(NotificationManager.class);
nm.notify(NOTIFICATION_SYNCHRONIZE,
getNotificationService(stats.accounts, stats.operations, stats.unsent).build());
@@ -241,37 +239,7 @@ public class ServiceSynchronize extends LifecycleService {
serviceManager.queue_stop();
else if ("reload".equals(action))
serviceManager.queue_reload();
else if ("until".equals(action)) {
Bundle args = new Bundle();
args.putLong("time", new Date().getTime());
new SimpleTask<Void>() {
@Override
protected Void onLoad(Context context, Bundle args) {
long time = args.getLong("time");
DB db = DB.getInstance(context);
try {
db.beginTransaction();
for (EntityAccount account : db.account().getAccounts(true))
db.account().setAccountSeenUntil(account.id, time);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
@Override
protected void onLoaded(Bundle args, Void data) {
Log.i(Helper.TAG, "Updated seen until");
}
}.load(this, args);
} else if (action.startsWith("seen:") || action.startsWith("trash:")) {
else if (action.startsWith("seen:") || action.startsWith("trash:")) {
Bundle args = new Bundle();
args.putLong("id", Long.parseLong(action.split(":")[1]));
args.putString("action", action.split(":")[0]);
@@ -323,7 +291,7 @@ public class ServiceSynchronize extends LifecycleService {
Intent intent = new Intent(this, ActivityView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pi = PendingIntent.getActivity(
this, ActivityView.REQUEST_SERVICE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
this, ActivityView.REQUEST_UNIFIED, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Build notification
Notification.Builder builder;
@@ -364,15 +332,9 @@ public class ServiceSynchronize extends LifecycleService {
// Build pending intent
Intent view = new Intent(this, ActivityView.class);
view.setAction("notification");
view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent piView = PendingIntent.getActivity(
this, ActivityView.REQUEST_UNSEEN, view, PendingIntent.FLAG_UPDATE_CURRENT);
Intent until = new Intent(this, ServiceSynchronize.class);
until.setAction("until");
PendingIntent piUntil = PendingIntent.getService(
this, PI_UNSEEN, until, PendingIntent.FLAG_UPDATE_CURRENT);
this, ActivityView.REQUEST_UNIFIED, view, PendingIntent.FLAG_UPDATE_CURRENT);
// Build notification
Notification.Builder builder;
@@ -388,10 +350,10 @@ public class ServiceSynchronize extends LifecycleService {
.setContentIntent(piView)
.setNumber(messages.size())
.setShowWhen(false)
.setOngoing(true)
.setPriority(Notification.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_STATUS)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setDeleteIntent(piUntil)
.setGroup(BuildConfig.APPLICATION_ID)
.setGroupSummary(true);
@@ -428,6 +390,12 @@ public class ServiceSynchronize extends LifecycleService {
Bundle args = new Bundle();
args.putLong("id", message.id);
Intent thread = new Intent(this, ActivityView.class);
thread.setAction("thread:" + message.id);
thread.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent piThread = PendingIntent.getActivity(
this, ActivityView.REQUEST_THREAD, thread, PendingIntent.FLAG_UPDATE_CURRENT);
Intent seen = new Intent(this, ServiceSynchronize.class);
seen.setAction("seen:" + message.id);
PendingIntent piSeen = PendingIntent.getService(this, PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -456,9 +424,10 @@ public class ServiceSynchronize extends LifecycleService {
.addExtras(args)
.setSmallIcon(R.drawable.baseline_mail_24)
.setContentTitle(MessageHelper.getFormattedAddresses(message.from, true))
.setContentIntent(piView)
.setContentIntent(piThread)
.setSound(uri)
.setWhen(message.sent == null ? message.received : message.sent)
.setOngoing(true)
.setPriority(Notification.PRIORITY_DEFAULT)
.setCategory(Notification.CATEGORY_STATUS)
.setVisibility(Notification.VISIBILITY_PRIVATE)

View File

@@ -25,7 +25,6 @@ public class TupleMessageEx extends EntityMessage {
public String folderName;
public String folderDisplay;
public String folderType;
public boolean threaded;
public int count;
public int unseen;
public int unflagged;
@@ -41,7 +40,6 @@ public class TupleMessageEx extends EntityMessage {
this.folderName.equals(other.folderName) &&
(this.folderDisplay == null ? other.folderDisplay == null : this.folderDisplay.equals(other.folderDisplay)) &&
this.folderType.equals(other.folderType) &&
this.threaded == other.threaded &&
this.count == other.count &&
this.unseen == other.unseen &&
this.unflagged == other.unflagged &&