use new experimental maps integration (#3005)

* use new maps integration

* Update src/com/b44t/messenger/DcContext.java

Co-authored-by: bjoern <r10s@b44t.com>

* Update src/org/thoughtcrime/securesms/WebxdcActivity.java

Co-authored-by: bjoern <r10s@b44t.com>

* fix typo

---------

Co-authored-by: bjoern <r10s@b44t.com>
This commit is contained in:
Asiel Díaz Benítez 2024-04-25 18:04:13 +02:00 committed by GitHub
parent cb084df369
commit 9eaf8bb1f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 68 additions and 2260 deletions

BIN
assets/webxdc/maps.xdc Normal file

Binary file not shown.

View file

@ -60,9 +60,6 @@ dependencies {
} }
implementation 'com.annimon:stream:1.1.8' // brings future java streams api to SDK Version < 24 implementation 'com.annimon:stream:1.1.8' // brings future java streams api to SDK Version < 24
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' // glues the current time segment text in the gallery to the top. implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' // glues the current time segment text in the gallery to the top.
implementation ('org.maplibre.gl:android-sdk:9.5.2') {
exclude group: 'com.google.android.gms'
}
// Replacement for ContentResolver // Replacement for ContentResolver
// that protects against the Surreptitious Sharing attack. // that protects against the Surreptitious Sharing attack.
@ -99,7 +96,7 @@ android {
// Even though we compile our libraries outside Gradle with `scripts/ndk-make.sh`, // Even though we compile our libraries outside Gradle with `scripts/ndk-make.sh`,
// without ndkVersion `./gradlew clean` followed by `./gradlew assembleDebug --warning-mode=all` emits the following warning: // without ndkVersion `./gradlew clean` followed by `./gradlew assembleDebug --warning-mode=all` emits the following warning:
// > Task :stripFatDebugDebugSymbols // > Task :stripFatDebugDebugSymbols
// Unable to strip the following libraries, packaging them as they are: libanimation-decoder-gif.so, libmapbox-gl.so, libnative-utils.so. // Unable to strip the following libraries, packaging them as they are: libanimation-decoder-gif.so, libnative-utils.so.
// See <https://issuetracker.google.com/issues/237187538> for details. // See <https://issuetracker.google.com/issues/237187538> for details.
ndkVersion "23.2.8568313" ndkVersion "23.2.8568313"
useLibrary 'org.apache.http.legacy' useLibrary 'org.apache.http.legacy'
@ -120,7 +117,6 @@ android {
project.ext.set("archivesBaseName", "deltachat"); project.ext.set("archivesBaseName", "deltachat");
buildConfigField "boolean", "DEV_BUILD", "false" buildConfigField "boolean", "DEV_BUILD", "false"
buildConfigField "String", "MAP_ACCESS_TOKEN", '"pk.eyJ1IjoiZGVsdGFjaGF0IiwiYSI6ImNqc3c1aWczMzBjejY0M28wZmU0a3cwMzMifQ.ZPTH9dFJaav06RAu4rTYHw"'
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"

View file

@ -792,6 +792,20 @@ JNIEXPORT jstring Java_com_b44t_messenger_DcContext_getWebxdcStatusUpdates(JNIEn
} }
JNIEXPORT jint Java_com_b44t_messenger_DcContext_initWebxdcIntegration(JNIEnv *env, jobject obj, jint chat_id)
{
return dc_init_webxdc_integration(get_dc_context(env, obj), chat_id);
}
JNIEXPORT void Java_com_b44t_messenger_DcContext_setWebxdcIntegration(JNIEnv *env, jobject obj, jstring file)
{
CHAR_REF(file);
dc_set_webxdc_integration(get_dc_context(env, obj), filePtr);
CHAR_UNREF(file);
}
JNIEXPORT jint Java_com_b44t_messenger_DcContext_addDeviceMsg(JNIEnv *env, jobject obj, jstring label, jobject msg) JNIEXPORT jint Java_com_b44t_messenger_DcContext_addDeviceMsg(JNIEnv *env, jobject obj, jstring label, jobject msg)
{ {
CHAR_REF(label); CHAR_REF(label);

View file

@ -202,6 +202,8 @@ public class DcContext {
public native int sendVideochatInvitation(int chat_id); public native int sendVideochatInvitation(int chat_id);
public native boolean sendWebxdcStatusUpdate(int msg_id, String payload, String descr); public native boolean sendWebxdcStatusUpdate(int msg_id, String payload, String descr);
public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial); public native String getWebxdcStatusUpdates(int msg_id, int last_known_serial);
public native void setWebxdcIntegration (String file);
public native int initWebxdcIntegration(int chat_id);
public native int addDeviceMsg (String label, DcMsg msg); public native int addDeviceMsg (String label, DcMsg msg);
public native boolean wasDeviceMsgEverAdded(String label); public native boolean wasDeviceMsgEverAdded(String label);
public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); } public DcLot checkQr (String qr) { return new DcLot(checkQrCPtr(qr)); }

View file

@ -16,6 +16,11 @@
*/ */
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.util.RelayUtil.getSharedText;
import static org.thoughtcrime.securesms.util.RelayUtil.isForwarding;
import static org.thoughtcrime.securesms.util.RelayUtil.isSharing;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
@ -50,7 +55,6 @@ import android.view.WindowManager;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -93,7 +97,6 @@ import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.DirectShareUtil; import org.thoughtcrime.securesms.connect.DirectShareUtil;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.map.MapActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView; import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
@ -131,11 +134,6 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.util.RelayUtil.getSharedText;
import static org.thoughtcrime.securesms.util.RelayUtil.isForwarding;
import static org.thoughtcrime.securesms.util.RelayUtil.isSharing;
/** /**
* Activity for displaying a message thread, as well as * Activity for displaying a message thread, as well as
* composing/sending a new message into that thread. * composing/sending a new message into that thread.
@ -532,7 +530,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_clear_chat: fragment.handleClearChat(); return true; case R.id.menu_clear_chat: fragment.handleClearChat(); return true;
case R.id.menu_delete_chat: handleDeleteChat(); return true; case R.id.menu_delete_chat: handleDeleteChat(); return true;
case R.id.menu_mute_notifications: handleMuteNotifications(); return true; case R.id.menu_mute_notifications: handleMuteNotifications(); return true;
case R.id.menu_show_map: handleShowMap(); return true; case R.id.menu_show_map: WebxdcActivity.openMaps(this, chatId); return true;
case R.id.menu_search_up: handleMenuSearchNext(false); return true; case R.id.menu_search_up: handleMenuSearchNext(false); return true;
case R.id.menu_search_down: handleMenuSearchNext(true); return true; case R.id.menu_search_down: handleMenuSearchNext(true); return true;
case android.R.id.home: handleReturnToConversationList(); return true; case android.R.id.home: handleReturnToConversationList(); return true;
@ -584,12 +582,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
new VideochatUtil().invite(this, chatId); new VideochatUtil().invite(this, chatId);
} }
private void handleShowMap() {
Intent intent = new Intent(this, MapActivity.class);
intent.putExtra(MapActivity.CHAT_ID, chatId);
startActivity(intent);
}
private void handleReturnToConversationList() { private void handleReturnToConversationList() {
handleReturnToConversationList(null); handleReturnToConversationList(null);
} }

View file

@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.ConversationActivity.CHAT_ID_EXTRA; import static org.thoughtcrime.securesms.ConversationActivity.CHAT_ID_EXTRA;
import static org.thoughtcrime.securesms.ConversationActivity.STARTING_POSITION_EXTRA; import static org.thoughtcrime.securesms.ConversationActivity.STARTING_POSITION_EXTRA;
import static org.thoughtcrime.securesms.map.MapDataManager.ALL_CHATS_GLOBAL_MAP;
import static org.thoughtcrime.securesms.util.RelayUtil.acquireRelayMessageContent; import static org.thoughtcrime.securesms.util.RelayUtil.acquireRelayMessageContent;
import static org.thoughtcrime.securesms.util.RelayUtil.getDirectSharingChatId; import static org.thoughtcrime.securesms.util.RelayUtil.getDirectSharingChatId;
import static org.thoughtcrime.securesms.util.RelayUtil.getSharedTitle; import static org.thoughtcrime.securesms.util.RelayUtil.getSharedTitle;
@ -60,7 +59,6 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.connect.AccountManager; import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.DirectShareUtil; import org.thoughtcrime.securesms.connect.DirectShareUtil;
import org.thoughtcrime.securesms.map.MapActivity;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.QrActivity; import org.thoughtcrime.securesms.qr.QrActivity;
@ -74,9 +72,6 @@ import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import java.io.FileOutputStream;
import java.io.InputStream;
public class ConversationListActivity extends PassphraseRequiredActionBarActivity public class ConversationListActivity extends PassphraseRequiredActionBarActivity
implements ConversationListFragment.ConversationSelectedListener implements ConversationListFragment.ConversationSelectedListener
{ {
@ -384,7 +379,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
new IntentIntegrator(this).setCaptureActivity(QrActivity.class).initiateScan(); new IntentIntegrator(this).setCaptureActivity(QrActivity.class).initiateScan();
return true; return true;
case R.id.menu_global_map: case R.id.menu_global_map:
handleShowMap(); WebxdcActivity.openMaps(this, 0);
return true; return true;
case R.id.menu_switch_account: case R.id.menu_switch_account:
AccountManager.getInstance().showSwitchAccountMenu(this); AccountManager.getInstance().showSwitchAccountMenu(this);
@ -423,13 +418,6 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
invalidateOptionsMenu(); invalidateOptionsMenu();
} }
private void handleShowMap() {
Intent intent = new Intent(this, MapActivity.class);
intent.putExtra(MapActivity.CHAT_IDS, ALL_CHATS_GLOBAL_MAP);
startActivity(intent);
}
@Override @Override
public void onCreateConversation(int chatId) { public void onCreateConversation(int chatId) {
openConversation(chatId, -1); openConversation(chatId, -1);

View file

@ -48,6 +48,8 @@ import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
@ -55,33 +57,64 @@ import java.util.Map;
public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate { public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcEventDelegate {
private static final String TAG = WebxdcActivity.class.getSimpleName(); private static final String TAG = WebxdcActivity.class.getSimpleName();
private static final String EXTRA_ACCOUNT_ID = "accountId";
private static final String EXTRA_APP_MSG_ID = "appMessageId";
private static final String EXTRA_HIDE_ACTION_BAR = "hideActionBar";
private static final int REQUEST_CODE_FILE_PICKER = 51426; private static final int REQUEST_CODE_FILE_PICKER = 51426;
private ValueCallback<Uri[]> filePathCallback; private ValueCallback<Uri[]> filePathCallback;
private DcContext dcContext; private DcContext dcContext;
private DcMsg dcAppMsg; private DcMsg dcAppMsg;
private String baseURL; private String baseURL;
private String sourceCodeUrl = ""; private String sourceCodeUrl = "";
private boolean internetAccess = false; private boolean internetAccess = false;
private boolean hideActionBar = false;
public static void openMaps(Context context, int chatId) {
DcContext dcContext = DcHelper.getContext(context);
int msgId = dcContext.initWebxdcIntegration(chatId);
if (msgId == 0) {
try {
InputStream inputStream = context.getResources().getAssets().open("webxdc/maps.xdc");
String outputFile = DcHelper.getBlobdirFile(dcContext, "maps", ".xdc");
Util.copy(inputStream, new FileOutputStream(outputFile));
dcContext.setWebxdcIntegration(outputFile);
msgId = dcContext.initWebxdcIntegration(chatId);
} catch (IOException e) {
e.printStackTrace();
}
if (msgId == 0) {
Toast.makeText(context, "Cannot get maps.xdc, see log for details.", Toast.LENGTH_LONG).show();
return;
}
}
openWebxdcActivity(context, msgId, true);
}
public static void openWebxdcActivity(Context context, DcMsg instance) { public static void openWebxdcActivity(Context context, DcMsg instance) {
openWebxdcActivity(context, instance.getId(), false);
}
public static void openWebxdcActivity(Context context, int msgId, boolean hideActionBar) {
if (!Util.isClickedRecently()) { if (!Util.isClickedRecently()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Prefs.isDeveloperModeEnabled(context)) { if (Prefs.isDeveloperModeEnabled(context)) {
WebView.setWebContentsDebuggingEnabled(true); WebView.setWebContentsDebuggingEnabled(true);
} }
context.startActivity(getWebxdcIntent(context, instance.getId())); context.startActivity(getWebxdcIntent(context, msgId, hideActionBar));
} else { } else {
Toast.makeText(context, "At least Android 5.0 (Lollipop) required for Webxdc.", Toast.LENGTH_LONG).show(); Toast.makeText(context, "At least Android 5.0 (Lollipop) required for Webxdc.", Toast.LENGTH_LONG).show();
} }
} }
} }
private static Intent getWebxdcIntent(Context context, int msgId) { private static Intent getWebxdcIntent(Context context, int msgId, boolean hideActionBar) {
DcContext dcContext = DcHelper.getContext(context); DcContext dcContext = DcHelper.getContext(context);
Intent intent = new Intent(context, WebxdcActivity.class); Intent intent = new Intent(context, WebxdcActivity.class);
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.putExtra("accountId", dcContext.getAccountId()); intent.putExtra(EXTRA_ACCOUNT_ID, dcContext.getAccountId());
intent.putExtra("appMessageId", msgId); intent.putExtra(EXTRA_APP_MSG_ID, msgId);
intent.putExtra(EXTRA_HIDE_ACTION_BAR, hideActionBar);
return intent; return intent;
} }
@ -92,7 +125,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId()) .putExtra(ConversationActivity.CHAT_ID_EXTRA, dcContext.getMsg(msgId).getChatId())
.setAction(Intent.ACTION_VIEW); .setAction(Intent.ACTION_VIEW);
final Intent webxdcIntent = getWebxdcIntent(context, msgId); final Intent webxdcIntent = getWebxdcIntent(context, msgId, false);
return TaskStackBuilder.create(context) return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(chatIntent) .addNextIntentWithParentStack(chatIntent)
@ -104,6 +137,9 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
protected void onCreate(Bundle state, boolean ready) { protected void onCreate(Bundle state, boolean ready) {
super.onCreate(state, ready); super.onCreate(state, ready);
Bundle b = getIntent().getExtras();
hideActionBar = b.getBoolean(EXTRA_HIDE_ACTION_BAR, false);
// enter fullscreen mode if necessary, // enter fullscreen mode if necessary,
// this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered // this is needed here because if the app is opened while already in landscape mode, onConfigurationChanged() is not triggered
setScreenMode(getResources().getConfiguration()); setScreenMode(getResources().getConfiguration());
@ -129,10 +165,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this); eventCenter.addObserver(DcContext.DC_EVENT_WEBXDC_STATUS_UPDATE, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this); eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
Bundle b = getIntent().getExtras(); int appMessageId = b.getInt(EXTRA_APP_MSG_ID);
int appMessageId = b.getInt("appMessageId"); int accountId = b.getInt(EXTRA_ACCOUNT_ID);
int accountId = b.getInt("accountId");
this.dcContext = DcHelper.getContext(getApplicationContext()); this.dcContext = DcHelper.getContext(getApplicationContext());
if (accountId != dcContext.getAccountId()) { if (accountId != dcContext.getAccountId()) {
AccountManager.getInstance().switchAccount(getApplicationContext(), accountId); AccountManager.getInstance().switchAccount(getApplicationContext(), accountId);
@ -224,7 +258,7 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
getWindow().getDecorView().setSystemUiVisibility(enable? View.SYSTEM_UI_FLAG_FULLSCREEN : 0); getWindow().getDecorView().setSystemUiVisibility(enable? View.SYSTEM_UI_FLAG_FULLSCREEN : 0);
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {
if (enable) { if (hideActionBar || enable) {
actionBar.hide(); actionBar.hide();
} else { } else {
actionBar.show(); actionBar.show();

View file

@ -1,78 +0,0 @@
package org.thoughtcrime.securesms.map;
import android.content.Context;
import androidx.appcompat.widget.LinearLayoutCompat;
import android.util.AttributeSet;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.mapbox.mapboxsdk.geometry.LatLng;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.DcHelper;
/**
* Created by cyberta on 24.04.19.
*/
public class AddPoiView extends LinearLayoutCompat {
private final ImageButton sendView;
private final EditText messageView;
private final ProgressBar progressBar;
private LatLng latLng;
private SendingTask.OnMessageSentListener listener;
private int chatId;
public AddPoiView(Context context) {
this(context, null);
}
public AddPoiView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AddPoiView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.add_poi_view, this);
sendView = this.findViewById(R.id.sendView);
messageView = this.findViewById(R.id.message_view);
progressBar = this.findViewById(R.id.sending_progress);
messageView.requestFocus();
sendView.setOnClickListener(v -> {
if (messageView.getText().toString().length() == 0) {
return;
}
progressBar.setVisibility(VISIBLE);
sendView.setVisibility(INVISIBLE);
DcContext dcContext = DcHelper.getContext(AddPoiView.this.getContext());
DcMsg msg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
msg.setLocation((float) latLng.getLatitude(), (float) latLng.getLongitude());
msg.setText(messageView.getText().toString());
SendingTask.Model model = new SendingTask.Model(msg, chatId, listener);
new SendingTask(AddPoiView.this.getContext()).execute(model);
});
}
public void setLatLng(LatLng latLng) {
this.latLng = latLng;
}
public void setChatId(int chatId) {
this.chatId = chatId;
}
public void setOnMessageSentListener(SendingTask.OnMessageSentListener listener) {
this.listener = listener;
}
public EditText getMessageView() {
return messageView;
}
}

View file

@ -1,220 +0,0 @@
package org.thoughtcrime.securesms.map;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.mapbox.mapboxsdk.maps.MapFragment;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.utils.MapFragmentUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Support Fragment wrapper around a map view.
* <p>
* A Map component in an app. This fragment is the simplest way to place a map in an application.
* It's a wrapper around a view of a map to automatically handle the necessary life cycle needs.
* Being a fragment, this component can be added to an activity's layout or can dynamically be added
* using a FragmentManager.
* </p>
* <p>
* To get a reference to the MapView, use {@link #getMapAsync(OnMapReadyCallback)}}
* </p>
*
* @see #getMapAsync(OnMapReadyCallback)
*/
public class DCMapFragment extends Fragment implements OnMapReadyCallback {
private final List<OnMapReadyCallback> mapReadyCallbackList = new ArrayList<>();
private MapFragment.OnMapViewReadyCallback mapViewReadyCallback;
private MapboxMap mapboxMap;
private MapView map;
/**
* Creates a default MapFragment instance
*
* @return MapFragment created
*/
public static DCMapFragment newInstance() {
return new DCMapFragment();
}
/**
* Creates a MapFragment instance
*
* @param mapboxMapOptions The configuration options to be used.
* @return MapFragment created.
*/
@NonNull
public static DCMapFragment newInstance(@Nullable MapboxMapOptions mapboxMapOptions) {
DCMapFragment mapFragment = new DCMapFragment();
mapFragment.setArguments(MapFragmentUtils.createFragmentArgs(mapboxMapOptions));
return mapFragment;
}
/**
* Called when the context attaches to this fragment.
*
* @param context the context attaching
*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof MapFragment.OnMapViewReadyCallback) {
mapViewReadyCallback = (MapFragment.OnMapViewReadyCallback) context;
}
}
/**
* Called when this fragment is inflated, parses XML tag attributes.
*
* @param context The context inflating this fragment.
* @param attrs The XML tag attributes.
* @param savedInstanceState The saved instance state for the map fragment.
*/
@Override
public void onInflate(@NonNull Context context, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
setArguments(MapFragmentUtils.createFragmentArgs(MapboxMapOptions.createFromAttributes(context, attrs)));
}
/**
* Creates the fragment view hierarchy.
*
* @param inflater Inflater used to inflate content.
* @param container The parent layout for the map fragment.
* @param savedInstanceState The saved instance state for the map fragment.
* @return The view created
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
Context context = inflater.getContext();
map = new MapView(context, MapFragmentUtils.resolveArgs(context, getArguments()));
return map;
}
/**
* Called when the fragment view hierarchy is created.
*
* @param view The content view of the fragment
* @param savedInstanceState THe saved instance state of the framgnt
*/
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
map.onCreate(savedInstanceState);
map.getMapAsync(this);
// notify listeners about MapView creation
if (mapViewReadyCallback != null) {
mapViewReadyCallback.onMapViewReady(map);
}
}
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
for (OnMapReadyCallback onMapReadyCallback : mapReadyCallbackList) {
onMapReadyCallback.onMapReady(mapboxMap);
}
}
/**
* Called when the fragment is visible for the users.
*/
@Override
public void onStart() {
super.onStart();
map.onStart();
}
/**
* Called when the fragment is ready to be interacted with.
*/
@Override
public void onResume() {
super.onResume();
map.onResume();
}
/**
* Called when the fragment is pausing.
*/
@Override
public void onPause() {
super.onPause();
map.onPause();
}
/**
* Called when the fragment state needs to be saved.
*
* @param outState The saved state
*/
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (map != null && !map.isDestroyed()) {
map.onSaveInstanceState(outState);
}
}
/**
* Called when the fragment is no longer visible for the user.
*/
@Override
public void onStop() {
super.onStop();
map.onStop();
}
/**
* Called when the fragment receives onLowMemory call from the hosting Activity.
*/
@Override
public void onLowMemory() {
super.onLowMemory();
if (map != null && !map.isDestroyed()) {
map.onLowMemory();
}
}
/**
* Called when the fragment is view hierarchy is being destroyed.
*/
@Override
public void onDestroyView() {
super.onDestroyView();
map.onDestroy();
mapReadyCallbackList.clear();
}
/**
* Sets a callback object which will be triggered when the MapboxMap instance is ready to be used.
*
* @param onMapReadyCallback The callback to be invoked.
*/
public void getMapAsync(@NonNull final OnMapReadyCallback onMapReadyCallback) {
if (mapboxMap == null) {
mapReadyCallbackList.add(onMapReadyCallback);
} else {
onMapReadyCallback.onMapReady(mapboxMap);
}
}
public MapView getMapView() {
return map;
}
}

View file

@ -1,109 +0,0 @@
package org.thoughtcrime.securesms.map;
import android.os.AsyncTask;
import android.util.Log;
import com.b44t.messenger.DcContext;
import com.mapbox.geojson.Feature;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
import org.thoughtcrime.securesms.map.model.MapSource;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP_NOW;
import static org.thoughtcrime.securesms.map.MapDataManager.TIME_FRAME;
/**
* Created by cyberta on 15.04.19.
*/
public class DataCollectionTask extends AsyncTask<Void, Void, Set<String>> {
public interface DataCollectionCallback {
void onDataCollectionFinished(Set<String> emojiCodepoints);
}
private static final String TAG = DataCollectionTask.class.getSimpleName();
private static final Set<DataCollectionTask> instances = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final int chatId;
private final int[] contactIds;
private final ConcurrentHashMap<Integer, MapSource> contactMapSources;
private final ConcurrentHashMap<String, LinkedList<Feature>> featureCollections;
private final ConcurrentHashMap<Integer, Feature> lastPositions;
private final LatLngBounds.Builder boundingBuilder;
private final DcContext dcContext;
private final DataCollectionCallback callback;
private final EmojiProvider emojiProvider;
public DataCollectionTask(DcContext context,
int chatId,
int[] contactIds,
ConcurrentHashMap<Integer, MapSource> contactMapSources,
ConcurrentHashMap featureCollections,
ConcurrentHashMap<Integer, Feature> lastPositions,
LatLngBounds.Builder boundingBuilder,
EmojiProvider emojiProvider,
DataCollectionCallback callback) {
this.chatId = chatId;
this.contactMapSources = contactMapSources;
this.featureCollections = featureCollections;
this.lastPositions = lastPositions;
this.boundingBuilder = boundingBuilder;
this.dcContext = context;
this.callback = callback;
this.contactIds = contactIds;
this.emojiProvider = emojiProvider;
instances.add(this);
}
public static void cancelRunningTasks() {
for (DataCollectionTask task : instances) {
task.cancel(true);
}
}
@Override
protected Set<String> doInBackground(Void... voids) {
Log.d(TAG, "performance test - collect Data start");
HashSet<String> emojiCodePoints = new HashSet<>();
DataCollector dataCollector = new DataCollector(dcContext,
contactMapSources,
featureCollections,
lastPositions,
emojiCodePoints,
emojiProvider,
boundingBuilder);
for (int contactId : contactIds) {
dataCollector.updateSource(chatId,
contactId,
System.currentTimeMillis() - TIME_FRAME,
TIMESTAMP_NOW);
if (this.isCancelled()) {
break;
}
}
return emojiCodePoints;
}
@Override
protected void onPostExecute(Set<String> emojiCodePoints) {
if (!this.isCancelled()) {
callback.onDataCollectionFinished(emojiCodePoints);
}
instances.remove(this);
Log.d(TAG, "performance test - collect Data finished");
}
@Override
protected void onCancelled() {
super.onCancelled();
instances.remove(this);
}
}

View file

@ -1,171 +0,0 @@
package org.thoughtcrime.securesms.map;
import com.b44t.messenger.DcArray;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.google.gson.JsonObject;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.LineString;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
import org.thoughtcrime.securesms.map.model.MapSource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static org.thoughtcrime.securesms.map.MapDataManager.ACCURACY;
import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.IS_EMOJI_CHAR;
import static org.thoughtcrime.securesms.map.MapDataManager.IS_POI;
import static org.thoughtcrime.securesms.map.MapDataManager.LAST_LOCATION;
import static org.thoughtcrime.securesms.map.MapDataManager.LAST_POSITION_ICON;
import static org.thoughtcrime.securesms.map.MapDataManager.LAST_POSITION_LABEL;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_CHAR;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_ICON;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_SELECTED;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.POI_LONG_DESCRIPTION;
import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
/**
* Created by cyberta on 18.04.19.
*/
public class DataCollector {
private final DcContext dcContext;
private final ConcurrentHashMap<Integer, MapSource> contactMapSources;
private final ConcurrentHashMap<String, LinkedList<Feature>> featureCollections;
private final ConcurrentHashMap<Integer, Feature> lastPositions;
private final Set<String> emojiCodePoints;
private final LatLngBounds.Builder boundingBuilder;
private final EmojiProvider emojiProvider;
public DataCollector(DcContext dcContext,
ConcurrentHashMap<Integer, MapSource> contactMapSources,
ConcurrentHashMap<String, LinkedList<Feature>> featureCollections,
ConcurrentHashMap<Integer, Feature> lastPositions,
Set<String> emojiCodePoints,
EmojiProvider emojiProvider,
LatLngBounds.Builder boundingBuilder) {
this.dcContext = dcContext;
this.contactMapSources = contactMapSources;
this.featureCollections = featureCollections;
this.lastPositions = lastPositions;
this.boundingBuilder = boundingBuilder;
this.emojiCodePoints = emojiCodePoints;
this.emojiProvider = emojiProvider;
}
public void updateSource(int chatId,
int contactId,
long startTimestamp,
long endTimestamp) {
DcArray locations = dcContext.getLocations(chatId, contactId, startTimestamp, endTimestamp);
MapSource contactMapMetadata = contactMapSources.get(contactId);
if (contactMapMetadata == null) {
contactMapMetadata = addContactMapSource(contactMapSources, contactId);
}
int count = locations.getCnt();
LinkedList<Feature> sortedPointFeatures = featureCollections.get(contactMapMetadata.getMarkerFeatureCollection());
if (sortedPointFeatures != null && sortedPointFeatures.size() == count) {
return;
} else {
sortedPointFeatures = new LinkedList<>();
}
LinkedList<Feature> sortedLineFeatures = new LinkedList<>();
for (int i = count - 1; i >= 0; i--) {
Point point = Point.fromLngLat(locations.getLongitude(i), locations.getLatitude(i));
String codepointChar =
locations.getMarker(i) != null ?
locations.getMarker(i) :
"";
boolean isPoi = locations.isIndependent(i);
int messageId = locations.getMsgId(i);
boolean isEmojiChar = !codepointChar.isEmpty() && emojiProvider.isEmoji(codepointChar);
Feature pointFeature = Feature.fromGeometry(point, new JsonObject(), String.valueOf(locations.getLocationId(i)));
pointFeature.addBooleanProperty(MARKER_SELECTED, false);
pointFeature.addBooleanProperty(LAST_LOCATION, false);
pointFeature.addNumberProperty(CONTACT_ID, contactId);
pointFeature.addNumberProperty(TIMESTAMP, locations.getTimestamp(i));
pointFeature.addNumberProperty(MESSAGE_ID, messageId);
pointFeature.addNumberProperty(ACCURACY, locations.getAccuracy(i));
pointFeature.addStringProperty(MARKER_CHAR, codepointChar);
pointFeature.addBooleanProperty(IS_EMOJI_CHAR, isEmojiChar);
if (isPoi && isEmojiChar) {
//we save emoji bitmaps in mapboxstyle with the codepoint as the key
pointFeature.addStringProperty(MARKER_ICON, codepointChar);
emojiCodePoints.add(codepointChar);
} else if (isPoi) {
pointFeature.addStringProperty(MARKER_ICON, contactMapMetadata.getMarkerPoi());
} else {
pointFeature.addStringProperty(MARKER_ICON, contactMapMetadata.getMarkerIcon());
}
pointFeature.addBooleanProperty(IS_POI, isPoi);
if (isPoi && codepointChar.length() == 0 && messageId != 0) {
//has a long poi label
DcMsg poiMsg = dcContext.getMsg(messageId);
String poiLongDescription = poiMsg.getSummarytext(16);
pointFeature.addStringProperty(POI_LONG_DESCRIPTION, poiLongDescription);
}
sortedPointFeatures.addFirst(pointFeature);
if (!locations.isIndependent(i) && sortedPointFeatures.size() > 1) {
Point lastPoint = (Point) sortedPointFeatures.get(1).geometry();
ArrayList<Point> lineSegmentPoints = new ArrayList<>(3);
lineSegmentPoints.add(lastPoint);
lineSegmentPoints.add(point);
LineString l = LineString.fromLngLats(lineSegmentPoints);
Feature lineFeature = Feature.fromGeometry(l, new JsonObject(), "l_" + pointFeature.id());
lineFeature.addNumberProperty(TIMESTAMP, pointFeature.getNumberProperty(TIMESTAMP));
sortedLineFeatures.addFirst(lineFeature);
}
if (boundingBuilder != null) {
boundingBuilder.include(new LatLng(locations.getLatitude(i), locations.getLongitude(i)));
}
}
if (sortedPointFeatures.size() > 0) {
for (Feature position : sortedPointFeatures) {
if (!position.getBooleanProperty(IS_POI)) {
position.addStringProperty(LAST_POSITION_ICON, contactMapMetadata.getMarkerLastPositon());
position.addStringProperty(LAST_POSITION_LABEL, contactMapMetadata.getDisplayName());
position.removeProperty(MARKER_ICON);
position.addBooleanProperty(LAST_LOCATION, true);
lastPositions.put(contactId, position);
break;
}
}
}
featureCollections.put(contactMapMetadata.getMarkerFeatureCollection(), sortedPointFeatures);
featureCollections.put(contactMapMetadata.getLineFeatureCollection(), sortedLineFeatures);
}
private MapSource addContactMapSource(ConcurrentHashMap<Integer, MapSource> contactMapSources, int contactId) {
if (contactMapSources.get(contactId) != null) {
return contactMapSources.get(contactId);
}
DcContact contact = dcContext.getContact(contactId);
MapSource contactMapSource = new MapSource(contact);
contactMapSources.put(contactId, contactMapSource);
return contactMapSource;
}
}

View file

@ -1,163 +0,0 @@
package org.thoughtcrime.securesms.map;
/**
* Created by cyberta on 13.03.19.
*/
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.mapbox.geojson.Feature;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ConversationItemFooter;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Locale;
import static android.view.View.GONE;
import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
/**
* AsyncTask to generate Bitmap from Views to be used as iconImage in a SymbolLayer.
* <p>
* Call be optionally be called to update the underlying data source after execution.
* </p>
* <p>
* Generating Views on background thread since we are not going to be adding them to the view hierarchy.
* </p>
*/
class GenerateInfoWindowTask extends AsyncTask<Feature, Bitmap, Bitmap> {
public interface GenerateInfoWindowCallback {
Context getContext();
/**
* Invoked when the bitmaps have been generated from a view.
*/
void setInfoWindowResults(Bitmap result);
}
private static final String TAG = GenerateInfoWindowCallback.class.getName();
private final WeakReference<GenerateInfoWindowCallback> callbackRef;
private final static HashSet<GenerateInfoWindowTask> instances = new HashSet<>();
GenerateInfoWindowTask(GenerateInfoWindowCallback callback) {
this.callbackRef = new WeakReference<>(callback);
instances.add(this);
}
public static void cancelRunningTasks() {
for (GenerateInfoWindowTask task : instances) {
task.cancel(true);
}
}
@SuppressWarnings("WrongThread")
@Override
protected Bitmap doInBackground(Feature... params) {
Log.d(TAG, "GenerateInfoWindowTask start");
Thread.currentThread().setName(GenerateInfoWindowTask.class.getName());
Bitmap bitmap = null;
try {
LayoutInflater inflater = LayoutInflater.from(callbackRef.get().getContext());
Feature feature = params[0];
Log.d(TAG, "GenerateInfoWindowTask: feature " + feature.id());
LinearLayout bubbleLayout = (LinearLayout)
inflater.inflate(R.layout.map_bubble_layout, null);
bubbleLayout.setBackgroundResource(R.drawable.message_bubble_background_received_alone);
EmojiTextView conversationItemBody = bubbleLayout.findViewById(R.id.conversation_item_body);
Locale locale = DynamicLanguage.getSelectedLocale(callbackRef.get().getContext());
int messageId = (int) feature.getNumberProperty(MESSAGE_ID);
int contactId = (int) feature.getNumberProperty(CONTACT_ID);
DcContact contact = DcHelper.getContext(callbackRef.get().getContext()).getContact(contactId);
TextView contactTextView = bubbleLayout.findViewById(R.id.message_sender);
contactTextView.setText(contact.getDisplayName());
String msgText;
if (messageId != 0) {
DcContext dcContext = DcHelper.getContext(callbackRef.get().getContext());
DcMsg msg = dcContext.getMsg(messageId);
if (hasImgThumbnail(msg)) {
ImageView thumbnailView = bubbleLayout.findViewById(R.id.map_bubble_img_thumbnail);
thumbnailView.setImageURI(getThumbnailUri(msg));
thumbnailView.setVisibility(View.VISIBLE);
msgText = msg.getText();
} else {
msgText = msg.getSummarytext(75);
}
ConversationItemFooter footer = bubbleLayout.findViewById(R.id.conversation_item_footer);
footer.setVisibility(View.VISIBLE);
footer.setMessageRecord(msg, locale);
} else {
msgText = "Reported: " + DateUtils.getExtendedRelativeTimeSpanString(callbackRef.get().getContext(), locale, (long) feature.getNumberProperty(TIMESTAMP));
}
if (msgText.length() == 0) {
conversationItemBody.setVisibility(GONE);
} else {
conversationItemBody.setText(msgText);
}
bitmap = BitmapUtil.generate(bubbleLayout);
} catch (NullPointerException npe) {
npe.printStackTrace();
Log.e(TAG, "Callback was GC'ed before task finished.");
}
Log.d(TAG, "GenerateInfoWindowTask finished");
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (!isCancelled() && bitmap != null) {
try {
callbackRef.get().setInfoWindowResults(bitmap);
} catch (NullPointerException npe) {
npe.printStackTrace();
Log.e(TAG, "Callback was GC'ed before task finished.");
}
}
instances.remove(this);
}
@Override
protected void onCancelled() {
super.onCancelled();
instances.remove(this);
}
private boolean hasImgThumbnail(DcMsg dcMsg) {
int type = dcMsg.getType();
return type == DcMsg.DC_MSG_IMAGE && dcMsg.hasFile();
}
public Uri getThumbnailUri(DcMsg dcMsg) {
return Uri.fromFile(new File(dcMsg.getFile()));
}
}

View file

@ -1,370 +0,0 @@
package org.thoughtcrime.securesms.map;
import static com.b44t.messenger.DcChat.DC_CHAT_NO_CHAT;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.MINIMUM_ZOOM;
import static org.thoughtcrime.securesms.map.MapDataManager.CONTACT_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.IS_POI;
import static org.thoughtcrime.securesms.map.MapDataManager.MARKER_SELECTED;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
import static org.thoughtcrime.securesms.map.model.MapSource.INFO_WINDOW_LAYER;
import android.content.Context;
import android.content.Intent;
import android.graphics.PointF;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.RelativeLayout;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.FragmentTransaction;
import com.b44t.messenger.DcMsg;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.mapbox.geojson.Feature;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.WellKnownTileServer;
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 org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import org.thoughtcrime.securesms.components.rangeslider.TimeRangeSlider;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.geolocation.DcLocation;
import org.thoughtcrime.securesms.util.Prefs;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
public class MapActivity extends BaseActivity implements Observer,
TimeRangeSlider.OnTimestampChangedListener,
KeyboardAwareLinearLayout.OnKeyboardShownListener,
KeyboardAwareLinearLayout.OnKeyboardHiddenListener {
public static final String TAG = MapActivity.class.getSimpleName();
public static final String CHAT_ID = "chat_id";
public static final String CHAT_IDS = "chat_id";
public static final String MAP_TAG = "org.thoughtcrime.securesms.map";
private DcLocation dcLocation;
private MapDataManager mapDataManager;
private MapboxMap mapboxMap;
DCMapFragment mapFragment;
MarkerViewManager markerViewManager;
private int chatId;
private InputAwareLayout inputAwareContainer;
public static void lazyMapboxInit(Context context) {
try {
Mapbox.getInstance(context, BuildConfig.MAP_ACCESS_TOKEN, WellKnownTileServer.Mapbox);
// disable telemetry. these functions are currently partly redundant,
// however, implementations may change
// and the two explicit calls seems to have worked in the past,
// see https://github.com/mapbox/mapbox-gl-native/issues/13304
// We are now using MapLibre, so there is no telemetry
// TelemetryEnabler.updateTelemetryState(TelemetryEnabler.State.DISABLED);
// TelemetryDefinition telemetry = Mapbox.getTelemetry();
// if (telemetry != null) {
// telemetry.setUserTelemetryRequestState(false);
// }
}
catch(Exception e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lazyMapboxInit(this);
setContentView(R.layout.activity_map);
chatId = getIntent().getIntExtra(CHAT_ID, -1);
if (chatId == -1) {
finish();
return;
}
dcLocation = DcLocation.getInstance();
if (savedInstanceState == null) {
final FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
mapFragment = DCMapFragment.newInstance();
transaction.add(R.id.container, mapFragment, MAP_TAG);
transaction.commit();
} else {
mapFragment = (DCMapFragment) getSupportFragmentManager().findFragmentByTag(MAP_TAG);
}
mapFragment.getMapAsync(mapboxMap -> mapboxMap.setStyle(Style.getPredefinedStyle("Streets"), style -> {
this.mapboxMap = mapboxMap;
this.markerViewManager = new MarkerViewManager(mapFragment.getMapView(), mapboxMap);
inputAwareContainer = findViewById(R.id.inputAwareContainer);
inputAwareContainer.addOnKeyboardHiddenListener(this);
inputAwareContainer.addOnKeyboardShownListener(this);
final int accountId = DcHelper.getContext(this.getApplicationContext()).getAccountId();
final LatLng lastMapCenter = Prefs.getMapCenter(this.getApplicationContext(), accountId, chatId);
if (lastMapCenter != null) {
double lastZoom = Prefs.getMapZoom(this.getApplicationContext(), accountId, chatId);
mapboxMap.setCameraPosition(new CameraPosition.Builder()
.target(lastMapCenter)
.zoom(lastZoom)
.build());
} else if (dcLocation.getLastLocation().getProvider().equals("?")) {
double randomLongitude = getRandomLongitude();
mapboxMap.setCameraPosition(new CameraPosition.Builder()
.target(new LatLng(0d, randomLongitude))
.zoom(MINIMUM_ZOOM)
.build());
} else {
mapboxMap.setCameraPosition(new CameraPosition.Builder()
.target(new LatLng(dcLocation.getLastLocation().getLatitude(), dcLocation.getLastLocation().getLongitude()))
.zoom(9)
.build());
}
mapboxMap.getUiSettings().setLogoEnabled(false);
mapboxMap.getUiSettings().setAttributionEnabled(false);
Style mapBoxStyle = mapboxMap.getStyle();
if (mapBoxStyle == null) {
return;
}
mapDataManager = new MapDataManager(this,
mapBoxStyle,
mapboxMap.getLocationComponent(),
chatId,
(latLngBounds) -> {
Log.d(TAG, "on Data initialized");
if (latLngBounds != null && lastMapCenter == null) {
mapboxMap.easeCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 50), 1000);
}
mapboxMap.addOnMapClickListener(point ->
handleInfoWindowClick(point) ||
handleMarkerClick(point) ||
handleAddPoiClick(point));
mapboxMap.addOnMapLongClickListener(this::handlePoiLongClick);
SwitchCompat switchCompat = this.findViewById(R.id.locationTraceSwitch);
switchCompat.setOnCheckedChangeListener((buttonView, isChecked) -> {
mapDataManager.showTraces(isChecked);
});
});
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
protected void onResume() {
super.onResume();
DcLocation.getInstance().addObserver(this);
if (mapDataManager != null) {
mapDataManager.onResume();
}
}
@Override
protected void onPause() {
super.onPause();
DcLocation.getInstance().deleteObserver(this);
if (mapDataManager != null) {
mapDataManager.onPause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mapDataManager != null) {
Prefs.setMapCenter(this.getApplicationContext(), mapDataManager.getAccountId(), mapDataManager.getChatId(), mapboxMap.getCameraPosition().target);
Prefs.setMapZoom(this.getApplicationContext(), mapDataManager.getAccountId(), mapDataManager.getChatId(), mapboxMap.getCameraPosition().zoom);
mapDataManager.onDestroy();
}
if (markerViewManager != null) {
markerViewManager.onDestroy();
}
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof DcLocation) {
this.dcLocation = (DcLocation) o;
Log.d(TAG, "show marker on map: " +
dcLocation.getLastLocation().getLatitude() + ", " +
dcLocation.getLastLocation().getLongitude());
//TODO: consider implementing a button -> center map to current location
}
}
@Override
public void onTimestampChanged(long startTimestamp, long stopTimestamp) {
if (this.mapboxMap == null) {
return;
}
mapDataManager.filterRange(startTimestamp, stopTimestamp);
}
@Override
public void onFilterLastPosition(long startTimestamp) {
if (this.mapboxMap == null) {
return;
}
mapDataManager.filterLastPositions(startTimestamp);
}
private double getRandomLongitude() {
double start = -180;
double end = 180;
double random = new Random().nextDouble();
return start + (random * (end - start));
}
private boolean handleMarkerClick(LatLng point) {
final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
Log.d(TAG, "on item clicked.");
List<Feature> features = mapboxMap.queryRenderedFeatures(pixel, mapDataManager.getMarkerLayers());
for (Feature feature : features) {
Log.d(TAG, "found feature: " + feature.toJson());
//show first feature that has meta data infos
if (feature.hasProperty(MARKER_SELECTED)) {
mapDataManager.setMarkerSelected(feature.id());
if (markerViewManager.hasMarkers()) {
markerViewManager.removeMarkers();
}
return true;
}
}
return mapDataManager.unselectMarker();
}
private boolean handleInfoWindowClick(LatLng point) {
final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
List<Feature> features = mapboxMap.queryRenderedFeatures(pixel, INFO_WINDOW_LAYER);
Log.d(TAG, "on info window clicked." + features.size());
for (Feature feature : features) {
Log.d(TAG, "found feature: " + feature.toJson());
int messageId = feature.getNumberProperty(MESSAGE_ID).intValue();
DcMsg dcMsg = DcHelper.getContext(this).getMsg(messageId);
int dcMsgChatId = dcMsg.getChatId();
if (dcMsgChatId == DC_CHAT_NO_CHAT) {
continue;
}
int startingPosition = DcMsg.getMessagePosition(dcMsg, DcHelper.getContext(this));
Intent intent = new Intent(MapActivity.this, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, dcMsgChatId);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
startActivity(intent);
return true;
}
return false;
}
private boolean handleAddPoiClick(LatLng point) {
if (chatId == DC_CHAT_NO_CHAT || !DcHelper.getContext(this).getChat(chatId).canSend()) {
return false;
}
if (markerViewManager.hasMarkers()) {
MarkerView markerView = markerViewManager.getCenteredMarker();
if (markerView != null && markerView.getView() instanceof AddPoiView) {
AddPoiView view = (AddPoiView) markerView.getView();
inputAwareContainer.hideCurrentInput(view.getMessageView());
}
markerViewManager.removeMarkers();
} else {
AddPoiView addPoiView = new AddPoiView(this);
addPoiView.setLatLng(point);
addPoiView.setChatId(chatId);
addPoiView.setOnMessageSentListener(markerViewManager);
MarkerView markerView = new MarkerView(point, addPoiView);
markerViewManager.addMarker(markerView);
inputAwareContainer.showSoftkey(addPoiView.getMessageView());
markerViewManager.center(markerView);
}
return true;
}
private boolean handlePoiLongClick(LatLng point) {
final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
List<Feature> features = mapboxMap.queryRenderedFeatures(pixel, mapDataManager.getMarkerLayers());
for (Feature feature : features) {
if (feature.getBooleanProperty(IS_POI)) {
new AlertDialog.Builder(MapActivity.this)
.setMessage(getString(R.string.menu_delete_location))
.setPositiveButton(R.string.yes, (dialog, which) -> {
int messageId = feature.getNumberProperty(MESSAGE_ID).intValue();
int[] messages = new int[1];
messages[0] = messageId;
DcHelper.getContext(MapActivity.this).deleteMsgs(messages);
int contactId = feature.getNumberProperty(CONTACT_ID).intValue();
if (mapDataManager.isSelected(feature)) {
mapDataManager.unselectMarker();
}
mapDataManager.updateSource(contactId);
})
.setNegativeButton(R.string.no, null)
.show();
return true;
}
}
return false;
}
@Override
public void onKeyboardHidden() {
markerViewManager.onKeyboardShown(0);
}
@Override
public void onKeyboardShown() {
markerViewManager.onKeyboardShown(inputAwareContainer.getKeyboardHeight());
}
}

View file

@ -1,623 +0,0 @@
package org.thoughtcrime.securesms.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.location.modes.RenderMode.COMPASS;
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.has;
import static com.mapbox.mapboxsdk.style.expressions.Expression.length;
import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
import static com.mapbox.mapboxsdk.style.expressions.Expression.neq;
import static com.mapbox.mapboxsdk.style.expressions.Expression.not;
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;
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
import static com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_CENTER;
import static com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP;
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineJoin;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineOpacity;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAnchor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textOffset;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
import static org.thoughtcrime.securesms.map.model.MapSource.INFO_WINDOW_LAYER;
import static org.thoughtcrime.securesms.map.model.MapSource.LINE_FEATURE_LIST;
import static org.thoughtcrime.securesms.util.BitmapUtil.generateColoredBitmap;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.mapboxsdk.exceptions.InvalidLatLngBoundsException;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.location.LocationComponent;
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
import com.mapbox.mapboxsdk.location.permissions.PermissionsManager;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.style.expressions.Expression;
import com.mapbox.mapboxsdk.style.layers.LineLayer;
import com.mapbox.mapboxsdk.style.layers.Property;
import com.mapbox.mapboxsdk.style.layers.PropertyFactory;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.map.DataCollectionTask.DataCollectionCallback;
import org.thoughtcrime.securesms.map.GenerateInfoWindowTask.GenerateInfoWindowCallback;
import org.thoughtcrime.securesms.map.model.FilterProvider;
import org.thoughtcrime.securesms.map.model.MapSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by cyberta on 07.03.19.
*/
public class MapDataManager implements DcEventCenter.DcEventDelegate,
GenerateInfoWindowCallback,
DataCollectionCallback {
public static final String MARKER_SELECTED = "MARKER_SELECTED";
public static final String LAST_LOCATION = "LAST_LOCATION";
public static final String CONTACT_ID = "CONTACT_ID";
public static final String INFO_WINDOW_ID = "INFO_WINDOW_ID";
public static final String TIMESTAMP = "TIMESTAMP";
public static final String MESSAGE_ID = "MESSAGE_ID";
public static final String ACCURACY = "ACCURACY";
public static final String MARKER_CHAR = "MARKER_CHAR";
public static final String IS_EMOJI_CHAR = "IS_EMOJI_CHAR";
public static final String POI_LONG_DESCRIPTION = "POI_LONG_DESCRIPTION";
public static final String MARKER_ICON = "MARKER_ICON";
public static final String IS_POI = "IS_POI";
public static final String LAST_POSITION_ICON = "LAST_POSITION_ICON";
public static final String LAST_POSITION_LABEL = "LAST_POSITION_LABEL";
private static final String INFO_WINDOW_SRC = "INFO_WINDOW_SRC";
private static final String LAST_POSITION_LAYER = "LAST_POSITION_LAYER";
private static final String LAST_POSITION_SOURCE = "LAST_POSITION_SRC";
public static final int ALL_CHATS_GLOBAL_MAP = 0;
public static final long TIMESTAMP_NOW = 0L;
public static final long TIME_FRAME = 1000 * 60 * 60 * 24 * 2; // 2d
private static final long DEFAULT_LAST_POSITION_DELTA = 1000 * 60 * 60 * 24; // 1d
private static final String TAG = MapDataManager.class.getSimpleName();
private final Style mapboxStyle;
private final ConcurrentHashMap<Integer, MapSource> contactMapSources = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LinkedList<Feature>> featureCollections = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Feature> lastPositions = new ConcurrentHashMap<>();
private final Set<String> emojiCodePoints = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final FilterProvider filterProvider = new FilterProvider();
private Feature selectedFeature;
private final int chatId;
private final LatLngBounds.Builder boundingBuilder;
private final Context context;
private final DcContext dcContext;
private final MapDataState callback;
private boolean isInitial = true;
private boolean showTraces = false;
private final LocationComponent locationComponent;
private final EmojiProvider emojiProvider;
public interface MapDataState {
void onDataInitialized(LatLngBounds bounds);
}
public MapDataManager(Context context, @NonNull Style mapboxMapStyle, LocationComponent locationComponent, int chatId, MapDataState updateCallback) {
Log.d(TAG, "performance test - create map manager");
this.mapboxStyle = mapboxMapStyle;
this.context = context;
this.dcContext = DcHelper.getContext(context);
this.chatId = chatId;
boundingBuilder = new LatLngBounds.Builder();
this.callback = updateCallback;
this.locationComponent = locationComponent;
emojiProvider = EmojiProvider.getInstance(context);
initInfoWindowLayer();
initLastPositionLayer();
initLocationComponent();
filterProvider.setMessageFilter(true);
filterProvider.setLastPositionFilter(System.currentTimeMillis() - DEFAULT_LAST_POSITION_DELTA);
applyLastPositionFilter();
updateSources();
DcHelper.getEventCenter(context).addObserver(DC_EVENT_LOCATION_CHANGED, this);
Log.d(TAG, "performance test - create map manager finished");
}
public void onResume() {
DcHelper.getEventCenter(context).addObserver(DC_EVENT_LOCATION_CHANGED, this);
if (!isInitial) {
updateSources();
}
isInitial = false;
}
public void onPause() {
DcHelper.getEventCenter(context).removeObserver(DC_EVENT_LOCATION_CHANGED, this);
}
public void onDestroy() {
GenerateInfoWindowTask.cancelRunningTasks();
DataCollectionTask.cancelRunningTasks();
Log.d(TAG, "performance test - Map manager destroyed");
}
@Override
public Context getContext() {
return context;
}
public void refreshSource(int contactId) {
MapSource source = contactMapSources.get(contactId);
LinkedList<Feature> collection = featureCollections.get(source.getMarkerFeatureCollection());
GeoJsonSource pointSource = (GeoJsonSource) mapboxStyle.getSource(source.getMarkerSource());
pointSource.setGeoJson(FeatureCollection.fromFeatures(collection));
LinkedList<Feature> lineFeatures = featureCollections.get(source.getLineFeatureCollection());
GeoJsonSource lineSource = (GeoJsonSource) mapboxStyle.getSource(source.getLineSource());
lineSource.setGeoJson(FeatureCollection.fromFeatures(lineFeatures));
GeoJsonSource lastPostionSource = (GeoJsonSource) mapboxStyle.getSource(LAST_POSITION_SOURCE);
lastPostionSource.setGeoJson(FeatureCollection.fromFeatures(new LinkedList<>(lastPositions.values())));
}
@Override
public void handleEvent(@NonNull DcEvent event) {
int eventId = event.getId();
Log.d(TAG, "updateEvent in MapDataManager called. eventId: " + eventId);
int contactId = event.getData1Int();
if (contactMapSources.containsKey(contactId)) {
HashSet<String> emojiCodePoints = new HashSet<>();
DataCollector collector = new DataCollector(dcContext,
contactMapSources,
featureCollections,
lastPositions,
emojiCodePoints,
emojiProvider,
null
);
collector.updateSource(chatId,
contactId,
System.currentTimeMillis() - TIME_FRAME,
TIMESTAMP_NOW);
refreshSource(contactId);
handleEmojiCodepoints(emojiCodePoints);
}
Log.d(TAG, "updateEvent in MapDataManager called. finished: " + eventId);
}
@Override
public boolean runOnMain() {
return true;
}
public String[] getMarkerLayers() {
String markerLayers[] = new String[contactMapSources.size() + 1];
int i = 0;
for (Map.Entry<Integer, MapSource> entry : contactMapSources.entrySet()) {
markerLayers[i] = entry.getValue().getMarkerLayer();
i += 1;
}
markerLayers[contactMapSources.size()] = LAST_POSITION_LAYER;
return markerLayers;
}
public boolean unselectMarker() {
if (selectedFeature != null) {
selectedFeature.addBooleanProperty(MARKER_SELECTED, false);
refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue());
selectedFeature = null;
GeoJsonSource source = (GeoJsonSource) mapboxStyle.getSource(INFO_WINDOW_SRC);
source.setGeoJson(FeatureCollection.fromFeatures(new ArrayList<>()));
return true;
}
return false;
}
public void setMarkerSelected(String featureId) {
if (selectedFeature == null) {
setNewMarkerSelected(featureId);
} else if (selectedFeature.id().equals(featureId)) {
updateSelectedMarker();
} else {
replaceSelectedMarker(featureId);
}
new GenerateInfoWindowTask(this).execute(selectedFeature);
}
public boolean isSelected(@NonNull Feature feature) {
return selectedFeature != null && selectedFeature.id().equals(feature.id());
}
/**
* Invoked when the bitmaps have been generated from a view.
*/
@Override
public void setInfoWindowResults(Bitmap result) {
mapboxStyle.addImage(INFO_WINDOW_ID, result);
GeoJsonSource infoWindowSource = (GeoJsonSource) mapboxStyle.getSource(INFO_WINDOW_SRC);
infoWindowSource.setGeoJson(selectedFeature);
}
@Override
public void onDataCollectionFinished(Set <String> emojiCodePoints) {
handleEmojiCodepoints(emojiCodePoints);
for (MapSource source : contactMapSources.values()) {
initContactBasedLayers(source);
refreshSource(source.getContactId());
applyMarkerFilter(source);
applyLineFilter(source);
}
if (boundingBuilder != null && callback != null) {
LatLngBounds bound = null;
try {
bound = boundingBuilder.build();
} catch (InvalidLatLngBoundsException e) {
Log.w(TAG, e.getLocalizedMessage());
}
callback.onDataInitialized(bound);
}
}
public void handleEmojiCodepoints(Set<String> emojiCodePoints) {
// generate only new emoji bitmaps, so remove the ones from the set that we already generated
emojiCodePoints.removeAll(this.emojiCodePoints);
if (emojiCodePoints.isEmpty()) {
return;
}
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
for (String codePoint : emojiCodePoints) {
Bitmap emoji = emojiProvider.getEmojiBitmap(codePoint, 0.5f, true);
handler.post(() -> {
mapboxStyle.addImage(codePoint, emoji);
emojiCodePoints.add(codePoint);
});
}
});
}
public void filterRange(long startTimestamp, long endTimestamp) {
int[] contactIds = getContactIds(chatId);
filterProvider.setRangeFilter(startTimestamp, endTimestamp);
applyFilters(contactIds);
}
public void filterLastPositions(long timestamp) {
int[] contactIds = getContactIds(chatId);
filterProvider.setLastPositionFilter(timestamp);
applyFilters(contactIds);
}
public void showTraces(boolean show) {
int[] contactIds = getContactIds(chatId);
this.showTraces = show;
filterProvider.setMessageFilter(!show);
applyFilters(contactIds);
}
public int getChatId() {
return chatId;
}
public int getAccountId() {
return dcContext.getAccountId();
}
private void showLineLayer(MapSource source) {
LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(source.getLineLayer());
if (lineLayer != null) {
lineLayer.setProperties(visibility(showTraces ? VISIBLE : NONE));
}
}
private void applyFilters(int[] contactIds) {
for (int contactId : contactIds) {
MapSource contactMapMetadata = contactMapSources.get(contactId);
if (contactMapMetadata == null) {
continue;
}
showLineLayer(contactMapMetadata);
applyMarkerFilter(contactMapMetadata);
applyLineFilter(contactMapMetadata);
}
applyLastPositionFilter();
}
private void applyLastPositionFilter() {
SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(LAST_POSITION_LAYER);
if (markerLayer != null) {
markerLayer.setFilter(filterProvider.getTimeFilter());
}
}
private void applyMarkerFilter(MapSource source) {
SymbolLayer markerLayer = (SymbolLayer) mapboxStyle.getLayer(source.getMarkerLayer());
if (markerLayer != null) {
markerLayer.setFilter(filterProvider.getMarkerFilter());
}
}
private void applyLineFilter(MapSource source) {
LineLayer lineLayer = (LineLayer) mapboxStyle.getLayer(source.getLineLayer());
if (lineLayer != null) {
lineLayer.setFilter(filterProvider.getTimeFilter());
}
}
private int[] getContactIds(int chatId) {
if (chatId == ALL_CHATS_GLOBAL_MAP) {
return dcContext.getContacts(DC_GCL_ADD_SELF, "");
} else {
int[] contactIds = dcContext.getChatContacts(chatId);
boolean hasSelf = false;
for (int contact : contactIds) {
if (contact == 1) {
hasSelf = true;
break;
}
}
if (!hasSelf) {
contactIds = Arrays.copyOf(contactIds, contactIds.length + 1);
contactIds[contactIds.length - 1] = 1;
}
return contactIds;
}
}
private void initInfoWindowLayer() {
Expression iconOffset = switchCase(
toBool(get(IS_EMOJI_CHAR)), literal(new Float[] {-2f, -23f}),
toBool(get(LAST_LOCATION)), literal(new Float[] {-2f, -25f}),
literal(new Float[] {-2f, -20f}));
GeoJsonSource infoWindowSource = new GeoJsonSource(INFO_WINDOW_SRC);
mapboxStyle.addSource(infoWindowSource);
mapboxStyle.addLayer(new SymbolLayer(INFO_WINDOW_LAYER, INFO_WINDOW_SRC).withProperties(
iconImage(INFO_WINDOW_ID),
iconAnchor(ICON_ANCHOR_BOTTOM_LEFT),
/* all info window and marker image to appear at the same time*/
iconAllowOverlap(true),
/* offset the info window to be above the marker */
iconOffset(iconOffset)
));
}
private void initLastPositionLayer() {
GeoJsonSource lastPositionSource = new GeoJsonSource(LAST_POSITION_SOURCE);
mapboxStyle.addSource(lastPositionSource);
Expression markerSize =
switchCase(toBool(get(MARKER_SELECTED)), literal(1.75f), literal(1.25f));
mapboxStyle.addLayerBelow(new SymbolLayer(LAST_POSITION_LAYER, LAST_POSITION_SOURCE).withProperties(
iconImage(get(LAST_POSITION_ICON)),
/* all info window and marker image to appear at the same time*/
iconAllowOverlap(true),
iconIgnorePlacement(true),
iconSize(markerSize),
textField(get(LAST_POSITION_LABEL)),
textAnchor(TEXT_ANCHOR_TOP),
textOffset(new Float[]{0.0f, 1.0f}),
textAllowOverlap(true),
textIgnorePlacement(true)
).withFilter(filterProvider.getTimeFilter()), INFO_WINDOW_LAYER);
}
@SuppressWarnings( {"MissingPermission"})
private void initLocationComponent() {
if (! PermissionsManager.areLocationPermissionsGranted(context)) {
return;
}
LocationComponentActivationOptions locationComponentActivationOptions = LocationComponentActivationOptions
.builder(context, mapboxStyle)
.build();
locationComponent.activateLocationComponent(locationComponentActivationOptions);
locationComponent.setRenderMode(COMPASS);
locationComponent.setLocationComponentEnabled(true);
}
private void initContactBasedLayers(MapSource source) {
if (mapboxStyle.getLayer(source.getMarkerLayer()) != null) {
return;
}
GeoJsonSource markerPositionSource = new GeoJsonSource(source.getMarkerSource());
GeoJsonSource linePositionSource = new GeoJsonSource(source.getLineSource());
try {
mapboxStyle.addSource(markerPositionSource);
mapboxStyle.addSource(linePositionSource);
} catch (RuntimeException e) {
//TODO: specify exception more
Log.e(TAG, "Unable to init GeoJsonSources. Already added to mapBoxMap? " + e.getMessage());
}
mapboxStyle.addImage(source.getMarkerLastPositon(),
generateColoredLastPositionIcon(source.getColorArgb()));
mapboxStyle.addImage(source.getMarkerIcon(),
generateColoredLocationIcon(source.getColorArgb()));
mapboxStyle.addImage(source.getMarkerPoi(),
generateColoredPoiIcon(source.getColorArgb()));
Expression markerSize =
switchCase(
neq(length(get(MARKER_CHAR)), literal(0)),
switchCase(toBool(get(MARKER_SELECTED)), literal(2.25f), literal(2.0f)),
neq(get(MESSAGE_ID), literal(0)),
switchCase(toBool(get(MARKER_SELECTED)), literal(2.25f), literal(2.0f)),
switchCase(toBool(get(MARKER_SELECTED)), literal(1.1f), literal(0.7f)));
Expression markerIcon = get(MARKER_ICON);
mapboxStyle.addLayerBelow(new LineLayer(source.getLineLayer(), source.getLineSource())
.withProperties(PropertyFactory.lineCap(Property.LINE_CAP_ROUND),
lineJoin(Property.LINE_JOIN_ROUND),
lineWidth(3f),
lineOpacity(0.5f),
lineColor(source.getColorArgb()),
visibility(NONE)
)
.withFilter(filterProvider.getTimeFilter()),
LAST_POSITION_LAYER);
Expression textField = switchCase(eq(length(get(MARKER_CHAR)), 1), get(MARKER_CHAR),
get(POI_LONG_DESCRIPTION));
Float[] offset = new Float[] {0.0f, 1.25f};
Float[] zeroOffset = new Float[] {0.0f, 0.0f};
Expression textOffset = switchCase(
has(POI_LONG_DESCRIPTION), literal(offset),
literal(zeroOffset));
Expression textColor = switchCase(
has(POI_LONG_DESCRIPTION), literal("#000000"),
literal("#FFFFFF")
);
Expression textAnchor = switchCase(
has(POI_LONG_DESCRIPTION), literal(TEXT_ANCHOR_TOP),
literal(TEXT_ANCHOR_CENTER)
);
Expression textSize = switchCase(
has(POI_LONG_DESCRIPTION), literal(12.0f),
literal(15.0f)
);
mapboxStyle.addLayerBelow(new SymbolLayer(source.getMarkerLayer(), source.getMarkerSource())
.withProperties(
iconImage(markerIcon),
iconSize(markerSize),
iconIgnorePlacement(false),
iconAllowOverlap(false),
textField(textField),
textOffset(textOffset),
textAnchor(textAnchor),
textSize(textSize),
textColor(textColor))
.withFilter(all(filterProvider.getMarkerFilter(),
not(get(LAST_LOCATION)))),
LAST_POSITION_LAYER);
}
private Bitmap generateColoredLastPositionIcon(int colorFilter) {
return generateColoredBitmap(context, colorFilter, R.drawable.ic_location_on_white_48dp);
}
private Bitmap generateColoredLocationIcon(int colorFilter) {
return generateColoredBitmap(context, colorFilter, R.drawable.ic_location_dot);
}
private Bitmap generateColoredPoiIcon(int colorFilter) {
return generateColoredBitmap(context, colorFilter, R.drawable.ic_location_poi_dot);
}
private void updateSources() {
new DataCollectionTask(dcContext,
chatId,
getContactIds(chatId),
contactMapSources,
featureCollections,
lastPositions,
boundingBuilder,
emojiProvider,
this).execute();
}
public void updateSource(int contactId) {
new DataCollectionTask(dcContext,
chatId,
new int[]{contactId},
contactMapSources,
featureCollections,
lastPositions,
boundingBuilder,
emojiProvider,
this).execute();
}
private void replaceSelectedMarker(String featureId) {
Feature feature = getFeatureWithId(featureId);
feature.addBooleanProperty(MARKER_SELECTED, true);
selectedFeature.addBooleanProperty(MARKER_SELECTED, false);
int lastContactId = selectedFeature.getNumberProperty(CONTACT_ID).intValue();
int currentContactId = feature.getNumberProperty(CONTACT_ID).intValue();
selectedFeature = feature;
refreshSource(currentContactId);
if (lastContactId != currentContactId) {
refreshSource(lastContactId);
}
}
private void updateSelectedMarker() {
boolean isSelected = selectedFeature.getBooleanProperty(MARKER_SELECTED);
selectedFeature.addBooleanProperty(MARKER_SELECTED, !isSelected);
refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue());
}
private void setNewMarkerSelected(String featureId) {
Feature feature = getFeatureWithId(featureId);
feature.addBooleanProperty(MARKER_SELECTED, true);
selectedFeature = feature;
refreshSource(selectedFeature.getNumberProperty(CONTACT_ID).intValue());
}
private Feature getFeatureWithId(String id) {
for (Map.Entry<String, LinkedList<Feature>> e : featureCollections.entrySet()) {
String key = e.getKey();
if (key.startsWith(LINE_FEATURE_LIST)) {
continue;
}
LinkedList<Feature> featureCollection = e.getValue();
for (Feature f : featureCollection) {
if (f.id().equals(id)) {
return f;
}
}
}
return null;
}
}

View file

@ -1,88 +0,0 @@
package org.thoughtcrime.securesms.map;
import android.graphics.PointF;
import androidx.annotation.NonNull;
import android.view.View;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.Projection;
/**
* MarkerView class wraps a latitude-longitude pair with a Android SDK View.
* <p>
* It can be used in conjunction with {@link com.mapbox.mapboxsdk.plugins.markerview.MarkerViewManager} to synchronise the Android SDK View on top
* of map at the latitude-longitude pair.
* </p>
*/
public class MarkerView {
private final View view;
private LatLng latLng;
private Projection projection;
private OnPositionUpdateListener onPositionUpdateListener;
/**
* Create a MarkerView
*
* @param latLng latitude-longitude pair
* @param view an Android SDK View
*/
public MarkerView(@NonNull LatLng latLng, @NonNull View view) {
this.latLng = latLng;
this.view = view;
}
/**
* Update the location of the MarkerView on the map.
* <p>
* Provided as a latitude-longitude pair.
* </p>
*
* @param latLng latitude-longitude pair
*/
public void setLatLng(@NonNull LatLng latLng) {
this.latLng = latLng;
update();
}
public LatLng getLatLng() {
return latLng;
}
/**
* Set a callback to be invoked when position placement is calculated.
* <p>
* Can be used to offset a MarkerView on screen.
* </p>
*
* @param onPositionUpdateListener callback to be invoked when position placement is calculated
*/
public void setOnPositionUpdateListener(OnPositionUpdateListener onPositionUpdateListener) {
this.onPositionUpdateListener = onPositionUpdateListener;
}
/**
* Callback definition that is invoked when position placement of a MarkerView is calculated.
*/
public interface OnPositionUpdateListener {
PointF onUpdate(PointF pointF);
}
void setProjection(Projection projection) {
this.projection = projection;
}
View getView() {
return view;
}
void update() {
PointF point = projection.toScreenLocation(latLng);
if (onPositionUpdateListener != null) {
point = onPositionUpdateListener.onUpdate(point);
}
view.setX(point.x);
view.setY(point.y);
}
}

View file

@ -1,132 +0,0 @@
package org.thoughtcrime.securesms.map;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraUpdate;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import java.util.ArrayList;
import java.util.List;
/**
* Class responsible for synchronising views at a LatLng on top of a Map.
*/
public class MarkerViewManager implements MapView.OnDidFinishRenderingFrameListener, SendingTask.OnMessageSentListener {
private final MapView mapView;
private final MapboxMap mapboxMap;
private final List<MarkerView> markers = new ArrayList<>();
private boolean initialised;
private MarkerView centeredMarker;
private int keyboardHeight = 0;
/**
* Create a MarkerViewManager.
*
* @param mapView the MapView used to synchronise views on
* @param mapboxMap the MapboxMap to synchronise views with
*/
public MarkerViewManager(MapView mapView, MapboxMap mapboxMap) {
this.mapView = mapView;
this.mapboxMap = mapboxMap;
}
/**
* Destroys the MarkerViewManager.
* <p>
* Should be called before MapView#onDestroy
* </p>
*/
@UiThread
public void onDestroy() {
markers.clear();
mapView.removeOnDidFinishRenderingFrameListener(this);
initialised = false;
}
/**
* Add a MarkerView to the map using MarkerView and LatLng.
*
* @param markerView the markerView to synchronise on the map
*/
@UiThread
public void addMarker(@NonNull MarkerView markerView) {
if (mapView.isDestroyed() || markers.contains(markerView)) {
return;
}
if (!initialised) {
initialised = true;
mapView.addOnDidFinishRenderingFrameListener(this);
}
markerView.setProjection(mapboxMap.getProjection());
mapView.addView(markerView.getView());
markers.add(markerView);
}
public boolean hasMarkers() {
return markers.size() > 0;
}
@UiThread
public void removeMarkers() {
if (!mapView.isDestroyed()) {
for (MarkerView markerView : markers) {
mapView.removeView(markerView.getView());
}
}
centeredMarker = null;
markers.clear();
}
@Override
public void onDidFinishRenderingFrame(boolean fully) {
update();
}
private void update() {
for (MarkerView marker : markers) {
marker.update();
}
}
@Override
public void onMessageSent() {
removeMarkers();
}
@UiThread
public void center(MarkerView view) {
centeredMarker = view;
view.getView().post(() -> {
int markerWidth = view.getView().getWidth();
CameraPosition currentPosition = mapboxMap.getCameraPosition();
CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(
new CameraPosition.Builder().
target(view.getLatLng()).
bearing(currentPosition.bearing).
tilt(currentPosition.tilt).
zoom(currentPosition.zoom).
padding(0d , 0d, markerWidth / 2d, keyboardHeight).
build());
mapboxMap.easeCamera(cameraUpdate);
});
}
MarkerView getCenteredMarker() {
return centeredMarker;
}
@UiThread
void onKeyboardShown(int keyboardHeight) {
this.keyboardHeight = keyboardHeight;
if (centeredMarker != null) {
center(centeredMarker);
}
}
}

View file

@ -1,52 +0,0 @@
package org.thoughtcrime.securesms.map;
import android.content.Context;
import android.os.AsyncTask;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.connect.DcHelper;
public class SendingTask extends AsyncTask<SendingTask.Model, SendingTask.Model, SendingTask.Model> {
public interface OnMessageSentListener {
void onMessageSent();
}
public static class Model {
private final DcMsg msg;
private final int chatId;
private final OnMessageSentListener callback;
public Model(DcMsg msg,
int chatId,
OnMessageSentListener callback) {
this.msg = msg;
this.chatId = chatId;
this.callback = callback;
}
}
private final DcContext dcContext;
public SendingTask(Context context) {
this.dcContext = DcHelper.getContext(context);
}
@Override
protected Model doInBackground(Model... param) {
Model m = param[0];
if(m.msg!=null) {
dcContext.sendMsg(m.chatId, m.msg);
}
return m;
}
@Override
protected void onPostExecute(Model result) {
if (result.callback != null) {
result.callback.onMessageSent();
}
}
}

View file

@ -1,77 +0,0 @@
package org.thoughtcrime.securesms.map.model;
import androidx.annotation.NonNull;
import com.mapbox.mapboxsdk.style.expressions.Expression;
import java.util.HashMap;
import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
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.neq;
import static org.thoughtcrime.securesms.map.MapDataManager.MESSAGE_ID;
import static org.thoughtcrime.securesms.map.MapDataManager.TIMESTAMP;
import static org.thoughtcrime.securesms.map.model.FilterProvider.FilterType.LAST_POSITION;
import static org.thoughtcrime.securesms.map.model.FilterProvider.FilterType.MESSAGES;
import static org.thoughtcrime.securesms.map.model.FilterProvider.FilterType.RANGE;
/**
* Created by cyberta on 11.04.19.
*/
public class FilterProvider {
public enum FilterType {
LAST_POSITION,
RANGE,
MESSAGES
}
private final HashMap<FilterType, Expression> expressions = new HashMap();
public void setRangeFilter(long startTimestamp, long endTimestamp) {
removeFilter(LAST_POSITION);
addFilter(RANGE, all(
lte(get(TIMESTAMP), endTimestamp),
gte(get(TIMESTAMP), startTimestamp)));
}
public void setLastPositionFilter(long startTimestamp) {
removeFilter(RANGE);
addFilter(LAST_POSITION, gte(get(TIMESTAMP), startTimestamp));
}
public void setMessageFilter(boolean filter) {
if (filter) {
addFilter(MESSAGES, neq(get(MESSAGE_ID), literal(0)));
} else {
removeFilter(MESSAGES);
}
}
private void addFilter(FilterType type, @NonNull Expression expression) {
expressions.put(type, expression);
}
private void removeFilter(FilterType type) {
expressions.remove(type);
}
public Expression getMarkerFilter() {
return all(expressions.values().toArray(new Expression[expressions.values().size()]));
}
public Expression getTimeFilter() {
if (expressions.get(LAST_POSITION) != null) {
return expressions.get(LAST_POSITION);
} else if (expressions.get(RANGE) != null) {
return expressions.get(RANGE);
}
return all();
}
}

View file

@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.map.model;
import android.graphics.Color;
import com.b44t.messenger.DcContact;
/**
* Created by cyberta on 07.03.19.
*/
public class MapSource {
public static final String LINE_LAYER = "line_layer";
public static final String MARKER_LAYER = "symbol_layer";
public static final String INFO_WINDOW_LAYER = "info_window_layer";
public static final String LINE_SOURCE = "line_source";
public static final String MARKER_POSITION_SOURCE = "marker_position";
public static final String MARKER_ICON = "marker_icon_id";
public static final String MARKER_POI = "marker_poi";
public static final String MARKER_LAST_POSITON = "marker_last_position";
public static final String MARKER_FEATURE_LIST = "marker_feature_list";
public static final String LINE_FEATURE_LIST = "line_feature_list";
private final String markerSource;
private final String lineSource;
private final String markerLayer;
private final String lineLayer;
private final String markerIcon;
private final String markerLastPositon;
private final String markerPoi;
private final String markerFeatureCollection;
private final String lineFeatureCollection;
private final String displayName;
private final int color;
private final int colorArgb;
private final int contactId;
public MapSource(DcContact contact) {
int contactId = contact.getId();
markerSource = MARKER_POSITION_SOURCE + "_" + contactId;
lineSource = LINE_SOURCE + "_" + contactId;
markerLayer = MARKER_LAYER + "_" + contactId;
lineLayer = LINE_LAYER + "_" + contactId;
markerIcon = MARKER_ICON + "_" + contactId;
markerLastPositon = MARKER_LAST_POSITON + "_" + contactId;
markerPoi = MARKER_POI + "_" + contactId;
markerFeatureCollection = MARKER_FEATURE_LIST + "_" + contactId;
lineFeatureCollection = LINE_FEATURE_LIST + "_" + contactId;
this.contactId = contactId;
displayName = contact.getDisplayName();
color = contact.getColor();
colorArgb = Color.argb(0xFF, Color.red(color), Color.green(color), Color.blue(color));
}
public int getColorArgb() {
return colorArgb;
}
public int getColor() {
return color;
}
public String getMarkerSource() {
return markerSource;
}
public String getLineSource() {
return lineSource;
}
public String getMarkerLayer() {
return markerLayer;
}
public String getLineLayer() {
return lineLayer;
}
public String getMarkerIcon() {
return markerIcon;
}
public String getMarkerLastPositon() {
return markerLastPositon;
}
public String getMarkerPoi() {
return markerPoi;
}
public String getMarkerFeatureCollection() { return markerFeatureCollection; }
public String getLineFeatureCollection() { return lineFeatureCollection; }
public int getContactId() { return contactId; }
public String getDisplayName() {
return displayName;
}
}

View file

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.MINIMUM_ZOOM;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -16,7 +14,6 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import com.b44t.messenger.DcContext; import com.b44t.messenger.DcContext;
import com.mapbox.mapboxsdk.geometry.LatLng;
import org.thoughtcrime.securesms.connect.DcHelper; import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@ -64,10 +61,6 @@ public class Prefs {
private static final String PREF_CONTACT_PHOTO_IDENTIFIERS = "pref_contact_photo_identifiers"; private static final String PREF_CONTACT_PHOTO_IDENTIFIERS = "pref_contact_photo_identifiers";
private static final String MAP_CENTER_LATITUDE = "pref_map_center_latitude";
private static final String MAP_CENTER_LONGITUDE = "pref_map_center_longitude";
private static final String MAP_ZOOM = "pref_map_zoom";
public static final String ALWAYS_LOAD_REMOTE_CONTENT = "pref_always_load_remote_content"; public static final String ALWAYS_LOAD_REMOTE_CONTENT = "pref_always_load_remote_content";
public static final boolean ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT = false; public static final boolean ALWAYS_LOAD_REMOTE_CONTENT_DEFAULT = false;
@ -269,31 +262,6 @@ public class Prefs {
return getStringPreference(context, LED_COLOR_PREF, "blue"); return getStringPreference(context, LED_COLOR_PREF, "blue");
} }
// map
public static void setMapCenter(Context context, int accountId, int chatId, LatLng latLng) {
setLongPreference(context, MAP_CENTER_LATITUDE+accountId+"."+chatId, Double.doubleToRawLongBits(latLng.getLatitude()));
setLongPreference(context, MAP_CENTER_LONGITUDE+accountId+"."+chatId, Double.doubleToRawLongBits(latLng.getLongitude()));
}
public static void setMapZoom(Context context, int accountId, int chatId, double zoom) {
setLongPreference(context, MAP_ZOOM+accountId+"."+chatId, Double.doubleToRawLongBits(zoom));
}
public static LatLng getMapCenter(Context context, int accountId, int chatId) {
long latitude = getLongPreference(context, MAP_CENTER_LATITUDE+accountId+"."+chatId, Long.MAX_VALUE);
long longitude = getLongPreference(context, MAP_CENTER_LONGITUDE+accountId+"."+chatId, Long.MAX_VALUE);
if (latitude == Long.MAX_VALUE || longitude == Long.MAX_VALUE) {
return null;
}
return new LatLng(Double.longBitsToDouble(latitude), Double.longBitsToDouble(longitude));
}
public static double getMapZoom(Context context, int accountId, int chatId) {
long zoom = getLongPreference(context, MAP_ZOOM+accountId+"."+chatId, Double.doubleToLongBits(MINIMUM_ZOOM));
return Double.longBitsToDouble(zoom);
}
// misc. // misc.
public static String getBackgroundImagePath(Context context, int accountId) { public static String getBackgroundImagePath(Context context, int accountId) {