diff --git a/.gitignore b/.gitignore index 51f7a526..79ff8c27 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ /captures .externalNativeBuild /fakeinput_dist -app/src/main/res/raw/flyinn_fakeinputlib.jar +/app/src/main/res/raw/fakeinputlib.jar BuildInfo.java app/lint-report.html -server/lint-report.html \ No newline at end of file +server/lint-report.html diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9379ab16..18219fb2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ android:required="true" /> + + + + + - + - - - - - - - - diff --git a/app/src/main/java/com/amos/flyinn/ClientConnAuthActivity.java b/app/src/main/java/com/amos/flyinn/ClientConnAuthActivity.java index 5d3c8288..04c742ab 100644 --- a/app/src/main/java/com/amos/flyinn/ClientConnAuthActivity.java +++ b/app/src/main/java/com/amos/flyinn/ClientConnAuthActivity.java @@ -299,8 +299,6 @@ public void onRequestPermissionsResult( /** * Returns the name of this client, which is set in onCreate(). - * - * @return The name of the client. */ protected abstract String generateName(); } diff --git a/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java b/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java index b2a03081..1c044ba9 100644 --- a/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java +++ b/app/src/main/java/com/amos/flyinn/ConnectionSetupActivity.java @@ -91,25 +91,6 @@ public void onClick(View view) { switchToHomeScreen(); } }); - - - String addr; - //Preparing and initializing the ADB service to listen for incoming connections. - try { - WifiConnectorSingleton wifiConnector = WifiConnectorSingleton.getInstance(); - WifiStateMachine stateMachine = wifiConnector.getWifiReceiverP2P(); - Log.d("IP", stateMachine.getHostAddr()); - 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(stateMachine.getHostAddr()); - } catch (Exception e) { - e.printStackTrace(); - } - - //Init components for WebRTC and ask permissions for Screen capture functionalities. - this.initViewsWebRTC(); - this.initScreenCapturePermissions(); } /** diff --git a/app/src/main/java/com/amos/flyinn/MainActivity.java b/app/src/main/java/com/amos/flyinn/MainActivity.java index 720f2797..7e2cf5b5 100644 --- a/app/src/main/java/com/amos/flyinn/MainActivity.java +++ b/app/src/main/java/com/amos/flyinn/MainActivity.java @@ -96,10 +96,6 @@ private void initViewsWebRTC() { public boolean onOptionsItemSelected(MenuItem item) { Intent intent; switch (item.getItemId()) { - case R.id.search_connections: - // intent = new Intent(this,WifiP2PActivity.class); - intent = new Intent(this, NearbyConnectionActivity.class); - break; case R.id.adb_activity: intent = new Intent(this, ADBActivity.class); break; diff --git a/app/src/main/java/com/amos/flyinn/NearbyConnectionActivity.java b/app/src/main/java/com/amos/flyinn/NearbyConnectionActivity.java deleted file mode 100644 index 2a4cf38f..00000000 --- a/app/src/main/java/com/amos/flyinn/NearbyConnectionActivity.java +++ /dev/null @@ -1,450 +0,0 @@ -package com.amos.flyinn; - -import android.Manifest; -import android.app.AlertDialog; -import android.app.ListActivity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.CallSuper; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.util.Log; -import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; -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; - -/** - * Activity that handles connection to servers via nearby connection. - * Updates a list (that is viewed by the user) containing all reachable servers. - * Includes permission handling and simple token authentication. - */ -public class NearbyConnectionActivity extends ListActivity { - - /** Permissions required for Nearby Connection */ - private static final String[] REQUIRED_PERMISSIONS = - new String[] { - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.CHANGE_WIFI_STATE, - Manifest.permission.ACCESS_COARSE_LOCATION - }; - - 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 serverID; - private String serverName; - - /** Toast to publish user notifications */ - private Toast mToast; - - /** List of all discovered servers by name, continuously updated. */ - private List servers = new ArrayList<>(); - - /** Maps server names to their nearby connection IDs. */ - private HashMap serverNamesToIDs = new HashMap<>(); - - /** Maps server IDs to their nearby connection names. */ - private HashMap serverIDsToNames = new HashMap<>(); - - /** Tag for logging purposes. */ - private static final String NEARBY_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) { - // TODO - } - - @Override - public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) { - // TODO - } - }; - - /** - * 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 (!(serverIDsToNames.containsKey(endpointId) - || serverNamesToIDs.containsKey(endpointName))) { - servers.add(endpointName); - serverNamesToIDs.put(endpointName, endpointId); - serverIDsToNames.put(endpointId, endpointName); - ((ArrayAdapter) NearbyConnectionActivity.this.getListAdapter()) - .notifyDataSetChanged(); - Log.i(NEARBY_TAG, clientName + " digscovered endpoint " + endpointId); - - } else { - // this should not happen - while (servers.remove(endpointName)) {} - servers.add(endpointName); - serverIDsToNames.put(endpointId, endpointName); - serverNamesToIDs.put(endpointName, endpointId); - ((ArrayAdapter) NearbyConnectionActivity.this.getListAdapter()) - .notifyDataSetChanged(); - Log.w(NEARBY_TAG, clientName + " rediscovered endpoint " + endpointId); - } - } - - @Override - public void onEndpointLost(String endpointId) { - // previously discovered server is no longer reachable, remove from data maps - String lostEndpointName = serverIDsToNames.get(endpointId); - serverIDsToNames.remove(endpointId); - serverNamesToIDs.remove(lostEndpointName); - while (servers.remove(lostEndpointName)) {} - ((ArrayAdapter) NearbyConnectionActivity.this.getListAdapter()) - .notifyDataSetChanged(); - Log.i(NEARBY_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(NEARBY_TAG, "Connection initiated to " + endpointId); - - if (endpointId.equals(serverID)) { - // authentication via tokens - // TODO replace token authentication with QR code/manual code input - new AlertDialog.Builder(NearbyConnectionActivity.this) - .setTitle("Accept connection to " + serverName + "?") - .setMessage("Confirm the code matches on both devices: " + - connectionInfo.getAuthenticationToken()) - .setPositiveButton(android.R.string.yes, - (DialogInterface dialog, int which) -> - // accept the connection - connectionsClient.acceptConnection(endpointId, - payloadCallback)) - .setNegativeButton(android.R.string.cancel, - (DialogInterface dialog, int which) -> - // reject the connection - connectionsClient.rejectConnection(endpointId)) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); - - } else { - // initiated connection is not with server selected by user - connectionsClient.rejectConnection(endpointId); - Log.i(NEARBY_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(NEARBY_TAG, "Connected with " + endpointId); - mToast.setText(R.string.nearby_connection_success); - mToast.show(); - connectedToServer(); - break; - - case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED: - // connection was rejected by one side (or both) - Log.i(NEARBY_TAG, "Connection rejected with " + endpointId); - mToast.setText(R.string.nearby_connection_rejected); - mToast.show(); - serverName = null; - serverID = null; - break; - - case ConnectionsStatusCodes.STATUS_ERROR: - // connection was lost - Log.w(NEARBY_TAG, "Connection lost: " + endpointId); - mToast.setText(R.string.nearby_connection_error); - mToast.show(); - serverName = null; - serverID = null; - break; - - default: - // unknown status code. we shouldn't be here - Log.e(NEARBY_TAG, "Unknown error when attempting to connect with " - + endpointId); - mToast.setText(R.string.nearby_connection_error); - mToast.show(); - serverName = null; - serverID = null; - } - } - - @Override - public void onDisconnected(String endpointId) { - // disconnected from server - Log.i(NEARBY_TAG, "Disconnected from " + endpointId); - mToast.setText(R.string.nearby_disconnected); - mToast.show(); - clearServerData(); - finish(); - } - }; - - - /** - * Initialises nearby's connectionsClient and our list adapter to showcase servers to the user, - * checks permissions and starts discovery - * @param savedInstanceState - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (!hasPermissions(this, REQUIRED_PERMISSIONS) && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS); - } else { - Log.w(NEARBY_TAG, "Could not check permissions due to version"); - } - - connectionsClient = Nearby.getConnectionsClient(this); - mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT); - - ArrayAdapter adapter = new ArrayAdapter<>( - this, android.R.layout.simple_list_item_1, servers); - setListAdapter(adapter); - - startDiscovering(); - } - - /** - * Checks needed permissions for nearby connection - */ - @Override - 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(NEARBY_TAG, "Could not check permissions due to version"); - } - } - - /** - * Stops all connection discovery and connections from this client - * before calling super.onDestroy() - */ - @Override - protected void onDestroy() { - connectionsClient.stopDiscovery(); - connectionsClient.stopAllEndpoints(); - clearServerData(); - super.onDestroy(); - } - - /** - * 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.flyinn", endpointDiscoveryCallback, - discoveryOptions) - .addOnSuccessListener( (Void unused) -> { - // started searching for servers successfully - Log.i(NEARBY_TAG, "Discovering connections on " + clientName); - mToast.setText(R.string.nearby_discovering_success); - mToast.show(); - }) - .addOnFailureListener( (Exception e) -> { - // unable to start discovery - Log.e(NEARBY_TAG, "Unable to start discovery on " + clientName); - mToast.setText(R.string.nearby_discovering_error); - mToast.show(); - finish(); - }); - } - - /** - * Handles selection of server from list by user and requesting connections to those servers - * when not connected to another device, and "close connection" button actions if a server - * connection is active. - * @param l The ListView where the click happened - * @param v The view that was clicked within the ListView - * @param position The position of the view in the list - * @param id The row id of the item that was clicked - */ - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - - // user chose to disconnect from server - if (servers.get(position).startsWith( - getResources().getString(R.string.nearby_close_connection))) { - - connectionsClient.stopAllEndpoints(); - Log.i(NEARBY_TAG, "User chose to disconnect from " + serverID); - mToast.setText(R.string.nearby_disconnected); - mToast.show(); - clearServerData(); - finish(); - return; - } - - // store user selection - serverName = servers.get(position); - serverID = serverNamesToIDs.get(serverName); - - //request connection to server selected by user - connectionsClient.requestConnection(clientName, serverID, connectionLifecycleCallback) - .addOnSuccessListener( (Void unused) -> { - // connection request successful - Log.i(NEARBY_TAG, clientName + " requested connection to " + serverID); - }) - .addOnFailureListener( (Exception e) -> { - // failed to request connection - serverName = null; - serverID = null; - Log.w(NEARBY_TAG, clientName + " failed requesting connection to " + - serverID); - mToast.setText(R.string.nearby_connection_error); - mToast.show(); - }); - } - - /** - * Clears serverName/serverID and all server data maps as well as the servers list - */ - private void clearServerData() { - servers.clear(); - ((ArrayAdapter) this.getListAdapter()).notifyDataSetChanged(); - serverIDsToNames.clear(); - serverNamesToIDs.clear(); - serverID = null; - serverName = null; - } - - /** - * Clears servers data maps, stops discovery of new servers and adds close connection button - */ - private void connectedToServer() { - connectionsClient.stopDiscovery(); - servers.clear(); - serverNamesToIDs.clear(); - serverIDsToNames.clear(); - - //add close connection button - servers.add(getResources().getString(R.string.nearby_close_connection) + " " + serverName); - ((ArrayAdapter) this.getListAdapter()).notifyDataSetChanged(); - } - - /** - * 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) { - if (ContextCompat.checkSelfPermission(context, permission) - != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - return true; - } - - /** - * 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 grantResults Results of granting permissions - */ - @CallSuper - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) { - return; - } - - for (int grantResult : grantResults) { - if (grantResult == PackageManager.PERMISSION_DENIED) { - Log.w(NEARBY_TAG, "Permissions necessary for " + - "Nearby Connection were not granted."); - mToast.setText(R.string.nearby_missing_permissions); - mToast.show(); - 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(NEARBY_TAG, "Current name is: " + name); - return name; - } -} diff --git a/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java b/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java index 437c4792..7adc952e 100644 --- a/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java +++ b/app/src/main/java/com/amos/flyinn/ShowCodeActivity.java @@ -1,40 +1,117 @@ package com.amos.flyinn; -import android.net.wifi.p2p.WifiP2pDevice; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; import android.widget.TextView; +import android.widget.Toast; -import com.amos.flyinn.wificonnector.WifiConnectorBase; +import com.amos.flyinn.nearbyservice.NearbyService; -import java.util.List; import java.util.concurrent.ThreadLocalRandom; - -public class ShowCodeActivity extends WifiConnectorBase { - private String nameNum = "1234"; +/** + * Initial activity showing code used for connection from remote display. + */ +public class ShowCodeActivity extends AppCompatActivity { + private String nameNum = ""; private TextView display; + private Toast mToast; + + private static final String TAG = "showCode"; + + private static final int REQUEST_CODE_REQUIRED_PERMISSIONS = 1; + /** + * Set state and information in android service. + */ + private void setService() { + Intent intent = NearbyService.createNearbyIntent(NearbyService.ACTION_START, this); + intent.putExtra("code", nameNum); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent); + } else { + startService(intent); + } + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_show_code); display = findViewById(R.id.textView2); + + 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"); + } + + nameNum = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 9998 + 1)); + display.setText(nameNum); + setService(); } @Override public void onResume() { super.onResume(); - nameNum = String.valueOf(ThreadLocalRandom.current().nextInt(1000, 9998 + 1)); - display.setText(nameNum); } + + @Override + protected void onDestroy() { + Intent intent = NearbyService.createNearbyIntent("", this); + stopService(intent); + super.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 grantResults Results of granting permissions + */ + @CallSuper @Override - public void setPeers(List listOfPeers) { - for (WifiP2pDevice device : listOfPeers) { - if (device.deviceName.endsWith("flyinn-" + nameNum)) { - this.connectToPeer(device); + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) { + return; + } + + for (int grantResult : grantResults) { + if (grantResult == PackageManager.PERMISSION_DENIED) { + Log.w("flyinn.ShowCode", "Permissions necessary for connections were not granted."); + mToast.setText(R.string.nearby_missing_permissions); + mToast.show(); + finish(); + } + } + recreate(); + } + + /** + * Determines whether the FlyInn server app has the necessary permissions to run nearby. + * + * @return True if the app was granted all the permissions, false otherwise + */ + public boolean hasPermissions(String[] permissions) { + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(this, permission) + != PackageManager.PERMISSION_GRANTED) { + return false; } } + return true; } } diff --git a/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyServer.java b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyServer.java new file mode 100644 index 00000000..8761c5b9 --- /dev/null +++ b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyServer.java @@ -0,0 +1,182 @@ +package com.amos.flyinn.nearbyservice; + +import android.Manifest; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.nearby.Nearby; +import com.google.android.gms.nearby.connection.AdvertisingOptions; +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.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; + +/** + * Handle creating a nearby service that will advertise itself and manage + * incoming connections. + */ +class NearbyServer { + public static final String TAG = NearbyServer.class.getPackage().getName(); + /** + * Required permissions for Nearby connections + */ + public static final String[] REQUIRED_PERMISSIONS = + new String[]{ + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.ACCESS_COARSE_LOCATION + }; + 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 serviceName = "nearby_server"; + private final String serviceID = "com.amos.server"; + private String clientID; + private String clientName; + + private NearbyService nearbyService; + + /** + * Create new nearby server with the given name. + * + * @param service + */ + public NearbyServer(@NonNull NearbyService service) throws SecurityException { + this.nearbyService = service; + this.connectionsClient = Nearby.getConnectionsClient(this.nearbyService); + } + + /** + * Server name as combination of name with suffix which is our nearby code + * @return + */ + public String getServerName() { + return serviceName + this.nearbyService.getNearbyCode(); + } + + /** + * 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) { + nearbyService.handlePayload(payload); + } + + @Override + public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) { + nearbyService.handlePayloadTransferUpdate(update); + } + }; + + /** + * Broadcast our presence using Nearby Connection so FlyInn users can find us. + * Resets clientID and clientName first. + */ + public void start() { + clearClientData(); + + AdvertisingOptions advertisingOptions = + new AdvertisingOptions.Builder().setStrategy(STRATEGY).build(); + + connectionsClient.startAdvertising(getServerName(), serviceID, + connectionLifecycleCallback, advertisingOptions) + .addOnSuccessListener((Void unused) -> { + Log.d(TAG, "Start advertising Android nearby"); + nearbyService.setServiceState(NearbyState.ADVERTISING, "Advertising android nearby"); + }) + .addOnFailureListener((Exception e) -> { + Log.d(TAG, "Error trying to advertise Android nearby"); + nearbyService.handleResponse(true, e.toString()); + nearbyService.setServiceState(NearbyState.STOPPED, "Failed to advertise android nearby"); + }); + } + + /** + * Stop all things nearby + */ + public void stop() { + connectionsClient.stopAllEndpoints(); + Log.d(TAG, "Stopped all endpoints"); + nearbyService.setServiceState(NearbyState.STOPPED, "Stopped all endpoints"); + } + + /** + * 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) { + clientName = connectionInfo.getEndpointName(); + connectionsClient.acceptConnection(endpointId, payloadCallback); + Log.i(TAG, "Auto accepting initiated connection from " + endpointId); + nearbyService.setServiceState(NearbyState.CONNECTING, "Connecting to " + endpointId); + } + + @Override + public void onConnectionResult(String endpointId, ConnectionResolution result) { + switch (result.getStatus().getStatusCode()) { + case ConnectionsStatusCodes.STATUS_OK: + // successful connection with client + Log.i(TAG, "Connected with " + endpointId); + connectionsClient.stopAdvertising(); + clientID = endpointId; + nearbyService.setServiceState(NearbyState.CONNECTED, "Connected to " + endpointId); + break; + + case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED: + // connection was rejected by one side (or both) + Log.i(TAG, "Connection rejected with " + endpointId); + nearbyService.setServiceState(NearbyState.ADVERTISING, "Rejected with " + endpointId); + clearClientData(); + break; + + case ConnectionsStatusCodes.STATUS_ERROR: + // connection was lost + Log.w(TAG, "Connection lost: " + endpointId); + nearbyService.setServiceState(NearbyState.ADVERTISING, "Connection lost to " + endpointId); + clearClientData(); + break; + + default: + // unknown status code. we shouldn't be here + Log.e(TAG, "Unknown error when attempting to connect with " + endpointId); + nearbyService.setServiceState(NearbyState.ADVERTISING, "Unknown error with " + endpointId); + clearClientData(); + } + } + + @Override + public void onDisconnected(String endpointId) { + // disconnected from client + Log.i(TAG, "Disconnected from " + endpointId); + nearbyService.setServiceState(NearbyState.ADVERTISING, "Disconnected from " + endpointId); + } + }; + + /** + * Resets client ID and client name to null. + */ + private void clearClientData() { + clientID = null; + clientName = null; + } +} diff --git a/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java new file mode 100644 index 00000000..53e041c9 --- /dev/null +++ b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyService.java @@ -0,0 +1,293 @@ +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; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +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.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; + +/** + * Manage nearby connections with a server. + *

+ * After starting this service. This will actively advertise + * the current device for nearby connections. A server can then + * 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"; + public static final String ACTION_STOP = "nearby_stop"; + + private NearbyServer server; + + private static final int FOREGROUND_ID = 1; + private static final int NOTIFY_ID = 2; + private static final String CHANNEL_ID = "flyinn_nearby"; + + private String nearbyCode = ""; + + /** + * Define the serviceState of our service. + */ + private NearbyState serviceState = NearbyState.STOPPED; + + public NearbyService() { + super("NearbyService"); + } + + public static String[] getRequiredPermissions() { + return NearbyServer.REQUIRED_PERMISSIONS; + } + + @Override + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + Log.d(TAG, "Creating channel"); + createChannel(); + return super.onStartCommand(intent, flags, 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) { + + NotificationChannel c=new NotificationChannel(CHANNEL_ID, + "flyinn_channel", NotificationManager.IMPORTANCE_HIGH); + + c.enableLights(true); + c.setLightColor(0xFFFF0000); + + mgr.createNotificationChannel(c); + } + } + + /** + * 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. + * @return + */ + private Notification buildForegroundNotification(String message, @Nullable Intent target) { + NotificationCompat.Builder b = + new NotificationCompat.Builder(this, CHANNEL_ID); + + b.setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) // makes notification pop up + .setContentTitle(String.format("Nearby service %s", nearbyCode)) + .setContentText(message) + .setSmallIcon(android.R.drawable.stat_notify_sync); + + if (target != null) { + TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); + stackBuilder.addNextIntentWithParentStack(target); + PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + b.setContentIntent(pendingIntent); + } + + return (b.build()); + } + + private void raiseNotification(Notification notification) { + NotificationManager mgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + startForeground(FOREGROUND_ID, buildForegroundNotification("Nearby Action running", null)); + mgr.notify(NOTIFY_ID, notification); + } + + /** + * Create or update shown notification. + * @param message + */ + public void notify(String message) { + Intent intent = null; + switch (serviceState) { + case STOPPED: + intent = new Intent(this, ShowCodeActivity.class); + intent.putExtra("code", getNearbyCode()); + break; + case ADVERTISING: + intent = new Intent(this, ShowCodeActivity.class); + intent.putExtra("code", getNearbyCode()); + break; + case CONNECTING: + intent = new Intent(this, ConnectionSetupActivity.class); + intent.putExtra("code", getNearbyCode()); + break; + case CONNECTED: + intent = new Intent(this, ConnectionSetupActivity.class); + intent.putExtra("code", getNearbyCode()); + break; + } + raiseNotification(buildForegroundNotification(message, intent)); + } + + /** + * Activate the given activity + * @param cls Class of the target activity + */ + private void switchActivity(Class cls) { + Intent intent = new Intent(this, cls); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + public NearbyState getServiceState() { + return serviceState; + } + + /** + * 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) { + case CONNECTING: + switchActivity(ConnectionSetupActivity.class); + break; + } + serviceState = state; + } + if (message != null) { + notify(message); + } + } + + /** + * Create an Intent to control NearbyService. + * + * @param state Defined serviceState for the application as enum. + * @param context + * @return Intent containing desired application serviceState. + */ + public static Intent createNearbyIntent(String state, Context context) { + Intent intent = new Intent(context, NearbyService.class); + intent.setAction(state); + return intent; + } + + public void handleResponse(boolean error, String message) { + if (error) { + Log.d(TAG, String.format("Error received: %s", message)); + } else { + Log.d(TAG, String.format("Status update: %s", message)); + } + } + + public void handlePayload(Payload payload) { + Log.d(TAG, "Received payload"); + } + + public void handlePayloadTransferUpdate(PayloadTransferUpdate update) { + Log.d(TAG, "Received transfer update"); + } + + /** + * Start advertising Android nearby. + */ + public void start() { + if (serviceState == NearbyState.STOPPED) { + Log.d(TAG, "Starting NearbyService"); + if (server == null) { + try { + server = new NearbyServer(this); + server.start(); + } catch (SecurityException error) { + notify("Insufficient permissions"); + } + } + } else { + Log.d(TAG, "NearbyService already started"); + } + } + + /** + * Stop advertising Android nearby. + */ + public void stop() { + if (serviceState != NearbyState.STOPPED) { + Log.d(TAG, "Stopping NearbyService"); + serviceState = NearbyState.STOPPED; + notify("Stopping nearby advertising"); + server.stop(); + } else { + Log.d(TAG, "NearbyService already stopped"); + } + } + + /** + * Handle intents for initiation of advertising and connection shutdown. + * + * @param intent An intent with a custom Extra field called action. + */ + @Override + protected void onHandleIntent(@Nullable Intent intent) { + Log.d(TAG, "Handling intent now"); + if (intent.hasExtra("code")) { + try { + String code = intent.getStringExtra("code"); + setNearbyCode(code); + Log.d(TAG, String.format("Setting code to %s", code)); + } catch (NullPointerException err) { + Log.d(TAG, "Could not get code from intent."); + } + } + + try { + switch (intent.getAction()) { + case ACTION_START: + start(); + break; + case ACTION_STOP: + stop(); + break; + default: + Log.d(TAG, "Unknown intent received. Will do nothing"); + break; + } + } catch (NullPointerException error) { + Log.d(TAG, "Null pointer for action received. Will do nothing"); + } + } + + public void setNearbyCode(String code) { + nearbyCode = code; + } + + public String getNearbyCode() { + return nearbyCode; + } +} diff --git a/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyState.java b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyState.java new file mode 100644 index 00000000..aabca6a1 --- /dev/null +++ b/app/src/main/java/com/amos/flyinn/nearbyservice/NearbyState.java @@ -0,0 +1,14 @@ +package com.amos.flyinn.nearbyservice; + +/** + * Define possible states of the Nearby service. These are set by the service itself or by the + * server. + */ +public enum NearbyState { + // Default state or state of errors + UNKNOWN, + STOPPED, + ADVERTISING, + CONNECTING, + CONNECTED, +} diff --git a/app/src/main/res/raw/fakeinputlib.jar b/app/src/main/res/raw/fakeinputlib.jar deleted file mode 100644 index 7d57effc..00000000 Binary files a/app/src/main/res/raw/fakeinputlib.jar and /dev/null differ diff --git a/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServerTest.java b/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServerTest.java new file mode 100644 index 00000000..2e09eee7 --- /dev/null +++ b/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServerTest.java @@ -0,0 +1,17 @@ +package com.amos.flyinn.nearbyservice; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * See inside NearbyService for tests including NearbyServer + */ +@RunWith(RobolectricTestRunner.class) +public class NearbyServerTest { + + @Test(expected = NullPointerException.class) + public void init_null() { + NearbyServer server = new NearbyServer(null); + } +} diff --git a/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServiceTest.java b/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServiceTest.java new file mode 100644 index 00000000..b8eaee34 --- /dev/null +++ b/app/src/test/java/com/amos/flyinn/nearbyservice/NearbyServiceTest.java @@ -0,0 +1,139 @@ +package com.amos.flyinn.nearbyservice; + +import android.content.Intent; + +import org.junit.After; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.android.controller.ServiceController; + +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; + +/** + * Wrap Nearby service in a way to test its functions individually + * Source: https://stackoverflow.com/a/33755494 + */ +@RunWith(RobolectricTestRunner.class) +public class NearbyServiceTest { + private NearbyService service; + private ServiceController controller; + + @Test + public void testBasicIntent() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + } + + /** + * Check that passing code with intent works correctly. + */ + @Test + public void testCodeIntent() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(""); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + assertEquals("1234", service.getNearbyCode()); + } + + /** + * Check that leaving action to be null will also work correctly. + */ + @Test + public void testNullActionIntent() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + assertEquals("1234", service.getNearbyCode()); + } + + @Test + public void testStartIntent() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(NearbyService.ACTION_START); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + } + + @Test + public void testStopIntent() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(NearbyService.ACTION_STOP); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + } + + @Test + public void testSetNearbyCode() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(""); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + assertEquals("1234", service.getNearbyCode()); + service.setNearbyCode("5678"); + assertEquals("5678", service.getNearbyCode()); + } + + @Test + public void testSetState() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(""); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + controller.startCommand(0, 0); + service.setServiceState(NearbyState.STOPPED, "Test"); + assertEquals(NearbyState.STOPPED, service.getServiceState()); + service.setServiceState(NearbyState.CONNECTED, "Test"); + assertEquals(NearbyState.CONNECTED, service.getServiceState()); + service.setServiceState(NearbyState.CONNECTING, "Test"); + assertEquals(NearbyState.CONNECTING, service.getServiceState()); + } + + @Test + public void testNotify() { + Intent intent = new Intent(ApplicationProvider.getApplicationContext(), TestService.class); + intent.putExtra("code", "1234"); + intent.setAction(""); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + service.notify("test"); + } + + @Test + public void testCreateNearbyIntent() { + Intent intent = NearbyService.createNearbyIntent(NearbyService.ACTION_START, ApplicationProvider.getApplicationContext()); + assertEquals(NearbyService.ACTION_START, intent.getAction()); + controller = Robolectric.buildService(TestService.class, intent); + service = controller.create().get(); + } + + @After + public void tearDown() { + controller.destroy(); + } + + public static class TestService extends NearbyService { + @Override + public void onStart(@Nullable Intent intent, int startId) { + onHandleIntent(intent); + stopSelf(startId); + } + } +} diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml index d41ced51..e1449139 100644 --- a/server/src/main/AndroidManifest.xml +++ b/server/src/main/AndroidManifest.xml @@ -43,19 +43,17 @@ android:name=".BuildInfoActivity" android:label="@string/title_activity_build_info" android:theme="@style/AppTheme.NoActionBar" /> - - + + + + + + - - - - diff --git a/server/src/main/java/com/amos/server/ConnectToClientActivity.java b/server/src/main/java/com/amos/server/ConnectToClientActivity.java index f19a3aba..cf2230d2 100644 --- a/server/src/main/java/com/amos/server/ConnectToClientActivity.java +++ b/server/src/main/java/com/amos/server/ConnectToClientActivity.java @@ -1,14 +1,240 @@ package com.amos.server; +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.util.Log; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; -import com.amos.server.wifibroadcaster.WifiHijackBase; +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; + +public class ConnectToClientActivity extends Activity { + + private static final String[] REQUIRED_PERMISSIONS = + new String[] { + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.ACCESS_COARSE_LOCATION + }; + + 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<>(); + + /** 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); + + + + } -public class ConnectToClientActivity extends WifiHijackBase { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -19,7 +245,7 @@ protected void onCreate(Bundle savedInstanceState) { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { String name = v.getText().toString(); // Get the String - onNameEnterFinished(name); + requestConnectionWithClient(name); return true; } return false; @@ -27,14 +253,165 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { }); + 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(); + + + } + + /** + * Checks needed permissions for nearby connection + */ + @Override + 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"); + } + } + + /** + * Stops all connection discovery and connections from this client + * before calling super.onDestroy() + */ + @Override + protected void onDestroy() { + connectionsClient.stopDiscovery(); + connectionsClient.stopAllEndpoints(); + clearServerData(); + super.onDestroy(); + } + + /** + * 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); + } + + /** + * 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) { + if (ContextCompat.checkSelfPermission(context, permission) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + return true; } + /** + * 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 grantResults Results of granting permissions + */ + @CallSuper + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) { + return; + } - private void onNameEnterFinished(String name) { - this.changeName(name); + for (int grantResult : grantResults) { + 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(); + 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 8668dbe3..4efadd7c 100644 --- a/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java +++ b/server/src/main/java/com/amos/server/ConnectionSetupServerActivity.java @@ -7,10 +7,8 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; -import android.os.Message; import android.support.annotation.Nullable; -import android.view.MotionEvent; +import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; @@ -21,11 +19,13 @@ 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 org.webrtc.SurfaceViewRenderer; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; public class ConnectionSetupServerActivity extends Activity { @@ -44,6 +44,10 @@ public class ConnectionSetupServerActivity extends Activity { private PeerWrapper peerWrapper; private SurfaceViewRenderer remoteRender; + private String endpointId; + + private static final String TAG = "ConnectionSetup"; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -54,39 +58,21 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { connectionInfo = findViewById(R.id.connectionInfo); connectionInfo.setVisibility(View.INVISIBLE); - //setStateText(SetupStates.LOCAL_DESCRIPTOR_CREATE); - // 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); + 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"); } - }; - eventSender = new EventServer(msgQueue, uiHandler); - - - //init WebRTC Signaling server - this.initViews(); - this.peerWrapper = new PeerWrapper(this); - this.webSocketServer = new WebServer(this.peerWrapper); - this.peerWrapper.setEmitter(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; - } - ); - - + } else { + Log.d(TAG, "Intent did not specify endpoint"); + } } /** @@ -104,7 +90,7 @@ private void initViews() { } - private void restarAPP() { + private void restartApp() { Intent i = getBaseContext().getPackageManager() .getLaunchIntentForPackage(getBaseContext().getPackageName()); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -135,7 +121,7 @@ public void setStateText(int state) { builder.setNegativeButton("Restart app", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { - restarAPP(); + restartApp(); } }); diff --git a/server/src/main/java/com/amos/server/NearbyServerActivity.java b/server/src/main/java/com/amos/server/NearbyServerActivity.java deleted file mode 100644 index 9e840175..00000000 --- a/server/src/main/java/com/amos/server/NearbyServerActivity.java +++ /dev/null @@ -1,318 +0,0 @@ -package com.amos.server; - -import android.Manifest; -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.app.Activity; -import android.os.Handler; -import android.support.annotation.CallSuper; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.util.Log; -import android.widget.Toast; - -import com.google.android.gms.nearby.Nearby; -import com.google.android.gms.nearby.connection.AdvertisingOptions; -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.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; - -/** - * Activity that handles connection to clients via nearby connection. - * Includes permission handling and simple token authentication. - */ -public class NearbyServerActivity extends Activity { - - /** Permissions required for Nearby Connection */ - private static final String[] REQUIRED_PERMISSIONS = - new String[] { - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.CHANGE_WIFI_STATE, - }; - - 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 serverName = generateName(5); - private String clientID; - private String clientName; - - final Handler handler = new Handler(); - - /** Toast to publish user notifications */ - private Toast mToast; - - /** Tag for logging purposes. */ - private static final String NEARBY_TAG = "ServerNearbyConnection"; - - - /** - * 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) { - // TODO - } - - @Override - public void onPayloadTransferUpdate(String endpointId, PayloadTransferUpdate update) { - // TODO - } - }; - - /** - * 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(NEARBY_TAG, "Connection initiated by " + endpointId); - clientName = connectionInfo.getEndpointName(); - - // authentication via tokens - // TODO replace token authentication with QR code/manual code input - new AlertDialog.Builder(NearbyServerActivity.this) - .setTitle("Accept connection to " + clientName + "?") - .setMessage("Confirm the code matches on both devices: " + - connectionInfo.getAuthenticationToken()) - .setPositiveButton(android.R.string.yes, - (DialogInterface dialog, int which) -> - // accept the connection - connectionsClient.acceptConnection(endpointId, - payloadCallback)) - .setNegativeButton(android.R.string.cancel, - (DialogInterface dialog, int which) -> - // reject the connection - connectionsClient.rejectConnection(endpointId)) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); - } - - @Override - public void onConnectionResult(String endpointId, ConnectionResolution result) { - switch (result.getStatus().getStatusCode()) { - - case ConnectionsStatusCodes.STATUS_OK: - // successful connection with client - Log.i(NEARBY_TAG, "Connected with " + endpointId); - mToast.setText(R.string.nearby_connection_success); - mToast.show(); - connectionsClient.stopAdvertising(); - clientID = endpointId; - break; - - case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED: - // connection was rejected by one side (or both) - Log.i(NEARBY_TAG, "Connection rejected with " + endpointId); - mToast.setText(R.string.nearby_connection_rejected); - mToast.show(); - clearClientData(); - break; - - case ConnectionsStatusCodes.STATUS_ERROR: - // connection was lost - Log.w(NEARBY_TAG, "Connection lost: " + endpointId); - mToast.setText(R.string.nearby_connection_error); - mToast.show(); - clearClientData(); - break; - - default: - // unknown status code. we shouldn't be here - Log.e(NEARBY_TAG, "Unknown error when attempting to connect with " - + endpointId); - mToast.setText(R.string.nearby_connection_error); - mToast.show(); - clearClientData(); - } - } - - @Override - public void onDisconnected(String endpointId) { - // disconnected from client - Log.i(NEARBY_TAG, "Disconnected from " + endpointId); - mToast.setText(R.string.nearby_disconnected); - mToast.show(); - - // better be safe - clearClientData(); - connectionsClient.stopAdvertising(); - connectionsClient.stopAllEndpoints(); - - // display toast for 2s, then start advertising again - handler.postDelayed(() -> startAdvertising(), 2000); - } - }; - - /** - * Starts a nearby connectionsClient, checks permissions and calls startAdvertising(). - * @param savedInstanceState - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_nearby_server); - - if (!hasPermissions(this, REQUIRED_PERMISSIONS) && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(REQUIRED_PERMISSIONS, REQUEST_CODE_REQUIRED_PERMISSIONS); - } else { - Log.w(NEARBY_TAG, "Could not check permissions due to version"); - } - - connectionsClient = Nearby.getConnectionsClient(this); - mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT); - - startAdvertising(); - } - - /** - * Checks whether the app has the required permissions to establish connections after the - * super.onStart() call (the user may have changed permissions after starting the app). - */ - @Override - 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(NEARBY_TAG, "Could not check permissions due to version"); - } - } - - /** - * Clears client data and stops all advertising and connections from this server - * before calling super.onDestroy(). - */ - @Override - protected void onDestroy() { - connectionsClient.stopAdvertising(); - connectionsClient.stopAllEndpoints(); - clearClientData(); - super.onDestroy(); - } - - - /** - * Broadcast our presence using Nearby Connection so FlyInn users can find us. - * Resets clientID and clientName first. - */ - private void startAdvertising() { - clearClientData(); - - AdvertisingOptions advertisingOptions = - new AdvertisingOptions.Builder().setStrategy(STRATEGY).build(); - - connectionsClient.startAdvertising(serverName, "com.amos.flyinn", - connectionLifecycleCallback, advertisingOptions) - .addOnSuccessListener( (Void unused) -> { - // started advertising successfully - Log.i(NEARBY_TAG, "Started advertising " + serverName); - mToast.setText(R.string.nearby_advertising_success); - mToast.show(); - }) - .addOnFailureListener( (Exception e) -> { - // unable to advertise - Log.e(NEARBY_TAG, "Unable to start advertising " + serverName); - mToast.setText(R.string.nearby_advertising_error); - mToast.show(); - finish(); - }); - } - - /** - * Resets client ID and client name to null. - */ - private void clearClientData() { - clientID = null; - clientName = null; - } - - /** - * 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) { - if (ContextCompat.checkSelfPermission(context, permission) - != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - return true; - } - - /** - * 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 grantResults Results of granting permissions - */ - @CallSuper - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode != REQUEST_CODE_REQUIRED_PERMISSIONS) { - return; - } - - for (int grantResult : grantResults) { - if (grantResult == PackageManager.PERMISSION_DENIED) { - Log.w(NEARBY_TAG, "Permissions necessary for connections were not granted."); - mToast.setText(R.string.nearby_missing_permissions); - mToast.show(); - 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(NEARBY_TAG, "Current name is: " + name); - return name; - } -} diff --git a/server/src/main/res/layout/activity_nearby_server.xml b/server/src/main/res/layout/activity_nearby_server.xml deleted file mode 100644 index b4b908fa..00000000 --- a/server/src/main/res/layout/activity_nearby_server.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file