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); + } }