diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/adapter/ChatMessageAdapter.java b/app/src/main/java/de/stephanlindauer/criticalmaps/adapter/ChatMessageAdapter.java index 25759279..7f609666 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/adapter/ChatMessageAdapter.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/adapter/ChatMessageAdapter.java @@ -15,50 +15,32 @@ import de.stephanlindauer.criticalmaps.R; import de.stephanlindauer.criticalmaps.databinding.ViewChatmessageBinding; -import de.stephanlindauer.criticalmaps.interfaces.IChatMessage; import de.stephanlindauer.criticalmaps.model.chat.ReceivedChatMessage; import de.stephanlindauer.criticalmaps.utils.TimeToWordStringConverter; public class ChatMessageAdapter extends RecyclerView.Adapter { - private List chatMessages; + private List chatMessages; static class ChatMessageViewHolder extends RecyclerView.ViewHolder { private final ViewChatmessageBinding binding; private final DateFormat dateFormatter = DateFormat.getDateTimeInstance( DateFormat.DEFAULT, DateFormat.SHORT, Locale.getDefault()); - private ObjectAnimator sendingAnimator; ChatMessageViewHolder(ViewChatmessageBinding binding) { super(binding.getRoot()); this.binding = binding; } - void bind(IChatMessage message) { + void bind(ReceivedChatMessage message) { binding.chatmessageMessageText.setText(message.getMessage()); - if (message instanceof ReceivedChatMessage) { - dateFormatter.setTimeZone(TimeZone.getDefault()); - binding.chatmessageLabelText.setText(TimeToWordStringConverter.getTimeAgo( - ((ReceivedChatMessage) message).getTimestamp(), itemView.getContext())); - } else { - binding.chatmessageLabelText.setText(R.string.chat_sending); - - sendingAnimator = (ObjectAnimator) AnimatorInflater.loadAnimator( - itemView.getContext(), R.animator.map_gps_fab_searching_animation); - sendingAnimator.setTarget(binding.chatmessageLabelText); - sendingAnimator.start(); - } - } - - void clearAnimation() { - if (sendingAnimator != null) { - sendingAnimator.cancel(); - binding.chatmessageLabelText.setAlpha(1f); - } + dateFormatter.setTimeZone(TimeZone.getDefault()); + binding.chatmessageLabelText.setText(TimeToWordStringConverter.getTimeAgo( + message.getTimestamp(), itemView.getContext())); } } - public ChatMessageAdapter(List chatMessages) { + public ChatMessageAdapter(List chatMessages) { this.chatMessages = chatMessages; } @@ -77,17 +59,12 @@ public void onBindViewHolder(@NonNull ChatMessageViewHolder holder, int position holder.bind(chatMessages.get(position)); } - @Override - public void onViewDetachedFromWindow(@NonNull ChatMessageViewHolder holder) { - holder.clearAnimation(); - } - @Override public int getItemCount() { return chatMessages.size(); } - public void updateData(List savedAndOutgoingMessages) { + public void updateData(List savedAndOutgoingMessages) { this.chatMessages = savedAndOutgoingMessages; notifyDataSetChanged(); } diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/ChatFragment.java b/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/ChatFragment.java index 588a33bf..8aed1951 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/ChatFragment.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/ChatFragment.java @@ -13,6 +13,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -22,10 +23,15 @@ import com.squareup.otto.Subscribe; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import javax.inject.Inject; +import javax.inject.Provider; import de.stephanlindauer.criticalmaps.App; import de.stephanlindauer.criticalmaps.R; @@ -33,13 +39,18 @@ import de.stephanlindauer.criticalmaps.databinding.FragmentChatBinding; import de.stephanlindauer.criticalmaps.events.NetworkConnectivityChangedEvent; import de.stephanlindauer.criticalmaps.events.NewServerResponseEvent; -import de.stephanlindauer.criticalmaps.interfaces.IChatMessage; +import de.stephanlindauer.criticalmaps.handler.GetChatmessagesHandler; +import de.stephanlindauer.criticalmaps.handler.PostChatmessagesHandler; import de.stephanlindauer.criticalmaps.model.ChatModel; -import de.stephanlindauer.criticalmaps.model.chat.OutgoingChatMessage; +import de.stephanlindauer.criticalmaps.model.chat.ReceivedChatMessage; import de.stephanlindauer.criticalmaps.provider.EventBus; import de.stephanlindauer.criticalmaps.utils.AxtUtils.SimpleTextWatcher; + public class ChatFragment extends Fragment { + @Inject + Provider getChatmessagesHandler; + @Inject ChatModel chatModel; @@ -49,6 +60,10 @@ public class ChatFragment extends Fragment { private boolean isTextInputEnabled = true; private ChatMessageAdapter chatMessageAdapter; private FragmentChatBinding binding; + // private ObjectAnimator sendingAnimator; + private Timer timerGetChatmessages; + + private final int SERVER_SYNC_INTERVAL = 20 * 1000; // 20 sec @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -70,9 +85,9 @@ public void onActivityCreated(final Bundle savedState) { binding.chatMessagesRecyclerview.setAdapter(chatMessageAdapter); displayNewData(); - binding.chatMessageTextinputlayout.setCounterMaxLength(IChatMessage.MAX_LENGTH); + binding.chatMessageTextinputlayout.setCounterMaxLength(ChatModel.MESSAGE_MAX_LENGTH); binding.chatMessageEdittext.setFilters( - new InputFilter[]{new InputFilter.LengthFilter(IChatMessage.MAX_LENGTH)}); + new InputFilter[]{new InputFilter.LengthFilter(ChatModel.MESSAGE_MAX_LENGTH)}); binding.chatMessageEdittext.setOnEditorActionListener( (v, actionId, event) -> handleEditorAction(actionId)); @@ -130,18 +145,57 @@ private void handleSendClicked() { return; } - chatModel.setNewOutgoingMessage(new OutgoingChatMessage(message)); + // TODO handle UI state while sending, even though it shouldn't be noticeable + + JSONObject messageObject = chatModel.createNewOutgoingMessage(message); + new PostChatmessagesHandler(messageObject, new Runnable() { + @Override + public void run() { + // TODO check if still alive; else bail! + // TODO reset UI state + // clearAnimation(); + + // Restart timer task so sent message shows up in list immediately + stopGetChatmessagesTimer(); + startGetChatmessagesTimer(); + } + }, new Runnable() { + @Override + public void run() { + // TODO check if still alive; else bail! + // TODO reset UI state + Toast.makeText(getContext(), R.string.something_went_wrong, Toast.LENGTH_LONG).show(); + } + }).execute(); binding.chatMessageEdittext.setText(""); displayNewData(); } + /* + private void setSendingAnimation() { + binding.chatmessageLabelText.setText(R.string.chat_sending); + + sendingAnimator = (ObjectAnimator) AnimatorInflater.loadAnimator( + itemView.getContext(), R.animator.map_gps_fab_searching_animation); + sendingAnimator.setTarget(binding.chatmessageLabelText); + sendingAnimator.start(); + } + + private void clearAnimation() { + if (sendingAnimator != null) { + sendingAnimator.cancel(); + binding.chatmessageLabelText.setAlpha(1f); + } + } + */ + private void displayNewData() { - final List savedAndOutgoingMessages = chatModel.getSavedAndOutgoingMessages(); - chatMessageAdapter.updateData(savedAndOutgoingMessages); + final List receivedChatMessages = chatModel.getReceivedChatMessages(); + chatMessageAdapter.updateData(receivedChatMessages); if (binding.chatMessagesRecyclerview.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) { - binding.chatMessagesRecyclerview.scrollToPosition(savedAndOutgoingMessages.size() - 1); + binding.chatMessagesRecyclerview.scrollToPosition(receivedChatMessages.size() - 1); } } @@ -150,11 +204,13 @@ public void onResume() { super.onResume(); displayNewData(); eventBus.register(this); + startGetChatmessagesTimer(); } @Override public void onPause() { super.onPause(); + stopGetChatmessagesTimer(); eventBus.unregister(this); hideKeyBoard(binding.chatMessageEdittext); } @@ -162,6 +218,8 @@ public void onPause() { @Override public void onDestroyView() { super.onDestroyView(); + // TODO + // clearAnimation(); binding = null; } @@ -175,6 +233,12 @@ public void handleNewServerData(NewServerResponseEvent e) { @Subscribe public void handleNetworkConnectivityChanged(NetworkConnectivityChangedEvent e) { setTextInputState(e.isConnected); + + if (e.isConnected && timerGetChatmessages == null) { + startGetChatmessagesTimer(); + } else { + stopGetChatmessagesTimer(); + } } private void setTextInputState(final boolean dataEnabled) { @@ -195,4 +259,25 @@ private void updateSendButtonEnabledState() { final String message = binding.chatMessageEdittext.getText().toString(); setSendButtonEnabledWithAnimation(!message.trim().isEmpty()); } + + private void startGetChatmessagesTimer() { + stopGetChatmessagesTimer(); + + timerGetChatmessages = new Timer(); + + TimerTask timerTaskPullServer = new TimerTask() { + @Override + public void run() { + getChatmessagesHandler.get().execute(); + } + }; + timerGetChatmessages.scheduleAtFixedRate(timerTaskPullServer, 0, SERVER_SYNC_INTERVAL); + } + + private void stopGetChatmessagesTimer() { + if (timerGetChatmessages != null) { + timerGetChatmessages.cancel(); + timerGetChatmessages = null; + } + } } diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/MapFragment.java b/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/MapFragment.java index 389b5967..6913ceda 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/MapFragment.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/fragments/MapFragment.java @@ -34,7 +34,11 @@ import org.osmdroid.views.overlay.gestures.RotationGestureOverlay; import org.osmdroid.views.overlay.infowindow.InfoWindow; +import java.util.Timer; +import java.util.TimerTask; + import javax.inject.Inject; +import javax.inject.Provider; import de.stephanlindauer.criticalmaps.App; import de.stephanlindauer.criticalmaps.R; @@ -43,6 +47,7 @@ import de.stephanlindauer.criticalmaps.events.NetworkConnectivityChangedEvent; import de.stephanlindauer.criticalmaps.events.NewLocationEvent; import de.stephanlindauer.criticalmaps.events.NewServerResponseEvent; +import de.stephanlindauer.criticalmaps.handler.GetLocationHandler; import de.stephanlindauer.criticalmaps.handler.ShowGpxHandler; import de.stephanlindauer.criticalmaps.managers.LocationUpdateManager; import de.stephanlindauer.criticalmaps.model.OtherUsersLocationModel; @@ -54,6 +59,7 @@ import de.stephanlindauer.criticalmaps.utils.MapViewUtils; import info.metadude.android.typedpreferences.BooleanPreference; + public class MapFragment extends Fragment { private final static String KEY_MAP_ZOOMLEVEL = "map_zoomlevel"; private final static String KEY_MAP_POSITION = "map_position"; @@ -62,6 +68,10 @@ public class MapFragment extends Fragment { private final static double DEFAULT_ZOOM_LEVEL = 12; private final static double NO_GPS_PERMISSION_ZOOM_LEVEL = 3; + private final int SERVER_SYNC_INTERVAL = 30 * 1000; // 30 sec + + @Inject + Provider getLocationHandler; @Inject OwnLocationModel ownLocationModel; @@ -95,6 +105,8 @@ public class MapFragment extends Fragment { private FragmentMapBinding binding; + private Timer timerGetLocation; + private final View.OnClickListener centerLocationOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { @@ -304,8 +316,8 @@ private void refreshView() { @Override public void onResume() { super.onResume(); - eventBus.register(this); + eventBus.register(this); sharedPreferences.registerOnSharedPreferenceChangeListener( observerModeOnSharedPreferenceChangeListener); @@ -314,6 +326,8 @@ public void onResume() { } else { zoomToLocation(defaultGeoPoint, NO_GPS_PERMISSION_ZOOM_LEVEL); } + + startGetLocationTimer(); } private void handleFirstLocationUpdate() { @@ -335,8 +349,9 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override public void onPause() { super.onPause(); - eventBus.unregister(this); + stopGetLocationTimer(); + eventBus.unregister(this); sharedPreferences.unregisterOnSharedPreferenceChangeListener( observerModeOnSharedPreferenceChangeListener); } @@ -376,6 +391,12 @@ public void handleNetworkConnectivityChanged(NetworkConnectivityChangedEvent e) } else { binding.mapNoDataConnectivityFab.show(); } + + if (e.isConnected && timerGetLocation == null) { + startGetLocationTimer(); + } else { + stopGetLocationTimer(); + } } @Subscribe @@ -472,4 +493,25 @@ private void animateToLocation(final GeoPoint location) { private void setToLocation(final GeoPoint location) { mapView.getController().setCenter(location); } + + private void startGetLocationTimer() { + stopGetLocationTimer(); + + timerGetLocation = new Timer(); + + TimerTask timerTaskPullServer = new TimerTask() { + @Override + public void run() { + getLocationHandler.get().execute(); + } + }; + timerGetLocation.scheduleAtFixedRate(timerTaskPullServer, 0, SERVER_SYNC_INTERVAL); + } + + private void stopGetLocationTimer() { + if (timerGetLocation != null) { + timerGetLocation.cancel(); + timerGetLocation = null; + } + } } diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetChatmessagesHandler.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetChatmessagesHandler.java new file mode 100644 index 00000000..6cee0499 --- /dev/null +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetChatmessagesHandler.java @@ -0,0 +1,58 @@ +package de.stephanlindauer.criticalmaps.handler; + +import android.os.AsyncTask; + +import java.io.IOException; + +import javax.inject.Inject; + +import de.stephanlindauer.criticalmaps.BuildConfig; +import de.stephanlindauer.criticalmaps.vo.Endpoints; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import timber.log.Timber; + + +public class GetChatmessagesHandler extends AsyncTask { + + private final OkHttpClient okHttpClient; + private final ServerResponseProcessor serverResponseProcessor; + + @Inject + public GetChatmessagesHandler(ServerResponseProcessor serverResponseProcessor, + OkHttpClient okHttpClient + ) { + this.okHttpClient = okHttpClient; + this.serverResponseProcessor = serverResponseProcessor; + } + + @Override + protected String doInBackground(Void... params) { + final Headers headers = Headers.of("app-version", BuildConfig.VERSION_NAME); + final Request request = new Request.Builder().url(Endpoints.CHAT_GET).get().headers(headers).build(); + + String responseString = ""; + try { + final Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + Timber.d("Get chatmessages unsuccessful with code %d", response.code()); + } + //noinspection ConstantConditions "Returns a non-null value if this response was [...] returned from Call.execute()." + responseString = response.body().string(); + response.body().close(); + } catch (IOException e) { + Timber.e(e); + } + + return responseString; + } + + @Override + protected void onPostExecute(String result) { + if (!result.isEmpty()) { + serverResponseProcessor.processChatmessages(result); + } + } +} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetLocationHandler.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetLocationHandler.java new file mode 100644 index 00000000..70c84def --- /dev/null +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/GetLocationHandler.java @@ -0,0 +1,59 @@ +package de.stephanlindauer.criticalmaps.handler; + +import android.os.AsyncTask; + +import java.io.IOException; + +import javax.inject.Inject; + +import de.stephanlindauer.criticalmaps.BuildConfig; +import de.stephanlindauer.criticalmaps.vo.Endpoints; + +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import timber.log.Timber; + + +public class GetLocationHandler extends AsyncTask { + + private final OkHttpClient okHttpClient; + private final ServerResponseProcessor serverResponseProcessor; + + @Inject + public GetLocationHandler(ServerResponseProcessor serverResponseProcessor, + OkHttpClient okHttpClient + ) { + this.okHttpClient = okHttpClient; + this.serverResponseProcessor = serverResponseProcessor; + } + + @Override + protected String doInBackground(Void... params) { + final Headers headers = Headers.of("app-version", BuildConfig.VERSION_NAME); + final Request request = new Request.Builder().url(Endpoints.LOCATION_GET).get().headers(headers).build(); + + String responseString = ""; + try { + final Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + Timber.d("Get locations unsuccessful with code %d", response.code()); + } + //noinspection ConstantConditions "Returns a non-null value if this response was [...] returned from Call.execute()." + responseString = response.body().string(); + response.body().close(); + } catch (IOException e) { + Timber.e(e); + } + + return responseString; + } + + @Override + protected void onPostExecute(String result) { + if (!result.isEmpty()) { + serverResponseProcessor.processLocations(result); + } + } +} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PostChatmessagesHandler.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PostChatmessagesHandler.java new file mode 100644 index 00000000..834befea --- /dev/null +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PostChatmessagesHandler.java @@ -0,0 +1,66 @@ +package de.stephanlindauer.criticalmaps.handler; + +import android.os.AsyncTask; + +import org.json.JSONObject; + +import java.io.IOException; + +import de.stephanlindauer.criticalmaps.App; +import de.stephanlindauer.criticalmaps.BuildConfig; +import de.stephanlindauer.criticalmaps.vo.Endpoints; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import timber.log.Timber; + + +public class PostChatmessagesHandler extends AsyncTask { + + private final OkHttpClient okHttpClient = App.components().okHttpClient(); + + private final JSONObject message; + private final Runnable onSuccessCallback; + private final Runnable onErrorCallback; + + public PostChatmessagesHandler(JSONObject message, Runnable onSuccessCallback, Runnable onErrorCallback) { + this.message = message; + this.onSuccessCallback = onSuccessCallback; + this.onErrorCallback = onErrorCallback; + } + + @Override + protected Boolean doInBackground(Void... params) { + + String jsonBody = message.toString(); + final RequestBody body = RequestBody.create(MediaType.parse("application/json"), jsonBody); + final Headers headers = Headers.of("app-version", BuildConfig.VERSION_NAME); + final Request request = new Request.Builder().url(Endpoints.CHAT_POST).post(body).headers(headers).build(); + + boolean wasSuccessful = false; + try { + final Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + Timber.d("Post chatmessages unsuccessful with code %d", response.code()); + } + wasSuccessful = response.isSuccessful(); + response.close(); + } catch (IOException e) { + Timber.e(e); + } + + return wasSuccessful; + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + onSuccessCallback.run(); + } else { + onErrorCallback.run(); + } + } +} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PullServerHandler.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PullServerHandler.java deleted file mode 100644 index 9618a6cc..00000000 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PullServerHandler.java +++ /dev/null @@ -1,107 +0,0 @@ -package de.stephanlindauer.criticalmaps.handler; - -import android.content.SharedPreferences; -import android.os.AsyncTask; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; - -import javax.inject.Inject; - -import de.stephanlindauer.criticalmaps.managers.LocationUpdateManager; -import de.stephanlindauer.criticalmaps.model.ChatModel; -import de.stephanlindauer.criticalmaps.model.OwnLocationModel; -import de.stephanlindauer.criticalmaps.model.UserModel; -import de.stephanlindauer.criticalmaps.prefs.SharedPrefsKeys; -import de.stephanlindauer.criticalmaps.vo.Endpoints; -import info.metadude.android.typedpreferences.BooleanPreference; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import timber.log.Timber; - -public class PullServerHandler extends AsyncTask { - - //dependencies - private final ChatModel chatModel; - private final OwnLocationModel ownLocationModel; - private final UserModel userModel; - private final ServerResponseProcessor serverResponseProcessor; - private final OkHttpClient okHttpClient; - private final SharedPreferences sharedPreferences; - private final LocationUpdateManager locationUpdateManager; - - @Inject - public PullServerHandler(ChatModel chatModel, - OwnLocationModel ownLocationModel, - UserModel userModel, - ServerResponseProcessor serverResponseProcessor, - OkHttpClient okHttpClient, - SharedPreferences sharedPreferences, - LocationUpdateManager locationUpdateManager) { - this.chatModel = chatModel; - this.ownLocationModel = ownLocationModel; - this.userModel = userModel; - this.serverResponseProcessor = serverResponseProcessor; - this.okHttpClient = okHttpClient; - this.sharedPreferences = sharedPreferences; - this.locationUpdateManager = locationUpdateManager; - } - - @Override - protected String doInBackground(Void... params) { - String jsonPostString = getJsonObject().toString(); - - final RequestBody body = RequestBody.create(MediaType.parse("application/json"), jsonPostString); - final Request request = new Request.Builder().url(Endpoints.MAIN_POST).post(body).build(); - - try { - final Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - //noinspection ConstantConditions "Returns a non-null value if this response was [...] returned from Call.execute()." - return response.body().string(); - } - } catch (IOException e) { - Timber.e(e); - } - return ""; - } - - @Override - protected void onPostExecute(String result) { - if (!result.isEmpty()) { - serverResponseProcessor.process(result); - } - } - - private JSONObject getJsonObject() { - JSONObject jsonObject = new JSONObject(); - - try { - jsonObject.put("device", userModel.getChangingDeviceToken()); - - final boolean isObserverModeActive = new BooleanPreference( - sharedPreferences, SharedPrefsKeys.OBSERVER_MODE_ACTIVE).get(); - - Timber.d("observer mode enabled: %s", isObserverModeActive); - - if (!isObserverModeActive && ownLocationModel.hasPreciseLocation() - && locationUpdateManager.isUpdating()) { - jsonObject.put("location", ownLocationModel.getLocationJson()); - } - - if (chatModel.hasOutgoingMessages()) { - JSONArray messages = chatModel.getOutgoingMessagesAsJson(); - jsonObject.put("messages", messages); - } - } catch (JSONException e) { - Timber.e(e); - } - return jsonObject; - } -} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PutLocationHandler.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PutLocationHandler.java new file mode 100644 index 00000000..3cec59ca --- /dev/null +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/PutLocationHandler.java @@ -0,0 +1,95 @@ +package de.stephanlindauer.criticalmaps.handler; + +import android.content.SharedPreferences; +import android.os.AsyncTask; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import javax.inject.Inject; + +import de.stephanlindauer.criticalmaps.BuildConfig; +import de.stephanlindauer.criticalmaps.managers.LocationUpdateManager; +import de.stephanlindauer.criticalmaps.model.OwnLocationModel; +import de.stephanlindauer.criticalmaps.model.UserModel; +import de.stephanlindauer.criticalmaps.prefs.SharedPrefsKeys; +import de.stephanlindauer.criticalmaps.vo.Endpoints; +import info.metadude.android.typedpreferences.BooleanPreference; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import timber.log.Timber; + + +public class PutLocationHandler extends AsyncTask { + + private final OwnLocationModel ownLocationModel; + private final UserModel userModel; + private final OkHttpClient okHttpClient; + private final SharedPreferences sharedPreferences; + private final LocationUpdateManager locationUpdateManager; + + @Inject + public PutLocationHandler( + OwnLocationModel ownLocationModel, + UserModel userModel, + OkHttpClient okHttpClient, + SharedPreferences sharedPreferences, + LocationUpdateManager locationUpdateManager) { + this.ownLocationModel = ownLocationModel; + this.userModel = userModel; + this.okHttpClient = okHttpClient; + this.sharedPreferences = sharedPreferences; + this.locationUpdateManager = locationUpdateManager; + } + + @Override + protected Void doInBackground(Void... params) { + final boolean isObserverModeActive = new BooleanPreference( + sharedPreferences, SharedPrefsKeys.OBSERVER_MODE_ACTIVE).get(); + + if (!isObserverModeActive && ownLocationModel.hasPreciseLocation() + && locationUpdateManager.isUpdating()) { + Timber.d("Heartbeat preconditions are fulfilled."); + } else { + Timber.d("Heartbeat preconditions are not fulfilled."); + return null; + } + + String jsonPutBody = getJsonObject().toString(); + + final RequestBody body = RequestBody.create(MediaType.parse("application/json"), jsonPutBody); + final Headers headers = Headers.of("app-version", BuildConfig.VERSION_NAME); + final Request request = new Request.Builder().url(Endpoints.LOCATION_PUT).put(body).headers(headers).build(); + + try { + final Response response = okHttpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + //TODO Display error to user + Timber.d("Put location unsuccessful with code %d", response.code()); + } + response.close(); + } catch (IOException e) { + Timber.e(e); + } + + return null; + } + + private JSONObject getJsonObject() { + JSONObject jsonObject = ownLocationModel.getLocationJson(); + + try { + jsonObject.put("device", userModel.getChangingDeviceToken()); + } catch (JSONException e) { + Timber.e(e); + } + + return jsonObject; + } +} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessor.java b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessor.java index 3b9c77e2..f82f545f 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessor.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessor.java @@ -1,6 +1,6 @@ package de.stephanlindauer.criticalmaps.handler; -import org.json.JSONObject; +import org.json.JSONArray; import javax.inject.Inject; @@ -25,15 +25,20 @@ public ServerResponseProcessor(OtherUsersLocationModel otherUsersLocationModel, this.chatModel = chatModel; } - public void process(final String jsonString) { + public void processLocations(final String jsonString) { try { - final JSONObject jsonObject = new JSONObject(jsonString); - if (jsonObject.has("locations")) { - otherUsersLocationModel.setFromJson(jsonObject.getJSONObject("locations")); - } - if (jsonObject.has("locations")) { - chatModel.setFromJson(jsonObject.getJSONObject("chatMessages")); - } + final JSONArray jsonArray = new JSONArray(jsonString); + otherUsersLocationModel.setFromJson(jsonArray); + eventBus.post(Events.NEW_SERVER_RESPONSE_EVENT); + } catch (Exception e) { + Timber.d(e); + } + } + + public void processChatmessages(final String jsonString) { + try { + final JSONArray jsonArray = new JSONArray(jsonString); + chatModel.setFromJson(jsonArray); eventBus.post(Events.NEW_SERVER_RESPONSE_EVENT); } catch (Exception e) { Timber.d(e); diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/interfaces/IChatMessage.java b/app/src/main/java/de/stephanlindauer/criticalmaps/interfaces/IChatMessage.java deleted file mode 100644 index bdc7b7b7..00000000 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/interfaces/IChatMessage.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.stephanlindauer.criticalmaps.interfaces; - -public interface IChatMessage { - - int MAX_LENGTH = 255; - - String getMessage(); -} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/model/ChatModel.java b/app/src/main/java/de/stephanlindauer/criticalmaps/model/ChatModel.java index bd16c2a5..783e9b61 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/model/ChatModel.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/model/ChatModel.java @@ -1,59 +1,64 @@ package de.stephanlindauer.criticalmaps.model; +import androidx.annotation.NonNull; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; -import java.util.Iterator; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import de.stephanlindauer.criticalmaps.interfaces.IChatMessage; -import de.stephanlindauer.criticalmaps.model.chat.OutgoingChatMessage; +import de.stephanlindauer.criticalmaps.handler.PostChatmessagesHandler; import de.stephanlindauer.criticalmaps.model.chat.ReceivedChatMessage; +import de.stephanlindauer.criticalmaps.utils.AeSimpleSHA1; import okhttp3.internal.Util; import timber.log.Timber; + @Singleton public class ChatModel { - private final List outgoingMessages = new ArrayList<>(); - private List chatMessages = new ArrayList<>(); + private final UserModel userModel; + private List receivedChatMessages = new ArrayList<>(); + + public static int MESSAGE_MAX_LENGTH = 255; @Inject - public ChatModel() { + public ChatModel(UserModel userModel) { + this.userModel = userModel; } - public void setFromJson(JSONObject jsonObject) throws JSONException, + @NonNull + public List getReceivedChatMessages() { + return this.receivedChatMessages; + } + + public void setFromJson(JSONArray jsonArray) throws JSONException, UnsupportedEncodingException { - chatMessages = new ArrayList<>(jsonObject.length()); - - Iterator identifiers = jsonObject.keys(); - while (identifiers.hasNext()) { - String identifier = identifiers.next(); - JSONObject value = jsonObject.getJSONObject(identifier); - String message = URLDecoder.decode(value.getString("message"), Util.UTF_8.name()); - Date timestamp = new Date(Long.parseLong(value.getString("timestamp")) * 1000); - - Iterator outgoingChatMessageIterator = outgoingMessages.iterator(); - while (outgoingChatMessageIterator.hasNext()) { - if (outgoingChatMessageIterator.next().getIdentifier().equals(identifier)) { - outgoingChatMessageIterator.remove(); - } - } + receivedChatMessages = new ArrayList<>(jsonArray.length()); + + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); - chatMessages.add(new ReceivedChatMessage(message, timestamp)); + String device = URLDecoder.decode(jsonObject.getString("device"), Util.UTF_8.name()); + String identifier = URLDecoder.decode(jsonObject.getString("identifier"), Util.UTF_8.name()); + String message = URLDecoder.decode(jsonObject.getString("message"), Util.UTF_8.name()); + Date timestamp = new Date(Long.parseLong(jsonObject.getString("timestamp")) * 1000); + + receivedChatMessages.add(new ReceivedChatMessage(message, timestamp)); } - Collections.sort(chatMessages, new Comparator() { + Collections.sort(receivedChatMessages, new Comparator() { @Override public int compare(ReceivedChatMessage oneChatMessages, ReceivedChatMessage otherChatMessage) { @@ -62,36 +67,23 @@ public int compare(ReceivedChatMessage oneChatMessages, }); } - public void setNewOutgoingMessage(OutgoingChatMessage newOutgoingMessage) { - outgoingMessages.add(newOutgoingMessage); - } - - public JSONArray getOutgoingMessagesAsJson() { - JSONArray jsonArray = new JSONArray(); - - for (OutgoingChatMessage outgoingChatMessage : outgoingMessages) { - try { - JSONObject messageObject = new JSONObject(); - messageObject.put("text", outgoingChatMessage.getUrlEncodedMessage()); - messageObject.put("timestamp", outgoingChatMessage.getTimestamp().getTime()); - messageObject.put("identifier", outgoingChatMessage.getIdentifier()); - jsonArray.put(messageObject); - } catch (JSONException e) { - Timber.d(e); - } + public JSONObject createNewOutgoingMessage(String message) { + JSONObject messageObject = new JSONObject(); + try { + messageObject.put("text", urlEncodeMessage(message)); + messageObject.put("identifier", AeSimpleSHA1.SHA1(message + Math.random())); + messageObject.put("device", userModel.getChangingDeviceToken()); + } catch (JSONException e) { + Timber.d(e); } - return jsonArray; + return messageObject; } - public ArrayList getSavedAndOutgoingMessages() { - int mergedListsSize = chatMessages.size() + outgoingMessages.size(); - ArrayList mergeArrayList = new ArrayList<>(mergedListsSize); - mergeArrayList.addAll(chatMessages); - mergeArrayList.addAll(outgoingMessages); - return mergeArrayList; - } - - public boolean hasOutgoingMessages() { - return !outgoingMessages.isEmpty(); + private String urlEncodeMessage(String messageToEncode) { + try { + return URLEncoder.encode(messageToEncode, Util.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + return ""; + } } } diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/model/OtherUsersLocationModel.java b/app/src/main/java/de/stephanlindauer/criticalmaps/model/OtherUsersLocationModel.java index c5b28e7d..31cd96c8 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/model/OtherUsersLocationModel.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/model/OtherUsersLocationModel.java @@ -1,5 +1,6 @@ package de.stephanlindauer.criticalmaps.model; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.osmdroid.util.GeoPoint; @@ -8,6 +9,7 @@ import java.util.Iterator; import javax.inject.Inject; +import javax.inject.Provider; import javax.inject.Singleton; @Singleton @@ -15,19 +17,23 @@ public class OtherUsersLocationModel { private ArrayList otherUsersLocations = new ArrayList<>(); + private final UserModel userModel; + @Inject - public OtherUsersLocationModel() { + public OtherUsersLocationModel(UserModel userModel) { + this.userModel = userModel; } - public void setFromJson(JSONObject jsonObject) throws JSONException { - otherUsersLocations = new ArrayList<>(jsonObject.length()); - Iterator keys = jsonObject.keys(); - while (keys.hasNext()) { - String key = keys.next(); - JSONObject value = jsonObject.getJSONObject(key); - int latitudeE6 = Integer.parseInt(value.getString("latitude")); - int longitudeE6 = Integer.parseInt(value.getString("longitude")); + public void setFromJson(JSONArray jsonArray) throws JSONException { + otherUsersLocations = new ArrayList<>(jsonArray.length()); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject locationObject = jsonArray.getJSONObject(i); + if (locationObject.getString("device").equals(userModel.getChangingDeviceToken())) { + continue; // Ignore own location + } + int latitudeE6 = Integer.parseInt(locationObject.getString("latitude")); + int longitudeE6 = Integer.parseInt(locationObject.getString("longitude")); otherUsersLocations.add( new GeoPoint(latitudeE6 / 1000000.0D, longitudeE6 / 1000000.0D)); diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/OutgoingChatMessage.java b/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/OutgoingChatMessage.java deleted file mode 100644 index 7d6e5b2d..00000000 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/OutgoingChatMessage.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.stephanlindauer.criticalmaps.model.chat; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Date; - -import de.stephanlindauer.criticalmaps.interfaces.IChatMessage; -import de.stephanlindauer.criticalmaps.utils.AeSimpleSHA1; -import okhttp3.internal.Util; - -public class OutgoingChatMessage implements IChatMessage { - - private final Date timestamp; - private final String urlEncodedMessage; - private final String identifier; - private final String message; - - public OutgoingChatMessage(String message) { - this.message = message; - this.urlEncodedMessage = urlEncodeMessage(message); - this.timestamp = new Date(); - this.identifier = AeSimpleSHA1.SHA1(message + Math.random()); - } - - public Date getTimestamp() { - return timestamp; - } - - public String getUrlEncodedMessage() { - return urlEncodedMessage; - } - - @Override - public String getMessage() { - return message; - } - - public String getIdentifier() { - return identifier; - } - - private String urlEncodeMessage(String messageToEncode) { - try { - return URLEncoder.encode(messageToEncode, Util.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - return ""; - } - } -} diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/ReceivedChatMessage.java b/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/ReceivedChatMessage.java index a4e42233..7f039934 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/ReceivedChatMessage.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/model/chat/ReceivedChatMessage.java @@ -2,9 +2,8 @@ import java.util.Date; -import de.stephanlindauer.criticalmaps.interfaces.IChatMessage; -public class ReceivedChatMessage implements IChatMessage { +public class ReceivedChatMessage { private final Date timestamp; private final String message; @@ -18,7 +17,6 @@ public Date getTimestamp() { return timestamp; } - @Override public String getMessage() { return message; } diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/service/ServerSyncService.java b/app/src/main/java/de/stephanlindauer/criticalmaps/service/ServerSyncService.java index 96a08606..aab35951 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/service/ServerSyncService.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/service/ServerSyncService.java @@ -17,7 +17,7 @@ import de.stephanlindauer.criticalmaps.App; import de.stephanlindauer.criticalmaps.events.NetworkConnectivityChangedEvent; import de.stephanlindauer.criticalmaps.handler.NetworkConnectivityChangeHandler; -import de.stephanlindauer.criticalmaps.handler.PullServerHandler; +import de.stephanlindauer.criticalmaps.handler.PutLocationHandler; import de.stephanlindauer.criticalmaps.managers.LocationUpdateManager; import de.stephanlindauer.criticalmaps.provider.EventBus; import de.stephanlindauer.criticalmaps.utils.TrackingInfoNotificationBuilder; @@ -25,7 +25,7 @@ public class ServerSyncService extends Service { @SuppressWarnings("FieldCanBeLocal") - private final int SERVER_SYNC_INTERVAL = 45 * 1000; // 45 sec + private final int SERVER_SYNC_INTERVAL = 30 * 1000; // 30 sec private Timer timerPullServer; @@ -36,7 +36,7 @@ public class ServerSyncService extends Service { NetworkConnectivityChangeHandler networkConnectivityChangeHandler; @Inject - Provider pullServerHandler; + Provider putLocationHandler; @Inject EventBus eventBus; @@ -66,7 +66,7 @@ private void startPullServerTimer() { TimerTask timerTaskPullServer = new TimerTask() { @Override public void run() { - pullServerHandler.get().execute(); + putLocationHandler.get().execute(); } }; timerPullServer.scheduleAtFixedRate(timerTaskPullServer, 0, SERVER_SYNC_INTERVAL); diff --git a/app/src/main/java/de/stephanlindauer/criticalmaps/vo/Endpoints.java b/app/src/main/java/de/stephanlindauer/criticalmaps/vo/Endpoints.java index 18fc2844..c77c2172 100644 --- a/app/src/main/java/de/stephanlindauer/criticalmaps/vo/Endpoints.java +++ b/app/src/main/java/de/stephanlindauer/criticalmaps/vo/Endpoints.java @@ -1,6 +1,12 @@ package de.stephanlindauer.criticalmaps.vo; public class Endpoints { - public static final String MAIN_POST = "https://api.criticalmaps.net/"; + // IMAGE_POST doesn't currently work. Once fixed CDN or API_GATEWAY should be used to prefix the URI. public static final String IMAGE_POST = "https://api.criticalmaps.net/gallery/"; + + public static final String LOCATION_GET = "https://api-cdn.criticalmaps.net/locations"; + public static final String LOCATION_PUT = "https://api-gw.criticalmaps.net/locations"; + + public static final String CHAT_GET = "https://api-gw.criticalmaps.net/messages"; + public static final String CHAT_POST = "https://api-gw.criticalmaps.net/messages"; } diff --git a/app/src/test/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessorTest.java b/app/src/test/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessorTest.java index e276510f..244de88f 100644 --- a/app/src/test/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessorTest.java +++ b/app/src/test/java/de/stephanlindauer/criticalmaps/handler/ServerResponseProcessorTest.java @@ -33,7 +33,7 @@ public void process_chatmessagesAreSetOnModel() throws IOException, URISyntaxExc final ServerResponseProcessor tested = new ServerResponseProcessor( mock(OtherUsersLocationModel.class), mock(EventBus.class), chatModel); - tested.process(json); + tested.processLocations(json); verify(chatModel).setFromJson(any(JSONObject.class)); } @@ -46,7 +46,7 @@ public void process_eventIsFiredForValidJSON() throws IOException, URISyntaxExce final ServerResponseProcessor tested = new ServerResponseProcessor( mock(OtherUsersLocationModel.class), eventMock, mock(ChatModel.class)); - tested.process(json); + tested.processLocations(json); verify(eventMock, times(1)).post(Events.NEW_SERVER_RESPONSE_EVENT); } @@ -57,7 +57,7 @@ public void process_noEventIsFiredForInvalidJSON() { final ServerResponseProcessor tested = new ServerResponseProcessor( mock(OtherUsersLocationModel.class), eventMock, mock(ChatModel.class)); - tested.process("borken"); + tested.processLocations("borken"); verify(eventMock, never()).post(Events.NEW_SERVER_RESPONSE_EVENT); } diff --git a/app/src/test/java/de/stephanlindauer/criticalmaps/model/ChatModelTest.java b/app/src/test/java/de/stephanlindauer/criticalmaps/model/ChatModelTest.java index fe3dc4dd..0dbf4b38 100644 --- a/app/src/test/java/de/stephanlindauer/criticalmaps/model/ChatModelTest.java +++ b/app/src/test/java/de/stephanlindauer/criticalmaps/model/ChatModelTest.java @@ -13,7 +13,6 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import de.stephanlindauer.criticalmaps.model.chat.OutgoingChatMessage; import de.stephanlindauer.criticalmaps.model.chat.ReceivedChatMessage; import static com.google.common.truth.Truth.assertThat; diff --git a/build.gradle b/build.gradle index d5e637e5..08addb44 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath 'com.github.bjoernq:unmockplugin:0.7.9' classpath 'net.ltgt.gradle:gradle-errorprone-plugin:2.0.2' }