diff --git a/res/drawable-xxhdpi/last_seen_background.9.png b/res/drawable-xxhdpi/last_seen_background.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..e84de6dd3d24d770d7f7510b8488a0898f9fd0e8
Binary files /dev/null and b/res/drawable-xxhdpi/last_seen_background.9.png differ
diff --git a/res/drawable/last_seen_divider_text_background_dark.xml b/res/drawable/last_seen_divider_text_background_dark.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ca657c96bb359d70929a59ab0525013ee35589d7
--- /dev/null
+++ b/res/drawable/last_seen_divider_text_background_dark.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/last_seen_divider_text_background_light.xml b/res/drawable/last_seen_divider_text_background_light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..63ce3665621d937d44f03ebad16a78ec9c6842de
--- /dev/null
+++ b/res/drawable/last_seen_divider_text_background_light.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/res/layout/conversation_item_last_seen.xml b/res/layout/conversation_item_last_seen.xml
new file mode 100644
index 0000000000000000000000000000000000000000..327d104ffaee8c812aa2f1d874e1e99be3c424cc
--- /dev/null
+++ b/res/layout/conversation_item_last_seen.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 7ad39d342ba2711b7ba6b6debb0e19ce9e9707c5..1a1dd743af5e8152cd0990f6c09affaed551c7db 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -67,6 +67,8 @@
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c7cab7c89fafe8a84d49909beedef2ce1bc4872a..82f4bce08deadc4ade96d8ee14a9e0c9383489a5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -124,6 +124,12 @@
Please choose a contact
Attachment exceeds size limits.
+
+
+ - %d unread message
+ - %d unread messages
+
+
Message details
@@ -857,6 +863,7 @@
You
Failed to preview this image
Unsupported media type
+ Draft
Save
@@ -877,7 +884,6 @@
Transport icon
-
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 04b925353bd7ffb333cdbb4ba9e5e53ef5ebd957..3e50ae116333d4acef1bb55acdb7da320746f477 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -144,6 +144,8 @@
- @color/white
- #BFffffff
- @drawable/conversation_item_header_background
+ - @drawable/last_seen_background
+ - @drawable/last_seen_divider_text_background_light
- @drawable/conversation_item_sent_indicator_text_shape
@@ -225,6 +227,8 @@
- #BFffffff
- @drawable/conversation_item_sent_indicator_text_shape_dark
- @drawable/conversation_item_header_background_dark
+ - #66333333
+ - @drawable/last_seen_divider_text_background_dark
- @drawable/ic_info_outline_dark
- @drawable/ic_warning_dark
diff --git a/res/xml/recipient_preferences.xml b/res/xml/recipient_preferences.xml
index d8d8a69b8841ac8a1025d0aaf43ba1a94a9b95c5..7b5f54f55304cdedc8605ce2646aaccf59a8f05e 100644
--- a/res/xml/recipient_preferences.xml
+++ b/res/xml/recipient_preferences.xml
@@ -14,7 +14,7 @@
android:key="pref_key_recipient_ringtone"
android:title="@string/recipient_preferences__notification_sound"
android:ringtoneType="notification"
- android:showSilent="false"
+ android:showSilent="true"
android:showDefault="true"
android:persistent="false"/>
diff --git a/src/org/smssecure/smssecure/ConversationActivity.java b/src/org/smssecure/smssecure/ConversationActivity.java
index 6b5cb844f015e4ce3618dca40a180bb95044ea4c..319c3597fbd13c4d4df8e6218cbfb652bbc10954 100644
--- a/src/org/smssecure/smssecure/ConversationActivity.java
+++ b/src/org/smssecure/smssecure/ConversationActivity.java
@@ -156,6 +156,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public static final String TEXT_EXTRA = "draft_text";
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
public static final String TIMING_EXTRA = "timing";
+ public static final String LAST_SEEN_EXTRA = "last_seen";
private static final int PICK_IMAGE = 1;
private static final int PICK_VIDEO = 2;
@@ -274,6 +275,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
super.onPause();
MessageNotifier.setVisibleThread(-1L);
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
+ fragment.setLastSeen(System.currentTimeMillis());
+ markLastSeen();
AudioSlidePlayer.stopAll();
}
@@ -1171,6 +1174,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(threadId);
}
+ private void markLastSeen() {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Long... params) {
+ DatabaseFactory.getThreadDatabase(ConversationActivity.this).setLastSeen(params[0]);
+ return null;
+ }
+ }.execute(threadId);
+ }
+
protected void sendComplete(long threadId) {
boolean refreshFragment = (threadId != this.threadId);
this.threadId = threadId;
@@ -1179,6 +1192,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return;
}
+ fragment.setLastSeen(0);
+
if (refreshFragment) {
fragment.reload(recipients, threadId);
diff --git a/src/org/smssecure/smssecure/ConversationAdapter.java b/src/org/smssecure/smssecure/ConversationAdapter.java
index 5d601685cc8203f435c0632eb7d18b1db36cc8d4..01d4859179be4ffa597d6adcdd27e8a112241a55 100644
--- a/src/org/smssecure/smssecure/ConversationAdapter.java
+++ b/src/org/smssecure/smssecure/ConversationAdapter.java
@@ -18,6 +18,8 @@ package org.smssecure.smssecure;
import android.content.Context;
import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Rect;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -105,15 +107,15 @@ public class ConversationAdapter
}
}
- protected static class HeaderViewHolder extends RecyclerView.ViewHolder {
- protected TextView textView;
+ static class HeaderViewHolder extends RecyclerView.ViewHolder {
+ TextView textView;
- public HeaderViewHolder(View itemView) {
+ HeaderViewHolder(View itemView) {
super(itemView);
textView = ViewUtil.findById(itemView, R.id.text);
}
- public HeaderViewHolder(TextView textView) {
+ HeaderViewHolder(TextView textView) {
super(textView);
this.textView = textView;
}
@@ -265,6 +267,24 @@ public class ConversationAdapter
getCursor().close();
}
+ public int findLastSeenPosition(long lastSeen) {
+ if (lastSeen <= 0) return -1;
+ if (!isActiveCursor()) return -1;
+
+ int count = getItemCount();
+
+ for (int i=0;i
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
}
+ public long getReceivedTimestamp(int position) {
+ if (!isActiveCursor()) return 0;
+ if (isHeaderPosition(position)) return 0;
+ if (isFooterPosition(position)) return 0;
+ if (position >= getItemCount()) return 0;
+ if (position < 0) return 0;
+
+ Cursor cursor = getCursorAtPositionOrThrow(position);
+ MessageRecord messageRecord = getMessageRecord(cursor);
+
+ if (messageRecord.isOutgoing()) return 0;
+ else return messageRecord.getDateReceived();
+ }
+
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false));
}
+ public HeaderViewHolder onCreateLastSeenViewHolder(ViewGroup parent) {
+ return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
+ }
+
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
Cursor cursor = getCursorAtPositionOrThrow(position);
viewHolder.setText(DateUtils.getRelativeDate(getContext(), locale, getMessageRecord(cursor).getDateReceived()));
}
+
+ public void onBindLastSeenViewHolder(HeaderViewHolder viewHolder, int position) {
+ viewHolder.setText(getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
+ }
+
+ static class LastSeenHeader extends StickyHeaderDecoration {
+
+ private final ConversationAdapter adapter;
+ private final long lastSeenTimestamp;
+
+ LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
+ super(adapter, false, false);
+ this.adapter = adapter;
+ this.lastSeenTimestamp = lastSeenTimestamp;
+ }
+
+ @Override
+ protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
+ if (!adapter.isActiveCursor()) {
+ return false;
+ }
+
+ if (lastSeenTimestamp <= 0) {
+ return false;
+ }
+
+ long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
+ long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
+
+ return (currentRecordTimestamp > lastSeenTimestamp) && (previousRecordTimestamp < lastSeenTimestamp);
+ }
+
+ @Override
+ protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
+ return parent.getLayoutManager().getDecoratedTop(child);
+ }
+
+ @Override
+ protected HeaderViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
+ HeaderViewHolder viewHolder = adapter.onCreateLastSeenViewHolder(parent);
+ adapter.onBindLastSeenViewHolder(viewHolder, position);
+
+ int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
+ int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
+
+ int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
+ int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
+
+ viewHolder.itemView.measure(childWidth, childHeight);
+ viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
+
+ return viewHolder;
+ }
+ }
+
}
diff --git a/src/org/smssecure/smssecure/ConversationFragment.java b/src/org/smssecure/smssecure/ConversationFragment.java
index adfd2ad5ee18be477d146ec2fb1f7903281e9551..b90522a66be129cf8571927a8cb8bc8c49e01b5f 100644
--- a/src/org/smssecure/smssecure/ConversationFragment.java
+++ b/src/org/smssecure/smssecure/ConversationFragment.java
@@ -88,16 +88,19 @@ public class ConversationFragment extends Fragment
private ConversationFragmentListener listener;
- private MasterSecret masterSecret;
- private Recipients recipients;
- private long threadId;
- private ActionMode actionMode;
- private Locale locale;
- private RecyclerView list;
- private View loadMoreView;
- private View composeDivider;
- private View scrollToBottomButton;
- private TextView scrollDateHeader;
+ private MasterSecret masterSecret;
+ private Recipients recipients;
+ private long threadId;
+ private long lastSeen;
+ private boolean firstLoad;
+ private ActionMode actionMode;
+ private Locale locale;
+ private RecyclerView list;
+ private RecyclerView.ItemDecoration lastSeenDecoration;
+ private View loadMoreView;
+ private View composeDivider;
+ private View scrollToBottomButton;
+ private TextView scrollDateHeader;
@Override
public void onCreate(Bundle icicle) {
@@ -180,6 +183,8 @@ public class ConversationFragment extends Fragment
private void initializeResources() {
this.recipients = RecipientFactory.getRecipientsForIds(getActivity(), getActivity().getIntent().getLongArrayExtra("recipients"), true);
this.threadId = this.getActivity().getIntent().getLongExtra("thread_id", -1);
+ this.lastSeen = this.getActivity().getIntent().getLongExtra(ConversationActivity.LAST_SEEN_EXTRA, -1);
+ this.firstLoad = true;
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
list.addOnScrollListener(scrollListener);
@@ -191,6 +196,7 @@ public class ConversationFragment extends Fragment
list.setAdapter(adapter);
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
+ setLastSeen(lastSeen);
getLoaderManager().restartLoader(0, Bundle.EMPTY, this);
list.getItemAnimator().setMoveDuration(120);
}
@@ -253,6 +259,16 @@ public class ConversationFragment extends Fragment
});
}
+ public void setLastSeen(long lastSeen) {
+ this.lastSeen = lastSeen;
+ if (lastSeenDecoration != null) {
+ list.removeItemDecoration(lastSeenDecoration);
+ }
+
+ lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter(), lastSeen);
+ list.addItemDecoration(lastSeenDecoration);
+ }
+
private void handleCopyMessage(final Set messageRecords) {
List messageList = new LinkedList<>(messageRecords);
Collections.sort(messageList, new Comparator() {
@@ -383,18 +399,38 @@ public class ConversationFragment extends Fragment
@Override
public Loader onCreateLoader(int id, Bundle args) {
- return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT));
+ return new ConversationLoader(getActivity(), threadId, args.getLong("limit", PARTIAL_CONVERSATION_LIMIT), lastSeen);
}
+
@Override
- public void onLoadFinished(Loader loader, Cursor cursor) {
+ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
+ Log.w(TAG, "onLoadFinished");
+ ConversationLoader loader = (ConversationLoader)cursorLoader;
+
if (list.getAdapter() != null) {
- if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && ((ConversationLoader)loader).hasLimit()) {
+ if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) {
getListAdapter().setFooterView(loadMoreView);
} else {
getListAdapter().setFooterView(null);
}
+
+ if (lastSeen == -1) {
+ setLastSeen(loader.getLastSeen());
+ }
+
getListAdapter().changeCursor(cursor);
+
+ int lastSeenPosition = getListAdapter().findLastSeenPosition(lastSeen);
+
+ if (firstLoad) {
+ scrollToLastSeenPosition(lastSeenPosition);
+ firstLoad = false;
+ }
+
+ if (lastSeenPosition <= 0) {
+ setLastSeen(0);
+ }
}
}
@@ -405,6 +441,17 @@ public class ConversationFragment extends Fragment
}
}
+ private void scrollToLastSeenPosition(final int lastSeenPosition) {
+ if (lastSeenPosition > 0) {
+ list.post(new Runnable() {
+ @Override
+ public void run() {
+ ((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(lastSeenPosition, list.getHeight());
+ }
+ });
+ }
+ }
+
public interface ConversationFragmentListener {
void setThreadId(long threadId);
}
diff --git a/src/org/smssecure/smssecure/ConversationListActivity.java b/src/org/smssecure/smssecure/ConversationListActivity.java
index dbaba7ef1f04df17f8bc11cf96e918bec64dedec..12d289a1a2c765a7502ade2015ace24f5f4ffc94 100644
--- a/src/org/smssecure/smssecure/ConversationListActivity.java
+++ b/src/org/smssecure/smssecure/ConversationListActivity.java
@@ -154,12 +154,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
}
@Override
- public void onCreateConversation(long threadId, Recipients recipients, int distributionType) {
+ public void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
+ intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeen);
startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
diff --git a/src/org/smssecure/smssecure/ConversationListArchiveActivity.java b/src/org/smssecure/smssecure/ConversationListArchiveActivity.java
index 690e66520213255aaebe939d81144ceb3c903807..c87a4397f0335dc74d6a9e78020f9537dee7684f 100644
--- a/src/org/smssecure/smssecure/ConversationListArchiveActivity.java
+++ b/src/org/smssecure/smssecure/ConversationListArchiveActivity.java
@@ -54,12 +54,13 @@ public class ConversationListArchiveActivity extends PassphraseRequiredActionBar
}
@Override
- public void onCreateConversation(long threadId, Recipients recipients, int distributionType) {
+ public void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeenTime) {
Intent intent = new Intent(this, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.IS_ARCHIVED_EXTRA, true);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
+ intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeenTime);
startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
diff --git a/src/org/smssecure/smssecure/ConversationListFragment.java b/src/org/smssecure/smssecure/ConversationListFragment.java
index 45e7e2fa0651214d449efb13c0e76c8d83fab47c..99ec6a8e461b4b738fd2c4dbf93d277cb4ddf6d1 100644
--- a/src/org/smssecure/smssecure/ConversationListFragment.java
+++ b/src/org/smssecure/smssecure/ConversationListFragment.java
@@ -304,8 +304,8 @@ public class ConversationListFragment extends Fragment
getListAdapter().getBatchSelections().size()));
}
- private void handleCreateConversation(long threadId, Recipients recipients, int distributionType) {
- ((ConversationSelectedListener)getActivity()).onCreateConversation(threadId, recipients, distributionType);
+ private void handleCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen) {
+ ((ConversationSelectedListener)getActivity()).onCreateConversation(threadId, recipients, distributionType, lastSeen);
}
private void handleSendDrafts() {
@@ -435,7 +435,7 @@ public class ConversationListFragment extends Fragment
public void onItemClick(ConversationListItem item) {
if (actionMode == null) {
handleCreateConversation(item.getThreadId(), item.getRecipients(),
- item.getDistributionType());
+ item.getDistributionType(), item.getLastSeen());
} else {
ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
adapter.toggleThreadInBatchSet(item.getThreadId());
@@ -468,7 +468,7 @@ public class ConversationListFragment extends Fragment
}
public interface ConversationSelectedListener {
- void onCreateConversation(long threadId, Recipients recipients, int distributionType);
+ void onCreateConversation(long threadId, Recipients recipients, int distributionType, long lastSeen);
void onSwitchToArchive();
}
diff --git a/src/org/smssecure/smssecure/ConversationListItem.java b/src/org/smssecure/smssecure/ConversationListItem.java
index 5449ec629e3d7ff7449ba522354f8901d1bc5230..bde6a119b4863bb6b2c2df636375f7d64a7cd5af 100644
--- a/src/org/smssecure/smssecure/ConversationListItem.java
+++ b/src/org/smssecure/smssecure/ConversationListItem.java
@@ -73,6 +73,7 @@ public class ConversationListItem extends RelativeLayout
private TextView archivedView;
private DeliveryStatusView deliveryStatusIndicator;
private AlertView alertView;
+ private long lastSeen;
private boolean read;
private AvatarImageView contactPhotoImage;
@@ -119,6 +120,7 @@ public class ConversationListItem extends RelativeLayout
this.threadId = thread.getThreadId();
this.read = thread.isRead();
this.distributionType = thread.getDistributionType();
+ this.lastSeen = thread.getLastSeen();
this.recipients.addListener(this);
this.fromView.setText(recipients, read);
@@ -171,6 +173,10 @@ public class ConversationListItem extends RelativeLayout
return distributionType;
}
+ public long getLastSeen() {
+ return lastSeen;
+ }
+
private void setThumbnailSnippet(MasterSecret masterSecret, ThreadRecord thread) {
if (thread.getSnippetUri() != null) {
this.thumbnailView.setVisibility(View.VISIBLE);
diff --git a/src/org/smssecure/smssecure/MediaPreviewActivity.java b/src/org/smssecure/smssecure/MediaPreviewActivity.java
index 29a5b802ccae6782a86789071e711f3c5f46344f..ac9ed2a40dacddfbbb9a8652c388453942a2258f 100644
--- a/src/org/smssecure/smssecure/MediaPreviewActivity.java
+++ b/src/org/smssecure/smssecure/MediaPreviewActivity.java
@@ -106,7 +106,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (date > 0) {
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this,dynamicLanguage.getCurrentLocale(),date);
} else {
- relativeTimeSpan = null;
+ relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
}
getSupportActionBar().setTitle(recipient == null ? getString(R.string.MediaPreviewActivity_you)
: recipient.toShortString());
@@ -147,7 +147,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
mediaUri = getIntent().getData();
mediaType = getIntent().getType();
- date = getIntent().getLongExtra(DATE_EXTRA, System.currentTimeMillis());
+ date = getIntent().getLongExtra(DATE_EXTRA, -1);
size = getIntent().getLongExtra(SIZE_EXTRA, 0);
threadId = getIntent().getLongExtra(THREAD_ID_EXTRA, -1);
@@ -208,7 +208,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onClick(DialogInterface dialogInterface, int i) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this, masterSecret);
- saveTask.execute(new Attachment(mediaUri, mediaType, date));
+ long saveDate = (date > 0) ? date : System.currentTimeMillis();
+ saveTask.execute(new Attachment(mediaUri, mediaType, saveDate));
}
});
}
diff --git a/src/org/smssecure/smssecure/RecipientPreferenceActivity.java b/src/org/smssecure/smssecure/RecipientPreferenceActivity.java
index 75ad3a5dd0ea57d26244ec60417e82784ef7f5bc..8a9377adeeedc71d1d001b01d3b5dffa5a0554f5 100644
--- a/src/org/smssecure/smssecure/RecipientPreferenceActivity.java
+++ b/src/org/smssecure/smssecure/RecipientPreferenceActivity.java
@@ -18,7 +18,6 @@ import android.support.v4.app.Fragment;
import android.support.v4.preference.PreferenceFragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
-import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
@@ -198,15 +197,21 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
mutePreference.setChecked(recipients.isMuted());
- if (recipients.getRingtone() != null) {
- Ringtone tone = RingtoneManager.getRingtone(getActivity(), recipients.getRingtone());
+ final Uri toneUri = recipients.getRingtone();
+
+ if (toneUri == null) {
+ ringtonePreference.setSummary(R.string.preferences__default);
+ ringtonePreference.setCurrentRingtone(Settings.System.DEFAULT_NOTIFICATION_URI);
+ } else if (toneUri.toString().isEmpty()) {
+ ringtonePreference.setSummary(R.string.preferences__silent);
+ ringtonePreference.setCurrentRingtone(null);
+ } else {
+ Ringtone tone = RingtoneManager.getRingtone(getActivity(), toneUri);
if (tone != null) {
ringtonePreference.setSummary(tone.getTitle(getActivity()));
- ringtonePreference.setCurrentRingtone(recipients.getRingtone());
+ ringtonePreference.setCurrentRingtone(toneUri);
}
- } else {
- ringtonePreference.setSummary(R.string.preferences__default);
}
if (recipients.getVibrate() == VibrateState.DEFAULT) {
@@ -249,7 +254,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
final Uri uri;
- if (TextUtils.isEmpty(value) || Settings.System.DEFAULT_NOTIFICATION_URI.toString().equals(value)) {
+ if (Settings.System.DEFAULT_NOTIFICATION_URI.toString().equals(value)) {
uri = null;
} else {
uri = Uri.parse(value);
diff --git a/src/org/smssecure/smssecure/database/DatabaseFactory.java b/src/org/smssecure/smssecure/database/DatabaseFactory.java
index 9040e011be5d152a5159a0a67f49ef4621f70600..a5765015184404272c3859eddc8966e4d77f88ab 100644
--- a/src/org/smssecure/smssecure/database/DatabaseFactory.java
+++ b/src/org/smssecure/smssecure/database/DatabaseFactory.java
@@ -73,8 +73,14 @@ public class DatabaseFactory {
private static final int INTRODUCED_CONVERSATION_LIST_STATUS_VERSION = 25;
private static final int MIGRATED_CONVERSATION_LIST_STATUS_VERSION = 26;
private static final int INTRODUCED_SUBSCRIPTION_ID_VERSION = 28;
- private static final int INTRODUCED_XMPP_TRANSPORT = 29;
- private static final int DATABASE_VERSION = 28;
+ private static final int INTRODUCED_LAST_SEEN = 29;
+
+ /*
+ * Yes, INTRODUCED_XMPP_TRANSPORT > DATABASE_VERSION to allow database
+ * downgrade when XMPP transport will be included in unstable branch.
+ */
+ private static final int INTRODUCED_XMPP_TRANSPORT = 30;
+ private static final int DATABASE_VERSION = 29;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@@ -828,6 +834,10 @@ public class DatabaseFactory {
db.execSQL("ALTER TABLE mms ADD COLUMN subscription_id INTEGER DEFAULT -1");
}
+ if (oldVersion < INTRODUCED_LAST_SEEN) {
+ db.execSQL("ALTER TABLE thread ADD COLUMN last_seen INTEGER DEFAULT 0");
+ }
+
db.setTransactionSuccessful();
db.endTransaction();
}
diff --git a/src/org/smssecure/smssecure/database/MmsDatabase.java b/src/org/smssecure/smssecure/database/MmsDatabase.java
index 1f43b599a6effe99df67a0c26fcb8f40fe03a11d..89a71b1f241942565e7f0b0d75d93bc19b6ebf5b 100644
--- a/src/org/smssecure/smssecure/database/MmsDatabase.java
+++ b/src/org/smssecure/smssecure/database/MmsDatabase.java
@@ -685,6 +685,7 @@ public class MmsDatabase extends MessagingDatabase {
long messageId = insertMediaMessage(masterSecret, addresses, message.getBody(),
message.getAttachments(), contentValues);
+ DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
return messageId;
diff --git a/src/org/smssecure/smssecure/database/SmsDatabase.java b/src/org/smssecure/smssecure/database/SmsDatabase.java
index 365d4de061ba4d60fd722c02b8026ed5776b3788..1164092ea66cddc5a746663d00ab63757c0b0c18 100644
--- a/src/org/smssecure/smssecure/database/SmsDatabase.java
+++ b/src/org/smssecure/smssecure/database/SmsDatabase.java
@@ -455,6 +455,7 @@ public class SmsDatabase extends MessagingDatabase {
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
DatabaseFactory.getThreadDatabase(context).update(threadId, true);
+ DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
notifyConversationListeners(threadId);
jobManager.add(new TrimThreadJob(context, threadId));
diff --git a/src/org/smssecure/smssecure/database/ThreadDatabase.java b/src/org/smssecure/smssecure/database/ThreadDatabase.java
index 12540630a88ad6f2f75eb4c8acb603809eac242a..de2f1521227f119211f7f69a410d7866e605de30 100644
--- a/src/org/smssecure/smssecure/database/ThreadDatabase.java
+++ b/src/org/smssecure/smssecure/database/ThreadDatabase.java
@@ -65,6 +65,7 @@ public class ThreadDatabase extends Database {
public static final String SNIPPET_URI = "snippet_uri";
public static final String ARCHIVED = "archived";
public static final String STATUS = "status";
+ public static final String LAST_SEEN = "last_seen";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
@@ -72,7 +73,8 @@ public class ThreadDatabase extends Database {
SNIPPET_CHARSET + " INTEGER DEFAULT 0, " + READ + " INTEGER DEFAULT 1, " +
TYPE + " INTEGER DEFAULT 0, " + ERROR + " INTEGER DEFAULT 0, " +
SNIPPET_TYPE + " INTEGER DEFAULT 0, " + SNIPPET_URI + " TEXT DEFAULT NULL, " +
- ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0);";
+ ARCHIVED + " INTEGER DEFAULT 0, " + STATUS + " INTEGER DEFAULT 0, " +
+ LAST_SEEN + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS thread_recipient_ids_index ON " + TABLE_NAME + " (" + RECIPIENT_IDS + ");",
@@ -376,6 +378,31 @@ public class ThreadDatabase extends Database {
notifyConversationListListeners();
}
+ public void setLastSeen(long threadId) {
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ ContentValues contentValues = new ContentValues(1);
+ contentValues.put(LAST_SEEN, System.currentTimeMillis());
+
+ db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});
+ notifyConversationListListeners();
+ }
+
+ public long getLastSeen(long threadId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ Cursor cursor = db.query(TABLE_NAME, new String[]{LAST_SEEN}, ID_WHERE, new String[]{String.valueOf(threadId)}, null, null, null);
+
+ try {
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getLong(0);
+ }
+
+ return -1;
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+
+ }
+
public void deleteConversation(long threadId) {
DatabaseFactory.getSmsDatabase(context).deleteThread(threadId);
DatabaseFactory.getMmsDatabase(context).deleteThread(threadId);
@@ -552,10 +579,11 @@ public class ThreadDatabase extends Database {
int distributionType = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.TYPE));
boolean archived = cursor.getInt(cursor.getColumnIndex(ThreadDatabase.ARCHIVED)) != 0;
int status = cursor.getInt(cursor.getColumnIndexOrThrow(ThreadDatabase.STATUS));
+ long lastSeen = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.LAST_SEEN));
Uri snippetUri = getSnippetUri(cursor);
return new ThreadRecord(context, body, snippetUri, recipients, date, count, read == 1,
- threadId, status, type, distributionType, archived);
+ threadId, status, type, distributionType, archived, lastSeen);
}
private DisplayRecord.Body getPlaintextBody(Cursor cursor) {
diff --git a/src/org/smssecure/smssecure/database/loaders/ConversationListLoader.java b/src/org/smssecure/smssecure/database/loaders/ConversationListLoader.java
index a580fed43396a263bd29932a4d5387e251a8f6e5..4a820d129398662f303d8cdf610e51e2956d1415 100644
--- a/src/org/smssecure/smssecure/database/loaders/ConversationListLoader.java
+++ b/src/org/smssecure/smssecure/database/loaders/ConversationListLoader.java
@@ -43,11 +43,11 @@ public class ConversationListLoader extends AbstractCursorLoader {
ThreadDatabase.ID, ThreadDatabase.DATE, ThreadDatabase.MESSAGE_COUNT,
ThreadDatabase.RECIPIENT_IDS, ThreadDatabase.SNIPPET, ThreadDatabase.READ,
ThreadDatabase.TYPE, ThreadDatabase.SNIPPET_TYPE, ThreadDatabase.SNIPPET_URI,
- ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS}, 1);
+ ThreadDatabase.ARCHIVED, ThreadDatabase.STATUS, ThreadDatabase.LAST_SEEN}, 1);
switchToArchiveCursor.addRow(new Object[] {-1L, System.currentTimeMillis(), archivedCount,
"-1", null, 1, ThreadDatabase.DistributionTypes.ARCHIVE,
- 0, null, 0, -1});
+ 0, null, 0, -1, 0});
cursorList.add(switchToArchiveCursor);
}
diff --git a/src/org/smssecure/smssecure/database/loaders/ConversationLoader.java b/src/org/smssecure/smssecure/database/loaders/ConversationLoader.java
index 7b7f612cab75fe10c81de3644c1d368192d09569..6d4e2600a782a8b983bbf04e0f505619be67ce97 100644
--- a/src/org/smssecure/smssecure/database/loaders/ConversationLoader.java
+++ b/src/org/smssecure/smssecure/database/loaders/ConversationLoader.java
@@ -9,19 +9,29 @@ import org.smssecure.smssecure.util.AbstractCursorLoader;
public class ConversationLoader extends AbstractCursorLoader {
private final long threadId;
private long limit;
+ private long lastSeen;
- public ConversationLoader(Context context, long threadId, long limit) {
+ public ConversationLoader(Context context, long threadId, long limit, long lastSeen) {
super(context);
this.threadId = threadId;
- this.limit = limit;
+ this.limit = limit;
+ this.lastSeen = lastSeen;
}
public boolean hasLimit() {
return limit > 0;
}
+ public long getLastSeen() {
+ return lastSeen;
+ }
+
@Override
public Cursor getCursor() {
+ if (lastSeen == -1) {
+ this.lastSeen = DatabaseFactory.getThreadDatabase(context).getLastSeen(threadId);
+ }
+
return DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId, limit);
}
}
diff --git a/src/org/smssecure/smssecure/database/model/ThreadRecord.java b/src/org/smssecure/smssecure/database/model/ThreadRecord.java
index 703cb9dbb0f545c62c414619095d1a09a6e9c5ec..eb518e84ad95aa3bfea06cb3d32c580235cee12d 100644
--- a/src/org/smssecure/smssecure/database/model/ThreadRecord.java
+++ b/src/org/smssecure/smssecure/database/model/ThreadRecord.java
@@ -46,10 +46,12 @@ public class ThreadRecord extends DisplayRecord {
private final boolean read;
private final int distributionType;
private final boolean archived;
+ private final long lastSeen;
public ThreadRecord(@NonNull Context context, @NonNull Body body, @Nullable Uri snippetUri,
@NonNull Recipients recipients, long date, long count, boolean read,
- long threadId, int status, long snippetType, int distributionType, boolean archived)
+ long threadId, int status, long snippetType, int distributionType, boolean archived,
+ long lastSeen)
{
super(context, body, recipients, date, date, date, threadId, status, snippetType);
this.context = context.getApplicationContext();
@@ -58,6 +60,7 @@ public class ThreadRecord extends DisplayRecord {
this.read = read;
this.distributionType = distributionType;
this.archived = archived;
+ this.lastSeen = lastSeen;
}
public @Nullable Uri getSnippetUri() {
@@ -134,4 +137,8 @@ public class ThreadRecord extends DisplayRecord {
public int getDistributionType() {
return distributionType;
}
+
+ public long getLastSeen() {
+ return lastSeen;
+ }
}
diff --git a/src/org/smssecure/smssecure/notifications/AbstractNotificationBuilder.java b/src/org/smssecure/smssecure/notifications/AbstractNotificationBuilder.java
index 0bff4bce4272e1c428f6b88cb37cf570b0ba2af8..1810f69d3cb997fa93ea3dd0b667b133d3398e2d 100644
--- a/src/org/smssecure/smssecure/notifications/AbstractNotificationBuilder.java
+++ b/src/org/smssecure/smssecure/notifications/AbstractNotificationBuilder.java
@@ -42,8 +42,8 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui
String defaultRingtoneName = SilencePreferences.getNotificationRingtone(context);
boolean defaultVibrate = SilencePreferences.isNotificationVibrateEnabled(context);
- if (ringtone != null) setSound(ringtone);
- else if (!TextUtils.isEmpty(defaultRingtoneName)) setSound(Uri.parse(defaultRingtoneName));
+ if (ringtone == null && !TextUtils.isEmpty(defaultRingtoneName)) setSound(Uri.parse(defaultRingtoneName));
+ else if (ringtone != null && !ringtone.toString().isEmpty()) setSound(ringtone);
if (vibrate != null &&
(vibrate == RecipientPreferenceDatabase.VibrateState.ENABLED ||
diff --git a/src/org/smssecure/smssecure/notifications/MarkReadReceiver.java b/src/org/smssecure/smssecure/notifications/MarkReadReceiver.java
index 3d0dbfe8e6964f9c12eddb2b163e05125113d65e..dd2d4469f3f56ea54677f198881e66d9db81c306 100644
--- a/src/org/smssecure/smssecure/notifications/MarkReadReceiver.java
+++ b/src/org/smssecure/smssecure/notifications/MarkReadReceiver.java
@@ -36,6 +36,7 @@ public class MarkReadReceiver extends MasterSecretBroadcastReceiver {
for (long threadId : threadIds) {
Log.w(TAG, "Marking as read: " + threadId);
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
+ DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
}
MessageNotifier.updateNotification(context, masterSecret);
diff --git a/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java b/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java
index d15fddc70c94fcc9739ac4f8f372cf83c53a9a81..690b6c4f2854840dbcace79bc49611802dc619f7 100644
--- a/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java
+++ b/src/org/smssecure/smssecure/notifications/RemoteReplyReceiver.java
@@ -88,6 +88,8 @@ public class RemoteReplyReceiver extends MasterSecretBroadcastReceiver {
}
DatabaseFactory.getThreadDatabase(context).setRead(threadId);
+ DatabaseFactory.getThreadDatabase(context).setLastSeen(threadId);
+
MessageNotifier.updateNotification(context, masterSecret);
return null;
diff --git a/src/org/smssecure/smssecure/util/StickyHeaderDecoration.java b/src/org/smssecure/smssecure/util/StickyHeaderDecoration.java
index 967e03cf611653de45def6cd9ddf09f3f5086504..c4d30d085d0225c19132168535ecfd49d7eba677 100644
--- a/src/org/smssecure/smssecure/util/StickyHeaderDecoration.java
+++ b/src/org/smssecure/smssecure/util/StickyHeaderDecoration.java
@@ -57,7 +57,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
outRect.set(0, headerHeight, 0, 0);
}
- private boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
+ protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter adapter, int adapterPos) {
boolean isReverse = isReverseLayout(parent);
int itemCount = ((RecyclerView.Adapter)adapter).getItemCount();
@@ -74,7 +74,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
return headerId != NO_HEADER_ID && previousHeaderId != NO_HEADER_ID && headerId != previousHeaderId;
}
- private ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
+ protected ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter adapter, int position) {
final long key = adapter.getHeaderId(position);
if (headerCache.containsKey(key)) {
@@ -127,7 +127,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
}
}
- private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
+ protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos,
int layoutPos)
{
int headerHeight = getHeaderHeightForLayout(header);
@@ -172,7 +172,7 @@ public class StickyHeaderDecoration extends RecyclerView.ItemDecoration {
}
}
- private int getHeaderHeightForLayout(View header) {
+ protected int getHeaderHeightForLayout(View header) {
return renderInline ? 0 : header.getHeight();
}