package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see .
Copyright 2018-2019 by Marcel Bokhorst (M66B)
*/
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.LongSparseArray;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.TranslateAnimation;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.SearchView;
import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.paging.PagedList;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.selection.Selection;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.android.colorpicker.ColorPickerDialog;
import com.android.colorpicker.ColorPickerSwatch;
import com.bugsnag.android.Bugsnag;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.text.Collator;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
import static android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY;
public class FragmentMessages extends FragmentBase implements SharedPreferences.OnSharedPreferenceChangeListener {
private ViewGroup view;
private SwipeRefreshLayout swipeRefresh;
private TextView tvSupport;
private ImageButton ibHintSupport;
private ImageButton ibHintSwipe;
private ImageButton ibHintSelect;
private TextView tvNoEmail;
private FixedRecyclerView rvMessage;
private SeekBar seekBar;
private ImageButton ibDown;
private ImageButton ibUp;
private BottomNavigationView bottom_navigation;
private ContentLoadingProgressBar pbWait;
private Group grpSupport;
private Group grpHintSupport;
private Group grpHintSwipe;
private Group grpHintSelect;
private Group grpReady;
private FloatingActionButton fab;
private FloatingActionButton fabMore;
private FloatingActionButton fabSearch;
private FloatingActionButton fabError;
private long account;
private long folder;
private boolean server;
private String thread;
private long id;
private boolean found;
private String query;
private boolean pane;
private boolean date;
private boolean threading;
private boolean pull;
private boolean swipenav;
private boolean autoscroll;
private boolean actionbar;
private boolean autoexpand;
private boolean autoclose;
private boolean autonext;
private boolean addresses;
private int colorPrimary;
private int colorAccent;
private long primary;
private boolean outbox = false;
private boolean connected;
private boolean reset = false;
private String searching = null;
private boolean loading = false;
private boolean manual = false;
private Integer lastUnseen = null;
private boolean swiping = false;
private AdapterMessage adapter;
private AdapterMessage.ViewType viewType;
private SelectionPredicateMessage selectionPredicate = null;
private SelectionTracker selectionTracker = null;
private Long previous = null;
private Long next = null;
private Long closeNext = null;
private int autoCloseCount = 0;
private boolean autoExpanded = true;
private Map> values = new HashMap<>();
private LongSparseArray bodies = new LongSparseArray<>();
private LongSparseArray html = new LongSparseArray<>();
private LongSparseArray> attachments = new LongSparseArray<>();
private LongSparseArray accountSwipes = new LongSparseArray<>();
private NumberFormat nf = NumberFormat.getNumberInstance();
private static final int UNDO_TIMEOUT = 5000; // milliseconds
private static final int SWIPE_DISABLE_SELECT_DURATION = 1500; // milliseconds
private static final List DUPLICATE_ORDER = Collections.unmodifiableList(Arrays.asList(
EntityFolder.INBOX,
EntityFolder.OUTBOX,
EntityFolder.DRAFTS,
EntityFolder.SENT,
EntityFolder.SYSTEM,
EntityFolder.USER,
EntityFolder.ARCHIVE,
EntityFolder.TRASH,
EntityFolder.JUNK
));
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get arguments
Bundle args = getArguments();
account = args.getLong("account", -1);
folder = args.getLong("folder", -1);
server = args.getBoolean("server", false);
thread = args.getString("thread");
id = args.getLong("id", -1);
found = args.getBoolean("found", false);
query = args.getString("query");
pane = args.getBoolean("pane", false);
primary = args.getLong("primary", -1);
connected = args.getBoolean("connected", false);
if (TextUtils.isEmpty(query))
if (thread == null)
if (folder < 0)
viewType = AdapterMessage.ViewType.UNIFIED;
else
viewType = AdapterMessage.ViewType.FOLDER;
else
viewType = AdapterMessage.ViewType.THREAD;
else
viewType = AdapterMessage.ViewType.SEARCH;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
if (viewType == AdapterMessage.ViewType.UNIFIED || viewType == AdapterMessage.ViewType.FOLDER)
pull = prefs.getBoolean("pull", true);
else
pull = false;
swipenav = prefs.getBoolean("swipenav", true);
autoscroll = (prefs.getBoolean("autoscroll", false) || viewType == AdapterMessage.ViewType.THREAD);
date = prefs.getBoolean("date", true);
threading = prefs.getBoolean("threading", true);
actionbar = prefs.getBoolean("actionbar", true);
autoexpand = prefs.getBoolean("autoexpand", true);
autoclose = prefs.getBoolean("autoclose", true);
autonext = (!autoclose && prefs.getBoolean("autonext", false));
addresses = prefs.getBoolean("addresses", false);
colorPrimary = Helper.resolveColor(getContext(), R.attr.colorPrimary);
colorAccent = Helper.resolveColor(getContext(), R.attr.colorAccent);
}
@Override
@Nullable
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
setHasOptionsMenu(true);
view = (ViewGroup) inflater.inflate(R.layout.fragment_messages, container, false);
// Get controls
swipeRefresh = view.findViewById(R.id.swipeRefresh);
tvSupport = view.findViewById(R.id.tvSupport);
ibHintSupport = view.findViewById(R.id.ibHintSupport);
ibHintSwipe = view.findViewById(R.id.ibHintSwipe);
ibHintSelect = view.findViewById(R.id.ibHintSelect);
tvNoEmail = view.findViewById(R.id.tvNoEmail);
rvMessage = view.findViewById(R.id.rvMessage);
seekBar = view.findViewById(R.id.seekBar);
ibDown = view.findViewById(R.id.ibDown);
ibUp = view.findViewById(R.id.ibUp);
bottom_navigation = view.findViewById(R.id.bottom_navigation);
pbWait = view.findViewById(R.id.pbWait);
grpSupport = view.findViewById(R.id.grpSupport);
grpHintSupport = view.findViewById(R.id.grpHintSupport);
grpHintSwipe = view.findViewById(R.id.grpHintSwipe);
grpHintSelect = view.findViewById(R.id.grpHintSelect);
grpReady = view.findViewById(R.id.grpReady);
fab = view.findViewById(R.id.fab);
fabSearch = view.findViewById(R.id.fabSearch);
fabMore = view.findViewById(R.id.fabMore);
fabError = view.findViewById(R.id.fabError);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
// Wire controls
swipeRefresh.setColorSchemeColors(Color.WHITE, Color.WHITE, Color.WHITE);
swipeRefresh.setProgressBackgroundColorSchemeColor(colorPrimary);
swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
onSwipeRefresh();
}
});
tvSupport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(new Intent(ActivityView.ACTION_SHOW_PRO));
}
});
ibHintSupport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
prefs.edit().putBoolean("app_support", true).apply();
grpHintSupport.setVisibility(View.GONE);
}
});
ibHintSwipe.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
prefs.edit().putBoolean("message_swipe", true).apply();
grpHintSwipe.setVisibility(View.GONE);
}
});
ibHintSelect.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
prefs.edit().putBoolean("message_select", true).apply();
grpHintSelect.setVisibility(View.GONE);
}
});
rvMessage.setHasFixedSize(false);
//rvMessage.setItemViewCacheSize(10);
//rvMessage.getRecycledViewPool().setMaxRecycledViews(0, 10);
final LinearLayoutManager llm = new LinearLayoutManager(getContext());
rvMessage.setLayoutManager(llm);
DividerItemDecoration itemDecorator = new DividerItemDecoration(getContext(), llm.getOrientation()) {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (view.findViewById(R.id.clItem).getVisibility() == View.GONE)
outRect.setEmpty();
else
super.getItemOffsets(outRect, view, parent, state);
}
};
itemDecorator.setDrawable(getContext().getDrawable(R.drawable.divider));
rvMessage.addItemDecoration(itemDecorator);
DividerItemDecoration dateDecorator = new DividerItemDecoration(getContext(), llm.getOrientation()) {
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
for (int i = 0; i < parent.getChildCount(); i++) {
View view = parent.getChildAt(i);
int pos = parent.getChildAdapterPosition(view);
View header = getView(parent, pos);
if (header != null) {
canvas.save();
canvas.translate(0, parent.getChildAt(i).getTop() - header.getMeasuredHeight());
header.draw(canvas);
canvas.restore();
}
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int pos = parent.getChildAdapterPosition(view);
View header = getView(parent, pos);
if (header == null)
outRect.setEmpty();
else
outRect.top = header.getMeasuredHeight();
}
private View getView(RecyclerView parent, int pos) {
if (!date || !"time".equals(adapter.getSort()))
return null;
if (pos == RecyclerView.NO_POSITION)
return null;
TupleMessageEx prev = (pos > 0 ? adapter.getCurrentList().get(pos - 1) : null);
TupleMessageEx message = adapter.getCurrentList().get(pos);
if (pos > 0 && prev == null)
return null;
if (message == null)
return null;
if (pos > 0) {
Calendar cal0 = Calendar.getInstance();
Calendar cal1 = Calendar.getInstance();
cal0.setTimeInMillis(prev.received);
cal1.setTimeInMillis(message.received);
int year0 = cal0.get(Calendar.YEAR);
int year1 = cal1.get(Calendar.YEAR);
int day0 = cal0.get(Calendar.DAY_OF_YEAR);
int day1 = cal1.get(Calendar.DAY_OF_YEAR);
if (year0 == year1 && day0 == day1)
return null;
}
View header = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message_date, parent, false);
TextView tvDate = header.findViewById(R.id.tvDate);
tvDate.setTextSize(TypedValue.COMPLEX_UNIT_PX, Helper.getTextSize(parent.getContext(), adapter.getZoom()));
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.DAY_OF_MONTH, -2);
if (message.received <= cal.getTimeInMillis())
tvDate.setText(
DateUtils.formatDateRange(
parent.getContext(),
message.received,
message.received,
FORMAT_SHOW_WEEKDAY | FORMAT_SHOW_DATE));
else
tvDate.setText(
DateUtils.getRelativeTimeSpanString(
message.received,
new Date().getTime(),
DAY_IN_MILLIS, 0));
header.measure(View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
return header;
}
};
rvMessage.addItemDecoration(dateDecorator);
boolean compact = prefs.getBoolean("compact", false);
int zoom = prefs.getInt("zoom", compact ? 0 : 1);
String sort = prefs.getString("sort", "time");
boolean filter_duplicates = prefs.getBoolean("filter_duplicates", false);
adapter = new AdapterMessage(
getContext(), getViewLifecycleOwner(),
viewType, compact, zoom, sort, filter_duplicates, iProperties);
rvMessage.setAdapter(adapter);
/*
rvMessage.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
int pos = (rv.getChildCount() == 0 ? 0 : rv.getChildAt(0).getTop());
swipeRefresh.setEnabled(pull && pos >= 0);
}
});
*/
seekBar.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
ibDown.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scrollToVisibleItem(llm, true);
}
});
ibUp.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scrollToVisibleItem(llm, false);
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
switch (menuItem.getItemId()) {
case R.id.action_delete:
onActionMove(EntityFolder.TRASH);
return true;
case R.id.action_snooze:
onActionSnooze();
return true;
case R.id.action_archive:
onActionMove(EntityFolder.ARCHIVE);
return true;
case R.id.action_prev:
navigate(previous, true);
return true;
case R.id.action_next:
navigate(next, false);
return true;
default:
return false;
}
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(getContext(), ActivityCompose.class)
.putExtra("action", "new")
.putExtra("account", account)
);
}
});
fab.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Bundle args = new Bundle();
args.putLong("account", account);
new SimpleTask() {
@Override
protected EntityFolder onExecute(Context context, Bundle args) {
long account = args.getLong("account");
DB db = DB.getInstance(context);
if (account < 0)
return db.folder().getPrimaryDrafts();
else
return db.folder().getFolderByType(account, EntityFolder.DRAFTS);
}
@Override
protected void onExecuted(Bundle args, EntityFolder drafts) {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
lbm.sendBroadcast(
new Intent(ActivityView.ACTION_VIEW_MESSAGES)
.putExtra("account", drafts.account)
.putExtra("folder", drafts.id));
}
@Override
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex);
}
}.execute(FragmentMessages.this, args, "messages:drafts");
return true;
}
});
fabSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (folder < 0) {
Bundle args = new Bundle();
new SimpleTask