package com.door43.translationstudio.device2device;
import android.app.DialogFragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.view.View;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;
import com.door43.tools.reporting.Logger;
import com.door43.translationstudio.R;
import com.door43.translationstudio.network.Peer;
import com.door43.translationstudio.newui.BaseActivity;
import com.door43.translationstudio.service.BroadcastListenerService;
import com.door43.translationstudio.service.BroadcastService;
import com.door43.translationstudio.service.PeerStatusKeys;
import com.door43.translationstudio.service.Request;
import com.door43.translationstudio.service.ServerService;
import com.door43.translationstudio.service.ClientService;
import com.door43.util.RSAEncryption;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class DeviceToDeviceActivity extends BaseActivity implements ServerService.OnServerEventListener, ClientService.OnClientEventListener, BroadcastListenerService.Callbacks {
private static final int REFRESH_FREQUENCY = 5000;
private static final int SERVER_TTL = 5000; // time before a slient server is considered lost
private boolean mStartAsServer = false;
private PeerAdapter mAdapter;
private File mPublicKeyFile;
private File mPrivateKeyFile;
private static Map<String, DialogFragment> mPeerDialogs = new HashMap<>();
private static final String SERVICE_NAME = "tS";
private static final int PORT_CLIENT_UDP = 9939;
private ServerService mExportService;
private ClientService mImportService;
private BroadcastService mBroadcastService;
private BroadcastListenerService mBroadcastListenerService;
private ServiceConnection mExportConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ServerService.LocalBinder binder = (ServerService.LocalBinder) service;
mExportService = binder.getServiceInstance();
mExportService.setOnServerEventListener(DeviceToDeviceActivity.this);
Logger.i(DeviceToDeviceActivity.class.getName(), "Connected to export service");
}
@Override
public void onServiceDisconnected(ComponentName name) {
mExportService.setOnServerEventListener(null);
Logger.i(DeviceToDeviceActivity.class.getName(), "Disconnected from export service");
// TODO: notify activity that service was dropped.
}
};
private ServiceConnection mBroadcastConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
BroadcastService.LocalBinder binder = (BroadcastService.LocalBinder) service;
mBroadcastService = binder.getServiceInstance();
Logger.i(DeviceToDeviceActivity.class.getName(), "Connected to broadcast service");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.i(DeviceToDeviceActivity.class.getName(), "Disconnected from broadcast service");
// TODO: notify activity that service was dropped.
}
};
private ServiceConnection mImportConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ClientService.LocalBinder binder = (ClientService.LocalBinder) service;
mImportService = binder.getServiceInstance();
mImportService.setOnClientEventListener(DeviceToDeviceActivity.this);
Logger.i(DeviceToDeviceActivity.class.getName(), "Connected to import service");
}
@Override
public void onServiceDisconnected(ComponentName name) {
mImportService.setOnClientEventListener(null);
Logger.i(DeviceToDeviceActivity.class.getName(), "Disconnected from import service");
// TODO: notify activity that service was dropped.
}
};
private ServiceConnection mBroadcastListenerConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
BroadcastListenerService.LocalBinder binder = (BroadcastListenerService.LocalBinder) service;
mBroadcastListenerService = binder.getServiceInstance();
mBroadcastListenerService.registerCallback(DeviceToDeviceActivity.this);
Logger.i(DeviceToDeviceActivity.class.getName(), "Connected to broadcast listener service");
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mBroadcastListenerService.getPeers());
}
});
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBroadcastListenerService.registerCallback(null);
Logger.i(DeviceToDeviceActivity.class.getName(), "Disconnected from broadcast listener service");
// TODO: notify activity that service was dropped.
}
};
private Intent exportServiceIntent;
private Intent broadcastServiceIntent;
private Intent importServiceIntent;
private Intent broadcastListenerServiceIntent;
// private ProjectTranslationImportApprovalDialog mImportDialog = null;
private LinearLayout mLoadingLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_device_to_device);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mPublicKeyFile = new File(getFilesDir(), getResources().getString(R.string.p2p_keys_dir) + "/id_rsa.pub");
mPrivateKeyFile = new File(getFilesDir(), getResources().getString(R.string.p2p_keys_dir) + "/id_rsa");
mPublicKeyFile.getParentFile().mkdirs();
mStartAsServer = getIntent().getBooleanExtra("startAsServer", false);
// if(mProgressDialog == null) mProgressDialog = new ProgressDialog(this);
// set up the ui
final Handler handler = new Handler(getMainLooper());
mLoadingLayout = (LinearLayout)findViewById(R.id.loadingLayout);
ListView peerListView = (ListView)findViewById(R.id.peerListView);
mAdapter = new PeerAdapter(this);
peerListView.setAdapter(mAdapter);
peerListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if(mStartAsServer) {
Peer client = mAdapter.getItem(i);
if(!client.isSecure()) {
mExportService.acceptConnection(client);
handler.post(new Runnable() {
@Override
public void run() {
updatePeerList(mExportService.getPeers());
}
});
} else {
// TODO: maybe display a popup to disconnect the client.
}
} else {
Peer server = mAdapter.getItem(i);
if(!server.isSecure()) {
// TRICKY: we don't let the client connect again otherwise it may get an encryption exception due to miss-matched keys
if(!server.keyStore.getBool(PeerStatusKeys.WAITING)) {
// connect to the server, implicitly requesting permission to access it
server.keyStore.add(PeerStatusKeys.WAITING, true);
updatePeerList(mBroadcastListenerService.getPeers());
mImportService.connectToServer(server);
}
} else {
// request a list of projects from the server.
showProgress(getResources().getString(R.string.loading));
// Include the suggested language(s) in which the results should be returned (if possible)
// This just makes it easier for users to read the results
ArrayList<String> preferredLanguages = new ArrayList<>();
// device
preferredLanguages.add(Locale.getDefault().getLanguage());
// current project
// Project p = null;//AppContext.projectManager().getSelectedProject();
// if(p != null) {
// preferredLanguages.add(p.getSelectedSourceLanguage().getId());
// }
// default
preferredLanguages.add("en");
mImportService.requestProjectList(server, preferredLanguages);
}
}
}
});
}
public void onStart() {
super.onStart();
// start and/or connect to services
if(mStartAsServer) {
setTitle(R.string.export_to_device);
exportServiceIntent = new Intent(this, ServerService.class);
broadcastServiceIntent = new Intent(this, BroadcastService.class);
// begin export service
if(!ServerService.isRunning()) {
try {
generateSessionKeys();
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to generate the session keys", e);
finish();
}
try {
exportServiceIntent.putExtra(ServerService.PARAM_PRIVATE_KEY, RSAEncryption.readPrivateKeyFromFile(mPrivateKeyFile));
exportServiceIntent.putExtra(ServerService.PARAM_PUBLIC_KEY, RSAEncryption.getPublicKeyAsString(RSAEncryption.readPublicKeyFromFile(mPublicKeyFile)));
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to retreive the encryption keys", e);
finish();
}
Logger.i(this.getClass().getName(), "Starting export service");
startService(exportServiceIntent);
}
bindService(exportServiceIntent, mExportConnection, Context.BIND_AUTO_CREATE);
} else {
setTitle(R.string.import_from_friend);
importServiceIntent = new Intent(this, ClientService.class);
broadcastListenerServiceIntent = new Intent(this, BroadcastListenerService.class);
// begin import service
if(!ClientService.isRunning()) {
try {
generateSessionKeys();
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to generate the session keys", e);
finish();
}
try {
importServiceIntent.putExtra(ServerService.PARAM_PRIVATE_KEY, RSAEncryption.readPrivateKeyFromFile(mPrivateKeyFile));
importServiceIntent.putExtra(ServerService.PARAM_PUBLIC_KEY, RSAEncryption.getPublicKeyAsString(RSAEncryption.readPublicKeyFromFile(mPublicKeyFile)));
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to retreive the encryption keys", e);
finish();
}
Logger.i(this.getClass().getName(), "Starting import service");
startService(importServiceIntent);
}
bindService(importServiceIntent, mImportConnection, Context.BIND_AUTO_CREATE);
}
}
@Override
public void onStop() {
// disconnect from services
if(mExportService != null) {
mExportService.setOnServerEventListener(null);
try {
unbindService(mExportConnection);
Logger.i(this.getClass().getName(), "Disconnected from export service");
} catch (Exception e) {
Logger.w(this.getClass().getName(), "Failed to unbind connection to export service", e);
}
}
if(mImportService != null) {
mImportService.setOnClientEventListener(null);
try {
unbindService(mImportConnection);
Logger.i(this.getClass().getName(), "Disconnected from import service");
} catch (Exception e) {
Logger.w(this.getClass().getName(), "Failed to unbind connection to import service", e);
}
}
if(mBroadcastListenerService != null) {
mBroadcastListenerService.registerCallback(null);
try {
unbindService(mBroadcastListenerConnection);
Logger.i(this.getClass().getName(), "Disconnected from broadcast listener service");
} catch (Exception e) {
Logger.w(this.getClass().getName(), "Failed to unbind connection to listener service", e);
}
}
super.onStop();
}
@Override
public void onDestroy() {
if(isFinishing()) {
// stop services
if (BroadcastService.isRunning() && broadcastServiceIntent != null) {
if(!stopService(broadcastServiceIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + BroadcastService.class.getName());
}
}
if (ServerService.isRunning() && exportServiceIntent != null) {
if(!stopService(exportServiceIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + ServerService.class.getName());
}
}
if (BroadcastListenerService.isRunning() && broadcastListenerServiceIntent != null) {
if(!stopService(broadcastListenerServiceIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + BroadcastListenerService.class.getName());
}
}
if (ClientService.isRunning() && importServiceIntent != null) {
if(!stopService(importServiceIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + ClientService.class.getName());
}
}
// mProgressDialog = null;
mPeerDialogs.clear();
}
super.onDestroy();
}
/**
* Generates new encryption keys to be used durring this session
*/
public void generateSessionKeys() throws Exception {
// TODO: this is not throwing exceptiosn like it should. When the directory of the keys does not exist it doesn't throw an exception
if(!mPrivateKeyFile.exists() || !mPublicKeyFile.exists()) {
RSAEncryption.generateKeys(mPrivateKeyFile, mPublicKeyFile);
}
}
/**
* Updates the peer list on the screen.
* This should always be ran on the main thread or a handler
*/
public void updatePeerList(ArrayList<Peer> peers) {
if(mLoadingLayout != null) {
if(peers.size() == 0) {
mLoadingLayout.setVisibility(View.VISIBLE);
} else {
mLoadingLayout.setVisibility(View.GONE);
}
}
// update the adapter
if(mAdapter != null) {
mAdapter.setPeers(peers);
}
}
/**
* Displays a dialog to choose the project to import
* @param models an array of projects and sudo projects to choose from
*/
// private void showProjectSelectionDialog(Peer server, Model[] models) {
// AppContext.context().closeToastMessage();
// if(!isFinishing()) {
// FragmentTransaction ft = getFragmentManager().beginTransaction();
// Fragment prev = getFragmentManager().findFragmentByTag("dialog");
// if (prev != null) {
// ft.remove(prev);
// }
// ft.addToBackStack(null);
// ChooseProjectToImportDialog newFragment = new ChooseProjectToImportDialog();
// mPeerDialogs.put(server.getIpAddress(), newFragment);
// newFragment.setImportDetails(server, models);
// newFragment.show(ft, "dialog");
// }
// }
// /**
// * Triggered when the client chooses a project from the server's project list
// * @param event the event fired
// */
// @Subscribe
// public void onChoseProjectToImport(ChoseProjectToImportEvent event) {
// // TODO: if we do not have this project yet we need to fetch the project image if it exists.
// event.getDialog().dismiss();
// showProjectLanguageSelectionDialog(event.getPeer(), event.getProject());
// }
// /**
// * Triggered when the client chooses the translations they wish to import with the project.
// * @param event the event fired
// */
// @Subscribe
// public void onChoseProjectTranslationsToImport(ChoseProjectLanguagesToImportEvent event) {
// Handler handle = new Handler(getMainLooper());
//
// showProgress(getResources().getString(R.string.loading));
//
// // send the request to the server
// Peer server = event.getPeer();
//
// JSONObject json = new JSONObject();
// try {
// json.put("id", event.getProject().getId());
// // check if we have the source for this project
// Project existingProject = null;//AppContext.projectManager().getProject(event.getProject().getId());
// if(existingProject == null || existingProject.getSelectedSourceLanguage() == null) {
// JSONArray sourceLanguagesJson = new JSONArray();
// sourceLanguagesJson.put(event.getProject().getSelectedSourceLanguage().getId());
// json.put("source_languages", sourceLanguagesJson);
// }
// JSONArray languagesJson = new JSONArray();
// for(Language l:event.getLanguages()) {
// languagesJson.put(l.getId());
// }
// json.put("target_languages", languagesJson);
// mImportService.requestProjectArchive(server, json);
// } catch (final JSONException e) {
// handle.post(new Runnable() {
// @Override
// public void run() {
// AppContext.context().showException(e);
// }
// });
// }
// }
/**
* Displays a dialog to choose the languages that will be imported with the project.
* @param p
*/
// private void showProjectLanguageSelectionDialog(Peer peer, Project p) {
// FragmentTransaction ft = getFragmentManager().beginTransaction();
// AppContext.context().closeToastMessage();
// // Create and show the dialog.
// ChooseProjectLanguagesToImportDialog newFragment = new ChooseProjectLanguagesToImportDialog();
// mPeerDialogs.put(peer.getIpAddress(), newFragment);
// newFragment.setImportDetails(peer, p);
// newFragment.show(ft, "dialog");
// }
/**
* shows or updates the progress dialog
* @param message the message to display in the progress dialog.
*/
private void showProgress(final String message) {
// Handler handle = new Handler(getMainLooper());
// handle.post(new Runnable() {
// @Override
// public void run() {
// mProgressDialog.setMessage(message);
// if (!mProgressDialog.isShowing()) {
// mProgressDialog.show();
// }
// }
// });
}
/**
* closes the progress dialog
*/
private void hideProgress() {
// Handler handle = new Handler(getMainLooper());
// handle.post(new Runnable() {
// @Override
// public void run() {
// mProgressDialog.dismiss();
// }
// });
}
@Override
public void onServerServiceReady(int port) {
// begin broadcasting services
if(!BroadcastService.isRunning()) {
broadcastServiceIntent.putExtra(BroadcastService.PARAM_BROADCAST_PORT, PORT_CLIENT_UDP);
broadcastServiceIntent.putExtra(BroadcastService.PARAM_SERVICE_PORT, port);
broadcastServiceIntent.putExtra(BroadcastService.PARAM_FREQUENCY, 2000);
// broadcastServiceIntent.putExtra(BroadcastService.PARAM_SERVICE_NAME, SERVICE_NAME);
startService(broadcastServiceIntent);
}
bindService(broadcastServiceIntent, mBroadcastConnection, Context.BIND_AUTO_CREATE);
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mExportService.getPeers());
}
});
}
@Override
public void onClientConnected(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mExportService.getPeers());
}
});
}
@Override
public void onClientLost(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mExportService.getPeers());
}
});
}
@Override
public void onClientChanged(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mExportService.getPeers());
}
});
}
@Override
public void onServerServiceError(Throwable e) {
Logger.e(this.getClass().getName(), "Export service encountered an exception: " + e.getMessage(), e);
}
@Override
public void onClientServiceReady() {
// begin listening for service broadcasts
if(!BroadcastListenerService.isRunning()) {
broadcastListenerServiceIntent.putExtra(BroadcastListenerService.PARAM_BROADCAST_PORT, PORT_CLIENT_UDP);
// broadcastListenerServiceIntent.putExtra(BroadcastListenerService.PARAM_SERVICE_NAME, SERVICE_NAME);
broadcastListenerServiceIntent.putExtra(BroadcastListenerService.PARAM_REFRESH_FREQUENCY, REFRESH_FREQUENCY);
broadcastListenerServiceIntent.putExtra(BroadcastListenerService.PARAM_SERVER_TTL, SERVER_TTL);
startService(broadcastListenerServiceIntent);
}
bindService(broadcastListenerServiceIntent, mBroadcastListenerConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onServerConnectionLost(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mBroadcastListenerService.getPeers());
}
});
}
@Override
public void onServerConnectionChanged(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mBroadcastListenerService.getPeers());
}
});
}
@Override
public void onClientServiceError(Throwable e) {
Logger.e(this.getClass().getName(), "Import service encountered an exception: " + e.getMessage(), e);
}
@Override
public void onReceivedTargetTranslations(Peer server, String[] targetTranslations) {
}
@Override
public void onReceivedRequest(Peer peer, Request request) {
}
// @Override
// public void onReceivedProjectList(final Peer server, Model[] models) {
// if(models.length > 0) {
//// showProjectSelectionDialog(server, models);
// } else {
//// app().showToastMessage(getResourceSlugs().getString(R.string.no_projects_available_on_server));
// Handler hand = new Handler(Looper.getMainLooper());
// hand.post(new Runnable() {
// @Override
// public void run() {
// AlertDialog.Builder builder = new AlertDialog.Builder(DeviceToDeviceActivity.this);
// builder.setTitle(server.getIpAddress()).setMessage(getResources().getString(R.string.no_projects_available_on_server)).show();
// }
// });
// }
// }
// @Override
// public void onReceivedProject(Peer server, ProjectImport[] importStatuses) {
// FragmentTransaction ft = getFragmentManager().beginTransaction();
// Fragment prev = getFragmentManager().findFragmentByTag("dialog");
// if (prev != null) {
// ft.remove(prev);
// }
// ft.addToBackStack(null);
// mImportDialog = new ProjectTranslationImportApprovalDialog();
// mImportDialog.setOnClickListener(new ProjectTranslationImportApprovalDialog.OnClickListener() {
// @Override
// public void onOk(ProjectImport[] requests) {
// showProgress(getResources().getString(R.string.loading));
// // TODO: we need to tell the import service what we want to import. It needs to be able to keep track of multiple imports.
// for (ProjectImport r : requests) {
// Sharing.importProject(r);
// }
// Sharing.cleanImport(requests);
//// file.delete();
// hideProgress();
// AppContext.context().showToastMessage(R.string.success);
// // TODO: success dialog
// }
//
// @Override
// public void onCancel(ProjectImport[] requests) {
// // TODO: tell the import service to cancel.
// // import was aborted
// Sharing.cleanImport(requests);
// hideProgress();
//// file.delete();
// }
// });
// // NOTE: we don't place this dialog into the peer dialog map because this will work even if the server disconnects
// mImportDialog.setImportRequests(importStatuses);
// mImportDialog.show(ft, "dialog");
// hideProgress();
// }
@Override
public void onFoundServer(Peer server) {
switch(server.getVersion()) {
default:
// TODO: initialize the api that will be used to handle this version of the server
}
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mBroadcastListenerService.getPeers());
}
});
}
@Override
public void onLostServer(Peer server) {
// close dialogs
if(mPeerDialogs.containsKey(server.getIpAddress())) {
DialogFragment dialog = mPeerDialogs.get(server.getIpAddress());
if(dialog.getActivity() != null) {
dialog.dismiss();
}
mPeerDialogs.remove(server.getIpAddress());
}
// reload the list
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(mBroadcastListenerService.getPeers());
}
});
}
}