/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program 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.
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk;
import java.io.IOException;
import java.io.OutputStream;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import org.spongycastle.openpgp.PGPException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.preference.PreferenceManager;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.PGP;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.data.Contact;
import org.kontalk.provider.MessagesProvider;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.DownloadService;
import org.kontalk.service.NetworkStateReceiver;
import org.kontalk.service.ServerListUpdater;
import org.kontalk.service.SystemBootStartup;
import org.kontalk.service.UploadService;
import org.kontalk.service.msgcenter.IPushService;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.service.msgcenter.PushServiceManager;
import org.kontalk.service.msgcenter.SecureConnectionManager;
import org.kontalk.sync.SyncAdapter;
import org.kontalk.ui.ComposeMessage;
import org.kontalk.ui.MessagingNotification;
import org.kontalk.ui.SearchActivity;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.Preferences;
/**
* The Application.
* @author Daniele Ricci
*/
public class Kontalk extends Application {
public static final String TAG = Kontalk.class.getSimpleName();
// @deprecated
static {
try {
Class.forName(MediaStorage.class.getName());
}
catch (ClassNotFoundException ignored) {
}
}
private PersonalKey mDefaultKey;
/**
* Passphrase to decrypt the personal private key.
* This should be asked to the user and stored in memory - otherwise use
* a dummy password if user doesn't want to remember it (or optionally do
* not encrypt the private key).
* For the moment, this is random-generated and stored as the account
* password in Android Account Manager.
*/
private String mKeyPassphrase;
/**
* Keep-alive reference counter.
* This is used throughout the activities to keep track of application
* usage. Please note that this is not to be confused with
* {@link MessageCenterService.IdleConnectionHandler} reference counter,
* since this counter here is used only by {@link NetworkStateReceiver} and
* a few others to check if the Message Center should be started or not.<br>
* Call {@link #hold} to increment the counter, {@link #release} to
* decrement it.
*/
private int mRefCounter;
/** Messages controller singleton instance. */
private MessagesController mMessagesController;
private final SharedPreferences.OnSharedPreferenceChangeListener mPrefListener =
new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
// debug log
if ("pref_debug_log".equals(key)) {
Log.init(Kontalk.this);
}
// reporting opt-in
else if ("pref_reporting".equals(key)) {
if (Preferences.isReportingEnabled(Kontalk.this)) {
ReportingManager.register(Kontalk.this);
}
else {
ReportingManager.unregister(Kontalk.this);
}
}
// actions requiring an account
else if (Authenticator.getDefaultAccount(Kontalk.this) != null) {
// manual server address
if ("pref_network_uri".equals(key)) {
// temporary measure for users coming from old betas
// this is triggered because manual server address is cleared
if (Authenticator.getDefaultServer(Kontalk.this) != null) {
// just restart the message center for now
Log.w(TAG, "network address changed");
MessageCenterService.restart(Kontalk.this);
}
}
// hide presence flag / encrypt user data flag
else if ("pref_hide_presence".equals(key) || "pref_encrypt_userdata".equals(key)) {
MessageCenterService.updateStatus(Kontalk.this);
}
// changing remove prefix
else if ("pref_remove_prefix".equals(key)) {
SyncAdapter.requestSync(Kontalk.this, true);
}
}
}
};
@Override
public void onCreate() {
super.onCreate();
// init preferences
// This must be done before registering the reporting manager
// because we need access to the reporting opt-in preference.
// However this call will not be reported if it crashes
Preferences.init(this);
// init logging system
// done after preferences because we need to access debug log preference
Log.init(this);
// register reporting manager
if (Preferences.isReportingEnabled(this))
ReportingManager.register(this);
// register security provider
SecureConnectionManager.init(this);
try {
PGP.registerProvider();
}
catch (PGP.PRNGFixException e) {
ReportingManager.logException(e);
Log.w(TAG, "Unable to install PRNG fix - ignoring", e);
}
// init contacts
Contact.init(this, new Handler());
// init notification system
MessagingNotification.init(this);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(mPrefListener);
// TODO listen for changes to phone numbers
AccountManager am = AccountManager.get(this);
Account account = Authenticator.getDefaultAccount(am);
if (account != null) {
if (!Authenticator.hasPersonalKey(am, account))
xmppUpgrade();
// update notifications from locally unread messages
MessagingNotification.delayedUpdateMessagesNotification(this, false);
// register account change listener
final OnAccountsUpdateListener listener = new OnAccountsUpdateListener() {
@Override
public void onAccountsUpdated(Account[] accounts) {
Account my = null;
for (Account acc : accounts) {
if (acc.type.equals(Authenticator.ACCOUNT_TYPE)) {
my = acc;
break;
}
}
// account removed!!! Shutdown everything.
if (my == null) {
Log.w(TAG, "my account has been removed, shutting down");
// stop message center
MessageCenterService.stop(Kontalk.this);
// disable components
setServicesEnabled(Kontalk.this, false);
// unregister from push notifications
IPushService pushMgr = PushServiceManager.getInstance(Kontalk.this);
if (pushMgr != null && pushMgr.isServiceAvailable())
pushMgr.unregister(PushServiceManager.getDefaultListener());
// delete all messages
MessagesProvider.deleteDatabase(Kontalk.this);
// invalidate cached personal key
invalidatePersonalKey();
}
}
};
// register listener to handle account removal
am.addOnAccountsUpdatedListener(listener, null, true);
}
else {
// ensure everything is cleared up
MessagesProvider.deleteDatabase(Kontalk.this);
}
// enable/disable components
setServicesEnabled(this, account != null);
}
private void xmppUpgrade() {
// delete custom server
Preferences.setServerURI(null);
// delete cached server list
ServerListUpdater.deleteCachedList(this);
}
public PersonalKey getPersonalKey() throws PGPException, IOException, CertificateException {
try {
if (mDefaultKey == null)
mDefaultKey = Authenticator.loadDefaultPersonalKey(this, getCachedPassphrase());
}
catch (NoSuchProviderException e) {
// this shouldn't happen, so crash the application
throw new RuntimeException("no such crypto provider!?", e);
}
return mDefaultKey;
}
public void exportPersonalKey(OutputStream out, String exportPassphrase)
throws CertificateException, PGPException, IOException,
NoSuchProviderException, KeyStoreException, NoSuchAlgorithmException {
Authenticator.exportDefaultPersonalKey(this, out, getCachedPassphrase(), exportPassphrase, true);
}
/** Invalidates the cached personal key. */
public void invalidatePersonalKey() {
mDefaultKey = null;
mKeyPassphrase = null;
}
private void ensureCachedPassphrase() {
if (mKeyPassphrase == null) {
AccountManager am = AccountManager.get(this);
Account account = Authenticator.getDefaultAccount(am);
// cache passphrase from account
mKeyPassphrase = am.getPassword(account);
}
}
public String getCachedPassphrase() {
ensureCachedPassphrase();
return mKeyPassphrase;
}
/** Returns the messages controller singleton instance. */
public MessagesController getMessagesController() {
if (mMessagesController == null)
mMessagesController = new MessagesController(this);
return mMessagesController;
}
/** Returns the messages controller singleton instance. */
public static MessagesController getMessagesController(Context context) {
return get(context).getMessagesController();
}
/** Returns true if we are using a two-panes UI. */
public static boolean hasTwoPanesUI(Context context) {
return context.getResources().getBoolean(R.bool.has_two_panes);
}
/** Returns the singleton {@link Kontalk} instance. */
public static Kontalk get(Context context) {
return (Kontalk) context.getApplicationContext();
}
/** Enable/disable application components when account is added or removed. */
public static void setServicesEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
enableService(context, pm, ComposeMessage.class, enabled);
enableService(context, pm, SearchActivity.class, enabled);
setBackendEnabled(context, enabled);
}
/** Enable/disable backend application components when account is added or removed. */
public static void setBackendEnabled(Context context, boolean enabled) {
PackageManager pm = context.getPackageManager();
enableService(context, pm, MessageCenterService.class, enabled);
enableService(context, pm, DownloadService.class, enabled);
enableService(context, pm, UploadService.class, enabled);
enableService(context, pm, SystemBootStartup.class, enabled);
enableService(context, pm, NetworkStateReceiver.class, enabled);
}
private static void enableService(Context context, PackageManager pm, Class<?> klass, boolean enabled) {
pm.setComponentEnabledSetting(new ComponentName(context, klass),
enabled ? PackageManager.COMPONENT_ENABLED_STATE_DEFAULT : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
/** Increments the reference counter. */
public void hold() {
mRefCounter++;
}
/** Decrements the reference counter. */
public void release() {
if (mRefCounter > 0)
mRefCounter--;
}
/** Returns true if the reference counter is greater than zero. */
public boolean hasReference() {
return mRefCounter > 0;
}
/**
* Returns the reference counter. Used only by the message center to restore
* its reference counter when restarting or handling exceptions.
*/
public int getReferenceCounter() {
return mRefCounter;
}
}