diff --git a/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatLogicContract.java b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatLogicContract.java new file mode 100644 index 0000000..ea509c0 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatLogicContract.java @@ -0,0 +1,5 @@ +package io.github.chronosx88.influence.contracts.startchat; + +public interface StartChatLogicContract { + void sendStartChatMessage(String peerID); +} diff --git a/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatPresenterContract.java b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatPresenterContract.java new file mode 100644 index 0000000..607f78a --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatPresenterContract.java @@ -0,0 +1,5 @@ +package io.github.chronosx88.influence.contracts.startchat; + +public interface StartChatPresenterContract { + void startChatWithPeer(String peerID); +} diff --git a/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatViewContract.java b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatViewContract.java new file mode 100644 index 0000000..20327d0 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/contracts/startchat/StartChatViewContract.java @@ -0,0 +1,6 @@ +package io.github.chronosx88.influence.contracts.startchat; + +public interface StartChatViewContract { + void showMessage(String message); + void showProgressDialog(boolean enabled); +} diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java b/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java new file mode 100644 index 0000000..ae117c6 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkHandler.java @@ -0,0 +1,60 @@ +package io.github.chronosx88.influence.helpers; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import net.tomp2p.dht.PeerDHT; +import net.tomp2p.peers.PeerAddress; + +import io.github.chronosx88.influence.contracts.observer.Observer; +import io.github.chronosx88.influence.helpers.actions.NetworkActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; +import io.github.chronosx88.influence.observable.MainObservable; + +public class NetworkHandler implements Observer { + private Gson gson; + private PeerDHT peerDHT; + + public NetworkHandler() { + gson = new Gson(); + peerDHT = AppHelper.getPeerDHT(); + AppHelper.getObservable().register(this, MainObservable.OTHER_ACTIONS_CHANNEL); + } + + @Override + public void handleEvent(JsonObject object) { + new Thread(() -> { + switch (object.get("action").getAsInt()) { + case NetworkActions.START_CHAT: { + String chatStarterPlainAddress = object.get("senderAddress").getAsString(); + createChatEntry(object.get("chatID").getAsString(), chatStarterPlainAddress); + handleIncomingChat(object.get("chatID").getAsString(), PrepareData.prepareFromStore(chatStarterPlainAddress)); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("action", UIActions.NEW_CHAT); + AppHelper.getObservable().notifyObservers(jsonObject, MainObservable.UI_ACTIONS_CHANNEL); + break; + } + + case NetworkActions.SUCCESSFULL_CREATE_CHAT: { + createChatEntry(object.get("chatID").getAsString(), object.get("senderAddress").getAsString()); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("action", UIActions.SUCCESSFULL_CREATE_CHAT); + AppHelper.getObservable().notifyObservers(jsonObject, MainObservable.UI_ACTIONS_CHANNEL); + break; + } + } + }).start(); + } + + private void createChatEntry(String chatID, String peerAddress) { + // TODO: make saving chat in db + } + + private void handleIncomingChat(String chatID, PeerAddress chatStarterAddress) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("action", NetworkActions.SUCCESSFULL_CREATE_CHAT); + jsonObject.addProperty("chatID", chatID); + jsonObject.addProperty("senderAddress", PrepareData.prepareToStore(peerDHT.peerAddress())); + AppHelper.getPeerDHT().peer().sendDirect(chatStarterAddress).object(gson.toJson(jsonObject)).start().awaitUninterruptibly(); + } +} diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/PrepareData.java b/app/src/main/java/io/github/chronosx88/influence/helpers/PrepareData.java new file mode 100644 index 0000000..ba5ba59 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/PrepareData.java @@ -0,0 +1,17 @@ +package io.github.chronosx88.influence.helpers; + +import android.util.Base64; + +import java.nio.charset.StandardCharsets; + +public class PrepareData { + public static String prepareToStore(T object) { + String serializedObject = Serializer.serializeObject(object); + return Base64.encodeToString(serializedObject.getBytes(StandardCharsets.UTF_8), Base64.URL_SAFE); + } + + public static T prepareFromStore(String object) { + String decodedString = new String(Base64.decode(object, Base64.URL_SAFE), StandardCharsets.UTF_8); + return Serializer.deserializeObject(decodedString); + } +} diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkActions.java b/app/src/main/java/io/github/chronosx88/influence/helpers/actions/NetworkActions.java similarity index 68% rename from app/src/main/java/io/github/chronosx88/influence/helpers/NetworkActions.java rename to app/src/main/java/io/github/chronosx88/influence/helpers/actions/NetworkActions.java index 3c6ca26..dc5c773 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/NetworkActions.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/actions/NetworkActions.java @@ -1,4 +1,4 @@ -package io.github.chronosx88.influence.helpers; +package io.github.chronosx88.influence.helpers.actions; public class NetworkActions { public static final int START_CHAT = 0x0; @@ -6,4 +6,5 @@ public class NetworkActions { public static final int MESSAGE_SENT = 0x2; public static final int PING = 0x3; public static final int PONG = 0x4; + public static final int SUCCESSFULL_CREATE_CHAT = 0x5; } diff --git a/app/src/main/java/io/github/chronosx88/influence/helpers/UIActions.java b/app/src/main/java/io/github/chronosx88/influence/helpers/actions/UIActions.java similarity index 70% rename from app/src/main/java/io/github/chronosx88/influence/helpers/UIActions.java rename to app/src/main/java/io/github/chronosx88/influence/helpers/actions/UIActions.java index 1d03247..a784fcb 100644 --- a/app/src/main/java/io/github/chronosx88/influence/helpers/UIActions.java +++ b/app/src/main/java/io/github/chronosx88/influence/helpers/actions/UIActions.java @@ -1,4 +1,4 @@ -package io.github.chronosx88.influence.helpers; +package io.github.chronosx88.influence.helpers.actions; public class UIActions { public static final int BOOTSTRAP_NOT_SPECIFIED = 0x0; @@ -8,4 +8,6 @@ public class UIActions { public static final int RELAY_CONNECTION_ERROR = 0x4; public static final int BOOTSTRAP_ERROR = 0x5; public static final int NEW_CHAT = 0x6; + public static final int PEER_NOT_EXIST = 0x7; + public static final int SUCCESSFULL_CREATE_CHAT = 0x8; } diff --git a/app/src/main/java/io/github/chronosx88/influence/logic/ChatListLogic.java b/app/src/main/java/io/github/chronosx88/influence/logic/ChatListLogic.java index 3f8971a..3b6c7ea 100644 --- a/app/src/main/java/io/github/chronosx88/influence/logic/ChatListLogic.java +++ b/app/src/main/java/io/github/chronosx88/influence/logic/ChatListLogic.java @@ -7,8 +7,8 @@ import java.util.List; import io.github.chronosx88.influence.contracts.chatlist.ChatListLogicContract; import io.github.chronosx88.influence.contracts.observer.Observer; import io.github.chronosx88.influence.helpers.AppHelper; -import io.github.chronosx88.influence.helpers.NetworkActions; -import io.github.chronosx88.influence.helpers.UIActions; +import io.github.chronosx88.influence.helpers.actions.NetworkActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; import io.github.chronosx88.influence.models.roomEntities.ChatEntity; import io.github.chronosx88.influence.observable.MainObservable; diff --git a/app/src/main/java/io/github/chronosx88/influence/logic/MainLogic.java b/app/src/main/java/io/github/chronosx88/influence/logic/MainLogic.java index d9b5187..4cc8905 100644 --- a/app/src/main/java/io/github/chronosx88/influence/logic/MainLogic.java +++ b/app/src/main/java/io/github/chronosx88/influence/logic/MainLogic.java @@ -2,6 +2,7 @@ package io.github.chronosx88.influence.logic; import android.content.Context; import android.content.SharedPreferences; +import android.util.Base64; import android.util.Log; import com.google.gson.Gson; @@ -19,20 +20,30 @@ import net.tomp2p.p2p.PeerBuilder; import net.tomp2p.peers.Number160; import net.tomp2p.peers.PeerAddress; import net.tomp2p.relay.tcp.TCPRelayClientConfig; +import net.tomp2p.replication.AutoReplication; +import net.tomp2p.storage.Data; import org.json.JSONObject; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.util.UUID; import io.github.chronosx88.influence.contracts.main.MainLogicContract; import io.github.chronosx88.influence.helpers.AppHelper; -import io.github.chronosx88.influence.helpers.NetworkActions; +import io.github.chronosx88.influence.helpers.Serializer; import io.github.chronosx88.influence.helpers.StorageMVStore; -import io.github.chronosx88.influence.helpers.UIActions; +import io.github.chronosx88.influence.helpers.actions.NetworkActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; import io.github.chronosx88.influence.observable.MainObservable; public class MainLogic implements MainLogicContract { @@ -45,6 +56,7 @@ public class MainLogic implements MainLogicContract { private InetAddress bootstrapAddress = null; private PeerAddress bootstrapPeerAddress = null; private Gson gson; + private AutoReplication replication; public MainLogic() { this.context = AppHelper.getContext(); @@ -120,6 +132,11 @@ public class MainLogic implements MainLogicContract { AppHelper.storePeerID(preferences.getString("peerID", null)); AppHelper.storePeerDHT(peerDHT); setReceiveHandler(); + Gson gson = new Gson(); + JsonObject publicProfile = new JsonObject(); + publicProfile.addProperty("peerAddress", Base64.encodeToString(Serializer.serializeObject(peerDHT.peerAddress()).getBytes(StandardCharsets.UTF_8), Base64.URL_SAFE)); + peerDHT.put(Number160.createHash(preferences.getString("peerID", null) + "_profile")).data(new Data(gson.toJson(publicProfile)).protectEntry(createMainKeyPair())).start().awaitUninterruptibly(); + replication = new AutoReplication(peerDHT.peer()).start(); } catch (IOException e) { e.printStackTrace(); } @@ -184,7 +201,8 @@ public class MainLogic implements MainLogicContract { @Override public void shutdownPeer() { - peerDHT.shutdown(); + replication.shutdown().start(); + peerDHT.peer().announceShutdown().start().awaitUninterruptibly(); } private boolean checkFirstRun() { @@ -196,4 +214,34 @@ public class MainLogic implements MainLogicContract { } return false; } + + private KeyPair createMainKeyPair() { + KeyPair kp = null; + try { + File keyPairDir = new File(AppHelper.getContext().getFilesDir().getAbsoluteFile(), "keyPairs"); + if (!keyPairDir.exists()) + keyPairDir.mkdir(); + File mainKeyPairFile = new File(keyPairDir, "mainKeyPair.kp"); + if (!mainKeyPairFile.exists()) { + mainKeyPairFile.createNewFile(); + try { + kp = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + FileOutputStream outputStream = new FileOutputStream(mainKeyPairFile); + outputStream.write(Serializer.serializeObject(kp).getBytes(StandardCharsets.UTF_8)); + outputStream.close(); + return kp; + } + FileInputStream inputStream = new FileInputStream(mainKeyPairFile); + byte[] serializedKeyPair = new byte[(int) mainKeyPairFile.length()]; + inputStream.read(serializedKeyPair); + inputStream.close(); + kp = Serializer.deserializeObject(new String(serializedKeyPair, StandardCharsets.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + } + return kp; + } } diff --git a/app/src/main/java/io/github/chronosx88/influence/logic/StartChatLogic.java b/app/src/main/java/io/github/chronosx88/influence/logic/StartChatLogic.java new file mode 100644 index 0000000..9f3b3ba --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/logic/StartChatLogic.java @@ -0,0 +1,79 @@ +package io.github.chronosx88.influence.logic; + +import android.util.Base64; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.tomp2p.dht.FutureGet; +import net.tomp2p.dht.PeerDHT; +import net.tomp2p.futures.FuturePing; +import net.tomp2p.peers.Number160; +import net.tomp2p.peers.PeerAddress; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import io.github.chronosx88.influence.contracts.startchat.StartChatLogicContract; +import io.github.chronosx88.influence.helpers.AppHelper; +import io.github.chronosx88.influence.helpers.PrepareData; +import io.github.chronosx88.influence.helpers.Serializer; +import io.github.chronosx88.influence.helpers.actions.NetworkActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; +import io.github.chronosx88.influence.observable.MainObservable; + +public class StartChatLogic implements StartChatLogicContract { + private PeerDHT peerDHT; + private Gson gson; + + public StartChatLogic() { + peerDHT = AppHelper.getPeerDHT(); + gson = new Gson(); + } + + @Override + public void sendStartChatMessage(String peerID) { + new Thread(() -> { + JsonObject recipientPublicProfile = getPublicProfile(peerID); + if(recipientPublicProfile == null) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("action", UIActions.PEER_NOT_EXIST); + AppHelper.getObservable().notifyObservers(jsonObject, MainObservable.UI_ACTIONS_CHANNEL); + return; + } + String peerAddressString = recipientPublicProfile.get("peerAddress").getAsString(); + PeerAddress peerAddress = Serializer.deserializeObject(new String(Base64.decode(peerAddressString, Base64.URL_SAFE), StandardCharsets.UTF_8)); + FuturePing ping = peerDHT.peer().ping().peerAddress(peerAddress).start().awaitUninterruptibly(); + if(ping.isSuccess()) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("action", NetworkActions.START_CHAT); + jsonObject.addProperty("chatID", UUID.randomUUID().toString()); + // TODO: Append public key to new chat request (for encryption) + jsonObject.addProperty("senderAddress", PrepareData.prepareToStore(peerDHT.peerAddress())); + peerDHT.peer().sendDirect(peerAddress).object(gson.toJson(jsonObject)).start(); + } else { + // TODO: put chat entry to "*peerID*_newChats". That peer later will fetch this new chats + } + }).start(); + } + + private JsonObject getPublicProfile(String peerID) { + JsonObject publicProfile; + FutureGet futureGetProfile = peerDHT.get(Number160.createHash(peerID + "_profile")).start().awaitUninterruptibly(); + if (futureGetProfile.isSuccess()) { + String jsonString = null; + try { + jsonString = (String) futureGetProfile.data().object(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + publicProfile = new JsonParser().parse(jsonString).getAsJsonObject(); + return publicProfile; + } + return null; + } +} diff --git a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java index 79861f1..6476703 100644 --- a/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java +++ b/app/src/main/java/io/github/chronosx88/influence/models/roomEntities/ChatEntity.java @@ -9,11 +9,13 @@ import androidx.room.PrimaryKey; public class ChatEntity { @NonNull @PrimaryKey String id; @ColumnInfo String name; + @ColumnInfo String peerAddresses; @ColumnInfo String keyPairID; - public ChatEntity(String id, String name, String keyPairID) { + public ChatEntity(String id, String name, String peerAddresses, String keyPairID) { this.id = id; this.name = name; + this.peerAddresses = peerAddresses; this.keyPairID = keyPairID; } @@ -25,6 +27,8 @@ public class ChatEntity { return keyPairID; } + public String getPeerAddress() { return peerAddresses; } + public String getName() { return name; } diff --git a/app/src/main/java/io/github/chronosx88/influence/presenters/StartChatPresenter.java b/app/src/main/java/io/github/chronosx88/influence/presenters/StartChatPresenter.java new file mode 100644 index 0000000..f527732 --- /dev/null +++ b/app/src/main/java/io/github/chronosx88/influence/presenters/StartChatPresenter.java @@ -0,0 +1,45 @@ +package io.github.chronosx88.influence.presenters; + +import com.google.gson.JsonObject; + +import io.github.chronosx88.influence.contracts.observer.Observer; +import io.github.chronosx88.influence.contracts.startchat.StartChatLogicContract; +import io.github.chronosx88.influence.contracts.startchat.StartChatPresenterContract; +import io.github.chronosx88.influence.contracts.startchat.StartChatViewContract; +import io.github.chronosx88.influence.helpers.AppHelper; +import io.github.chronosx88.influence.helpers.actions.UIActions; +import io.github.chronosx88.influence.logic.StartChatLogic; +import io.github.chronosx88.influence.observable.MainObservable; + +public class StartChatPresenter implements StartChatPresenterContract, Observer { + private StartChatViewContract view; + private StartChatLogicContract logic; + + public StartChatPresenter(StartChatViewContract view) { + this.view = view; + this.logic = new StartChatLogic(); + AppHelper.getObservable().register(this, MainObservable.UI_ACTIONS_CHANNEL); + } + + @Override + public void startChatWithPeer(String peerID) { + // TODO + } + + @Override + public void handleEvent(JsonObject object) { + switch (object.get("action").getAsInt()) { + case UIActions.PEER_NOT_EXIST: { + view.showProgressDialog(false); + view.showMessage("Данный узел не существует!"); + break; + } + + case UIActions.SUCCESSFULL_CREATE_CHAT: { + view.showProgressDialog(false); + view.showMessage("Чат успешно создан"); + break; + } + } + } +} diff --git a/app/src/main/java/io/github/chronosx88/influence/views/MainActivity.java b/app/src/main/java/io/github/chronosx88/influence/views/MainActivity.java index 644478a..4778f21 100644 --- a/app/src/main/java/io/github/chronosx88/influence/views/MainActivity.java +++ b/app/src/main/java/io/github/chronosx88/influence/views/MainActivity.java @@ -17,7 +17,7 @@ import io.github.chronosx88.influence.contracts.main.MainPresenterContract; import io.github.chronosx88.influence.contracts.main.MainViewContract; import io.github.chronosx88.influence.contracts.observer.Observer; import io.github.chronosx88.influence.helpers.AppHelper; -import io.github.chronosx88.influence.helpers.UIActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; import io.github.chronosx88.influence.observable.MainObservable; import io.github.chronosx88.influence.presenters.MainPresenter; import io.github.chronosx88.influence.views.fragments.ChatListFragment; diff --git a/app/src/main/java/io/github/chronosx88/influence/views/fragments/ChatListFragment.java b/app/src/main/java/io/github/chronosx88/influence/views/fragments/ChatListFragment.java index 4384a9d..36da478 100644 --- a/app/src/main/java/io/github/chronosx88/influence/views/fragments/ChatListFragment.java +++ b/app/src/main/java/io/github/chronosx88/influence/views/fragments/ChatListFragment.java @@ -17,7 +17,7 @@ import io.github.chronosx88.influence.contracts.chatlist.ChatListViewContract; import io.github.chronosx88.influence.contracts.observer.Observer; import io.github.chronosx88.influence.helpers.AppHelper; import io.github.chronosx88.influence.helpers.ChatListAdapter; -import io.github.chronosx88.influence.helpers.UIActions; +import io.github.chronosx88.influence.helpers.actions.UIActions; import io.github.chronosx88.influence.observable.MainObservable; import io.github.chronosx88.influence.presenters.ChatListPresenter; diff --git a/app/src/main/java/io/github/chronosx88/influence/views/fragments/StartChatFragment.java b/app/src/main/java/io/github/chronosx88/influence/views/fragments/StartChatFragment.java index b629abc..7302748 100644 --- a/app/src/main/java/io/github/chronosx88/influence/views/fragments/StartChatFragment.java +++ b/app/src/main/java/io/github/chronosx88/influence/views/fragments/StartChatFragment.java @@ -1,20 +1,23 @@ package io.github.chronosx88.influence.views.fragments; +import android.app.ProgressDialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputLayout; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import io.github.chronosx88.influence.R; +import io.github.chronosx88.influence.contracts.startchat.StartChatViewContract; -public class StartChatFragment extends Fragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } +public class StartChatFragment extends Fragment implements StartChatViewContract { + private TextInputLayout textInputPeerID; + private ProgressDialog progressDialog; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -25,6 +28,25 @@ public class StartChatFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // TODO + textInputPeerID = view.findViewById(R.id.textInputPeerID); + progressDialog = new ProgressDialog(getActivity(), R.style.AlertDialogTheme); + progressDialog.setCancelable(false); + progressDialog.setProgressStyle(android.R.style.Widget_ProgressBar_Small); } + + @Override + public void showMessage(String message) { + Snackbar.make(getView().findViewById(R.id.start_chat_coordinator), message, Snackbar.LENGTH_SHORT).show(); + } + + @Override + public void showProgressDialog(boolean enabled) { + if(enabled) { + progressDialog.show(); + } else { + progressDialog.dismiss(); + } + } + + // TODO: clear text input } diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index 8a58a8a..bf08408 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -1,18 +1,32 @@ - + - -