diff --git a/res/drawable-hdpi/ic_timeline.png b/res/drawable-hdpi/ic_timeline.png
new file mode 100755
index 000000000..ec39d34d8
Binary files /dev/null and b/res/drawable-hdpi/ic_timeline.png differ
diff --git a/res/drawable-xhdpi/ic_timeline.png b/res/drawable-xhdpi/ic_timeline.png
new file mode 100755
index 000000000..35bbcf599
Binary files /dev/null and b/res/drawable-xhdpi/ic_timeline.png differ
diff --git a/res/drawable-xxhdpi/ic_timeline.png b/res/drawable-xxhdpi/ic_timeline.png
new file mode 100755
index 000000000..b151d978a
Binary files /dev/null and b/res/drawable-xxhdpi/ic_timeline.png differ
diff --git a/res/drawable-xxxhdpi/ic_timeline.png b/res/drawable-xxxhdpi/ic_timeline.png
new file mode 100755
index 000000000..bfe700020
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_timeline.png differ
diff --git a/res/layout/activity_map.xml b/res/layout/activity_map.xml
index c62eab440..b1b8c722b 100644
--- a/res/layout/activity_map.xml
+++ b/res/layout/activity_map.xml
@@ -1,6 +1,5 @@
-
-
+
+
+
diff --git a/res/layout/bottom_map_sheet.xml b/res/layout/bottom_map_sheet.xml
new file mode 100644
index 000000000..ab7a194c2
--- /dev/null
+++ b/res/layout/bottom_map_sheet.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml
index 55acd399e..1d75e1ddc 100644
--- a/res/menu/text_secure_normal.xml
+++ b/res/menu/text_secure_normal.xml
@@ -17,7 +17,9 @@
android:id="@+id/menu_deaddrop" />
+ android:id="@+id/menu_global_map"
+ android:visible="false"
+ />
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 276753894..977c6a0b8 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -239,4 +239,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 161564139..5b9673df9 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -50,4 +50,8 @@
#44000000
+ #552090ea
+ #ff2090ea
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 759c9c1ce..e43a98519 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -53,4 +53,12 @@
2dp
+ 6dp
+ 14dp
+ 4dp
+ 12sp
+ 8dp
+ 100
+ 100
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3c8dfe476..67860c29a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -219,6 +219,11 @@
Contact request
No contact requests.\n\nIf you want classic emails to appear here as contact requests, you can change the corresponding setting in the app settings.
+
+ Show location traces in time frame
+ Last position
+
+
Search
Search for chats, contacts, and messages
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index 396a88ca4..bf4e69f99 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -92,6 +92,10 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
menu.clear();
inflater.inflate(R.menu.text_secure_normal, menu);
+ MenuItem item = menu.findItem(R.id.menu_global_map);
+ if (Prefs.isLocationStreamingEnabled(this)) {
+ item.setVisible(true);
+ }
if (!Prefs.isLocationStreamingEnabled(this)) {
menu.findItem(R.id.menu_global_map).setVisible(false);
diff --git a/src/org/thoughtcrime/securesms/components/rangeslider/RangeSliderView.java b/src/org/thoughtcrime/securesms/components/rangeslider/RangeSliderView.java
new file mode 100644
index 000000000..4efd72fbd
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/rangeslider/RangeSliderView.java
@@ -0,0 +1,322 @@
+package org.thoughtcrime.securesms.components.rangeslider;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.thoughtcrime.securesms.R;
+
+import java.util.ArrayList;
+
+public class RangeSliderView extends View {
+
+ private static final String TAG = RangeSliderView.class.getName();
+
+ protected int trackTintColor;
+ protected int trackHighlightTintColor;
+ protected float trackHeight;
+ protected float thumbInnerRadius;
+ protected float thumbOutlineSize;
+ protected float displayTextFontSize;
+ protected float displayTextBasicOffsetY;
+ protected int sliderPaddingLeft;
+ protected int sliderPaddingRight;
+
+ private ThumbLayer minValueThumb;
+ private TextLayer minValueDisplayLabel;
+
+ private ThumbLayer maxValueThumb;
+ private TextLayer maxValueDisplayLabel;
+
+ private TrackLayer track;
+
+ private ArrayList values;
+
+ protected int minValue;
+ protected int maxValue;
+ protected float delta;
+
+ protected float offsetY;
+ protected float maxValueDisplayLabelOffsetY;
+ protected float minValueDisplayLabelOffsetY;
+ protected float trackOffsetY;
+ protected float thumbRadius;
+ protected float sliderWidth;
+
+ private float beginTrackOffsetX;
+ private boolean isThumbViewLocked;
+
+ private OnValueChangedListener onValueChangedListener;
+ GestureDetector longPressDetector;
+
+
+ public interface OnValueChangedListener {
+ void onValueChanged(int minValue, int maxValue);
+ String parseMinValueDisplayText(int minValue);
+ String parseMaxValueDisplayText(int maxValue);
+ }
+
+ public RangeSliderView(Context context) {
+ this(context, null);
+ }
+
+ public RangeSliderView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RangeSliderView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ int [] attributes = new int [] {android.R.attr.paddingLeft,
+ android.R.attr.paddingStart,
+ android.R.attr.paddingRight,
+ android.R.attr.paddingEnd};
+
+ TypedArray arr = context.obtainStyledAttributes(attrs, attributes);
+
+ sliderPaddingLeft = arr.getDimensionPixelOffset(0,
+ arr.getDimensionPixelOffset(1, -1));
+ sliderPaddingRight = arr.getDimensionPixelOffset(2,
+ arr.getDimensionPixelOffset(3, -1));
+
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RangeSliderView);
+ trackTintColor = typedArray.getColor(
+ R.styleable.RangeSliderView_trackTintColor,
+ ContextCompat.getColor(context, R.color.trackTintColor)
+ );
+ trackHighlightTintColor = typedArray.getColor(
+ R.styleable.RangeSliderView_trackHighlightTintColor,
+ ContextCompat.getColor(context, R.color.trackHighlightTintColor)
+ );
+
+ Resources resources = context.getResources();
+ trackHeight = typedArray.getDimension(
+ R.styleable.RangeSliderView_trackHeight,
+ resources.getDimension(R.dimen.slider_trackHeight)
+ );
+ thumbInnerRadius = typedArray.getDimension(
+ R.styleable.RangeSliderView_thumbRadius,
+ resources.getDimension(R.dimen.slider_thumbRadius)
+ );
+ thumbOutlineSize = typedArray.getDimension(
+ R.styleable.RangeSliderView_thumbOutlineSize,
+ resources.getDimension(R.dimen.slider_thumbOutlineSize)
+ );
+ displayTextFontSize = typedArray.getDimension(
+ R.styleable.RangeSliderView_displayTextFontSize,
+ resources.getDimension(R.dimen.slider_displayTextFontSize)
+ );
+ displayTextBasicOffsetY = resources.getDimension(R.dimen.slider_displayTextBasicOffsetY);
+ minValue = typedArray.getInt(
+ R.styleable.RangeSliderView_minValue,
+ 100
+ );
+ maxValue = typedArray.getInt(
+ R.styleable.RangeSliderView_maxValue,
+ 100
+ );
+
+ minValueThumb = new ThumbLayer(thumbInnerRadius, thumbOutlineSize, trackHighlightTintColor, trackTintColor);
+ minValueDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
+
+ maxValueThumb = new ThumbLayer(thumbInnerRadius, thumbOutlineSize, trackHighlightTintColor, trackTintColor);
+ maxValueDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
+
+ track = new TrackLayer(sliderPaddingLeft, sliderPaddingRight, trackHeight, trackTintColor, trackHighlightTintColor);
+
+ values = new ArrayList<>();
+ for (int index = 1; index <= 100; index++) {
+ values.add(index);
+ }
+ delta = 0;
+
+ longPressDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
+ public void onLongPress(MotionEvent e) {
+ if (minValue == maxValue && (minValueThumb.isHighlight || maxValueThumb.isHighlight)) {
+ isThumbViewLocked = true;
+ minValueThumb.isHighlight = true;
+ maxValueThumb.isHighlight = true;
+ invalidate();
+ }
+ }
+ });
+ }
+
+ void setOnValueChangedListener(OnValueChangedListener onValueChangedListener) {
+ this.onValueChangedListener = onValueChangedListener;
+ invalidate();
+ }
+
+ public void setRangeValues(ArrayList values) {
+ this.values = values;
+ setMinAndMaxValue(this.values.get(0), this.values.get(this.values.size() - 1));
+ }
+
+ public void setMinAndMaxValue(int minValue, int maxValue) {
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ invalidate();
+
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ this.offsetY = getHeight() / 5 * 3;
+ maxValueDisplayLabelOffsetY = offsetY - thumbInnerRadius - thumbOutlineSize - trackHeight / 2 - displayTextBasicOffsetY - displayTextFontSize;
+ minValueDisplayLabelOffsetY = maxValueDisplayLabelOffsetY + displayTextFontSize + displayTextFontSize / 2;
+ trackOffsetY = offsetY - trackHeight / 2;
+ sliderWidth = getWidth() - sliderPaddingLeft - sliderPaddingRight;
+ thumbRadius = thumbInnerRadius + thumbOutlineSize / 2;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ float minValueOffsetX = getPositionFromIndex(minValue);
+ float maxValueOffsetX = getPositionFromIndex(maxValue);
+ track.draw(canvas, getWidth(), minValueOffsetX, maxValueOffsetX, trackOffsetY, getDeltaInPixel());
+ minValueThumb.draw(canvas, minValueOffsetX, offsetY);
+ maxValueThumb.draw(canvas, maxValueOffsetX, offsetY);
+
+ if (onValueChangedListener != null) {
+ minValueDisplayLabel.draw(
+ canvas,
+ onValueChangedListener.parseMinValueDisplayText(minValue),
+ minValueOffsetX,
+ minValueDisplayLabelOffsetY
+ );
+ maxValueDisplayLabel.draw(
+ canvas,
+ onValueChangedListener.parseMaxValueDisplayText(maxValue),
+ maxValueOffsetX,
+ maxValueDisplayLabelOffsetY
+ );
+ } else {
+ minValueDisplayLabel.draw(canvas, String.valueOf(minValue), minValueOffsetX, minValueDisplayLabelOffsetY);
+ maxValueDisplayLabel.draw(canvas, String.valueOf(maxValue), maxValueOffsetX, maxValueDisplayLabelOffsetY);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ longPressDetector.onTouchEvent(event);
+
+ if (event.getAction() == MotionEvent.ACTION_CANCEL) {
+ minValueThumb.isHighlight = false;
+ maxValueThumb.isHighlight = false;
+ isThumbViewLocked = false;
+ invalidate();
+ return true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ isThumbViewLocked = false;
+ if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
+ if (onValueChangedListener != null) {
+ onValueChangedListener.onValueChanged(minValue, maxValue);
+ }
+ minValueThumb.isHighlight = false;
+ maxValueThumb.isHighlight = false;
+ invalidate();
+ return true;
+ }
+ }
+
+ int offsetX = (int) event.getX();
+ if (offsetX < (sliderPaddingLeft - thumbRadius) || offsetX > getWidth() - sliderPaddingRight + thumbRadius) {
+ //Log.d(TAG, "ignore touch in padding");
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ float minValuePosition = getPositionFromIndex(minValue);
+ Log.d(TAG, "action_down minVal: " + minValue + ", " + minValuePosition );
+ if (offsetX >= minValuePosition - thumbRadius && offsetX <= minValuePosition + thumbRadius) {
+ minValueThumb.isHighlight = true;
+ } else {
+ float maxValuePosition = getPositionFromIndex(maxValue);
+ if (offsetX >= maxValuePosition - thumbRadius && offsetX <= maxValuePosition + thumbRadius) {
+ maxValueThumb.isHighlight = true;
+ }
+ }
+
+ if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
+ beginTrackOffsetX = offsetX;
+ invalidate();
+ } else {
+ beginTrackOffsetX = -1;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (minValue == maxValue && beginTrackOffsetX > -1 && offsetX > beginTrackOffsetX && !maxValueThumb.isHighlight) {
+ minValueThumb.isHighlight = false;
+ maxValueThumb.isHighlight = true;
+ }
+
+ int count = values.size();
+ int index = getIndexFromPosition(offsetX);
+ Log.d(TAG, "move - index: " + index + ", offsetX: " + offsetX);
+ if (index < 0) {
+ index = 0;
+ } else if (index > count - 1) {
+ index = count - 1;
+ }
+
+ if (isThumbViewLocked) {
+ minValue = values.get(index);
+ maxValue = values.get(index);
+ } else if (minValueThumb.isHighlight) {
+ if (index > values.indexOf(maxValue)) {
+ minValue = maxValue;
+ } else {
+ minValue = values.get(index);
+ }
+ } else if (maxValueThumb.isHighlight) {
+ if (index < values.indexOf(minValue)) {
+ maxValue = minValue;
+ } else {
+ maxValue = values.get(index);
+ }
+ }
+
+ if (minValueThumb.isHighlight || maxValueThumb.isHighlight) {
+ invalidate();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ protected int getIndexFromPosition(int offsetX) {
+ return (int) (((offsetX - sliderPaddingLeft) * getCount()) / sliderWidth);
+ }
+
+ protected float getPositionFromIndex(int value) {
+ return ((sliderWidth / (float) getCount()) * value) + sliderPaddingLeft;
+ }
+
+ public float getDeltaInPixel() {
+ // '* 2' is just an offset factor to increase the visibility
+ return (sliderWidth / getCount()) * delta + (2 * thumbInnerRadius + thumbOutlineSize);
+ }
+
+ public float getDeltaInValues() {
+ return delta;
+ }
+
+ public int getCount() {
+ return values.size();
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/rangeslider/TextLayer.java b/src/org/thoughtcrime/securesms/components/rangeslider/TextLayer.java
new file mode 100644
index 000000000..4f6928aa2
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/rangeslider/TextLayer.java
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.components.rangeslider;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+class TextLayer {
+
+ private Paint paint;
+
+ TextLayer(float fontSize, int color) {
+ paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ paint.setTextSize(fontSize);
+ paint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ void draw(Canvas canvas, String text, float offsetX, float offsetY) {
+ canvas.drawText(text, offsetX, offsetY, paint);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/rangeslider/ThumbLayer.java b/src/org/thoughtcrime/securesms/components/rangeslider/ThumbLayer.java
new file mode 100644
index 000000000..da0864971
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/rangeslider/ThumbLayer.java
@@ -0,0 +1,46 @@
+package org.thoughtcrime.securesms.components.rangeslider;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+
+class ThumbLayer {
+
+ private float radius;
+ private float outlineSize;
+
+ private int highlightColor;
+
+ private Paint outlinePaint;
+ private Paint fillPaint;
+
+ boolean isHighlight = false;
+
+ ThumbLayer(float radius, float outlineSize, int outlineColor, int highlightColor) {
+ this.radius = radius;
+ this.outlineSize = outlineSize;
+ this.highlightColor = highlightColor;
+
+ outlinePaint = new Paint();
+ outlinePaint.setAntiAlias(true);
+ outlinePaint.setColor(outlineColor);
+ outlinePaint.setStyle(Paint.Style.STROKE);
+ outlinePaint.setStrokeWidth(outlineSize);
+
+ fillPaint = new Paint();
+ fillPaint.setAntiAlias(true);
+ fillPaint.setStyle(Paint.Style.FILL);
+ fillPaint.setStrokeWidth(0);
+ }
+
+ void draw(Canvas canvas, float cx, float cy) {
+ canvas.drawCircle(cx, cy, radius, outlinePaint);
+
+ if (isHighlight) {
+ fillPaint.setColor(highlightColor);
+ } else {
+ fillPaint.setColor(Color.WHITE);
+ }
+ canvas.drawCircle(cx, cy, radius - outlineSize / 2, fillPaint);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/rangeslider/TimeRangeSlider.java b/src/org/thoughtcrime/securesms/components/rangeslider/TimeRangeSlider.java
new file mode 100644
index 000000000..311701f28
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/rangeslider/TimeRangeSlider.java
@@ -0,0 +1,154 @@
+package org.thoughtcrime.securesms.components.rangeslider;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.util.DateUtils;
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+
+import java.util.Locale;
+
+/**
+ * Created by cyberta on 24.03.19.
+ */
+
+public class TimeRangeSlider extends RangeSliderView implements RangeSliderView.OnValueChangedListener {
+ private static final int DEFAULT_TIMEFRAME_2D = 60 * 60 * 24 * 2; // 2d
+ private static final float DEFAULT_DELTA = 1000*60*30; // 30 min
+ int timeFrame; // timeframe in seconds
+
+ Locale locale;
+ OnTimestampChangedListener listener;
+ private TextLayer minRangeDisplayLabel;
+ private TextLayer maxRangeDisplayLabel;
+
+ float displayLabelOffsetBelow;
+
+
+ public TimeRangeSlider(Context context) {
+ this(context, null, 0);
+ }
+
+ public TimeRangeSlider(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TimeRangeSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RangeSliderView);
+ timeFrame = typedArray.getInt(
+ R.styleable.TimeRangeSlider_timeFrame,
+ DEFAULT_TIMEFRAME_2D
+ );
+
+ locale = DynamicLanguage.getSelectedLocale(context);
+ super.setOnValueChangedListener(this);
+ minRangeDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
+ maxRangeDisplayLabel = new TextLayer(displayTextFontSize, trackHighlightTintColor);
+ delta = DEFAULT_DELTA / (timeFrame * 1000) * getCount();
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ displayLabelOffsetBelow = offsetY + thumbInnerRadius + thumbOutlineSize + trackHeight / 2 + displayTextBasicOffsetY;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ float minValueOffsetX = getPositionFromIndex(minValue);
+ float maxValueOffsetX = getPositionFromIndex(maxValue);
+
+ if (minValue == maxValue) {
+ if (minValueOffsetX - getDeltaInPixel() >= sliderPaddingLeft) {
+ minRangeDisplayLabel.draw(
+ canvas,
+ getDeltaInTime(getContext()),
+ minValueOffsetX - getDeltaInPixel(),
+ displayLabelOffsetBelow
+ );
+ }
+
+ if (maxValueOffsetX + getDeltaInPixel() <= (getWidth() - sliderPaddingRight)) {
+ maxRangeDisplayLabel.draw(
+ canvas,
+ getDeltaInTime(getContext()),
+ maxValueOffsetX + getDeltaInPixel(),
+ displayLabelOffsetBelow
+ );
+ }
+
+ }
+ }
+
+ public interface OnTimestampChangedListener {
+ void onTimestampChanged(long startTimestamp, long stopTimestamp);
+
+ /**
+ * filter for lastPosition beginning from startTimestamp to now
+ * @param startTimestamp begin of time frame
+ */
+ void onFilterLastPosition(long startTimestamp);
+ }
+
+
+ public void setOnTimestampChangedListener(OnTimestampChangedListener timestampChangedListener) {
+ listener = timestampChangedListener;
+ }
+
+ @Override
+ public void onValueChanged(int minValue, int maxValue) {
+ if (listener != null) {
+ long minTimeStamp = getTimestampForValue(minValue);
+ if (minValue == maxValue) {
+ if (minValue == getCount()) {
+ // filter last location
+ listener.onFilterLastPosition(System.currentTimeMillis() - (long) DEFAULT_DELTA);
+ } else {
+ // filter for time of event with delta before and after
+ listener.onTimestampChanged(minTimeStamp - (long) DEFAULT_DELTA, minTimeStamp + (long) DEFAULT_DELTA);
+ }
+ } else {
+ //filter for time span
+ listener.onTimestampChanged(minTimeStamp, getTimestampForValue(maxValue));
+ }
+ }
+ }
+
+ @Override
+ public String parseMinValueDisplayText(int minValue) {
+ if (minValue == maxValue) {
+ return "";
+ }
+ return getStringForValue(minValue);
+ }
+
+ @Override
+ public String parseMaxValueDisplayText(int maxValue) {
+ if (minValue == maxValue && maxValue == getCount()) {
+ return getContext().getResources().getString(R.string.filter_last_position);
+ }
+ return getStringForValue(maxValue);
+ }
+
+
+ private String getStringForValue(int value) {
+ return DateUtils.getExtendedRelativeTimeSpanString(this.getContext(), locale, getTimestampForValue(value));
+ }
+
+ private long getTimestampForValue(int value) {
+ return System.currentTimeMillis() - ((timeFrame / getCount()) * (getCount() - value) * 1000);
+ }
+
+ public String getDeltaInTime(Context context) {
+ return DateUtils.getFormattedTimespan(context, (int) (timeFrame * 1000 / getCount() * getDeltaInValues()));
+ }
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/rangeslider/TrackLayer.java b/src/org/thoughtcrime/securesms/components/rangeslider/TrackLayer.java
new file mode 100644
index 000000000..391932138
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/rangeslider/TrackLayer.java
@@ -0,0 +1,65 @@
+package org.thoughtcrime.securesms.components.rangeslider;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+class TrackLayer {
+
+ private float height;
+ private float radius;
+ private int paddingLeft = 0;
+ private int paddingRight = 0;
+
+ private Paint centerPaint;
+ private Paint backgoundPaint;
+
+ TrackLayer(int paddingLeft, int paddingRight, float height, int color, int highlightColor) {
+ this.height = height;
+ radius = this.height / 2;
+
+ centerPaint = new Paint();
+ centerPaint.setAntiAlias(true);
+ centerPaint.setColor(highlightColor);
+ centerPaint.setStyle(Paint.Style.FILL);
+
+ backgoundPaint = new Paint();
+ backgoundPaint.setAntiAlias(true);
+ backgoundPaint.setColor(color);
+ backgoundPaint.setStyle(Paint.Style.FILL);
+
+ this.paddingLeft = paddingLeft;
+ this.paddingRight = paddingRight;
+ }
+
+ void draw(Canvas canvas, int width, float offsetLeft, float offsetRight, float offsetY, float delta) {
+ draw(canvas, backgoundPaint, paddingLeft, width - paddingRight, offsetY);
+ if (offsetLeft == offsetRight) {
+ float maxEndX = width - paddingRight;
+ float minStartX = paddingLeft;
+ float offsetDeltaLeft = offsetLeft - delta;
+ float offsetDeltaRight = offsetRight + delta;
+
+ if (offsetLeft - delta < minStartX) {
+ offsetDeltaLeft = minStartX;
+ } else if (offsetRight + delta > maxEndX) {
+ offsetDeltaRight = maxEndX;
+ }
+
+ draw(canvas, centerPaint, offsetDeltaLeft, offsetDeltaRight, offsetY);
+ }
+ else {
+ draw(canvas, centerPaint, offsetLeft, offsetRight, offsetY);
+ }
+
+ }
+
+ void draw(Canvas canvas, Paint paint, float startX, float endX, float offsetY) {
+ canvas.drawRoundRect(
+ new RectF(startX, offsetY, endX, offsetY + height),
+ radius,
+ radius,
+ paint
+ );
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java b/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
index 60b900d88..fd1912a9e 100644
--- a/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
+++ b/src/org/thoughtcrime/securesms/connect/ApplicationDcContext.java
@@ -719,20 +719,4 @@ public class ApplicationDcContext extends DcContext {
return 0;
}
-
- /***********************************************************************************************
- * core related helper methods
- **********************************************************************************************/
-
- public int[] getChatIds() {
- DcChatlist chats = getChatlist(0, null, 0);
- int count = chats.getCnt();
- int[] chatIds = new int[count];
- for (int i = 0; i < count; i++) {
- DcChat dcChat = chats.getChat(i);
- chatIds[i] = dcChat.getId();
- }
- return chatIds;
- }
-
}
diff --git a/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java b/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
index 6f3699d9c..6d584690b 100644
--- a/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
+++ b/src/org/thoughtcrime/securesms/map/GenerateInfoWindowTask.java
@@ -116,7 +116,7 @@ public class GenerateInfoWindowTask extends AsyncTask, HashMa
String id = feature.getStringProperty(INFO_WINDOW_ID);
imagesMap.put(id, bitmap);
- if (i % 10 == 0) {
+ if (i % 20 == 0) {
publishProgress(new HashMap<>(imagesMap));
imagesMap.clear();
}
diff --git a/src/org/thoughtcrime/securesms/map/MapActivity.java b/src/org/thoughtcrime/securesms/map/MapActivity.java
index a29cbe566..b562be297 100644
--- a/src/org/thoughtcrime/securesms/map/MapActivity.java
+++ b/src/org/thoughtcrime/securesms/map/MapActivity.java
@@ -3,15 +3,19 @@ package org.thoughtcrime.securesms.map;
import android.content.Intent;
import android.graphics.PointF;
import android.os.Bundle;
+import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
import android.util.Log;
+import android.view.View;
+import android.widget.RelativeLayout;
import com.b44t.messenger.DcMsg;
import com.mapbox.geojson.Feature;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.SupportMapFragment;
@@ -20,6 +24,7 @@ import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.rangeslider.TimeRangeSlider;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.geolocation.DcLocation;
@@ -27,11 +32,13 @@ import java.util.List;
import java.util.Observable;
import java.util.Observer;
+import static android.support.design.widget.BottomSheetBehavior.STATE_COLLAPSED;
+import static android.support.design.widget.BottomSheetBehavior.STATE_EXPANDED;
import static com.b44t.messenger.DcChat.DC_CHAT_NO_CHAT;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_SELECTED;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
-public class MapActivity extends BaseActivity implements Observer {
+public class MapActivity extends BaseActivity implements Observer, TimeRangeSlider.OnTimestampChangedListener {
public static final String TAG = MapActivity.class.getSimpleName();
public static final String CHAT_ID = "chat_id";
@@ -40,6 +47,7 @@ public class MapActivity extends BaseActivity implements Observer {
private DcLocation dcLocation;
private MapDataManager mapDataManager;
+ private MapboxMap mapboxMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -67,6 +75,7 @@ public class MapActivity extends BaseActivity implements Observer {
mapFragment.getMapAsync(mapboxMap -> mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
+ this.mapboxMap = mapboxMap;
mapboxMap.setCameraPosition(new CameraPosition.Builder()
.target(new LatLng(dcLocation.getLastLocation().getLatitude(), dcLocation.getLastLocation().getLongitude()))
.zoom(9)
@@ -80,6 +89,7 @@ public class MapActivity extends BaseActivity implements Observer {
}
mapDataManager = new MapDataManager(this, mapBoxStyle, chatId, (latLngBounds) -> {
+ Log.d(TAG, "on Data initialized");
mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 50), 1000);
});
@@ -126,6 +136,11 @@ public class MapActivity extends BaseActivity implements Observer {
}
}
+ if (chatId == 0) {
+ Log.e(TAG, "Chat id is 0. Cannot open chat");
+ return true;
+ }
+
Intent intent = new Intent(MapActivity.this, ConversationActivity.class);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, chatId);
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, 0);
@@ -151,6 +166,25 @@ public class MapActivity extends BaseActivity implements Observer {
}
}));
+ TimeRangeSlider timeRangeSlider = this.findViewById(R.id.timeRangeSlider);
+ timeRangeSlider.setOnTimestampChangedListener(this);
+
+ View bottomSheet = this.findViewById(R.id.bottom_sheet);
+ BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
+
+
+ RelativeLayout bottomSheetSlider = this.findViewById(R.id.bottomSheetSlider);
+ bottomSheetSlider.setOnClickListener(v -> {
+ switch (behavior.getState()) {
+ case STATE_EXPANDED:
+ behavior.setState(STATE_COLLAPSED);
+ break;
+ default:
+ behavior.setState(STATE_EXPANDED);
+ break;
+ }
+ });
+
}
@Override
@@ -182,16 +216,17 @@ public class MapActivity extends BaseActivity implements Observer {
}
}
- private int[] getChatIds(Intent intent) {
- if (intent == null) {
- return new int[]{-1};
+ @Override
+ public void onTimestampChanged(long startTimestamp, long stopTimestamp) {
+ if (this.mapboxMap == null) {
+ return;
}
+ mapDataManager.filter(startTimestamp, stopTimestamp);
- int[] chatIds = intent.getIntArrayExtra(CHAT_IDS);
- if (chatIds == null || chatIds.length == 0) {
- chatIds = new int[1];
- chatIds[0] = getIntent().getIntExtra(CHAT_ID, -1);
- }
- return chatIds;
+ }
+
+ @Override
+ public void onFilterLastPosition(long startTimestamp) {
+ mapDataManager.filterLastPositions(startTimestamp);
}
}
diff --git a/src/org/thoughtcrime/securesms/map/MapDataManager.java b/src/org/thoughtcrime/securesms/map/MapDataManager.java
index 2cf377ea6..bdbedfb1b 100644
--- a/src/org/thoughtcrime/securesms/map/MapDataManager.java
+++ b/src/org/thoughtcrime/securesms/map/MapDataManager.java
@@ -46,9 +46,13 @@ import java.util.Map;
import static com.b44t.messenger.DcContext.DC_EVENT_LOCATION_CHANGED;
import static com.b44t.messenger.DcContext.DC_GCL_ADD_SELF;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.gt;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.gte;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.lte;
import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase;
import static com.mapbox.mapboxsdk.style.expressions.Expression.toBool;
import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM_LEFT;
@@ -73,6 +77,8 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
public static final int ALL_CHATS_GLOBAL_MAP = 0;
public static final long TIMESTAMP_NOW = 0L;
public static final long TIMEOUT = 3 * 60 * 60 * 1000;
+ private static final long DEFAULT_LAST_POSITION_DELTA = 1000*60*30; // 30 min
+
private static final String TAG = MapDataManager.class.getSimpleName();
private Style mapboxStyle;
private HashMap contactMapSources;
@@ -98,9 +104,11 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
int[] contactIds = getContactIds(chatId);
+ long now = System.currentTimeMillis();
for (int contactId : contactIds) {
updateSource(chatId, contactId, boundingBuilder);
generateInfoWindows(contactId);
+ filterLastPosition(contactId, now - DEFAULT_LAST_POSITION_DELTA);
}
dcContext.eventCenter.addObserver(DC_EVENT_LOCATION_CHANGED, this);
@@ -204,12 +212,50 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
}
+
+ public void filter(long startTimestamp, long endTimestamp) {
+ int[] contactIds = getContactIds(chatId);
+ for (int contactId : contactIds) {
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(contactMapMetadata.getMarkerLayer());
+ Expression filter = all(
+ lte(get(TIMESTAMP), endTimestamp),
+ gte(get(TIMESTAMP), startTimestamp));
+ markerLayer.withFilter(filter);
+ LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(contactMapMetadata.getLineLayer());
+ lineLayer.withFilter(filter);
+ }
+ }
+
+ public void filterLastPositions(long startTimestamp) {
+ int[] contactIds = getContactIds(chatId);
+ for (int contactId : contactIds) {
+ filterLastPosition(contactId, startTimestamp);
+ }
+ }
+
+ private void filterLastPosition(int contactId, long startTimestamp) {
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(contactMapMetadata.getMarkerLayer());
+ Expression filter = all(
+ eq((get(LAST_LOCATION)), literal(true)),
+ gte(get(TIMESTAMP), startTimestamp));
+ markerLayer.withFilter(filter);
+ LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(contactMapMetadata.getLineLayer());
+ lineLayer.withFilter(filter);
+ }
+
private void updateSource(int chatId, int contactId) {
updateSource(chatId, contactId, null);
}
private void updateSource(int chatId, int contactId, LatLngBounds.Builder boundingBuilder) {
- DcArray locations = dcContext.getLocations(chatId, contactId, System.currentTimeMillis() - TIMEOUT, TIMESTAMP_NOW);
+ updateSource(chatId, contactId, System.currentTimeMillis() - TIMEOUT, TIMESTAMP_NOW, boundingBuilder );
+ }
+
+ private void updateSource(int chatId, int contactId, long startTimestamp, long endTimestamp, LatLngBounds.Builder boundingBuilder) {
+ //long start = System.currentTimeMillis();
+ DcArray locations = dcContext.getLocations(chatId, contactId, startTimestamp, endTimestamp);
MapSource contactMapMetadata = contactMapSources.get(contactId);
if (contactMapMetadata == null) {
contactMapMetadata = addContactMapSource(contactId);
@@ -242,10 +288,21 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
sortedPointFeatures.first().getFeature().addBooleanProperty(LAST_LOCATION, true);
}
- FeatureCollection pointFeatureCollection = FeatureCollection.fromFeatures(sortedPointFeatures.getFeatureList());
- FeatureCollection lineFeatureCollection = FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry(
- LineString.fromLngLats(sortedPointFeatures.getPointList())
- )});
+ ArrayList sortedPointList = sortedPointFeatures.getPointList();
+ ArrayList sortedFeatureList = sortedPointFeatures.getFeatureList();
+ FeatureCollection pointFeatureCollection = FeatureCollection.fromFeatures(sortedFeatureList);
+ ArrayList lineFeatures = new ArrayList<>();
+
+ for (int i = 0; i < sortedPointFeatures.size() - 1; i++) {
+ Feature f = sortedFeatureList.get(i);
+ Number numberProperty = f.getNumberProperty(TIMESTAMP);
+ LineString l = LineString.fromLngLats(sortedPointList.subList(i, i+2));
+ Feature lineFeature = Feature.fromGeometry(l, new JsonObject(), "l_" + f.id());
+ lineFeature.addNumberProperty(TIMESTAMP, numberProperty);
+ lineFeatures.add(lineFeature);
+ }
+
+ FeatureCollection lineFeatureCollection = FeatureCollection.fromFeatures(lineFeatures);
GeoJsonSource lineSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getLineSource());
lineSource.setGeoJson(lineFeatureCollection);
@@ -254,6 +311,8 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), sortedPointFeatures);
generateMissingInfoWindows(contactId);
+
+ //Log.d(TAG, "update Source took " + (System.currentTimeMillis() - start) + " ms");
}
private void generateMissingInfoWindows(int contactId) {
@@ -281,7 +340,9 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
private void generateInfoWindows(int contactId) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
FeatureTreeSet collection = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
- new GenerateInfoWindowTask(this, contactId).execute(collection.getFeatureList());
+ if (collection.size() > 0) {
+ new GenerateInfoWindowTask(this, contactId).execute(collection.getFeatureList());
+ }
}
private void initGeoJsonSources(MapSource source) {
@@ -328,12 +389,10 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
switchCase(toBool(get(LAST_LOCATION)), literal(1.0f),
switchCase(eq(get(MESSAGE_ID), literal(0)), literal(0.7f), literal(1.1f))));
Expression markerIcon = switchCase(toBool(get(LAST_LOCATION)), literal(source.getMarkerLastPositon()), literal(source.getMarkerIcon()));
- Expression allowOverlap = eq(get(LAST_LOCATION), literal(true));
mapboxStyle.addLayer(new SymbolLayer(source.getMarkerLayer(), source.getMarkerSource())
.withProperties(
iconImage(markerIcon),
- iconSize(markerSize),
- iconAllowOverlap(allowOverlap))
+ iconSize(markerSize))
);
mapboxStyle.addLayer(new LineLayer(source.getLineLayer(), source.getLineSource())
@@ -405,6 +464,14 @@ public class MapDataManager implements DcEventCenter.DcEventDelegate, GenerateIn
}
}
+ private void resetSource(int contactId) {
+ MapSource contactMapMetadata = contactMapSources.get(contactId);
+ if (contactMapMetadata != null) {
+ featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), new FeatureTreeSet());
+ GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(contactMapMetadata.getMarkerSource());
+ pointSource.setGeoJson(FeatureCollection.fromFeatures(new ArrayList<>()));
+ }
+ }
private void replaceSelectedMarker(String featureId) {
Feature feature = getFeatureWithId(featureId);
diff --git a/src/org/thoughtcrime/securesms/util/DateUtils.java b/src/org/thoughtcrime/securesms/util/DateUtils.java
index 160f78b69..90662e731 100644
--- a/src/org/thoughtcrime/securesms/util/DateUtils.java
+++ b/src/org/thoughtcrime/securesms/util/DateUtils.java
@@ -175,4 +175,9 @@ public class DateUtils extends android.text.format.DateUtils {
TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis-(TimeUnit.MILLISECONDS.toMinutes(millis)*60000)));
}
+
+ public static String getFormattedTimespan(Context c, int timestamp) {
+ int mins = timestamp / (1000 * 60);
+ return c.getResources().getQuantityString(R.plurals.n_minutes, mins, mins);
+ }
}