/*
* 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.ui.prefs;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.afollestad.materialdialogs.folderselector.FolderChooserDialog;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.support.annotation.NonNull;
import android.text.InputType;
import android.widget.Toast;
import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.PersonalKey;
import org.kontalk.crypto.PersonalKeyPack;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.LockedDialog;
import org.kontalk.ui.PasswordInputDialog;
import org.kontalk.util.MediaStorage;
import org.kontalk.util.MessageUtils;
/**
* Maintenance settings fragment.
*/
public class MaintenanceFragment extends RootPreferenceFragment {
static final String TAG = Kontalk.TAG;
private static final int REQUEST_CREATE_KEYPACK = Activity.RESULT_FIRST_USER + 3;
// this is used after when exiting to SAF for exporting
String mPassphrase;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mPassphrase = savedInstanceState.getString("passphrase");
}
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences_maintenance);
// message center restart
final Preference restartMsgCenter = findPreference("pref_restart_msgcenter");
restartMsgCenter.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Log.w(TAG, "manual message center restart requested");
Context ctx = getActivity();
MessageCenterService.restart(ctx.getApplicationContext());
Toast.makeText(ctx, R.string.msg_msgcenter_restarted, Toast.LENGTH_SHORT).show();
return true;
}
});
// change passphrase
final Preference changePassphrase = findPreference("pref_change_passphrase");
changePassphrase.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (Authenticator.isUserPassphrase(getActivity())) {
OnPassphraseRequestListener action = new OnPassphraseRequestListener() {
public void onValidPassphrase(String passphrase) {
askNewPassphrase();
}
public void onInvalidPassphrase() {
new MaterialDialog.Builder(getActivity())
.content(R.string.err_password_invalid)
.positiveText(android.R.string.ok)
.show();
}
};
askCurrentPassphrase(action);
}
else {
askNewPassphrase();
}
return true;
}
});
// regenerate key pair
final Preference regenKeyPair = findPreference("pref_regenerate_keypair");
regenKeyPair.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
new MaterialDialog.Builder(getActivity())
.title(R.string.pref_regenerate_keypair)
.content(R.string.pref_regenerate_keypair_confirm)
.negativeText(android.R.string.cancel)
.positiveText(android.R.string.ok)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
Context ctx = getActivity();
Toast.makeText(ctx, R.string.msg_generating_keypair,
Toast.LENGTH_LONG).show();
MessageCenterService.regenerateKeyPair(ctx.getApplicationContext());
}
})
.show();
return true;
}
});
// export key pair
final Preference exportKeyPair = findPreference("pref_export_keypair");
exportKeyPair.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// TODO check for external storage presence
final OnPassphraseChangedListener action = new OnPassphraseChangedListener() {
public void onPassphraseChanged(String passphrase) {
mPassphrase = passphrase;
try {
if (MediaStorage.isStorageAccessFrameworkAvailable()) {
MediaStorage.createFile(MaintenanceFragment.this,
PersonalKeyPack.KEYPACK_MIME,
PersonalKeyPack.KEYPACK_FILENAME,
REQUEST_CREATE_KEYPACK);
return;
}
}
catch (ActivityNotFoundException e) {
Log.w(TAG, "Storage Access Framework not working properly");
ReportingManager.logException(e);
}
// also used as a fallback if SAF is not working properly
PreferencesActivity ctx = (PreferencesActivity) getActivity();
if (ctx != null) {
new FolderChooserDialog.Builder(ctx)
.initialPath(PersonalKeyPack.DEFAULT_KEYPACK.getParent())
.show();
}
}
};
// passphrase was never set by the user
// encrypt it with a user-defined passphrase first
if (!Authenticator.isUserPassphrase(getActivity())) {
askNewPassphrase(action);
}
else {
OnPassphraseRequestListener action2 = new OnPassphraseRequestListener() {
public void onValidPassphrase(String passphrase) {
action.onPassphraseChanged(passphrase);
}
public void onInvalidPassphrase() {
new MaterialDialog.Builder(getActivity())
.content(R.string.err_password_invalid)
.positiveText(android.R.string.ok)
.show();
}
};
askCurrentPassphrase(action2);
}
return true;
}
});
// delete account
final Preference deleteAccount = findPreference("pref_delete_account");
deleteAccount.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
new MaterialDialog.Builder(getActivity())
.title(R.string.pref_delete_account)
.content(R.string.msg_delete_account)
.negativeText(android.R.string.cancel)
.positiveText(android.R.string.ok)
.positiveColorRes(R.color.button_danger)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
// progress dialog
final Dialog progress = new LockedDialog
.Builder(getActivity())
.content(R.string.msg_delete_account_progress)
.progress(true, 0)
.show();
// stop the message center first
Context ctx = getActivity();
MessageCenterService.stop(ctx.getApplicationContext());
AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() {
public void run(AccountManagerFuture<Boolean> future) {
// dismiss progress
progress.dismiss();
// exit now
getActivity().finish();
}
};
Authenticator.removeDefaultAccount(ctx, callback);
}
})
.show();
return true;
}
});
}
@Override
public void onResume() {
super.onResume();
((PreferencesActivity) getActivity()).getSupportActionBar()
.setTitle(R.string.pref_maintenance);
}
interface OnPassphraseChangedListener {
void onPassphraseChanged(String passphrase);
}
interface OnPassphraseRequestListener {
void onValidPassphrase(String passphrase);
void onInvalidPassphrase();
}
void askCurrentPassphrase(final OnPassphraseRequestListener action) {
new MaterialDialog.Builder(getActivity())
.title(R.string.title_passphrase)
.inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
.input(0, 0, true, new MaterialDialog.InputCallback() {
@Override
public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {
String passphrase = input.toString();
// user-entered passphrase is hashed, so compare with SHA-1 version
String hashed = MessageUtils.sha1(passphrase);
if (hashed.equals(Kontalk.get(getActivity())
.getCachedPassphrase())) {
action.onValidPassphrase(passphrase);
}
else {
action.onInvalidPassphrase();
}
}
})
.negativeText(android.R.string.cancel)
.positiveText(android.R.string.ok)
.show();
}
void askNewPassphrase() {
askNewPassphrase(null);
}
void askNewPassphrase(final OnPassphraseChangedListener action) {
new PasswordInputDialog.Builder(getActivity())
.setMinLength(PersonalKey.MIN_PASSPHRASE_LENGTH)
.title(R.string.pref_change_passphrase)
.positiveText(android.R.string.ok, new PasswordInputDialog.OnPasswordInputListener() {
public void onClick(DialogInterface dialog, int which, String password) {
Context ctx = getActivity();
String oldPassword = Kontalk.get(getActivity()).getCachedPassphrase();
try {
// user-entered passphrase must be hashed
String hashed = MessageUtils.sha1(password);
Authenticator.changePassphrase(ctx, oldPassword, hashed, true);
Kontalk.get(ctx).invalidatePersonalKey();
if (action != null)
action.onPassphraseChanged(password);
}
catch (Exception e) {
Toast.makeText(ctx,
R.string.err_change_passphrase, Toast.LENGTH_LONG)
.show();
}
}
})
.negativeText(android.R.string.cancel)
.show();
}
public void exportPersonalKey(Context ctx, OutputStream out) {
try {
Kontalk.get(ctx).exportPersonalKey(out, mPassphrase);
mPassphrase = null;
Toast.makeText(ctx,
R.string.msg_keypair_exported,
Toast.LENGTH_LONG).show();
}
catch (Exception e) {
Log.e(TAG, "error exporting keys", e);
Toast.makeText(ctx, R.string.err_keypair_export_other,
Toast.LENGTH_LONG).show();
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("passphrase", mPassphrase);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CREATE_KEYPACK) {
if (resultCode == Activity.RESULT_OK) {
Context ctx = getActivity();
if (ctx != null && data != null && data.getData() != null) {
try {
OutputStream out = ctx.getContentResolver().openOutputStream(data.getData());
exportPersonalKey(ctx, out);
}
catch (FileNotFoundException e) {
Log.e(TAG, "error exporting keys", e);
Toast.makeText(ctx, R.string.err_keypair_export_write,
Toast.LENGTH_LONG).show();
}
}
}
}
else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}