package org.projectmaxs.transport.xmpp.activities;
import java.util.Iterator;
import java.util.Set;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smackx.iqregister.AccountManager;
import org.jivesoftware.smackx.ping.PingManager;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.jid.util.JidUtil;
import org.jxmpp.jid.util.JidUtil.NotAEntityBareJidStringException;
import org.jxmpp.stringprep.XmppStringprepException;
import org.projectmaxs.shared.global.jul.JULHandler;
import org.projectmaxs.shared.global.util.Log;
import org.projectmaxs.shared.global.util.SharedStringUtil;
import org.projectmaxs.shared.global.util.SpannedUtil;
import org.projectmaxs.shared.transport.AndroidDozeUtil;
import org.projectmaxs.transport.xmpp.R;
import org.projectmaxs.transport.xmpp.Settings;
import org.projectmaxs.transport.xmpp.util.ConnectivityManagerUtil;
import org.projectmaxs.transport.xmpp.xmppservice.StateChangeListener;
import org.projectmaxs.transport.xmpp.xmppservice.XMPPBundleAndDefer;
import org.projectmaxs.transport.xmpp.xmppservice.XMPPService;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Html;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class InfoAndSettings extends Activity {
static {
JULHandler.setAsDefaultUncaughtExceptionHandler();
}
private static final Log LOG = Log.getLog();
private Settings mSettings;
private PingServerButtonHandler mPingServerButtonHandler;
private LinearLayout mMasterAddresses;
private EditText mFirstMasterAddress;
private EditText mJID;
private EditTextWatcher mJidEditTextWatcher;
private String mLastJidText;
private EditText mPassword;
private EditTextWatcher mPasswordEditTextWachter;
private Button mAdvancedSettings;
public void openAdvancedSettings(View view) {
startActivity(new Intent(this, AdvancedSettings.class));
}
public void showAbout(View view) {
final SpannableStringBuilder sb = new SpannableStringBuilder();
final String appName = getResources().getString(R.string.app_name);
sb.append(Html.fromHtml("<h1>" + appName + "</h1>"));
sb.append(getResources().getString(R.string.version)).append('\n');
sb.append(getResources().getString(R.string.copyright)).append(" (").append(SpannedUtil
.createAuthorsLink("transport-xmpp", getResources().getString(R.string.authors)))
.append(")\n");
sb.append('\n');
sb.append(appName).append(' ').append(getResources().getText(R.string.gplv3)).append('\n');
sb.append('\n');
sb.append(Html.fromHtml(
// @formatter:off
"<h1>Open Source</h1>" +
"• <a href=\"http://www.igniterealtime.org/projects/smack\">Smack</a><br>" +
"• <a href=\"https://github.com/ge0rg/MemorizingTrustManager\">MemorizingTrustManager</a><br>" +
"• <a href=\"https://github.com/rtreffer/minidns\">MiniDNS</a><br>" +
"<h2>Smack (XMPP Client Library)</h2>" +
SmackConfiguration.getVersion() + "<br>" +
"<a href=\"http://www.igniterealtime.org/projects/smack\">http://www.igniterealtime.org/projects/smack</a><br>" +
"<br>" +
"Copyright © 2011-2016 Florian Schmaus<br>" +
"Copyright © 2013-2014 Georg Lukas<br>" +
"Copyright © 2014 Lars Noschinski<br>" +
"Copyright © 2014 Vyacheslav Blinov<br>" +
"Copyright © 2014 Andriy Tsykholyas<br>" +
"Copyright © 2009-2013 Robin Collier<br>" +
"Copyright © 2009 Jonas Ådahl<br>" +
"Copyright © 2003-2010 Jive Software<br>" +
"Copyright © 2001-2004 Apache Software Foundation<br>" +
"Apache License, Version 2.0<br>" +
"<h2>MemorizingTrustManager</h2>" +
"<a href=\"https://github.com/ge0rg/MemorizingTrustManager\">https://github.com/ge0rg/MemorizingTrustManager</a><br>" +
"<br>" +
"Copyright © 2010-2104 Georg Lukas<br>" +
"MIT License<br>" +
"<h2>MiniDNS</h2>" +
"<a href=\"https://github.com/rtreffer/minidns\">https://github.com/rtreffer/minidns<a/><br>" +
"<br>" +
"Apache License, Version 2.0<br>" +
"<h1>License Links</h1>" +
"• <a href=\"http://www.apache.org/licenses/LICENSE-2.0\">Apache License, Version 2.0</a><br>" +
"• <a href=\"http://opensource.org/licenses/MIT\">MIT License</a>"
// @formatter:on
));
final TextView textView = new TextView(this);
textView.setText(sb);
textView.setMovementMethod(LinkMovementMethod.getInstance());
// Sadly we can't make this text view also selectable
// http://stackoverflow.com/questions/14862750
// @formatter:off
final AlertDialog alertDialog = new AlertDialog.Builder(this)
.setPositiveButton(getResources().getString(R.string.close), null)
.setView(textView)
.create();
// @formatter:on
alertDialog.show();
}
public void registerAccount(View view) {
final EntityBareJid jid = mSettings.getJid();
final String password = mSettings.getPassword();
if (jid == null) {
Toast.makeText(this, "Please enter a valid bare JID", Toast.LENGTH_SHORT).show();
return;
}
if (password.isEmpty()) {
Toast.makeText(this, "Please enter a password", Toast.LENGTH_SHORT).show();
return;
}
(new Thread() {
@Override
public void run() {
if (!ConnectivityManagerUtil.hasDataConnection(InfoAndSettings.this)) {
showToast("Data connection not available", Toast.LENGTH_SHORT);
return;
}
try {
final Localpart username = jid.getLocalpart();
final AbstractXMPPConnection connection = new XMPPTCPConnection(
mSettings.getConnectionConfiguration(InfoAndSettings.this));
showToast("Connecting to server", Toast.LENGTH_SHORT);
connection.connect();
AccountManager accountManager = AccountManager.getInstance(connection);
showToast("Connected, trying to create account", Toast.LENGTH_SHORT);
accountManager.createAccount(username, password);
connection.disconnect();
} catch (Exception e) {
LOG.i("registerAccount", e);
showToast("Error creating account: " + e, Toast.LENGTH_LONG);
return;
}
showToast("Account created", Toast.LENGTH_SHORT);
}
private final void showToast(final String text, final int duration) {
InfoAndSettings.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(InfoAndSettings.this, text, duration).show();
}
});
}
}).start();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.infoandsettings);
mSettings = Settings.getInstance(this);
// Views
mMasterAddresses = (LinearLayout) findViewById(R.id.masterAddresses);
mFirstMasterAddress = (EditText) findViewById(R.id.firstMasterAddress);
mJID = (EditText) findViewById(R.id.jid);
mPassword = (EditText) findViewById(R.id.password);
mAdvancedSettings = (Button) findViewById(R.id.advancedSettings);
// Avoid the virtual keyboard by focusing a button
mAdvancedSettings.requestFocus();
new MasterAddressCallbacks(mFirstMasterAddress);
mJidEditTextWatcher = new EditTextWatcher(mJID) {
@Override
public void lostFocusOrDone(View v) {
String text = mJID.getText().toString();
EntityBareJid jid;
try {
jid = JidUtil.validateEntityBareJid(text);
} catch (NotAEntityBareJidStringException | XmppStringprepException e) {
Toast.makeText(InfoAndSettings.this,
"'" + text + "' is not a valid bare JID: " + e.getLocalizedMessage(),
Toast.LENGTH_LONG).show();
mJID.setText(mLastJidText);
return;
}
mSettings.setJid(jid);
}
};
mPasswordEditTextWachter = new EditTextWatcher(mPassword) {
@Override
public void lostFocusOrDone(View v) {
mSettings.setPassword(mPassword.getText().toString());
}
};
// initialize the master jid linear layout if there are already some
// configured
Set<EntityBareJid> masterJids = mSettings.getMasterJids();
if (!masterJids.isEmpty()) {
Iterator<EntityBareJid> it = masterJids.iterator();
mFirstMasterAddress.setText(it.next());
while (it.hasNext()) {
EditText et = addEmptyMasterJidEditText();
et.setText(it.next());
}
addEmptyMasterJidEditText();
}
if (mSettings.getJid() != null) mJID.setText(mSettings.getJid());
if (!mSettings.getPassword().equals("")) mPassword.setText(mSettings.getPassword());
mPingServerButtonHandler = new PingServerButtonHandler(this);
AndroidDozeUtil.requestWhitelistIfNecessary(this, mSettings.getSharedPreferences(),
R.string.DozeAskForWhitelist, R.string.DozeDoNotWhitelist, R.string.AskAgain,
R.string.DozeWhitelist);
}
@Override
protected void onPause() {
super.onPause();
mJidEditTextWatcher.onPause();
mPasswordEditTextWachter.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
// Removing this handler yields actually a problem: If onDestroy() is called shortly after
// onCreate() and the XMPPService was not yet initialized/constructed, then this call may
// lead to network IO and an NetworkOnMainThreadException. But a fix wouldn't be trivial
// and this is a corner case.
XMPPService.getInstance(this).removeListener(mPingServerButtonHandler);
}
private final EditText addEmptyMasterJidEditText() {
EditText newEditText = new EditText(this);
newEditText.setHint(getString(R.string.hint_jid));
newEditText.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
new MasterAddressCallbacks(newEditText);
mMasterAddresses.addView(newEditText);
return newEditText;
}
private final class MasterAddressCallbacks extends EditTextWatcher {
MasterAddressCallbacks(EditText editText) {
super(editText);
}
public void lostFocusOrDone(View v) {
String text = mEditText.getText().toString();
EntityBareJid beforeJid = null;
if (!mBeforeText.equals("")) {
try {
beforeJid = JidCreate.entityBareFrom(mBeforeText);
} catch (Exception e) {
LOG.d("Could not transform '" + mBeforeText + "' to bare JID", e);
}
}
if (text.equals("") && beforeJid != null) {
int childCount = mMasterAddresses.getChildCount();
mSettings.removeMasterJid(beforeJid);
mMasterAddresses.removeView(mEditText);
if (childCount <= 2) {
mMasterAddresses.addView(mEditText, 2);
mEditText.setHint(InfoAndSettings.this.getString(R.string.hint_jid));
}
return;
}
if (text.equals("")) return;
// an attempt to change an empty master jid to an invalid jid. abort
// here and leave the original value untouched
EntityBareJid newJid;
try {
newJid = JidUtil.validateEntityBareJid(text);
} catch (NotAEntityBareJidStringException | XmppStringprepException e) {
Toast.makeText(InfoAndSettings.this,
"This is not a valid bare JID: " + e.getLocalizedMessage(),
Toast.LENGTH_LONG).show();
mEditText.setText(mBeforeText);
return;
}
// an empty master jid was change to a valid jid
if (beforeJid == null) {
mSettings.addMasterJid(newJid);
addEmptyMasterJidEditText();
}
// a valid master jid was changed with another valid value
else if (!mBeforeText.equals(text)) {
mSettings.removeMasterJid(beforeJid);
mSettings.addMasterJid(newJid);
}
return;
}
}
class PingServerButtonHandler extends StateChangeListener implements OnClickListener {
private final Button mPingServerButton;
private volatile PingManager mPingManager;
public PingServerButtonHandler(Activity activity) {
mPingServerButton = (Button) activity.findViewById(R.id.pingServer);
mPingServerButton.setOnClickListener(this);
// Ugly workaround for NetworkOnMainThreadException, because XMPPService's constructor
// call leads to a call to Socks5Proxy.getSocks5Proxy, which does
// InetAddress.getLocalHost().getHostAddress() which finally leads to some network IO.
new AsyncTask<Activity, Void, XMPPService>() {
@Override
protected XMPPService doInBackground(Activity... activities) {
return XMPPService.getInstance(activities[0]);
}
@Override
protected void onPostExecute(XMPPService xmppService) {
if (xmppService.isConnected()) {
PingServerButtonHandler.this.mPingManager = PingManager
.getInstanceFor(xmppService.getConnection());
mPingServerButton.setEnabled(true);
}
xmppService.addListener(PingServerButtonHandler.this);
}
}.execute(activity);
}
/**
* This onClick() method can only be called when we are connected, because otherwise the
* button will be disabled. Therefore there is no need to check mPingManager for null.
*/
@Override
public synchronized void onClick(View v) {
Toast.makeText(InfoAndSettings.this, "Sending ping to server", Toast.LENGTH_SHORT)
.show();
new AsyncTask<PingManager, Void, Long>() {
@Override
protected Long doInBackground(PingManager... pingManagers) {
XMPPBundleAndDefer.disableBundleAndDefer();
try {
long start = System.currentTimeMillis();
boolean res = pingManagers[0].pingMyServer();
if (res) {
long stop = System.currentTimeMillis();
return stop - start;
}
} catch (InterruptedException | SmackException e) {
LOG.w("pingMyServer", e);
} catch (RuntimeException e) {
LOG.e("pingMyServer: RuntimeException", e);
} finally {
XMPPBundleAndDefer.enableBundleAndDefer();
}
return (long) -1;
}
@Override
protected void onPostExecute(Long result) {
String text;
if (result > 0) {
text = "Pong received within "
+ SharedStringUtil.humanReadableMilliseconds(result)
+ ". Ping successful. ☺";
} else {
text = "Pong timeout. Ping failed!";
}
Toast.makeText(InfoAndSettings.this, text, Toast.LENGTH_LONG).show();
}
}.execute(mPingManager);
}
@Override
public synchronized void connected(XMPPConnection connection) {
mPingManager = PingManager.getInstanceFor(connection);
setPingButtonEnabled(true);
}
@Override
public synchronized void disconnected(XMPPConnection connection) {
mPingManager = null;
setPingButtonEnabled(false);
}
@Override
public synchronized void disconnected(String reason) {
mPingManager = null;
setPingButtonEnabled(false);
}
private final void setPingButtonEnabled(final boolean enabled) {
InfoAndSettings.this.runOnUiThread(new Runnable() {
@Override
public void run() {
mPingServerButton.setEnabled(enabled);
}
});
}
}
}