diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 18219fb2..b47e09f2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -58,7 +58,7 @@
-
+
diff --git a/app/src/main/java/com/amos/flyinn/ADBActivity.java b/app/src/main/java/com/amos/flyinn/ADBActivity.java
index 8ab7503a..b6941ac6 100644
--- a/app/src/main/java/com/amos/flyinn/ADBActivity.java
+++ b/app/src/main/java/com/amos/flyinn/ADBActivity.java
@@ -22,7 +22,7 @@ protected void onCreate(Bundle savedInstanceState) {
try {
Point p = new Point();
getWindowManager().getDefaultDisplay().getRealSize(p);
- Demo.start(getApplicationContext(), "127.0.0.1", p);
+ Demo.start(getApplicationContext(), p);
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java b/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java
index 1c044ba9..e3a64aa2 100644
--- a/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java
+++ b/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java
@@ -194,13 +194,12 @@ private void restartAPP() {
/**
* This method is to create an ADB service
*
- * @param addr the address where the ADB service is going to listen for connections
* @return returns Daemon service to work with the ADB service
*/
- protected Daemon createADBService(String addr) {
+ protected Daemon createADBService() {
Point p = new Point();
getWindowManager().getDefaultDisplay().getRealSize(p);
- Daemon d = new Daemon(getApplicationContext(), addr, p);
+ Daemon d = new Daemon(getApplicationContext(), p);
try {
d.writeFakeInputToFilesystem();
d.spawn_adb();
diff --git a/app/src/main/java/com/amos/flyinn/MainActivity.java b/app/src/main/java/com/amos/flyinn/MainActivity.java
index 7e2cf5b5..90170553 100644
--- a/app/src/main/java/com/amos/flyinn/MainActivity.java
+++ b/app/src/main/java/com/amos/flyinn/MainActivity.java
@@ -1,9 +1,5 @@
package com.amos.flyinn;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.os.StrictMode;
-import android.provider.Settings;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
@@ -117,10 +113,10 @@ public boolean onOptionsItemSelected(MenuItem item) {
System.loadLibrary("native-lib");
}
- protected Daemon createADBService(String addr) {
+ protected Daemon createADBService() {
Point p = new Point();
getWindowManager().getDefaultDisplay().getRealSize(p);
- Daemon d = new Daemon(getApplicationContext(), addr, p);
+ Daemon d = new Daemon(getApplicationContext(), p);
try {
d.writeFakeInputToFilesystem();
d.spawn_adb();
@@ -146,13 +142,11 @@ public void onClick(View view) {
// Example of a call to a native method
connectionStatus = findViewById(R.id.connectionStatus);
- String addr;
try {
- addr = "127.0.0.1";
while (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
}
- adbDaemon = createADBService(addr);
+ adbDaemon = createADBService();
} catch (Exception e) {
}
@@ -169,9 +163,6 @@ public void onClick(View view) {
public native String stringFromJNI();
-
-
-
private void checkForDebuggingMode() {
if (Settings.Secure.getInt(this.getContentResolver(), Settings.Global.ADB_ENABLED, 0) != 1) {
AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create();
diff --git a/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java b/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java
index 7adc952e..3240668f 100644
--- a/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java
+++ b/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java
@@ -1,7 +1,9 @@
package com.amos.flyinn;
+import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.graphics.Point;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.CallSuper;
@@ -13,6 +15,7 @@
import android.widget.Toast;
import com.amos.flyinn.nearbyservice.NearbyService;
+import com.amos.flyinn.summoner.Daemon;
import java.util.concurrent.ThreadLocalRandom;
@@ -47,18 +50,40 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_show_code);
display = findViewById(R.id.textView2);
- if (!hasPermissions(NearbyService.getRequiredPermissions()) &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (!hasPermissions(NearbyService.getRequiredPermissions()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(NearbyService.getRequiredPermissions(), REQUEST_CODE_REQUIRED_PERMISSIONS);
} else {
Log.w(TAG, "Could not check permissions due to version");
}
+ String[] perms = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
+ if (!hasPermissions(perms) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ requestPermissions(perms, REQUEST_CODE_REQUIRED_PERMISSIONS);
+ }
+
+ try {
+ createADBService();
+ } catch (Exception e) {
+ Log.d("ShowCodeActivity", "Failed to start ADB service");
+ e.printStackTrace();
+ }
nameNum = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 9998 + 1));
display.setText(nameNum);
setService();
}
+ protected Daemon createADBService() throws Exception {
+ Point p = new Point();
+ getWindowManager().getDefaultDisplay().getRealSize(p);
+ Daemon d = new Daemon(getApplicationContext(), p);
+ d.writeFakeInputToFilesystem();
+ Log.d("ShowCodeActivity", "Wrote to FS");
+ Log.d("ShowCodeActivity", "Going to spawn ADB service");
+ d.spawn_adb();
+ Log.d("ShowCodeActivity", "Spawned ADB service");
+ return d;
+ }
+
@Override
public void onResume() {
super.onResume();
@@ -75,8 +100,8 @@ protected void onDestroy() {
/**
* Handles user acceptance (or denial) of our permission request.
*
- * @param requestCode The request code passed in requestPermissions()
- * @param permissions Permissions that must be granted to run nearby connections
+ * @param requestCode The request code passed in requestPermissions()
+ * @param permissions Permissions that must be granted to run nearby connections
* @param grantResults Results of granting permissions
*/
@CallSuper
diff --git a/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java
index 9854f6a0..a9995543 100644
--- a/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java
+++ b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java
@@ -1,7 +1,5 @@
package com.amos.flyinn.nearbyservice;
-import android.annotation.TargetApi;
-import android.app.Activity;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -11,21 +9,19 @@
import android.content.Context;
import android.content.Intent;
import android.os.Build;
-import android.os.Handler;
+import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
-
import com.amos.flyinn.ConnectionSetupActivity;
-import com.amos.flyinn.R;
import com.amos.flyinn.ShowCodeActivity;
+import com.amos.flyinn.summoner.ADBService;
+import com.amos.flyinn.summoner.ConnectionSigleton;
import com.google.android.gms.nearby.connection.Payload;
import com.google.android.gms.nearby.connection.PayloadTransferUpdate;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.ThreadLocalRandom;
+import java.util.Objects;
/**
* Manage nearby connections with a server.
@@ -35,7 +31,6 @@
* initiate a connection over nearby.
*/
public class NearbyService extends IntentService {
-
public static final String TAG = NearbyService.class.getPackage().getName();
public static final String ACTION_START = "nearby_start";
@@ -72,19 +67,19 @@ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
/**
* Create a notification channel.
- *
+ *
* Notifications are organized into different channels to theoretically enable the user to
* individually set what they want to be informed about.
- *
+ *
* This is required since Android 8.
*/
private void createChannel() {
- NotificationManager mgr=
- (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
- if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&
- mgr.getNotificationChannel(CHANNEL_ID)==null) {
+ NotificationManager mgr =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+ mgr.getNotificationChannel(CHANNEL_ID) == null) {
- NotificationChannel c=new NotificationChannel(CHANNEL_ID,
+ NotificationChannel c = new NotificationChannel(CHANNEL_ID,
"flyinn_channel", NotificationManager.IMPORTANCE_HIGH);
c.enableLights(true);
@@ -96,8 +91,9 @@ private void createChannel() {
/**
* Create a sticky notification that won't go away.
+ *
* @param message String message shown in the notification.
- * @param target Optional target intent to switch to after tapping the notification.
+ * @param target Optional target intent to switch to after tapping the notification.
* @return
*/
private Notification buildForegroundNotification(String message, @Nullable Intent target) {
@@ -127,6 +123,7 @@ private void raiseNotification(Notification notification) {
/**
* Create or update shown notification.
+ *
* @param message
*/
public void notify(String message) {
@@ -154,6 +151,7 @@ public void notify(String message) {
/**
* Activate the given activity
+ *
* @param cls Class of the target activity
*/
private void switchActivity(Class> cls) {
@@ -168,13 +166,14 @@ public NearbyState getServiceState() {
/**
* Set state of the nearby service. This is used by the nearby server.
+ *
* @param state
* @param message
*/
public void setServiceState(NearbyState state, @Nullable String message) {
// do extra things if we are switching state
if (serviceState != state) {
- switch(serviceState) {
+ switch (serviceState) {
case CONNECTING:
switchActivity(ConnectionSetupActivity.class);
break;
@@ -209,6 +208,15 @@ public void handleResponse(boolean error, String message) {
public void handlePayload(Payload payload) {
Log.d(TAG, "Received payload");
+ if (payload.asStream() == null) {
+ Log.wtf("AAAAAAAAAAAAAAAAAAA", "Payload is null!");
+ return;
+ }
+ Intent i = new Intent(this, ADBService.class);
+ i.setAction("stream");
+ ConnectionSigleton.getInstance().inputStream = Objects.requireNonNull(payload.asStream()).asInputStream();
+ startService(i);
+ Log.d(TAG, "Send payload to activity");
}
public void handlePayloadTransferUpdate(PayloadTransferUpdate update) {
diff --git a/app/src/main/java/com/amos/flyinn/summoner/ADBService.java b/app/src/main/java/com/amos/flyinn/summoner/ADBService.java
index 32185f03..1cca9b43 100644
--- a/app/src/main/java/com/amos/flyinn/summoner/ADBService.java
+++ b/app/src/main/java/com/amos/flyinn/summoner/ADBService.java
@@ -2,16 +2,18 @@
import android.app.IntentService;
import android.content.Intent;
+import android.util.Base64;
import android.util.Log;
-import com.tananaev.adblib.AdbBase64;
+import com.google.android.gms.common.util.IOUtils;
import com.tananaev.adblib.AdbConnection;
import com.tananaev.adblib.AdbCrypto;
import com.tananaev.adblib.AdbStream;
import java.io.IOException;
+import java.io.InputStream;
import java.net.Socket;
-import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
/**
* Background service controlling the start of fakeinputlib via an adb shell.
@@ -30,62 +32,113 @@ public ADBService() {
super("ADBService");
}
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d("ADBService", "Only called once in a lifetime!");
+ }
+
/**
* Create a socket connection to use adb over network with the local phone.
+ *
* @return
- * @throws IOException
- * All errors resulting in us not being able to connect to ADB over network.
+ * @throws IOException All errors resulting in us not being able to connect to ADB over network.
*/
protected AdbConnection connectNetworkADB() throws IOException {
AdbConnection connection;
try {
Socket socket = new Socket("127.0.0.1", 5555);
- AdbCrypto crypto = AdbCrypto.generateAdbKeyPair(new AdbBase64() {
- @Override
- public String encodeToString(byte[] data) {
- return android.util.Base64.encodeToString(data, 16);
- }
+ AdbCrypto crypto = AdbCrypto.generateAdbKeyPair(data -> {
+ String s = Base64.encodeToString(data, 16).replace("\n", "");
+ Log.d("ADBService", "New key is" + s);
+ return s;
});
+ Log.d("ADBService", "Acquiring connection to port :5555");
connection = AdbConnection.create(socket, crypto);
+ Log.d("ADBService", "Trying to connect to ADB session");
connection.connect();
} catch (Exception err) {
+ Log.d("ADBService", "Failed to connect to ADB");
+ Log.wtf("ADBService", err);
throw new IOException("Could not start fakeinputlib");
}
+ Log.d("ADBService", "Got the ADB connection");
return connection;
}
/**
* Run a command using adb shell.
- * @param connection
- * ADB connection used to spawn command on.
- * @param command
- * Custom command to run
- * @throws IOException
- * Issues in running the command correctly.
+ *
+ * @param connection ADB connection used to spawn command on.
+ * @param command Custom command to run
+ * @throws IOException Issues in running the command correctly.
*/
- protected void spawnApp(AdbConnection connection, String command) throws IOException {
+ protected void spawnApp(AdbConnection connection, String command) throws Exception {
if (connection == null) {
throw new IOException("Connection is null");
}
Log.d("AppDaemon", "Spawning the app");
try {
AdbStream stream = connection.open(command);
- while (true)
- stream.read();
- } catch (InterruptedException err) {
- // Do nothing on interrupts for now
+ new Thread(() -> {
+ try {
+ for (; ; ) {
+ Log.d("AppDaemon", "Read loop");
+ stream.read();
+ }
+ } catch (Exception e) {
+ Log.d("AppDaemon", "Failed the read loop");
+ e.printStackTrace();
+ }
+ Log.d("AppDaemon", "Stopping the App");
+ }).start();
+ } catch (Exception e) {
+ Log.d("AppDaemon", "Failed to satart listener", e);
+ throw e;
}
}
/**
* Start the adb shell process.
+ *
* @param workIntent
*/
@Override
protected void onHandleIntent(Intent workIntent) {
+ Log.d("ADBService", "Got intent");
+ if (workIntent.getStringExtra("cmd") != null && !"".equals(workIntent.getStringExtra("cmd"))) {
+ try {
+ Log.d("ADBService", "Got create intent");
+ AdbConnection connection = connectNetworkADB();
+ spawnApp(connection, workIntent.getStringExtra("cmd"));
+ Log.d("ADBService", "Launched adb connection");
+ } catch (Exception e) {
+ Log.d("ADBService", "Failed to start the adb service", e);
+ }
+ return;
+ }
+
+ Log.d("ADBService", "Get action");
try {
- AdbConnection connection = connectNetworkADB();
- spawnApp(connection, workIntent.getStringExtra("cmd"));
- } catch (Exception e) {}
+ Log.d("ADBService", workIntent.getAction());
+ switch (Objects.requireNonNull(workIntent.getAction())) {
+ case "stream":
+ Log.d("ADBService", "Start proxy...");
+ Socket s = new Socket("127.0.0.1", 1337);
+ Log.d("ADBService", "Proxy connected");
+ InputStream ss = ConnectionSigleton.getInstance().inputStream;
+ new Thread(() -> {
+ try {
+ Log.d("ADBService", "Proxy piping...");
+ IOUtils.copyStream(ss, s.getOutputStream());
+ Log.d("ADBService", "Proxy done piping");
+ } catch (Exception e) {
+ Log.d("ADBService", "Failed to pipe", e);
+ }
+ }).start();
+ }
+ } catch (Exception e) {
+ Log.d("ADBService", "Failed to connect to ADB", e);
+ }
}
}
diff --git a/app/src/main/java/com/amos/flyinn/summoner/ConnectionSigleton.java b/app/src/main/java/com/amos/flyinn/summoner/ConnectionSigleton.java
new file mode 100644
index 00000000..265b8de7
--- /dev/null
+++ b/app/src/main/java/com/amos/flyinn/summoner/ConnectionSigleton.java
@@ -0,0 +1,15 @@
+package com.amos.flyinn.summoner;
+
+import java.io.InputStream;
+
+public class ConnectionSigleton {
+ private static final ConnectionSigleton ourInstance = new ConnectionSigleton();
+ public InputStream inputStream;
+
+ public static ConnectionSigleton getInstance() {
+ return ourInstance;
+ }
+
+ private ConnectionSigleton() {
+ }
+}
diff --git a/app/src/main/java/com/amos/flyinn/summoner/Daemon.java b/app/src/main/java/com/amos/flyinn/summoner/Daemon.java
index eae1f5f5..c15517e2 100644
--- a/app/src/main/java/com/amos/flyinn/summoner/Daemon.java
+++ b/app/src/main/java/com/amos/flyinn/summoner/Daemon.java
@@ -9,6 +9,7 @@
import com.amos.flyinn.R;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -16,15 +17,19 @@
/**
* Handles deployment of the fakeinputlib package.
- *
+ *
* fakeinputlib is a separate java binary which is run with adb debugging permissions to
* allow injection of input events into other applications. This daemon controls copying the
* fakeinputlib binary to a specified location and its execution over adb over network.
*/
public class Daemon {
- private final String FAKE_INPUT_SERVER_PATH = Environment.getExternalStorageDirectory() + "/Android/data/flyinn_fakeinputlib.jar";
+ private final String[] WRITE_LOCATIONS = {
+ Environment.getExternalStorageDirectory() + "/Android/data/flyinn_fakeinputlib.jar",
+ "/sdcard/Android/data/flyinn_fakeinputlib.jar",
+ };
+ private final String FILE_LOCATION = "/sdcard/Android/data/flyinn_fakeinput.jar";
// NOTE: cant use the FAKE_INPUT_SERVER_PATH as this will lead to permission issues, use "/sdcard/", which is a symlink.
- private final String SHELL_FAKE_INPUT_NOHUP_COMMAND = "shell:CLASSPATH=/sdcard/Android/data/flyinn_fakeinputlib.jar app_process / com.amos.fakeinputlib.Main %s %d %d";
+ private final String SHELL_FAKE_INPUT_NOHUP_COMMAND = "shell:CLASSPATH=/sdcard/Android/data/flyinn_fakeinputlib.jar app_process / com.amos.fakeinputlib.Main %d %d";
//private final String CMD = "shell:CLASSPATH=%%s nohup app_process / com.amos.fakeinputlib.Main %s %d %d";
private final String execCMD;
private Context context;
@@ -34,50 +39,56 @@ public class Daemon {
/**
* Create a new Daemon instance.
*
- * @param context
- * Application context. Used to get application binary.
- * @param addr
- * Server address. This will be passed as a commandline argument to the fakeinputlib binary.
- * @param screenDimensions
- * Screen size including status bar and navbar.
+ * @param context Application context. Used to get application binary.
+ * @param screenDimensions Screen size including status bar and navbar.
*/
- public Daemon(Context context, String addr, Point screenDimensions) {
- execCMD = String.format(Locale.ENGLISH, SHELL_FAKE_INPUT_NOHUP_COMMAND, addr, screenDimensions.x, screenDimensions.y);
- Log.d("AdbDaemon", this.execCMD);
+ public Daemon(Context context, Point screenDimensions) {
+ execCMD = String.format(Locale.ENGLISH, SHELL_FAKE_INPUT_NOHUP_COMMAND, screenDimensions.x, screenDimensions.y);
+ Log.d("AdbDaemon", "Generated cmd: " + this.execCMD);
this.context = context;
}
/**
* Copy fakeinputlib binary from APK to local directory.
+ *
* @throws IOException
*/
public void writeFakeInputToFilesystem() throws IOException {
InputStream in = this.context.getResources().openRawResource(R.raw.fakeinputlib);
+ for (String destination : WRITE_LOCATIONS)
+ try {
+ File fpath = new File(destination);
+ FileOutputStream out = new FileOutputStream(fpath.toString());
- File fpath = new File(FAKE_INPUT_SERVER_PATH);
- FileOutputStream out = new FileOutputStream(fpath.toString());
-
- byte[] buff = new byte[2048];
- try {
- for (int read; (read = in.read(buff)) > 0; ) {
- out.write(buff, 0, read);
+ byte[] buff = new byte[2048];
+ for (int read; (read = in.read(buff)) > 0; ) {
+ out.write(buff, 0, read);
+ }
+ binaryPath = fpath.toString();
+ in.close();
+ out.close();
+ Log.d("AdbDaemon", "Wrote fakeinputlib bin to file system " + destination);
+ return;
+ } catch (FileNotFoundException ignored) {
+ Log.d("AdbDaemon", "Failed to write fakeinputlib bin to " + destination);
+ continue;
}
- binaryPath = fpath.toString();
- } finally {
- in.close();
- out.close();
- }
+
+ throw new IOException();
}
/**
* Start ADBService, which will call fakeinputlib on commandline. This is necessary in order to
* keep fakeinputlib alive even when the application itself is closed.
+ *
* @throws Exception
*/
public void spawn_adb() {
Intent i = new Intent(context, ADBService.class);
i.putExtra("cmd", execCMD);
+ Log.d("AdbDaemon", "Spawn ADB intent service");
+
context.startService(i);
}
}
diff --git a/app/src/main/java/com/amos/flyinn/summoner/Demo.java b/app/src/main/java/com/amos/flyinn/summoner/Demo.java
index 5dc9a1fb..dbfdc31a 100644
--- a/app/src/main/java/com/amos/flyinn/summoner/Demo.java
+++ b/app/src/main/java/com/amos/flyinn/summoner/Demo.java
@@ -23,13 +23,13 @@ private static void periodicNagging(FakeInputSender s) throws IOException, Inter
}
- public static void start(Context context, String addr, Point p) {
- Daemon d = new Daemon(context, addr, p);
+ public static void start(Context context, Point p) {
+ Daemon d = new Daemon(context, p);
FakeInputSender s = new FakeInputSender();
try {
d.writeFakeInputToFilesystem();
d.spawn_adb();
- s.connect(addr);
+ s.connect();
periodicNagging(s);
} catch (Exception e) {
e.printStackTrace();
diff --git a/app/src/main/java/com/amos/flyinn/summoner/FakeInputSender.java b/app/src/main/java/com/amos/flyinn/summoner/FakeInputSender.java
index 6a1a2093..10f8f040 100644
--- a/app/src/main/java/com/amos/flyinn/summoner/FakeInputSender.java
+++ b/app/src/main/java/com/amos/flyinn/summoner/FakeInputSender.java
@@ -15,8 +15,8 @@ public class FakeInputSender {
public FakeInputSender() {
}
- public void connect(String addr) throws Exception {
- Socket socket = new Socket(addr, 1337);
+ public void connect() throws Exception {
+ Socket socket = new Socket("127.0.0.1", 1337);
this.output = new ObjectOutputStream(socket.getOutputStream());
}
diff --git a/app/src/test/java/com/amos/flyinn/summoner/DaemonTest.java b/app/src/test/java/com/amos/flyinn/summoner/DaemonTest.java
index ce1054d2..98aa0421 100644
--- a/app/src/test/java/com/amos/flyinn/summoner/DaemonTest.java
+++ b/app/src/test/java/com/amos/flyinn/summoner/DaemonTest.java
@@ -23,7 +23,7 @@ public class DaemonTest {
*/
@Test
public void spawn_adbHappypathTest() {
- Daemon d = new Daemon(context, "127.0.0.1", new Point(0, 0));
+ Daemon d = new Daemon(context, new Point(0, 0));
d.spawn_adb();
}
@@ -32,7 +32,7 @@ public void spawn_adbHappypathTest() {
*/
@Test(expected = NullPointerException.class)
public void spawn_adbNoContextTest() {
- Daemon d = new Daemon(null, "127.0.0.1", new Point(0, 0));
+ Daemon d = new Daemon(null, new Point(0, 0));
d.spawn_adb();
}
@@ -42,7 +42,7 @@ public void spawn_adbNoContextTest() {
*/
@Test(expected = IOException.class)
public void writeFakeInputToFilesystem() throws IOException {
- Daemon d = new Daemon(context, "127.0.0.1", new Point(0, 0));
+ Daemon d = new Daemon(context, new Point(0, 0));
d.writeFakeInputToFilesystem();
}
}
\ No newline at end of file
diff --git a/fakeinputlib/src/main/java/com/amos/fakeinputlib/FakeInputReceiver.java b/fakeinputlib/src/main/java/com/amos/fakeinputlib/FakeInputReceiver.java
index d4288745..1c51e480 100644
--- a/fakeinputlib/src/main/java/com/amos/fakeinputlib/FakeInputReceiver.java
+++ b/fakeinputlib/src/main/java/com/amos/fakeinputlib/FakeInputReceiver.java
@@ -7,6 +7,7 @@
import java.io.IOException;
import java.io.ObjectInputStream;
+import java.net.ServerSocket;
import java.net.Socket;
class FakeInputReceiver {
@@ -23,12 +24,14 @@ class FakeInputReceiver {
this.maxY = maxY;
}
- void connectToHost(String addr) throws Exception {
+ void connectToHost() throws Exception {
+ ServerSocket ss;
Socket fd = null;
for (int i = 0; i < 5; i++) {
- Log.d("FakeInput", "Trying to connect to the host " + addr);
+ Log.d("FakeInput", "Listening to peers");
try {
- fd = new Socket(addr, 1337);
+ ss = new ServerSocket( 1337);
+ fd = ss.accept();
} catch (Exception e) {
e.printStackTrace();
@@ -64,7 +67,7 @@ private void runEvalLoop(FakeInput handler, Socket connection) throws IOExceptio
continue;
}
- Log.d("FakeInput", "Got Event: "+e.toString());
+ Log.d("FakeInput", "Got Event: " + e.toString());
MotionEvent ev = e.getConstructedMotionEvent(this.maxX, this.maxY);
Log.d("FakeInput", String.format("Event: (%f, %f, %f, %f)", ev.getX(), ev.getY(), ev.getRawX(), ev.getRawX()));
diff --git a/fakeinputlib/src/main/java/com/amos/fakeinputlib/Main.java b/fakeinputlib/src/main/java/com/amos/fakeinputlib/Main.java
index eee6b3da..b02d0544 100644
--- a/fakeinputlib/src/main/java/com/amos/fakeinputlib/Main.java
+++ b/fakeinputlib/src/main/java/com/amos/fakeinputlib/Main.java
@@ -4,9 +4,13 @@
public class Main {
public static void main(String[] in) throws Exception {
- FakeInputReceiver server = new FakeInputReceiver(new FakeInput(), Integer.parseInt(in[1]), Integer.parseInt(in[2]));
- Log.d("FakeInput", "Starting server");
- server.connectToHost(in[0]);
- Log.d("FakeInput", "Stopping Server");
+ Log.d("FakeInput", "Start bin");
+ FakeInput handler = new FakeInput();
+ FakeInputReceiver server = new FakeInputReceiver(handler, Integer.parseInt(in[0]), Integer.parseInt(in[1]));
+ while (true) {
+ Log.d("FakeInput", "Starting server");
+ server.connectToHost();
+ Log.d("FakeInput", "Stopping Server");
+ }
}
}
\ No newline at end of file
diff --git a/server/build.gradle b/server/build.gradle
index 5562205b..d235975c 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -64,27 +64,29 @@ dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:28.0.0'
- testImplementation 'junit:junit:4.12'
- testImplementation 'org.robolectric:robolectric:4.0.2'
- androidTestImplementation 'androidx.test:core:1.0.0'
- androidTestImplementation 'androidx.test:runner:1.1.0'
- androidTestImplementation 'androidx.test:rules:1.1.0'
implementation 'com.android.support:support-media-compat:28.0.0'
implementation 'com.android.support:support-v4:28.0.0'
-
implementation 'org.webrtc:google-webrtc:1.0.22672'
implementation "org.java-websocket:Java-WebSocket:1.3.9"
-
+ implementation 'com.android.support:preference-v7:28.0.0'
implementation 'com.google.android.gms:play-services-nearby:16.0.0'
embeddedCompileOnly 'com.google.android.things:androidthings:1.0'
- testImplementation 'androidx.test:core:1.0.0'
- testImplementation 'org.mockito:mockito-core:1.10.19'
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'org.robolectric:robolectric:4.0.2'
+ testImplementation 'androidx.test:core:1.1.0'
testImplementation 'org.json:json:20160810'
+ testImplementation 'org.mockito:mockito-core:1.10.19'
+ testImplementation "org.powermock:powermock-module-junit4:1.6.6"
+ testImplementation "org.powermock:powermock-module-junit4-rule:1.6.6"
+ testImplementation "org.powermock:powermock-api-mockito:1.6.6"
+ testImplementation "org.powermock:powermock-classloading-xstream:1.6.6"
// org.json is included with Android, but Android.jar can not be used from unit tests
- implementation 'com.android.support:preference-v7:28.0.0'
+ androidTestImplementation 'androidx.test:core:1.1.0'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test:rules:1.1.1'
}
task prePreBuild {
diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml
index e1449139..e6216081 100644
--- a/server/src/main/AndroidManifest.xml
+++ b/server/src/main/AndroidManifest.xml
@@ -28,17 +28,6 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
-
-
-
-
-
-
-
-
+
diff --git a/server/src/main/java/com/amos/server/ConnectToClientActivity.java b/server/src/main/java/com/amos/server/ConnectToClientActivity.java
index cf2230d2..677a00b2 100644
--- a/server/src/main/java/com/amos/server/ConnectToClientActivity.java
+++ b/server/src/main/java/com/amos/server/ConnectToClientActivity.java
@@ -17,24 +17,7 @@
import android.widget.TextView;
import android.widget.Toast;
-import com.google.android.gms.nearby.Nearby;
-import com.google.android.gms.nearby.connection.ConnectionInfo;
-import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback;
-import com.google.android.gms.nearby.connection.ConnectionResolution;
-import com.google.android.gms.nearby.connection.ConnectionsClient;
-import com.google.android.gms.nearby.connection.ConnectionsStatusCodes;
-import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo;
-import com.google.android.gms.nearby.connection.DiscoveryOptions;
-import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback;
-import com.google.android.gms.nearby.connection.Payload;
-import com.google.android.gms.nearby.connection.PayloadCallback;
-import com.google.android.gms.nearby.connection.PayloadTransferUpdate;
-import com.google.android.gms.nearby.connection.Strategy;
-
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
+import com.amos.server.nearby.ServerConnection;
public class ConnectToClientActivity extends Activity {
@@ -49,224 +32,40 @@ public class ConnectToClientActivity extends Activity {
private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 1;
- /** 1-to-1 since a device will be connected to only one other device at most. */
- private static final Strategy STRATEGY = Strategy.P2P_POINT_TO_POINT;
-
- /** Connection manager for the connection to FlyInn clients. */
- protected ConnectionsClient connectionsClient;
-
- private final String clientName = generateName(5);
- private String clientID;
- private String clientNameNow;
-
- /** Toast to publish user notifications */
- private Toast mToast;
-
- /** List of all discovered servers by name, continuously updated. */
- private List clients = new ArrayList<>();
-
- /** Maps server names to their nearby connection IDs. */
- private HashMap clientNamesToIDs = new HashMap<>();
-
- /** Maps server IDs to their nearby connection names. */
- private HashMap clientIDsToNames = new HashMap<>();
+ private ServerConnection connection = ServerConnection.getInstance();
/** Tag for logging purposes. */
private static final String TAG = "ClientNearbyConnection";
-
-
-
- /**
- * Obtain data from clientID/clientName and data transfer information via this handle.
- */
- private final PayloadCallback payloadCallback =
- new PayloadCallback() {
- @Override
- public void onPayloadReceived(String endpointId, Payload payload) {
- Log.d(TAG, "Payload received");
- }
-
- @Override
- public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) {
- Log.d(TAG, "Payload transfer update");
- }
- };
-
-
- /**
- * Handling of discovered endpoints (servers). Adds new endpoints to servers data maps/list,
- * and removes lost endpoints.
- */
- private final EndpointDiscoveryCallback endpointDiscoveryCallback =
- new EndpointDiscoveryCallback() {
- @Override
- public void onEndpointFound(String endpointId, DiscoveredEndpointInfo info) {
- // discovered a server, add to data maps
- String endpointName = info.getEndpointName();
-
- if (!(clientIDsToNames.containsKey(endpointId)
- || clientNamesToIDs.containsKey(endpointName))) {
- clients.add(endpointName);
- clientNamesToIDs.put(endpointName, endpointId);
- clientIDsToNames.put(endpointId, endpointName);
- Log.i(TAG, clientName + " discovered endpoint " + endpointId + " with name " + endpointName);
-
- } else {
- // this should not happen
- while (clients.remove(endpointName)) {}
- clients.add(endpointName);
- clientIDsToNames.put(endpointId, endpointName);
- clientNamesToIDs.put(endpointName, endpointId);
- Log.i(TAG, clientName + " rediscovered endpoint " + endpointId + " with name " + endpointName);
- }
- }
-
- @Override
- public void onEndpointLost(String endpointId) {
- // previously discovered server is no longer reachable, remove from data maps
- String lostEndpointName = clientIDsToNames.get(endpointId);
- clientIDsToNames.remove(endpointId);
- clientNamesToIDs.remove(lostEndpointName);
- Log.i(TAG, clientName + " lost discovered endpoint " + endpointId);
- }
- };
-
-
- /**
- * Callbacks for connections to other devices.
- * Includes token authentication and connection handling.
- */
- private final ConnectionLifecycleCallback connectionLifecycleCallback =
- new ConnectionLifecycleCallback() {
- @Override
- public void onConnectionInitiated(String endpointId, ConnectionInfo connectionInfo) {
- Log.i(TAG, "Connection initiated to " + endpointId);
-
- if (endpointId.equals(clientID)) {
- connectionsClient.acceptConnection(endpointId, payloadCallback);
- } else {
- // initiated connection is not with server selected by user
- connectionsClient.rejectConnection(endpointId);
- Log.i(TAG, "Connection rejected to non-selected server "
- + endpointId);
- }
- }
-
- @Override
- public void onConnectionResult(String endpointId, ConnectionResolution result) {
- switch (result.getStatus().getStatusCode()) {
-
- case ConnectionsStatusCodes.STATUS_OK:
- // successful connection with server
- Log.i(TAG, "Connected with " + endpointId);
- mToast.setText(R.string.nearby_connection_success);
- mToast.show();
- connectedToApp();
- break;
-
- case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED:
- // connection was rejected by one side (or both)
- Log.i(TAG, "Connection rejected with " + endpointId);
- mToast.setText(R.string.nearby_connection_rejected);
- mToast.show();
- clientNameNow = null;
- clientID = null;
- break;
-
- case ConnectionsStatusCodes.STATUS_ERROR:
- // connection was lost
- Log.w(TAG, "Connection lost: " + endpointId);
- mToast.setText(R.string.nearby_connection_error);
- mToast.show();
- clientNameNow = null;
- clientID = null;
- break;
-
- default:
- // unknown status code. we shouldn't be here
- Log.e(TAG, "Unknown error when attempting to connect with "
- + endpointId);
- mToast.setText(R.string.nearby_connection_error);
- mToast.show();
- clientNameNow = null;
- clientID = null;
- }
- }
-
- @Override
- public void onDisconnected(String endpointId) {
- // disconnected from server
- Log.i(TAG, "Disconnected from " + endpointId);
- mToast.setText(R.string.nearby_disconnected);
- mToast.show();
- clearServerData();
- finish();
- }
- };
-
-
-
- private void requestConnectionWithClient(String code){
-
- String searchedClientName = null;
- for (String clientName : clients){
- if(clientName.endsWith(code))
- {
- searchedClientName = clientName;
- break;
- }
- }
-
- if (searchedClientName == null)
- {
- Toast.makeText(this,"The code given was not found. Please try again",Toast.LENGTH_LONG).show();
- return;
- }
-
-
- String endpoint = clientNamesToIDs.get(searchedClientName);
- clientID = endpoint;
-
-
- connectionsClient.requestConnection(searchedClientName,endpoint,connectionLifecycleCallback);
-
-
-
- }
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_connect_to_client);
EditText text = findViewById(R.id.connect_editText);
- text.setOnEditorActionListener(new TextView.OnEditorActionListener() {
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- String name = v.getText().toString(); // Get the String
- requestConnectionWithClient(name);
- return true;
- }
- return false;
- }
- });
-
-
- if (!hasPermissions(this, REQUIRED_PERMISSIONS) &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
- } else {
- Log.w(TAG, "Could not check permissions due to version");
- }
-
- connectionsClient = Nearby.getConnectionsClient(this);
- mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
-
-
- startDiscovering();
+ checkPermissions();
+ // Ensure survival for life of entire application
+ connection.init(getApplicationContext());
+ connection.discover();
+ text.setOnEditorActionListener(
+ (TextView v, int actionId, KeyEvent event) -> {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ String name = v.getText().toString(); // Get the String
+ toConnectionSetup(name);
+ return true;
+ }
+ return false;
+ });
+ }
+ /**
+ * Switch to connection setup activity
+ */
+ private void toConnectionSetup(String name) {
+ Intent intent = new Intent(this, ConnectionSetupServerActivity.class);
+ intent.setAction("connect");
+ intent.putExtra("name", name);
+ startActivity(intent);
}
/**
@@ -277,12 +76,7 @@ protected void onStart() {
super.onStart();
// user may have changed permissions
- if (!hasPermissions(this, REQUIRED_PERMISSIONS) &&
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
- } else {
- Log.w(TAG, "Could not check permissions due to version");
- }
+ checkPermissions();
}
/**
@@ -291,74 +85,30 @@ protected void onStart() {
*/
@Override
protected void onDestroy() {
- connectionsClient.stopDiscovery();
- connectionsClient.stopAllEndpoints();
- clearServerData();
super.onDestroy();
+ connection.abort();
}
- /**
- * Clears all servers map data as well as serverName/serverID and starts discovery
- */
- private void startDiscovering() {
- clearServerData();
-
- DiscoveryOptions discoveryOptions =
- new DiscoveryOptions.Builder().setStrategy(STRATEGY).build();
-
- connectionsClient.startDiscovery("com.amos.server", endpointDiscoveryCallback,
- discoveryOptions)
- .addOnSuccessListener( (Void unused) -> {
- // started searching for servers successfully
- Log.i(TAG, "Discovering connections on " + clientName);
- mToast.setText(R.string.nearby_discovering_success);
- mToast.show();
- })
- .addOnFailureListener( (Exception e) -> {
- // unable to start discovery
- Log.e(TAG, "Unable to start discovery on " + clientName);
- mToast.setText(R.string.nearby_discovering_error);
- mToast.show();
- finish();
- });
- }
-
- /**
- * Clears serverName/serverID and all server data maps as well as the servers list
- */
- private void clearServerData() {
- clients.clear();
- clientIDsToNames.clear();
- clientNamesToIDs.clear();
- clientID = null;
- clientNameNow = null;
- }
-
- /**
- * Handle established connection with app.
- *
- * Clears servers data maps, stops discovery of new servers and adds close connection button
- */
- private void connectedToApp() {
- connectionsClient.stopDiscovery();
- clients.clear();
- clientNamesToIDs.clear();
- clientIDsToNames.clear();
-
- // Start setting up a connection with the other side
- Intent intent = new Intent(this, ConnectionSetupServerActivity.class);
- intent.putExtra("endpointId", clientID);
- startActivity(intent);
+ private void checkPermissions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (!hasPermissions(this)) {
+ requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS);
+ } else {
+ Log.d(TAG, "Permissions are ok.");
+ }
+ } else {
+ Log.w(TAG, "Could not check permissions due to version");
+ toast("Could not check permissions due to version");
+ }
}
/**
* Determines whether the FlyInn server app has the necessary permissions to run nearby.
* @param context Checks the permissions against this context/application environment
- * @param permissions The permissions to be checked
* @return True if the app was granted all the permissions, false otherwise
*/
- private static boolean hasPermissions(Context context, String[] permissions) {
- for (String permission : permissions) {
+ private static boolean hasPermissions(Context context) {
+ for (String permission : ConnectToClientActivity.REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
@@ -367,6 +117,11 @@ private static boolean hasPermissions(Context context, String[] permissions) {
return true;
}
+ private void toast(String message) {
+ Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
/**
* Handles user acceptance (or denial) of our permission request.
* @param requestCode The request code passed in requestPermissions()
@@ -387,31 +142,11 @@ public void onRequestPermissionsResult(
if (grantResult == PackageManager.PERMISSION_DENIED) {
Log.w(TAG, "Permissions necessary for " +
"Nearby Connection were not granted.");
- mToast.setText(R.string.nearby_missing_permissions);
- mToast.show();
+ toast("Permissions are missing for nearby");
finish();
}
}
recreate();
}
- /**
- * Generates a name for the server.
- * @return The server name, consisting of the build model + a random string
- */
- // TODO Define better name system?
- private String generateName(int appendixLength){
- String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- SecureRandom rnd = new SecureRandom();
-
- StringBuilder sb = new StringBuilder(appendixLength);
- for (int i = 0; i < appendixLength; i++) {
- sb.append(AB.charAt(rnd.nextInt(AB.length())));
- }
-
- String name = Build.MODEL + "_" + sb.toString();
- Log.i(TAG, "Current name is: " + name);
- return name;
- }
-
}
diff --git a/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java b/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java
index 4efadd7c..8b0ce421 100644
--- a/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java
+++ b/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java
@@ -1,51 +1,40 @@
package com.amos.server;
+import android.annotation.SuppressLint;
import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.os.Build;
+import android.graphics.Point;
import android.os.Bundle;
-import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.view.MotionEvent;
import android.view.View;
-import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
+import android.widget.Toast;
-import com.amos.server.eventsender.EventServer;
-import com.amos.server.signaling.WebServer;
-import com.amos.server.webrtc.PeerWrapper;
-import com.amos.server.webrtc.SetupStates;
-import com.amos.shared.TouchEvent;
-import com.google.android.gms.nearby.Nearby;
-import com.google.android.gms.nearby.connection.ConnectionsClient;
-import com.google.android.gms.nearby.connection.Payload;
+import com.amos.server.eventsender.EventWriter;
+import com.amos.server.nearby.ConnectCallback;
+import com.amos.server.nearby.ServerConnection;
import org.webrtc.SurfaceViewRenderer;
-import java.util.concurrent.BlockingQueue;
+import java.io.IOException;
public class ConnectionSetupServerActivity extends Activity {
private ProgressBar infiniteBar;
private TextView progressText;
+ /**
+ * Connection singleton managing nearby connection
+ */
+ private ServerConnection connection;
+ private EventWriter writer;
+
TextView connectionInfo;
- Button threadStarter;
- Thread senderRunner;
- EventServer eventSender;
- BlockingQueue msgQueue;
- Handler uiHandler;
SurfaceViewRenderer view;
- private WebServer webSocketServer;
- private PeerWrapper peerWrapper;
- private SurfaceViewRenderer remoteRender;
-
- private String endpointId;
-
private static final String TAG = "ConnectionSetup";
@Override
@@ -58,158 +47,78 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
connectionInfo = findViewById(R.id.connectionInfo);
connectionInfo.setVisibility(View.INVISIBLE);
+ connection = ServerConnection.getInstance();
+
+ // only correct actions will be processed
Intent intent = getIntent();
- if (intent.hasExtra("endpointId")) {
- try {
- endpointId = intent.getStringExtra("endpointId");
- ConnectionsClient connection = Nearby.getConnectionsClient(this);
- byte[] message = {0x61, 0x61, 0x61, 0x62};
- Payload payload = Payload.fromBytes(message);
- connection.sendPayload(endpointId, payload);
- Log.d(TAG, "Sent test payload to receiver " + endpointId);
- } catch (NullPointerException error) {
- Log.d(TAG, "Failed to get endpointId from intent");
- }
+ if ("connect".equals(intent.getAction())) {
+ buildConnection(intent.getStringExtra("name"));
} else {
- Log.d(TAG, "Intent did not specify endpoint");
+ toInitialActivity();
}
}
- /**
- * This method returns the SurfaceViewRender instance that
- * the client is using for the screen capture streaming
- *
- * @return SurfaceViewRenderer return instance of SurfaceView component
- */
- public SurfaceViewRenderer getRender() {
- return remoteRender;
- }
-
- private void initViews() {
- remoteRender = findViewById(R.id.surface_remote_viewer);
- }
-
-
- private void restartApp() {
- Intent i = getBaseContext().getPackageManager()
- .getLaunchIntentForPackage(getBaseContext().getPackageName());
- i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(i);
+ private void toInitialActivity() {
+ Intent intent = new Intent(this, ConnectToClientActivity.class);
+ startActivity(intent);
}
/**
- * This method should be use to update the TextView description state.
- * It will write a description about the current state of the WebRTC stream.
- * This method is going to handle all possible error states that the App could reach.
- *
- * Please see {@link com.amos.server.webrtc.SetupStates} for the states list.
- *
- * @param state the identification number to identify correct or error states.
+ * Create connection to the given destination server
*/
- public void setStateText(int state) {
-
- AlertDialog.Builder builder;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- builder = new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert);
- } else {
- builder = new AlertDialog.Builder(this);
- }
- builder.setIcon(android.R.drawable.ic_dialog_alert)
- .setCancelable(false)
- .setTitle("Setup error");
-
- builder.setNegativeButton("Restart app", new DialogInterface.OnClickListener() {
+ private void buildConnection(String name) {
+ setProgressText("Connecting to " + name);
+ connection.connectTo(name, new ConnectCallback() {
@Override
- public void onClick(DialogInterface dialogInterface, int i) {
- restartApp();
- }
- });
-
- switch (state) {
-
- case SetupStates.PERMISSIONS_FOR_SCREENCAST_DENIED:
- progressText.setVisibility(View.INVISIBLE);
+ public void success() {
+ Log.d(TAG, "Successfully connected to " + name);
+ toast(String.format("Successfully connected to %s", name));
infiniteBar.setVisibility(View.INVISIBLE);
- builder.setMessage("Error getting permissions for screen capture");
- builder.show();
- break;
-
- case SetupStates.ASKING_PERMISSIONS:
- progressText.setText("Asking permissions for screen capture");
- break;
-
- case SetupStates.SETUP_SCREEN_CONNECTION:
- progressText.setText("Setup screen share connection with server");
- break;
-
- case SetupStates.LOCAL_DESCRIPTOR_CREATE:
- progressText.setText("Local descriptor created Successfully");
- break;
-
- case SetupStates.REMOTE_DESCRIPTOR_CREATE:
- progressText.setText("Remote descriptor created Successfully");
- break;
-
- case SetupStates.LOCAL_DESCRIPTOR_SETTED:
- progressText.setText("Local descriptor setted Successfully");
- break;
-
- case SetupStates.REMOTE_DESCRIPTOR_SETTED:
- progressText.setText("Remote descriptor setted Successfully");
progressText.setVisibility(View.INVISIBLE);
- remoteRender.setVisibility(View.VISIBLE);
-
- break;
-
- case SetupStates.FAIL_CREATING_LOCAL_DESCRIPTOR:
-
- builder.setMessage("Creating local descriptor failed. Please restart the app");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
-
-
- case SetupStates.FAIL_CREATING_REMOTE_DESCRIPTOR:
-
- builder.setMessage("Creating remote descriptor failed. Please restart the app");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
-
-
- case SetupStates.FAIL_SETTED_LOCAL_DESCRIPTION:
-
- builder.setMessage("Setting local descriptor failed. Please restart the app");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
-
- case SetupStates.FAIL_SETTED_REMOTE_DESCRIPTION:
- builder.setMessage("Setting remote descriptor failed. Please restart the app");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
-
+ view.setVisibility(View.VISIBLE);
+ connectionInfo.setVisibility(View.VISIBLE);
+ transmitInputEvents();
+ }
- case SetupStates.FAIL_SENDING_SESSION_DESCRIPTOR:
- builder.setMessage("Sending remote descriptor failed. Please restart the app");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
+ @Override
+ public void failure() {
+ Log.d(TAG, "Failed to connect to " + name);
+ toast(String.format("Failed to connect to %s", name));
+ toInitialActivity();
+ }
+ });
+ }
- case SetupStates.ERROR_CONNECTING_SERVER:
- builder.setMessage("Error connecting server. Please restart the app and make sure you are connected");
- builder.show();
- progressText.setVisibility(View.INVISIBLE);
- infiniteBar.setVisibility(View.INVISIBLE);
- break;
+ /**
+ * Send touch events over established connection
+ */
+ @SuppressLint("ClickableViewAccessibility")
+ private void transmitInputEvents() {
+ Log.d(TAG, "Trying to transmit input events");
+ toast("Trying to transmit input events");
+ try {
+ writer = new EventWriter(connection.sendStream(), new Point(view.getWidth(), view.getHeight()));
+ } catch (IOException ignored) {
}
+ view.setOnTouchListener((View v, MotionEvent event) -> {
+ Log.d(TAG, event.toString());
+ if (writer != null) {
+ try {
+ writer.write(event);
+ } catch (IOException ignored) {
+ Log.d(TAG, "Failed to write touch event to EventWriter");
+ }
+ }
+ return true;
+ });
}
+ private void toast(String message) {
+ Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ private void setProgressText(String message) {
+ progressText.setText(message);
+ }
}
diff --git a/server/src/main/java/com/amos/server/EventGrabDemo.java b/server/src/main/java/com/amos/server/EventGrabDemo.java
deleted file mode 100644
index abc99d1b..00000000
--- a/server/src/main/java/com/amos/server/EventGrabDemo.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.amos.server;
-
-import android.os.Bundle;
-import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
-import android.widget.TextView;
-import android.view.View;
-import android.view.MotionEvent;
-import android.view.View.OnTouchListener;
-import android.view.View.OnKeyListener;
-import android.view.KeyEvent;
-import android.widget.ScrollView;
-
-import java.util.Locale;
-
-class MockOutput {
- TextView output;
-
- public MockOutput(TextView poutput) {
- output = poutput;
- }
-
- public void write(String msg) {
- output.append(msg + "\n");
- }
-}
-
-public class EventGrabDemo extends AppCompatActivity {
-
- View tracker;
- MockOutput output;
- ScrollView scroller;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_event_grab_demo);
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
- fab.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
- .setAction("Action", null).show();
- }
- });
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- scroller = findViewById(R.id.scroller);
-
- output = new MockOutput((TextView)findViewById(R.id.debug));
-
- tracker = findViewById(R.id.inputgrab);
- tracker.setOnTouchListener(new OnTouchListener(){
- @Override
- public boolean onTouch(View v, MotionEvent e) {
- String msg;
- int count = e.getPointerCount();
- int action = e.getActionMasked();
- int action_index = e.getActionIndex();
- int id;
- float x, y;
- for (int i = 0; i < count; i++) {
- id = e.getPointerId(i);
- x = e.getX(i);
- y = e.getY(i);
- msg = String.format(Locale.ENGLISH, "Count %d Index %d", count, i);
- output.write(msg);
- scroller.dispatchTouchEvent(
- MotionEvent.obtain(
- e.getDownTime(), e.getEventTime(),
- action,
- x + tracker.getHeight(), y,
- e.getMetaState()
- )
- );
- }
- return true;
- }
- });
- tracker.setOnKeyListener(new OnKeyListener(){
- @Override
- public boolean onKey(View v, int k, KeyEvent e) {
- String msg;
- int action = e.getAction();
- int keycode = e.getKeyCode();
- int meta = e.getMetaState();
- msg = String.format(Locale.ENGLISH, "Keycode: %d Action: %d Meta: %d", keycode, action, meta);
- output.write(msg);
- return true;
- }
- });
-
- }
-
-}
diff --git a/server/src/main/java/com/amos/server/EventSenderDemo.java b/server/src/main/java/com/amos/server/EventSenderDemo.java
deleted file mode 100644
index 6e01830a..00000000
--- a/server/src/main/java/com/amos/server/EventSenderDemo.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package com.amos.server;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.net.wifi.p2p.WifiP2pManager;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
-import android.view.View;
-import android.widget.Toast;
-
-import com.amos.server.eventsender.EventServer;
-import com.amos.shared.TouchEvent;
-
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-
-public class EventSenderDemo extends AppCompatActivity {
- View base;
- BlockingQueue mq;
- private final IntentFilter intentFilter = new IntentFilter();
- private static final int COARSE_LOCATION = 1001;
-
- protected void createP2P(Context context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
- Toast.makeText(this, "Requesting permission for peers", Toast.LENGTH_SHORT).show();
- requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, COARSE_LOCATION);
- }
- // Indicates a change in the Wi-Fi P2P status.
- intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
- // Indicates a change in the list of available peers.
- intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
- // Indicates the state of Wi-Fi P2P connectivity has changed.
- intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
- // Indicates this device's details have changed.
- intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
- WifiP2pManager mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
- WifiP2pManager.Channel mChannel = mManager.initialize(this, getMainLooper(), null);
- //Making the smartphone in discovery mode for other peers
- mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
- @Override
- public void onSuccess() {
- Toast.makeText(context, "Listening to Peers", Toast.LENGTH_SHORT).show();
- }
-
- @Override
- public void onFailure(int i) {
- Toast.makeText(context, "Error listening to Peers", Toast.LENGTH_SHORT).show();
- }
- });
-
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_event_sender_demo);
- base = findViewById(R.id.senderlayout);
- mq = new LinkedBlockingQueue<>();
- createP2P(this);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- new Thread(new EventServer(mq)).start();
- base.setOnTouchListener(
- (v, event) -> {
- event.setLocation(event.getX() / base.getWidth(), event.getY() / base.getHeight());
- TouchEvent te = new TouchEvent(event.getX(), event.getY(), event.getAction(), event.getDownTime());
- mq.add(te);
- return true;
- }
- );
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- }
-}
diff --git a/server/src/main/java/com/amos/server/MainActivity.java b/server/src/main/java/com/amos/server/MainActivity.java
deleted file mode 100644
index 2df3b0be..00000000
--- a/server/src/main/java/com/amos/server/MainActivity.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.amos.server;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.amos.server.eventsender.EventServer;
-import com.amos.server.signaling.Emitter;
-import com.amos.server.signaling.WebServer;
-import com.amos.server.webrtc.IPeer;
-import com.amos.server.webrtc.PeerWrapper;
-import com.amos.shared.TouchEvent;
-
-import org.webrtc.PeerConnection;
-import org.webrtc.SurfaceViewRenderer;
-
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-public class MainActivity extends Activity {
- TextView connectionInfo;
- Button threadStarter;
- Thread senderRunner;
- EventServer eventSender;
- BlockingQueue msgQueue;
- Handler uiHandler;
- SurfaceViewRenderer view;
-
- private PeerConnection localConnection;
- private WebServer webSocketServer;
- private PeerWrapper peerWrapper;
- private Button buttonInit;
- private SurfaceViewRenderer remoteRender;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- connectionInfo = findViewById(R.id.connectionInfo);
- connectionInfo.setVisibility(View.INVISIBLE);
- view = findViewById(R.id.surface_remote_viewer);
-
- // create touch listener components
- msgQueue = new LinkedBlockingQueue<>();
- uiHandler = new Handler(Looper.getMainLooper()) {
- @Override
- public void handleMessage(Message msg) {
- super.handleMessage(msg);
- connectionInfo.setText((String) msg.obj);
- }
- };
- eventSender = new EventServer(msgQueue, uiHandler);
- threadStarter = findViewById(R.id.threadStarter);
- threadStarter.setVisibility(View.INVISIBLE);
-
- //init WebRTC Signaling server
- this.initViews();
- this.peerWrapper = new PeerWrapper(this);
- this.webSocketServer = new WebServer((IPeer) this.peerWrapper);
- this.peerWrapper.setEmitter((Emitter) this.webSocketServer);
- this.webSocketServer.start();
-
- senderRunner = new Thread(eventSender);
- senderRunner.start();
- view.setOnTouchListener(
- (View v, MotionEvent e) -> {
- e.setLocation(e.getX() / view.getWidth(), e.getY() / view.getHeight());
- TouchEvent te = new TouchEvent(e.getX(), e.getY(), e.getAction(), e.getDownTime());
- msgQueue.add(te);
- return true;
- }
- );
- }
-
- public SurfaceViewRenderer getRender() {
- return remoteRender;
- }
-
- private void initViews() {
- remoteRender = findViewById(R.id.surface_remote_viewer);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main_menu, menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- Intent intent;
- switch (item.getItemId()) {
- case R.id.webrtc_server_activity:
- intent = new Intent(this, WebRTCServerActivity.class);
- break;
- case R.id.event_grab_activity:
- intent = new Intent(MainActivity.this, EventGrabDemo.class);
- break;
- case R.id.event_sender_activity:
- intent = new Intent(MainActivity.this, EventSenderDemo.class);
- break;
- case R.id.build_info_activity:
- intent = new Intent(this, BuildInfoActivity.class);
- break;
- default:
- return super.onOptionsItemSelected(item);
- }
-
- startActivity(intent);
- return super.onOptionsItemSelected(item);
- }
-
-}
diff --git a/server/src/main/java/com/amos/server/eventsender/EventServer.java b/server/src/main/java/com/amos/server/eventsender/EventServer.java
deleted file mode 100644
index af7e03cc..00000000
--- a/server/src/main/java/com/amos/server/eventsender/EventServer.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.amos.server.eventsender;
-
-import android.os.Handler;
-import android.os.Message;
-import android.os.SystemClock;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.amos.server.signaling.SocketServer;
-import com.amos.shared.TouchEvent;
-
-import java.io.IOException;
-import java.util.concurrent.BlockingQueue;
-
-/**
- * Pushes MotionEvents to EventWriter writing to a StreamSocket server.
- *
- * This is meant to be run in an independent thread in order to run networking
- * components in the main thread.
- *
- * Events will be processed from the BlockingQueue, which can be filled from another thread.
- */
-public class EventServer implements Runnable{
- SocketServer server;
- EventWriter writer;
- Handler uiHandler;
-
- BlockingQueue queue;
- Boolean accepting = true;
-
- /**
- * Create EventServer with a given input queue.
- * @param mq
- */
- public EventServer(BlockingQueue mq) {
- queue = mq;
- }
-
- /**
- * Create EventServer with given input queua as well as UI handler to pass
- * back messages to the UI thread.
- * @param mq
- * @param ui
- */
- public EventServer(BlockingQueue mq, Handler ui) {
- queue = mq;
- uiHandler = ui;
- }
-
- /**
- * Accept a connection and send back one single event.
- * This is meant for debugging purposes.
- */
- public void accept() {
- if (server == null) {
- Log.d("EventServer", "Server is null.");
- return;
- }
- Log.d("EventServer", "Waiting for connection");
- try {
- writer = new EventWriter(server.getStream());
- Log.d("EventServer", "Accepted connection");
- long time = SystemClock.uptimeMillis();
- MotionEvent e = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 1.0f, 1.0f, 0, 1.0f, 1.0f, 0, 0);
- writer.write(e);
- Log.d("EventServer", "Sent motion event");
- writer.close();
- } catch (IOException e) {
- }
- }
-
- /**
- * Close server.
- */
- public void close() {
- try {
- if (server != null) {
- server.close();
- server = null;
- }
- } catch (IOException e) {
- Log.d("EventServer", "Error closing writer or server.");
- }
- }
-
- /**
- * Accept connection and send messages from queue.
- *
- * Connection will be kept alive, even if no items are in the input queue.
- */
- public void acceptQueue() {
- if (server == null) {
- Log.d("EventServer", "Server is null.");
- return;
- }
- while (accepting) {
- try {
- Log.d("EventServer", "Waiting for connection");
- writer = new EventWriter(server.getStream());
- Log.d("EventServer", "Accepted connection");
- while (true) {
- try {
- TouchEvent e = queue.take();
- if (e != null) {
- writer.write(e);
- }
- } catch (IOException e) {
- Log.d("EventServer", "IO Except");
- break;
- } catch (InterruptedException e) {
- Log.d("EventServer", "Interrupted");
- break;
- }
- }
- writer.close();
- } catch (IOException e) {
- Log.d("EventServer", "Waiting for connections interrupted");
- break;
- }
- }
- }
-
- /**
- * Send a message to the thread on the other side of the handler.
- * @param msg
- */
- private void sendMessage(String msg) {
- if (uiHandler != null) {
- Message m = uiHandler.obtainMessage(0, msg);
- m.sendToTarget();
- }
- }
-
- /**
- * Run the SocketServer and wait for connections.
- */
- @Override
- public void run() {
- sendMessage("Starting server");
- try {
- server = new SocketServer();
- Log.d("EventServer", "Thread started");
- sendMessage("Accepting connections");
- acceptQueue();
- sendMessage("Closing server");
- server.close();
- sendMessage("Server closed. Restart application to initiate new connection.");
- } catch (IOException e) {
- }
- }
-}
-
diff --git a/server/src/main/java/com/amos/server/eventsender/EventWriter.java b/server/src/main/java/com/amos/server/eventsender/EventWriter.java
index 29e17fdd..1942931f 100644
--- a/server/src/main/java/com/amos/server/eventsender/EventWriter.java
+++ b/server/src/main/java/com/amos/server/eventsender/EventWriter.java
@@ -1,35 +1,38 @@
package com.amos.server.eventsender;
+import android.graphics.Point;
+import android.view.MotionEvent;
+
import com.amos.shared.TouchEvent;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.ObjectOutputStream;
-import android.view.MotionEvent;
-import android.util.Log;
+import java.io.OutputStream;
public class EventWriter {
- private OutputStream ostream;
+ private OutputStream outputStream;
private ObjectOutputStream output;
+ private Point screenSize;
- public EventWriter(OutputStream os) throws IOException {
- ostream = os;
- output = new ObjectOutputStream(ostream);
+ public EventWriter(OutputStream os, Point screen) throws IOException {
+ outputStream = os;
+ screenSize = screen;
+ output = new ObjectOutputStream(outputStream);
}
public void write(MotionEvent e) throws IOException {
- TouchEvent te = new TouchEvent(e.getX(), e.getY(), e.getAction(), e.getDownTime());
+ TouchEvent te = new TouchEvent(e, screenSize);
write(te);
}
- public void write(TouchEvent e) throws IOException {
+ void write(TouchEvent e) throws IOException {
output.writeObject(e);
output.flush();
}
public void close() throws IOException {
output.close();
- ostream.close();
+ outputStream.close();
}
}
diff --git a/server/src/main/java/com/amos/server/nearby/ConnectCallback.java b/server/src/main/java/com/amos/server/nearby/ConnectCallback.java
new file mode 100644
index 00000000..aaffec83
--- /dev/null
+++ b/server/src/main/java/com/amos/server/nearby/ConnectCallback.java
@@ -0,0 +1,6 @@
+package com.amos.server.nearby;
+
+public interface ConnectCallback {
+ void success();
+ void failure();
+}
diff --git a/server/src/main/java/com/amos/server/nearby/ServerConnection.java b/server/src/main/java/com/amos/server/nearby/ServerConnection.java
new file mode 100644
index 00000000..09d53dc8
--- /dev/null
+++ b/server/src/main/java/com/amos/server/nearby/ServerConnection.java
@@ -0,0 +1,329 @@
+package com.amos.server.nearby;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.google.android.gms.nearby.Nearby;
+import com.google.android.gms.nearby.connection.ConnectionInfo;
+import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback;
+import com.google.android.gms.nearby.connection.ConnectionResolution;
+import com.google.android.gms.nearby.connection.ConnectionsClient;
+import com.google.android.gms.nearby.connection.ConnectionsStatusCodes;
+import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo;
+import com.google.android.gms.nearby.connection.DiscoveryOptions;
+import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback;
+import com.google.android.gms.nearby.connection.Payload;
+import com.google.android.gms.nearby.connection.PayloadCallback;
+import com.google.android.gms.nearby.connection.PayloadTransferUpdate;
+import com.google.android.gms.nearby.connection.Strategy;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Singleton managing Android nearby connection on the server side
+ *
+ * The server needs to send input events to the client and correctly receive the recorded screen.
+ */
+public class ServerConnection {
+
+ private static final ServerConnection ourInstance = new ServerConnection();
+
+ /**
+ * 1-to-1 since a device will be connected to only one other device at most.
+ */
+ private static final Strategy STRATEGY = Strategy.P2P_POINT_TO_POINT;
+
+ private final String serverName = generateName();
+
+ private String clientID;
+
+ /**
+ * List of all discovered servers by name, continuously updated.
+ */
+ private List clients = new ArrayList<>();
+
+ /**
+ * Maps server names to their nearby connection IDs.
+ */
+ private HashMap clientNamesToIDs = new HashMap<>();
+
+ /**
+ * Maps server IDs to their nearby connection names.
+ */
+ private HashMap clientIDsToNames = new HashMap<>();
+
+ /**
+ * Tag for logging purposes.
+ */
+ private static final String TAG = "NearbyServer";
+
+ /**
+ * Connection manager for the connection to FlyInn clients.
+ */
+ private ConnectionsClient connectionsClient;
+
+ public static ServerConnection getInstance() {
+ return ourInstance;
+ }
+
+ /**
+ * Create a new server connection.
+ */
+ private ServerConnection() {
+ // All real work is in the init!
+ }
+
+ /**
+ * Bind application context to the singleton.
+ *
+ * @param ctx Application context should be passed to ensure survival between different activities.
+ */
+ public void init(Context ctx) {
+ connectionsClient = Nearby.getConnectionsClient(ctx);
+ }
+
+ /**
+ * Clears all servers map data as well as serverName/serverID and starts discovery
+ */
+ public void discover() {
+ if (connectionsClient == null) {
+ Log.e(TAG, "connectionsClient is null, cannot discover.");
+ return;
+ }
+ resetDiscovery();
+
+ DiscoveryOptions discoveryOptions =
+ new DiscoveryOptions.Builder().setStrategy(STRATEGY).build();
+
+ connectionsClient.startDiscovery("com.amos.server", endpointDiscoveryCallback,
+ discoveryOptions)
+ .addOnSuccessListener((Void unused) -> {
+ // started searching for servers successfully
+ Log.i(TAG, "Discovering connections on " + serverName);
+ })
+ .addOnFailureListener((Exception e) -> {
+ // unable to start discovery
+ Log.e(TAG, e.toString());
+ Log.e(TAG, "Unable to start discovery on " + serverName);
+ });
+ }
+
+ public List getClients() {
+ return clients;
+ }
+
+ public String getClientID() {
+ return clientID;
+ }
+
+ /**
+ * Connect to nearby service with client name ending with our required code.
+ *
+ * @param code Suffix of connection target
+ * @param callback Callbacks on connection success and failure
+ */
+ public void connectTo(String code, ConnectCallback callback) {
+ if (connectionsClient == null) {
+ Log.e(TAG, "connectionsClient is null, cannot discover.");
+ return;
+ }
+ String searchedClientName = null;
+ for (String clientName : clients) {
+ if (clientName.endsWith(code)) {
+ searchedClientName = clientName;
+ break;
+ }
+ }
+
+ String endpoint = clientNamesToIDs.get(searchedClientName);
+ if (endpoint != null && searchedClientName != null) {
+ clientID = endpoint;
+ // callback success will be called in the subsequent function
+ connectionsClient.requestConnection(searchedClientName, endpoint, buildConnectionLifecycleCallback(callback));
+ } else {
+ callback.failure();
+ }
+
+ }
+
+ public void abort() {
+ resetDiscovery();
+ resetClientData();
+ }
+
+ public PipedOutputStream sendStream() throws IOException {
+ if (connectionsClient == null) {
+ Log.e(TAG, "connectionsClient is null, cannot create stream.");
+ throw new IOException("connectionsClient is null, cannot create stream.");
+ }
+ PipedInputStream stream = new PipedInputStream();
+ PipedOutputStream data = new PipedOutputStream(stream);
+ Payload payload = Payload.fromStream(stream);
+ connectionsClient.sendPayload(clientID, payload);
+ Log.d(TAG, "Sent test payload to receiver " + clientID);
+ return data;
+ }
+
+ /**
+ * Obtain data from clientID/serverName and data transfer information via this handle.
+ */
+ private final PayloadCallback payloadCallback =
+ new PayloadCallback() {
+ @Override
+ public void onPayloadReceived(@NonNull String endpointId, @NonNull Payload payload) {
+ Log.d(TAG, "Payload received from " + endpointId);
+ Log.d(TAG, payload.toString());
+ }
+
+ @Override
+ public void onPayloadTransferUpdate(@NonNull String endpointId, @NonNull PayloadTransferUpdate update) {
+ Log.d(TAG, "Payload transfer update from " + endpointId);
+ Log.d(TAG, update.toString());
+ }
+ };
+
+ /**
+ * Handling of discovered endpoints (servers). Adds new endpoints to servers data maps/list,
+ * and removes lost endpoints.
+ */
+ private final EndpointDiscoveryCallback endpointDiscoveryCallback =
+ new EndpointDiscoveryCallback() {
+ @Override
+ public void onEndpointFound(@NonNull String endpointId, @NonNull DiscoveredEndpointInfo info) {
+ // discovered a server, add to data maps
+ String endpointName = info.getEndpointName();
+
+ if (!(clientIDsToNames.containsKey(endpointId)
+ || clientNamesToIDs.containsKey(endpointName))) {
+ clients.add(endpointName);
+ clientNamesToIDs.put(endpointName, endpointId);
+ clientIDsToNames.put(endpointId, endpointName);
+ Log.i(TAG, serverName + " discovered endpoint " + endpointId + " with name " + endpointName);
+
+ } else {
+ // this should not happen
+ while (true) {
+ if (!clients.remove(endpointName)) break;
+ }
+ clients.add(endpointName);
+ clientIDsToNames.put(endpointId, endpointName);
+ clientNamesToIDs.put(endpointName, endpointId);
+ Log.i(TAG, serverName + " rediscovered endpoint " + endpointId + " with name " + endpointName);
+ }
+ }
+
+ @Override
+ public void onEndpointLost(@NonNull String endpointId) {
+ // previously discovered server is no longer reachable, remove from data maps
+ String lostEndpointName = clientIDsToNames.get(endpointId);
+ clients.remove(lostEndpointName);
+ clientIDsToNames.remove(endpointId);
+ clientNamesToIDs.remove(lostEndpointName);
+ Log.i(TAG, serverName + " lost discovered endpoint " + endpointId);
+ }
+ };
+
+ private ConnectionLifecycleCallback buildConnectionLifecycleCallback(ConnectCallback callback) {
+ return new ConnectionLifecycleCallback() {
+ @Override
+ public void onConnectionInitiated(@NonNull String endpointId, @NonNull ConnectionInfo connectionInfo) {
+ Log.i(TAG, "Connection initiated to " + endpointId);
+
+ if (endpointId.equals(clientID)) {
+ connectionsClient.acceptConnection(endpointId, payloadCallback);
+ } else {
+ // initiated connection is not with server selected by user
+ connectionsClient.rejectConnection(endpointId);
+ Log.i(TAG, "Connection rejected to non-selected server "
+ + endpointId);
+ }
+ }
+
+ @Override
+ public void onConnectionResult(@NonNull String endpointId, @NonNull ConnectionResolution result) {
+ switch (result.getStatus().getStatusCode()) {
+ case ConnectionsStatusCodes.STATUS_OK:
+ // successful connection with server
+ Log.i(TAG, "Connected with " + endpointId);
+ resetDiscovery();
+ callback.success();
+ break;
+ case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED:
+ // connection was rejected by one side (or both)
+ Log.i(TAG, "Connection rejected with " + endpointId);
+ clientID = null;
+ callback.failure();
+ break;
+ case ConnectionsStatusCodes.STATUS_ERROR:
+ // connection was lost
+ Log.w(TAG, "Connection lost: " + endpointId);
+ clientID = null;
+ callback.failure();
+ break;
+ default:
+ // unknown status code. we shouldn't be here
+ Log.e(TAG, "Unknown error when attempting to connect with "
+ + endpointId);
+ clientID = null;
+ callback.failure();
+ break;
+ }
+ }
+
+ @Override
+ public void onDisconnected(@NonNull String endpointId) {
+ // disconnected from server
+ Log.i(TAG, "Disconnected from " + endpointId);
+ resetClientData();
+ }
+ };
+ }
+
+ /**
+ * Generates a name for the server.
+ *
+ * TODO: Create a better name for the server
+ *
+ * @return The server name, consisting of the build model + a random string
+ */
+ private String generateName() {
+ String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ SecureRandom rnd = new SecureRandom();
+
+ StringBuilder sb = new StringBuilder(5);
+ for (int i = 0; i < 5; i++) {
+ sb.append(AB.charAt(rnd.nextInt(AB.length())));
+ }
+
+ String name = Build.MODEL + "_" + sb.toString();
+ Log.i(TAG, "Current name is: " + name);
+ return name;
+ }
+
+ /**
+ * Clears serverName/serverID and all server data maps as well as the servers list
+ */
+ private void resetClientData() {
+ clientID = null;
+ }
+
+ /**
+ * Handle established connection with app.
+ *