package com.door43.translationstudio.newui;
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.support.design.widget.Snackbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import com.door43.tools.reporting.Logger;
import com.door43.translationstudio.AppContext;
import com.door43.translationstudio.R;
import com.door43.translationstudio.core.Library;
import com.door43.translationstudio.core.SourceTranslation;
import com.door43.translationstudio.core.TargetTranslation;
import com.door43.translationstudio.core.Translator;
import com.door43.translationstudio.device2device.PeerAdapter;
import com.door43.translationstudio.dialogs.CustomAlertDialog;
import com.door43.translationstudio.dialogs.NoteMarkerDialog;
import com.door43.translationstudio.network.Peer;
import com.door43.translationstudio.newui.home.HomeActivity;
import com.door43.translationstudio.service.BroadcastListenerService;
import com.door43.translationstudio.service.BroadcastService;
import com.door43.translationstudio.service.ClientService;
import com.door43.translationstudio.service.Request;
import com.door43.translationstudio.service.ServerService;
import com.door43.util.RSAEncryption;
import com.door43.util.StringUtilities;
import com.door43.widget.ViewUtil;
import org.json.JSONException;
import java.io.File;
import java.security.InvalidParameterException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Locale;
/**
* Created by joel on 11/19/2015.
*/
public class ShareWithPeerDialog extends DialogFragment implements ServerService.OnServerEventListener, BroadcastListenerService.Callbacks, ClientService.OnClientEventListener {
// TODO: 11/30/2015 get port from settings
private static final int PORT_CLIENT_UDP = 9939;
private static final int REFRESH_FREQUENCY = 2000;
private static final int SERVER_TTL = 2000;
public static final int MODE_CLIENT = 0;
public static final int MODE_SERVER = 1;
public static final String ARG_DEVICE_ALIAS = "arg_device_alias";
private PeerAdapter adapter;
public static final String ARG_OPERATION_MODE = "arg_operation_mode";
public static final String ARG_TARGET_TRANSLATION = "arg_target_translation";
private ClientService clientService;
private ServiceConnection clientConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ClientService.LocalBinder binder = (ClientService.LocalBinder) service;
clientService = binder.getServiceInstance();
clientService.setOnClientEventListener(ShareWithPeerDialog.this);
Logger.i(ShareWithPeerDialog.class.getName(), "Connected to import service");
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(clientService.getPeers());
}
});
}
@Override
public void onServiceDisconnected(ComponentName name) {
clientService.setOnClientEventListener(null);
Logger.i(ShareWithPeerDialog.class.getName(), "Disconnected from import service");
// TODO: notify fragment that service was dropped.
}
};
private ServerService serverService;
private ServiceConnection serverConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
ServerService.LocalBinder binder = (ServerService.LocalBinder) service;
serverService = binder.getServiceInstance();
serverService.setOnServerEventListener(ShareWithPeerDialog.this);
Logger.i(ShareWithPeerDialog.class.getName(), "Connected to export service");
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(serverService.getPeers());
}
});
}
@Override
public void onServiceDisconnected(ComponentName name) {
serverService.setOnServerEventListener(null);
Logger.i(ShareWithPeerDialog.class.getName(), "Disconnected from export service");
// TODO: notify fragment that service was dropped.
}
};
// TODO: 11/20/2015 we don't actually need to bind to the broadcast service
private BroadcastService broadcastService;
private ServiceConnection broadcastConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
BroadcastService.LocalBinder binder = (BroadcastService.LocalBinder) service;
broadcastService = binder.getServiceInstance();
Logger.i(ShareWithPeerDialog.class.getName(), "Connected to broadcast service");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.i(ShareWithPeerDialog.class.getName(), "Disconnected from broadcast service");
// TODO: notify fragment that service was dropped.
}
};
private BroadcastListenerService listenerService;
private ServiceConnection listenerConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
BroadcastListenerService.LocalBinder binder = (BroadcastListenerService.LocalBinder) service;
listenerService = binder.getServiceInstance();
listenerService.registerCallback(ShareWithPeerDialog.this);
Logger.i(ShareWithPeerDialog.class.getName(), "Connected to broadcast listener service");
}
@Override
public void onServiceDisconnected(ComponentName name) {
listenerService.registerCallback(null);
Logger.i(ShareWithPeerDialog.class.getName(), "Disconnected from broadcast listener service");
// TODO: notify fragment that service was dropped.
}
};
private File publicKeyFile;
private File privateKeyFile;
private static Intent serverIntent;
private static Intent clientIntent;
private static Intent broadcastIntent;
private static Intent listenerIntent;
private int operationMode;
private String targetTranslationSlug;
private boolean shutDownServices = true;
private String deviceAlias;
private TargetTranslation targetTranslation = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, final Bundle savedInstanceState) {
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View v = inflater.inflate(R.layout.dialog_share_with_peer, container, false);
Bundle args = getArguments();
if(args != null && args.containsKey(ARG_OPERATION_MODE) && args.containsKey(ARG_DEVICE_ALIAS)) {
operationMode = args.getInt(ARG_OPERATION_MODE, MODE_CLIENT);
targetTranslationSlug = args.getString(ARG_TARGET_TRANSLATION, null);
deviceAlias = args.getString(ARG_DEVICE_ALIAS, null);
targetTranslation = AppContext.getTranslator().getTargetTranslation(targetTranslationSlug);
if (operationMode == MODE_SERVER && targetTranslation == null) {
throw new InvalidParameterException("Server mode requires a valid target translation");
}
if(deviceAlias == null) {
throw new InvalidParameterException("The device alias cannot be null");
}
} else {
throw new InvalidParameterException("Missing intent arguments");
}
publicKeyFile = new File(getActivity().getFilesDir(), getResources().getString(R.string.p2p_keys_dir) + "/id_rsa.pub");
privateKeyFile = new File(getActivity().getFilesDir(), getResources().getString(R.string.p2p_keys_dir) + "/id_rsa");
publicKeyFile.getParentFile().mkdirs();
TextView title = (TextView)v.findViewById(R.id.title);
TextView subTitle = (TextView)v.findViewById(R.id.target_translation_title);
if(operationMode == MODE_SERVER) {
title.setText(getResources().getString(R.string.backup_to_friend));
SourceTranslation sourceTranslation = AppContext.getLibrary().getDefaultSourceTranslation(targetTranslation.getProjectId(), Locale.getDefault().getLanguage());
if(sourceTranslation != null) {
subTitle.setText(sourceTranslation.getProjectTitle() + " - " + targetTranslation.getTargetLanguageName());
} else {
Logger.w(this.getClass().getName(), "Could not find a default source translation for " + targetTranslation.getProjectId());
subTitle.setText(targetTranslation.getProjectId() + " - " + targetTranslation.getTargetLanguageName());
}
} else {
title.setText(getResources().getString(R.string.import_from_friend));
subTitle.setText("");
}
ListView list = (ListView)v.findViewById(R.id.list);
adapter = new PeerAdapter(getActivity());
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Peer peer = adapter.getItem(position);
if(operationMode == MODE_SERVER) {
// offer target translation to the client
serverService.offerTargetTranslation(peer, targetTranslationSlug);
} else if(operationMode == MODE_CLIENT) {
// TODO: 12/1/2015 eventually provide a ui for viewing multiple different requests from this peer
// display request user
Request[] requests = peer.getRequests();
if(requests.length > 0) {
final Request request = requests[0];
if(request.type == Request.Type.AlertTargetTranslation) {
// TRICKY: for now we are just looking at one request at a time.
try {
final String targetTranslationSlug = request.context.getString("target_translation_id");
String projectName = request.context.getString("project_name");
String targetLanguageName = request.context.getString("target_language_name");
int packageVersion = request.context.getInt("package_version");
if(packageVersion <= TargetTranslation.PACKAGE_VERSION) {
final CustomAlertDialog dialog = CustomAlertDialog.Create(getActivity());
dialog.setTitle(peer.getName())
.setMessage(String.format(getResources().getString(R.string.confirm_import_target_translation), projectName + " - " + targetLanguageName))
.setPositiveButton(R.string.label_import, new View.OnClickListener() {
@Override
public void onClick(View v) {
peer.dismissRequest(request);
if (adapter != null) {
adapter.notifyDataSetChanged();
}
clientService.requestTargetTranslation(peer, targetTranslationSlug);
dialog.dismiss();
}
})
.setNegativeButton(R.string.dismiss, new View.OnClickListener() {
@Override
public void onClick(View v) {
peer.dismissRequest(request);
if (adapter != null) {
adapter.notifyDataSetChanged();
}
dialog.dismiss();
}
})
.show("approve-request");
} else {
// our app is to old to import this version of a target translation
Logger.w(ShareWithPeerDialog.class.getName(), "Could not import target translation with package version " + TargetTranslation.PACKAGE_VERSION + ". Supported version is " + TargetTranslation.PACKAGE_VERSION);
peer.dismissRequest(request);
if (adapter != null) {
adapter.notifyDataSetChanged();
}
final CustomAlertDialog dialog = CustomAlertDialog.Create(getActivity());
dialog.setTitle(peer.getName())
.setMessage(String.format(getResources().getString(R.string.error_importing_unsupported_target_translation), projectName, targetLanguageName, getResources().getString(R.string.app_name)))
.setNeutralButton(R.string.dismiss, null)
.show("approve-request");
}
} catch (JSONException e) {
peer.dismissRequest(request);
if (adapter != null) {
adapter.notifyDataSetChanged();
}
final CustomAlertDialog dialog = CustomAlertDialog.Create(getActivity());
dialog.setTitle(peer.getName())
.setMessage(R.string.error)
.setNeutralButton(R.string.dismiss, null)
.show("approve-request");
Logger.e(ShareWithPeerDialog.class.getName(), "Invalid request context", e);
}
} else {
// we do not currently support other requests
}
}
}
}
});
Button dismissButton = (Button)v.findViewById(R.id.dismiss_button);
dismissButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
return v;
}
@Override
public void onStart() {
super.onStart();
shutDownServices = true;
if(operationMode == MODE_SERVER) {
serverIntent = new Intent(getActivity(), ServerService.class);
broadcastIntent = new Intent(getActivity(), BroadcastService.class);
if(!ServerService.isRunning()) {
try {
initializeService(serverIntent);
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to initialize the server service", e);
dismiss();
}
}
getActivity().bindService(serverIntent, serverConnection, Context.BIND_AUTO_CREATE);
} else if(operationMode == MODE_CLIENT) {
clientIntent = new Intent(getActivity(), ClientService.class);
listenerIntent = new Intent(getActivity(), BroadcastListenerService.class);
if(!ClientService.isRunning()) {
try {
initializeService(clientIntent);
} catch (Exception e) {
Logger.e(this.getClass().getName(), "Failed to initialize the client service", e);
dismiss();
}
}
getActivity().bindService(clientIntent, clientConnection, Context.BIND_AUTO_CREATE);
}
}
/**
* Initializes the service intent
* @param intent
* @throws Exception
*/
private void initializeService(Intent intent) throws Exception {
if(!privateKeyFile.exists() || !publicKeyFile.exists()) {
RSAEncryption.generateKeys(privateKeyFile, publicKeyFile);
}
// TODO: 11/30/2015 we should use a shared interface for setting parameters so we don't have to manage two sets
PrivateKey privateKey;
PublicKey publicKey;
try {
privateKey = RSAEncryption.readPrivateKeyFromFile(privateKeyFile);
publicKey = RSAEncryption.readPublicKeyFromFile(publicKeyFile);
} catch (Exception e) {
// try to regenerate the keys if loading fails
Logger.w(this.getClass().getName(), "Failed to load the p2p keys. Attempting to regenerate...", e);
RSAEncryption.generateKeys(privateKeyFile, publicKeyFile);
privateKey = RSAEncryption.readPrivateKeyFromFile(privateKeyFile);
publicKey = RSAEncryption.readPublicKeyFromFile(publicKeyFile);
}
intent.putExtra(ServerService.PARAM_PRIVATE_KEY, privateKey);
intent.putExtra(ServerService.PARAM_PUBLIC_KEY, RSAEncryption.getPublicKeyAsString(publicKey));
intent.putExtra(ServerService.PARAM_DEVICE_ALIAS, AppContext.getDeviceNetworkAlias());
Logger.i(this.getClass().getName(), "Starting service " + intent.getComponent().getClassName());
getActivity().startService(intent);
}
/**
* Updates the peer list on the screen
* @param peers
*/
public void updatePeerList(ArrayList<Peer> peers) {
if(adapter != null) {
adapter.setPeers(peers);
}
}
@Override
public void onSaveInstanceState(Bundle out) {
shutDownServices = false;
super.onSaveInstanceState(out);
}
@Override
public void onDestroy(){
// unbind services
try {
getActivity().unbindService(broadcastConnection);
} catch (Exception e) {
e.printStackTrace();
}
try {
getActivity().unbindService(listenerConnection);
} catch (Exception e) {
e.printStackTrace();
}
try {
getActivity().unbindService(serverConnection);
} catch (Exception e) {
e.printStackTrace();
}
try {
getActivity().unbindService(clientConnection);
} catch (Exception e) {
e.printStackTrace();
}
// shut down services
if(shutDownServices) {
if (BroadcastService.isRunning() && broadcastIntent != null) {
if (!getActivity().stopService(broadcastIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + BroadcastService.class.getName());
}
}
if (BroadcastListenerService.isRunning() && listenerIntent != null) {
if (!getActivity().stopService(listenerIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + BroadcastListenerService.class.getName());
}
}
if (ServerService.isRunning() && serverIntent != null) {
if (!getActivity().stopService(serverIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + ServerService.class.getName());
}
}
if (ClientService.isRunning() && clientIntent != null) {
if (!getActivity().stopService(clientIntent)) {
Logger.w(this.getClass().getName(), "Failed to stop service " + ClientService.class.getName());
}
}
}
super.onDestroy();
}
@Override
public void onServerServiceReady(int port) {
// begin broadcasting
if(!BroadcastService.isRunning()) {
broadcastIntent.putExtra(BroadcastService.PARAM_BROADCAST_PORT, PORT_CLIENT_UDP);
broadcastIntent.putExtra(BroadcastService.PARAM_SERVICE_PORT, port);
broadcastIntent.putExtra(BroadcastService.PARAM_FREQUENCY, 2000);
getActivity().startService(broadcastIntent);
}
getActivity().bindService(broadcastIntent, broadcastConnection, Context.BIND_AUTO_CREATE);
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(serverService.getPeers());
}
});
}
@Override
public void onClientConnected(Peer peer) {
serverService.acceptConnection(peer);
}
@Override
public void onClientLost(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(serverService.getPeers());
}
});
}
@Override
public void onClientChanged(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(serverService.getPeers());
}
});
}
@Override
public void onServerServiceError(Throwable e) {
Logger.e(this.getClass().getName(), "Server service encountered an exception: " + e.getMessage(), e);
}
@Override
public void onFoundServer(Peer server) {
clientService.connectToServer(server);
}
@Override
public void onLostServer(Peer server) {
}
@Override
public void onClientServiceReady() {
// begin listening for servers
if(!BroadcastListenerService.isRunning()) {
listenerIntent.putExtra(BroadcastListenerService.PARAM_BROADCAST_PORT, PORT_CLIENT_UDP);
listenerIntent.putExtra(BroadcastListenerService.PARAM_REFRESH_FREQUENCY, REFRESH_FREQUENCY);
listenerIntent.putExtra(BroadcastListenerService.PARAM_SERVER_TTL, SERVER_TTL);
getActivity().startService(listenerIntent);
}
getActivity().bindService(listenerIntent, listenerConnection, 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(clientService.getPeers());
}
});
}
@Override
public void onServerConnectionChanged(Peer peer) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
updatePeerList(clientService.getPeers());
}
});
}
@Override
public void onClientServiceError(Throwable e) {
Logger.e(this.getClass().getName(), "Client service encountered an exception: " + e.getMessage(), e);
}
@Override
public void onReceivedTargetTranslations(Peer server, String[] targetTranslations) {
// build name list
Translator translator = AppContext.getTranslator();
Library library = AppContext.getLibrary();
String targetTranslationNames = "";
for(String targetTranslationSlug:targetTranslations) {
TargetTranslation targetTranslation = translator.getTargetTranslation(targetTranslationSlug);
SourceTranslation sourceTranslation = library.getDefaultSourceTranslation(targetTranslation.getProjectId(), Locale.getDefault().getLanguage());
targetTranslationNames += sourceTranslation.getProjectTitle() + " - " + targetTranslation.getTargetLanguageName() + ", ";
}
final String names = targetTranslationNames.trim().replaceAll(",$", "");
// notify user
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
final CustomAlertDialog dialog = CustomAlertDialog.Create(getActivity());
dialog.setTitle(R.string.success)
.setMessage(String.format(getResources().getString(R.string.success_import_target_translation), names))
.setPositiveButton(R.string.dismiss, null)
.show("import-success");
// TODO: 12/1/2015 this is a bad hack
((HomeActivity) getActivity()).notifyDatasetChanged();
}
});
}
@Override
public void onReceivedRequest(final Peer peer, final Request request) {
Handler hand = new Handler(Looper.getMainLooper());
hand.post(new Runnable() {
@Override
public void run() {
if(adapter != null) {
adapter.newRequestAlert(peer, request);
}
}
});
}
}