/*
This file is part of Project MAXS.
MAXS and its modules is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MAXS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with MAXS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.projectmaxs.transport.xmpp;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.net.ssl.SSLContext;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.TLSUtils;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.stringprep.XmppStringprepException;
import org.projectmaxs.shared.global.GlobalConstants;
import org.projectmaxs.shared.global.jul.JULHandler;
import org.projectmaxs.shared.global.util.Log.DebugLogSettings;
import org.projectmaxs.shared.global.util.SharedStringUtil;
import org.projectmaxs.transport.xmpp.xmppservice.XMPPSocketFactory;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.PreferenceManager;
import de.duenndns.ssl.MemorizingTrustManager;
public class Settings implements OnSharedPreferenceChangeListener, DebugLogSettings {
private static final String MASTER_JIDS = "MASTER_JIDS";
private static final String JID = "JID";
private static final String PASSWORD = "PASSWORD";
private static final String LAST_RECIPIENT = "LAST_RECIPIENT";
private static final String CMD_ID = "CMD_ID";
private static final String STATUS = "STATUS";
private static final String EXCLUDED_RESOURCES = "EXCLUDED_RESOURCES";
/**
* A set of keys that should not get exported
*/
// @formatter:off
public static final Set<String> DO_NOT_EXPORT = new HashSet<String>(Arrays.asList(new String[] {
PASSWORD
}));
// @formatter:on
// XMPP settings
private final String MANUAL_SERVICE_SETTINGS;
private final String MANUAL_SERVICE_SETTINGS_HOST;
private final String MANUAL_SERVICE_SETTINGS_PORT;
private final String MANUAL_SERVICE_SETTINGS_SERVICE;
private final String XMPP_STREAM_MANAGEMENT;
private final String XMPP_STREAM_COMPRESSION;
private final String XMPP_STREAM_COMPRESSION_SYNC_FLUSH;
private final String XMPP_STREAM_ENCYPTION;
private final String XMPP_STREAM_PRIVACY;
private final String XMPP_STREAM_HOSTNAME_VERIFY;
// App settings
private final String DEBUG_LOG;
private final String XMPP_DEBUG;
private final String DEBUG_NETWORK;
private final String DEBUG_DNS;
private final String LAST_ACTIVE_NETWORK;
private final String XMPP_INTENT;
private final String XMPP_INTENT_SHARED_TOKEN;
private final Set<String> XMPP_CONNECTION_SETTINGS;
private static Settings sSettings;
public static synchronized Settings getInstance(Context context) {
if (sSettings == null) {
sSettings = new Settings(context);
}
return sSettings;
}
private SharedPreferences mSharedPreferences;
private XMPPTCPConnectionConfiguration mConnectionConfiguration;
private EntityBareJid mJidCache;
private Set<EntityBareJid> mMasterJidCache;
private Settings(Context context) {
// this.mSharedPreferences =
// context.getSharedPreferences(Constants.MAIN_PACKAGE,
// Context.MODE_PRIVATE);
this.mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
MANUAL_SERVICE_SETTINGS = context.getString(R.string.pref_manual_service_settings_key);
MANUAL_SERVICE_SETTINGS_HOST = context
.getString(R.string.pref_manual_service_settings_host_key);
MANUAL_SERVICE_SETTINGS_PORT = context
.getString(R.string.pref_manual_service_settings_port_key);
MANUAL_SERVICE_SETTINGS_SERVICE = context
.getString(R.string.pref_manual_service_settings_service_key);
XMPP_STREAM_MANAGEMENT = context.getString(R.string.pref_xmpp_stream_management_key);
XMPP_STREAM_COMPRESSION = context.getString(R.string.pref_xmpp_stream_compression_key);
XMPP_STREAM_COMPRESSION_SYNC_FLUSH = context
.getString(R.string.pref_xmpp_stream_compression_sync_flush_key);
XMPP_STREAM_ENCYPTION = context.getString(R.string.pref_xmpp_stream_encryption_key);
XMPP_STREAM_PRIVACY = context.getString(R.string.pref_xmpp_stream_privacy_key);
XMPP_STREAM_HOSTNAME_VERIFY = context
.getString(R.string.pref_xmpp_stream_hostname_verify_key);
DEBUG_NETWORK = context.getString(R.string.pref_app_debug_network_key);
DEBUG_DNS = context.getString(R.string.pref_app_debug_dns_key);
LAST_ACTIVE_NETWORK = context.getString(R.string.pref_app_last_active_network_key);
XMPP_DEBUG = context.getString(R.string.pref_app_xmpp_debug_key);
XMPP_CONNECTION_SETTINGS = new HashSet<String>(Arrays.asList(
new String[] { JID, PASSWORD, MANUAL_SERVICE_SETTINGS, MANUAL_SERVICE_SETTINGS_HOST,
MANUAL_SERVICE_SETTINGS_PORT, MANUAL_SERVICE_SETTINGS_SERVICE,
XMPP_STREAM_COMPRESSION, XMPP_STREAM_ENCYPTION, XMPP_DEBUG }));
DEBUG_LOG = context.getString(R.string.pref_app_debug_log_key);
XMPP_INTENT = context.getString(R.string.pref_app_xmpp_intent_key);
XMPP_INTENT_SHARED_TOKEN = context
.getString(R.string.pref_app_xmpp_intent_shared_token_key);
mSharedPreferences.registerOnSharedPreferenceChangeListener(this);
setDnsDebug();
}
public EntityBareJid getJid() {
if (mJidCache != null) {
return mJidCache;
}
String jidString = mSharedPreferences.getString(JID, "");
if (jidString.isEmpty()) {
return null;
}
EntityBareJid bareJid;
try {
bareJid = JidCreate.entityBareFrom(jidString);
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
return bareJid;
}
public void setJid(EntityBareJid jid) {
if (jid.hasResource()) {
throw new IllegalArgumentException();
}
mSharedPreferences.edit().putString(JID, jid.toString()).commit();
mJidCache = jid;
}
public String getPassword() {
return mSharedPreferences.getString(PASSWORD, "");
}
public void setPassword(String password) {
mSharedPreferences.edit().putString(PASSWORD, password).commit();
}
/**
* Returns a set of master JID Strings or an empty set if no master JID was
* ever set.
*
* @return A set containing the master JIDs.
*/
public Set<EntityBareJid> getMasterJids() {
if (mMasterJidCache != null) {
return mMasterJidCache;
}
String s = mSharedPreferences.getString(MASTER_JIDS, "");
Set<String> resString = SharedStringUtil.stringToSet(s);
Set<EntityBareJid> res = new HashSet<>();
for (String jidString : resString) {
try {
EntityBareJid bareJid = JidCreate.entityBareFrom(jidString);
res.add(bareJid);
} catch (XmppStringprepException e) {
throw new AssertionError(e);
}
}
return res;
}
public int getMasterJidCount() {
return getMasterJids().size();
}
public void addMasterJid(EntityBareJid jid) {
Set<EntityBareJid> masterJids = getMasterJids();
masterJids.add(jid);
saveMasterJids(masterJids);
}
public boolean removeMasterJid(EntityBareJid jid) {
Set<EntityBareJid> masterJids = getMasterJids();
if (masterJids.remove(jid)) {
saveMasterJids(masterJids);
return true;
}
return false;
}
public boolean isMasterJID(Jid jid) {
EntityBareJid bareJid = jid.asEntityBareJidIfPossible();
if (bareJid == null) {
return false;
}
if (getMasterJids().contains(bareJid)) {
return true;
}
return false;
}
public Set<String> getExcludedResources() {
String s = mSharedPreferences.getString(EXCLUDED_RESOURCES, "");
Set<String> res = SharedStringUtil.stringToSet(s);
return res;
}
/**
* Don't sent broadcasts to certain resources. This is mostly usefull to avoid notifcation
* loops, i.e. when MAXS broadcasts a message and a XMPP client on the same device running MAXS
* receives and displays it.
* <p>
* Therefore this method current returns true if:
* <li>The resource starts with 'android', to exclude hangout/gtalk
* <li>The resource matches one of the user configured excluded resources
*
* @param resourcepart
* @return true if resource should be excluded from broadcasts
*/
public boolean isExcludedResource(Resourcepart resourcepart) {
final String resource = resourcepart.toString();
final String[] ifStartsWith = new String[] { "android" };
for (String s : ifStartsWith) {
if (resource.startsWith(s)) return true;
}
Set<String> excludedResources = getExcludedResources();
if (excludedResources.contains(resource)) return true;
return false;
}
public void addExcludedResource(String resource) {
Set<String> excludedResources = getExcludedResources();
excludedResources.add(resource);
saveExcludedResources(excludedResources);
}
public boolean removeExcludedResource(String resource) {
Set<String> excludedResources = getExcludedResources();
if (excludedResources.remove(resource)) {
saveExcludedResources(excludedResources);
return true;
}
return false;
}
public void setLastRecipient(String lastRecipient) {
mSharedPreferences.edit().putString(LAST_RECIPIENT, lastRecipient).commit();
}
public String getLastRecipient() {
return mSharedPreferences.getString(LAST_RECIPIENT, "");
}
public int getNextCommandId() {
int id = mSharedPreferences.getInt(CMD_ID, 0);
mSharedPreferences.edit().putInt(CMD_ID, id + 1).commit();
return id;
}
public boolean isDebugLogEnabled() {
return mSharedPreferences.getBoolean(DEBUG_LOG, false);
}
public void setLastActiveNetwork(String network) {
mSharedPreferences.edit().putString(LAST_ACTIVE_NETWORK, network).commit();
}
public String getLastActiveNetwork() {
return mSharedPreferences.getString(LAST_ACTIVE_NETWORK, "");
}
public boolean isNetworkDebugLogEnabled() {
return mSharedPreferences.getBoolean(DEBUG_NETWORK, false);
}
public void setStatus(String status) {
mSharedPreferences.edit().putString(STATUS, status).commit();
}
public String getStatus() {
return mSharedPreferences.getString(STATUS, "");
}
/**
* Ensures that the user configured everything so that a connection attempt can be made. If
* something is missing, a String describing what is missing, will be returned.
*
* @return null if everything is fine, otherwise a string explaining what's wrong
*/
public String checkIfReadyToConnect() {
if (getPassword().isEmpty()) return "Password not set or empty";
if (getJid() == null) return "JID not set or empty";
if (getMasterJidCount() == 0) return "Master JID(s) not configured";
if (getManualServiceSettings()) {
if (getManualServiceSettingsHost().isEmpty()) return "XMPP Server Host not specified";
try {
getManualServiceSettingsService();
} catch (XmppStringprepException e) {
String causingString = e.getCausingString();
if (causingString.isEmpty()) {
return "XMPP Server service name not specified";
} else {
return "Not a valid service string: '" + causingString + "'";
}
}
}
return null;
}
public boolean isStreamManagementEnabled() {
return mSharedPreferences.getBoolean(XMPP_STREAM_MANAGEMENT, false);
}
/**
* Retrieve a ConnectionConfiguration.
*
* Note that because of MemorizingTrustManager, the given Context must be an instance of
* Application, Service or Activity
*
* @param context
* @return The ConnectionConfiguration.
* @throws XmppStringprepException
*/
public XMPPTCPConnectionConfiguration getConnectionConfiguration(Context context)
throws XmppStringprepException {
if (mConnectionConfiguration == null) {
DomainBareJid service;
XMPPTCPConnectionConfiguration.Builder confBuilder = XMPPTCPConnectionConfiguration
.builder();
if (getManualServiceSettings()) {
String host = getManualServiceSettingsHost();
int port = getManualServiceSettingsPort();
service = getManualServiceSettingsService();
confBuilder.setHost(host);
confBuilder.setPort(port);
confBuilder.setXmppDomain(service);
} else {
service = JidCreate.from(mSharedPreferences.getString(JID, "")).asDomainBareJid();
}
confBuilder.setUsernameAndPassword(getJid().getLocalpart(), getPassword());
confBuilder.setXmppDomain(service);
confBuilder.setResource(GlobalConstants.MAXS);
confBuilder.setSocketFactory(XMPPSocketFactory.getInstance());
confBuilder.setCompressionEnabled(
mSharedPreferences.getBoolean(XMPP_STREAM_COMPRESSION, false));
ConnectionConfiguration.SecurityMode securityMode;
final String securityModeString = mSharedPreferences.getString(XMPP_STREAM_ENCYPTION,
"opt");
if ("opt".equals(securityModeString)) {
securityMode = ConnectionConfiguration.SecurityMode.ifpossible;
} else if ("req".equals(securityModeString)) {
securityMode = ConnectionConfiguration.SecurityMode.required;
} else if ("dis".equals(securityModeString)) {
securityMode = ConnectionConfiguration.SecurityMode.disabled;
} else {
throw new IllegalArgumentException("Unknown security mode: " + securityModeString);
}
confBuilder.setSecurityMode(securityMode);
confBuilder.setSendPresence(false);
confBuilder.setDebuggerEnabled(mSharedPreferences.getBoolean(XMPP_DEBUG, false));
if (!mSharedPreferences.getBoolean(XMPP_STREAM_HOSTNAME_VERIFY, true)) {
TLSUtils.disableHostnameVerificationForTlsCertificates(confBuilder);
} else {
// Smack >= 4.1 verifies the hostname per default
}
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, MemorizingTrustManager.getInstanceList(context), new SecureRandom());
confBuilder.setCustomSSLContext(sc);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
} catch (KeyManagementException e) {
throw new IllegalStateException(e);
}
mConnectionConfiguration = confBuilder.build();
}
return mConnectionConfiguration;
}
public boolean privacyListsEnabled() {
return mSharedPreferences.getBoolean(XMPP_STREAM_PRIVACY, false);
}
public SharedPreferences getSharedPreferences() {
return mSharedPreferences;
}
/**
*
* @return true if the XMPP intent is enabled
*/
public boolean isXmppIntentEnabled() {
return mSharedPreferences.getBoolean(XMPP_INTENT, false);
}
/**
*
* @return the XMPP intent shared token or null
*/
public String getXmppIntentSharedToken() {
// always ensure that we return null if the XMPP intent is disabled, so that we don't end up
// comparing the token while the XMPP intent is disabled
if (!isXmppIntentEnabled()) {
return null;
}
String res = mSharedPreferences.getString(XMPP_INTENT_SHARED_TOKEN, "");
if (res.isEmpty()) {
return null;
}
return res;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
for (String s : XMPP_CONNECTION_SETTINGS) {
if (s.equals(key)) {
mConnectionConfiguration = null;
break;
}
}
if (key.equals(DEBUG_DNS)) {
setDnsDebug();
} else if (key.equals(XMPP_STREAM_COMPRESSION_SYNC_FLUSH)) {
setSyncFlush();
}
}
private void saveMasterJids(Set<EntityBareJid> newMasterJids) {
SharedPreferences.Editor e = mSharedPreferences.edit();
Set<String> jidStrings = new HashSet<String>();
for (BareJid bareJid : newMasterJids) {
jidStrings.add(bareJid.toString());
}
String masterJids = SharedStringUtil.setToString(jidStrings);
e.putString(MASTER_JIDS, masterJids);
e.commit();
mMasterJidCache = newMasterJids;
}
private void saveExcludedResources(Set<String> newExcludedResources) {
SharedPreferences.Editor e = mSharedPreferences.edit();
String excludedResources = SharedStringUtil.setToString(newExcludedResources);
e.putString(EXCLUDED_RESOURCES, excludedResources);
e.commit();
}
private boolean getManualServiceSettings() {
return mSharedPreferences.getBoolean(MANUAL_SERVICE_SETTINGS, false);
}
private String getManualServiceSettingsHost() {
return mSharedPreferences.getString(MANUAL_SERVICE_SETTINGS_HOST, "");
}
private int getManualServiceSettingsPort() {
return Integer.parseInt(mSharedPreferences.getString(MANUAL_SERVICE_SETTINGS_PORT, "5222"));
}
private DomainBareJid getManualServiceSettingsService() throws XmppStringprepException {
final String jidString = mSharedPreferences.getString(MANUAL_SERVICE_SETTINGS_SERVICE, "");
return JidCreate.domainBareFrom(jidString);
}
private void setDnsDebug() {
final String minidnsPkg = "de.measite.minidns";
if (mSharedPreferences.getBoolean(DEBUG_DNS, false)) {
JULHandler.removeNoLogPkg(minidnsPkg);
} else {
JULHandler.addNoLogPkg(minidnsPkg);
}
}
private void setSyncFlush() {
if (mSharedPreferences.getBoolean(XMPP_STREAM_COMPRESSION_SYNC_FLUSH, false)) {
XMPPInputOutputStream.setFlushMethod(FlushMethod.SYNC_FLUSH);
} else {
XMPPInputOutputStream.setFlushMethod(FlushMethod.FULL_FLUSH);
}
}
}