diff --git a/app/build.gradle b/app/build.gradle index d5daf4b2a..e6364fe7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,7 @@ android { defaultConfig { applicationId "org.kontalk" - versionCode 119 + versionCode 120 versionName "3.1.9-preview" targetSdkVersion 22 minSdkVersion 9 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88b3b0a6e..cdff898f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -177,7 +177,7 @@ - + diff --git a/app/src/main/java/org/kontalk/ui/ConversationsActivity.java b/app/src/main/java/org/kontalk/ui/ConversationsActivity.java index cbcda756e..40ae52339 100644 --- a/app/src/main/java/org/kontalk/ui/ConversationsActivity.java +++ b/app/src/main/java/org/kontalk/ui/ConversationsActivity.java @@ -44,6 +44,7 @@ import org.kontalk.provider.MyMessages.Threads; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.sync.Syncer; +import org.kontalk.ui.prefs.PreferencesActivity; import org.kontalk.ui.view.ContactPickerListener; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; diff --git a/app/src/main/java/org/kontalk/ui/NumberValidation.java b/app/src/main/java/org/kontalk/ui/NumberValidation.java index bcc414be9..053d87969 100644 --- a/app/src/main/java/org/kontalk/ui/NumberValidation.java +++ b/app/src/main/java/org/kontalk/ui/NumberValidation.java @@ -45,6 +45,7 @@ import org.jivesoftware.smack.util.StringUtils; import org.jxmpp.util.XmppStringUtils; +import org.kontalk.ui.prefs.PreferencesActivity; import org.spongycastle.openpgp.PGPException; import android.accounts.Account; @@ -723,7 +724,7 @@ public void onFileSelection(@NonNull FileChooserDialog fileChooserDialog, @NonNu startImport(new FileInputStream(file)); } catch (FileNotFoundException e) { - Log.e(PreferencesFragment.TAG, "error importing keys", e); + Log.e(TAG, "error importing keys", e); Toast.makeText(this, R.string.err_import_keypair_read, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/org/kontalk/ui/PreferencesFragment.java b/app/src/main/java/org/kontalk/ui/PreferencesFragment.java deleted file mode 100644 index 434c4855b..000000000 --- a/app/src/main/java/org/kontalk/ui/PreferencesFragment.java +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Kontalk Android client - * Copyright (C) 2016 Kontalk Devteam - - * 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 . - */ - -package org.kontalk.ui; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.OutputStream; - -import com.afollestad.materialdialogs.AlertDialogWrapper; -import com.afollestad.materialdialogs.MaterialDialog; -import com.afollestad.materialdialogs.color.ColorChooserDialog; -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.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.Preference.OnPreferenceChangeListener; -import android.preference.Preference.OnPreferenceClickListener; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.DialogFragment; -import android.text.InputType; -import android.text.TextUtils; -import android.util.Log; -import android.view.MenuItem; -import android.widget.Toast; - -import org.kontalk.Kontalk; -import org.kontalk.R; -import org.kontalk.authenticator.Authenticator; -import org.kontalk.client.EndpointServer; -import org.kontalk.client.ServerList; -import org.kontalk.crypto.PersonalKey; -import org.kontalk.crypto.PersonalKeyPack; -import org.kontalk.reporting.ReportingManager; -import org.kontalk.service.ServerListUpdater; -import org.kontalk.service.msgcenter.MessageCenterService; -import org.kontalk.service.msgcenter.PushServiceManager; -import org.kontalk.util.MediaStorage; -import org.kontalk.util.MessageUtils; -import org.kontalk.util.Preferences; - - -/** - * PreferencesFragment. - * @author Daniele Ricci - * @author Andrea Cappelli - */ -public final class PreferencesFragment extends RootPreferenceFragment { - static final String TAG = Kontalk.TAG; - - private static final int REQUEST_PICK_BACKGROUND = Activity.RESULT_FIRST_USER + 1; - private static final int REQUEST_PICK_RINGTONE = Activity.RESULT_FIRST_USER + 2; - private static final int REQUEST_CREATE_KEYPACK = Activity.RESULT_FIRST_USER + 3; - - // this is used after when exiting to SAF for exporting - private String mPassphrase; - - private ServerListUpdater mServerlistUpdater; - private Handler mHandler; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - mPassphrase = savedInstanceState.getString("passphrase"); - } - - mHandler = new Handler(); - - // upgrade from old version: pref_text_enter becomes string - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); - try { - prefs.getString("pref_text_enter", null); - } - catch (ClassCastException e) { - // legacy mode - prefs.edit() - .putString("pref_text_enter", - prefs.getBoolean("pref_text_enter", false) ? - "newline" : "default") - .commit(); - } - - addPreferencesFromResource(R.xml.preferences); - - // push notifications checkbox - final Preference pushNotifications = findPreference("pref_push_notifications"); - pushNotifications.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Context ctx = getActivity(); - CheckBoxPreference pref = (CheckBoxPreference) preference; - if (pref.isChecked()) - MessageCenterService.enablePushNotifications(ctx.getApplicationContext()); - else - MessageCenterService.disablePushNotifications(ctx.getApplicationContext()); - - return true; - } - }); - - // notification LED color - final Preference notificationLed = findPreference("pref_notification_led_color"); - notificationLed.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Resources res = getResources(); - int[] ledColors = new int[]{ - res.getColor(android.R.color.white), res.getColor(R.color.blue_light), - res.getColor(R.color.purple_light), res.getColor(R.color.green_light), - res.getColor(R.color.yellow_light), res.getColor(R.color.red_light), - }; - - new ColorChooserDialog.Builder((PreferencesActivity) getActivity(), - R.string.pref_notification_led_color) - .customColors(ledColors, null) - .preselect(Preferences.getNotificationLEDColor(getContext())) - .allowUserColorInput(false) - .dynamicButtonColor(false) - .show(); - return true; - } - }); - - // message center restart - final Preference restartMsgCenter = findPreference("pref_restart_msgcenter"); - restartMsgCenter.setOnPreferenceClickListener(new 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 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 AlertDialogWrapper.Builder(getActivity()) - .setMessage(R.string.err_password_invalid) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - }; - askCurrentPassphrase(action); - } - - else { - askNewPassphrase(); - } - - - return true; - } - }); - - // regenerate key pair - final Preference regenKeyPair = findPreference("pref_regenerate_keypair"); - regenKeyPair.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new AlertDialogWrapper.Builder(getActivity()) - .setTitle(R.string.pref_regenerate_keypair) - .setMessage(R.string.pref_regenerate_keypair_confirm) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int 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 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; - if (MediaStorage.isStorageAccessFrameworkAvailable()) { - MediaStorage.createFile(PreferencesFragment.this, - PersonalKeyPack.KEYPACK_MIME, - PersonalKeyPack.KEYPACK_FILENAME, - REQUEST_CREATE_KEYPACK); - } - else { - 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 AlertDialogWrapper.Builder(getActivity()) - .setMessage(R.string.err_password_invalid) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - }; - - askCurrentPassphrase(action2); - } - - return true; - } - }); - - // delete account - final Preference deleteAccount = findPreference("pref_delete_account"); - deleteAccount.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new AlertDialogWrapper.Builder(getActivity()) - .setTitle(R.string.pref_delete_account) - .setMessage(R.string.msg_delete_account) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int 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 callback = new AccountManagerCallback() { - public void run(AccountManagerFuture future) { - // dismiss progress - progress.dismiss(); - // exit now - getActivity().finish(); - } - }; - Authenticator.removeDefaultAccount(ctx, callback); - } - }) - .show(); - - return true; - } - }); - - // use custom background - final Preference customBg = findPreference("pref_custom_background"); - customBg.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - // discard reference to custom background drawable - Preferences.setCachedCustomBackground(null); - return false; - } - }); - - // set background - final Preference setBackground = findPreference("pref_background_uri"); - setBackground.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - final Intent i = new Intent(Intent.ACTION_GET_CONTENT); - i.addCategory(Intent.CATEGORY_OPENABLE); - i.setType("image/*"); - startActivityForResult(i, REQUEST_PICK_BACKGROUND); - return true; - } - }); - - // set balloon theme - final Preference balloons = findPreference("pref_balloons"); - balloons.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - Preferences.setCachedBalloonTheme((String) newValue); - return true; - } - }); - - // set ringtone - final Preference setRingtone = findPreference("pref_ringtone"); - setRingtone.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(preference.getContext()); - - String _currentRingtone = prefs.getString(preference.getKey(), - getString(R.string.pref_default_ringtone)); - Uri currentRingtone = !TextUtils.isEmpty(_currentRingtone) ? Uri.parse(_currentRingtone) : null; - - final Intent i = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - i.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentRingtone); - i.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); - i.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); - - i.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - i.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); - i.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, preference.getTitle()); - - startActivityForResult(i, REQUEST_PICK_RINGTONE); - return true; - } - }); - - // manual server address is handled in Application context - // we just handle validation here - - // server list last update timestamp - final Preference updateServerList = findPreference("pref_update_server_list"); - updateServerList.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Context ctx = getActivity(); - mServerlistUpdater = new ServerListUpdater(ctx); - - final DialogHelperFragment diag = DialogHelperFragment - .newInstance(DialogHelperFragment.DIALOG_SERVERLIST_UPDATER); - final Context appCtx = getContext().getApplicationContext(); - - mServerlistUpdater.setListener(new ServerListUpdater.UpdaterListener() { - @Override - public void error(Throwable t) { - try { - ReportingManager.logException(t); - message(R.string.serverlist_update_error); - diag.dismiss(); - } - catch (Exception e) { - // did our best - } - } - - @Override - public void networkNotAvailable() { - try { - message(R.string.serverlist_update_nonetwork); - diag.dismiss(); - } - catch (Exception e) { - // did our best - } - } - - @Override - public void offlineModeEnabled() { - try { - message(R.string.serverlist_update_offline); - diag.dismiss(); - } - catch (Exception e) { - // did our best - } - } - - @Override - public void noData() { - try { - message(R.string.serverlist_update_nodata); - diag.dismiss(); - } - catch (Exception e) { - // did our best - } - } - - @Override - public void updated(final ServerList list) { - Preferences.updateServerListLastUpdate(updateServerList, list); - // restart message center - MessageCenterService.restart(appCtx); - try { - diag.dismiss(); - } - catch (Exception e) { - // did our best - } - } - - private void message(final int textId) { - try { - diag.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getActivity(), textId, - Toast.LENGTH_LONG).show(); - } - }); - } - catch (Exception e) { - // did our best - } - } - }); - - diag.show(getFragmentManager(), DialogHelperFragment.class.getSimpleName()); - mServerlistUpdater.start(); - return true; - } - }); - - // update 'last update' string - ServerList list = ServerListUpdater.getCurrentList(getActivity()); - if (list != null) - Preferences.updateServerListLastUpdate(updateServerList, list); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - Activity ctx = getActivity(); - ctx.finish(); - startActivity(new Intent(ctx, ConversationsActivity.class)); - return true; - } - - return super.onOptionsItemSelected(item); - } - - @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_PICK_BACKGROUND) { - if (resultCode == Activity.RESULT_OK) { - Context ctx = getActivity(); - if (ctx != null) { - // invalidate any previous reference - Preferences.setCachedCustomBackground(null); - // resize and cache image - // TODO do this in background (might take some time) - try { - File image = Preferences.cacheConversationBackground(ctx, data.getData()); - // save to preferences - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); - prefs.edit() - .putString("pref_background_uri", Uri.fromFile(image).toString()) - .commit(); - } - catch (Exception e) { - Toast.makeText(ctx, R.string.err_custom_background, - Toast.LENGTH_LONG).show(); - } - } - } - } - else if (requestCode == REQUEST_PICK_RINGTONE) { - if (resultCode == Activity.RESULT_OK) { - Context ctx = getActivity(); - if (ctx != null) { - Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); - Preferences.setRingtone(ctx, uri != null ? uri.toString() : ""); - } - } - } - else 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); - } - } - - private interface OnPassphraseChangedListener { - void onPassphraseChanged(String passphrase); - } - - private interface OnPassphraseRequestListener { - void onValidPassphrase(String passphrase); - - void onInvalidPassphrase(); - } - - private 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(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(); - } - - private void askNewPassphrase() { - askNewPassphrase(null); - } - - private 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(); - } - - 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(); - } - } - - private void cancelServerlistUpdater() { - if (mServerlistUpdater != null) { - mServerlistUpdater.cancel(); - mServerlistUpdater = null; - } - } - - @Override - protected void setupPreferences() { - super.setupPreferences(); - - // disable push notifications if GCM is not available on the device - if (!PushServiceManager.getInstance(getActivity()).isServiceAvailable()) { - final CheckBoxPreference push = (CheckBoxPreference) findPreference("pref_push_notifications"); - push.setEnabled(false); - push.setChecked(false); - push.setSummary(R.string.pref_title_disabled_push_notifications); - } - - final Preference manualServer = findPreference("pref_network_uri"); - manualServer.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newValue) { - String value = newValue.toString().trim(); - if (value.length() > 0 && !EndpointServer.validate(value)) { - new AlertDialogWrapper.Builder(getActivity()) - .setTitle(R.string.pref_network_uri) - .setMessage(R.string.err_server_invalid_format) - .setPositiveButton(android.R.string.ok, null) - .show(); - return false; - } - return true; - } - }); - - } - - public static final class DialogHelperFragment extends DialogFragment { - public static final int DIALOG_SERVERLIST_UPDATER = 1; - - public static DialogHelperFragment newInstance(int dialogId) { - DialogHelperFragment f = new DialogHelperFragment(); - Bundle args = new Bundle(); - args.putInt("id", dialogId); - f.setArguments(args); - return f; - } - - public DialogHelperFragment() { - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - @Override - public void onDestroyView() { - if (getDialog() != null && getRetainInstance()) - getDialog().setOnDismissListener(null); - super.onDestroyView(); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - int id = args.getInt("id"); - - switch (id) { - case DIALOG_SERVERLIST_UPDATER: - return new MaterialDialog.Builder(getContext()) - .cancelable(true) - .content(R.string.serverlist_updating) - .progress(true, 0) - .cancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - ((PreferencesFragment) getTargetFragment()) - .cancelServerlistUpdater(); - } - }) - .build(); - } - - return super.onCreateDialog(savedInstanceState); - } - } - -} diff --git a/app/src/main/java/org/kontalk/ui/prefs/AppearanceFragment.java b/app/src/main/java/org/kontalk/ui/prefs/AppearanceFragment.java new file mode 100644 index 000000000..12bcb7f15 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/AppearanceFragment.java @@ -0,0 +1,120 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import org.kontalk.R; +import org.kontalk.util.Preferences; + +import java.io.File; + +/** + * Appearance settings fragment. + */ +public class AppearanceFragment extends RootPreferenceFragment { + + private static final int REQUEST_PICK_BACKGROUND = Activity.RESULT_FIRST_USER + 1; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(R.xml.preferences_appearance); + + // use custom background + final Preference customBg = findPreference("pref_custom_background"); + customBg.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // discard reference to custom background drawable + Preferences.setCachedCustomBackground(null); + return false; + } + }); + + // set background + final Preference setBackground = findPreference("pref_background_uri"); + setBackground.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + final Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("image/*"); + startActivityForResult(i, REQUEST_PICK_BACKGROUND); + return true; + } + }); + + // set balloon theme + final Preference balloons = findPreference("pref_balloons"); + balloons.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Preferences.setCachedBalloonTheme((String) newValue); + return true; + } + }); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.pref_appearance_settings); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_PICK_BACKGROUND) { + if (resultCode == Activity.RESULT_OK) { + Context ctx = getActivity(); + if (ctx != null) { + // invalidate any previous reference + Preferences.setCachedCustomBackground(null); + // resize and cache image + // TODO do this in background (might take some time) + try { + File image = Preferences.cacheConversationBackground(ctx, data.getData()); + // save to preferences + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx); + prefs.edit() + .putString("pref_background_uri", Uri.fromFile(image).toString()) + .commit(); + } catch (Exception e) { + Toast.makeText(ctx, R.string.err_custom_background, + Toast.LENGTH_LONG).show(); + } + } + } + } + else { + super.onActivityResult(requestCode, resultCode, data); + } + } +} diff --git a/app/src/main/java/org/kontalk/ui/BootstrapPreferences.java b/app/src/main/java/org/kontalk/ui/prefs/BootstrapPreferences.java similarity index 97% rename from app/src/main/java/org/kontalk/ui/BootstrapPreferences.java rename to app/src/main/java/org/kontalk/ui/prefs/BootstrapPreferences.java index 80b8b8d29..20c78a974 100644 --- a/app/src/main/java/org/kontalk/ui/BootstrapPreferences.java +++ b/app/src/main/java/org/kontalk/ui/prefs/BootstrapPreferences.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.ui; +package org.kontalk.ui.prefs; import android.os.Bundle; import android.view.MenuItem; diff --git a/app/src/main/java/org/kontalk/ui/prefs/MaintenanceFragment.java b/app/src/main/java/org/kontalk/ui/prefs/MaintenanceFragment.java new file mode 100644 index 000000000..e449d8d36 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/MaintenanceFragment.java @@ -0,0 +1,358 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.preference.Preference; +import android.text.InputType; +import android.util.Log; +import android.widget.Toast; + +import com.afollestad.materialdialogs.AlertDialogWrapper; +import com.afollestad.materialdialogs.MaterialDialog; +import com.afollestad.materialdialogs.folderselector.FolderChooserDialog; + +import org.kontalk.Kontalk; +import org.kontalk.R; +import org.kontalk.authenticator.Authenticator; +import org.kontalk.crypto.PersonalKey; +import org.kontalk.crypto.PersonalKeyPack; +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; + +import java.io.FileNotFoundException; +import java.io.OutputStream; + +/** + * 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 + private 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 AlertDialogWrapper.Builder(getActivity()) + .setMessage(R.string.err_password_invalid) + .setPositiveButton(android.R.string.ok, null) + .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 AlertDialogWrapper.Builder(getActivity()) + .setTitle(R.string.pref_regenerate_keypair) + .setMessage(R.string.pref_regenerate_keypair_confirm) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int 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; + if (MediaStorage.isStorageAccessFrameworkAvailable()) { + MediaStorage.createFile(MaintenanceFragment.this, + PersonalKeyPack.KEYPACK_MIME, + PersonalKeyPack.KEYPACK_FILENAME, + REQUEST_CREATE_KEYPACK); + } + else { + 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 AlertDialogWrapper.Builder(getActivity()) + .setMessage(R.string.err_password_invalid) + .setPositiveButton(android.R.string.ok, null) + .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 AlertDialogWrapper.Builder(getActivity()) + .setTitle(R.string.pref_delete_account) + .setMessage(R.string.msg_delete_account) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int 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 callback = new AccountManagerCallback() { + public void run(AccountManagerFuture 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); + } + + private interface OnPassphraseChangedListener { + void onPassphraseChanged(String passphrase); + } + + private interface OnPassphraseRequestListener { + void onValidPassphrase(String passphrase); + + void onInvalidPassphrase(); + } + + private 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(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(); + } + + private void askNewPassphrase() { + askNewPassphrase(null); + } + + private 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); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/kontalk/ui/prefs/MediaFragment.java b/app/src/main/java/org/kontalk/ui/prefs/MediaFragment.java new file mode 100644 index 000000000..0a36f7c66 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/MediaFragment.java @@ -0,0 +1,46 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.os.Bundle; + +import org.kontalk.R; + +/** + * Messaging settings fragment. + */ +public class MediaFragment extends RootPreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences_media); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.pref_media_settings); + } + +} diff --git a/app/src/main/java/org/kontalk/ui/prefs/MessagingFragment.java b/app/src/main/java/org/kontalk/ui/prefs/MessagingFragment.java new file mode 100644 index 000000000..053c19e3c --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/MessagingFragment.java @@ -0,0 +1,59 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.os.Bundle; +import android.preference.Preference; + +import org.kontalk.R; + +/** + * Messaging settings fragment. + */ +public class MessagingFragment extends RootPreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences_messaging); + } + + @Override + protected void setupPreferences() { + // privacy section + final Preference privacy = findPreference("pref_privacy_settings"); + privacy.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + invokeCallback(R.xml.privacy_preferences); + return true; + } + }); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.pref_messaging_settings); + } +} diff --git a/app/src/main/java/org/kontalk/ui/prefs/NetworkFragment.java b/app/src/main/java/org/kontalk/ui/prefs/NetworkFragment.java new file mode 100644 index 000000000..cf3621f66 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/NetworkFragment.java @@ -0,0 +1,275 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.widget.Toast; + +import com.afollestad.materialdialogs.AlertDialogWrapper; +import com.afollestad.materialdialogs.MaterialDialog; + +import org.kontalk.R; +import org.kontalk.client.EndpointServer; +import org.kontalk.client.ServerList; +import org.kontalk.reporting.ReportingManager; +import org.kontalk.service.ServerListUpdater; +import org.kontalk.service.msgcenter.MessageCenterService; +import org.kontalk.service.msgcenter.PushServiceManager; +import org.kontalk.util.Preferences; + +/** + * Network settings fragment. + */ +public class NetworkFragment extends RootPreferenceFragment { + + private ServerListUpdater mServerlistUpdater; + private Handler mHandler; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences_network); + + mHandler = new Handler(); + + // push notifications checkbox + final Preference pushNotifications = findPreference("pref_push_notifications"); + pushNotifications.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Context ctx = getActivity(); + CheckBoxPreference pref = (CheckBoxPreference) preference; + if (pref.isChecked()) + MessageCenterService.enablePushNotifications(ctx.getApplicationContext()); + else + MessageCenterService.disablePushNotifications(ctx.getApplicationContext()); + + return true; + } + }); + + // manual server address is handled in Application context + // we just handle validation here + + // server list last update timestamp + final Preference updateServerList = findPreference("pref_update_server_list"); + updateServerList.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Context ctx = getActivity(); + mServerlistUpdater = new ServerListUpdater(ctx); + + final DialogHelperFragment diag = DialogHelperFragment + .newInstance(DialogHelperFragment.DIALOG_SERVERLIST_UPDATER); + final Context appCtx = getContext().getApplicationContext(); + + mServerlistUpdater.setListener(new ServerListUpdater.UpdaterListener() { + @Override + public void error(Throwable t) { + try { + ReportingManager.logException(t); + message(R.string.serverlist_update_error); + diag.dismiss(); + } + catch (Exception e) { + // did our best + } + } + + @Override + public void networkNotAvailable() { + try { + message(R.string.serverlist_update_nonetwork); + diag.dismiss(); + } + catch (Exception e) { + // did our best + } + } + + @Override + public void offlineModeEnabled() { + try { + message(R.string.serverlist_update_offline); + diag.dismiss(); + } + catch (Exception e) { + // did our best + } + } + + @Override + public void noData() { + try { + message(R.string.serverlist_update_nodata); + diag.dismiss(); + } + catch (Exception e) { + // did our best + } + } + + @Override + public void updated(final ServerList list) { + Preferences.updateServerListLastUpdate(updateServerList, list); + // restart message center + MessageCenterService.restart(appCtx); + try { + diag.dismiss(); + } + catch (Exception e) { + // did our best + } + } + + private void message(final int textId) { + try { + diag.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getActivity(), textId, + Toast.LENGTH_LONG).show(); + } + }); + } + catch (Exception e) { + // did our best + } + } + }); + + diag.show(getFragmentManager(), DialogHelperFragment.class.getSimpleName()); + mServerlistUpdater.start(); + return true; + } + }); + + // update 'last update' string + ServerList list = ServerListUpdater.getCurrentList(getActivity()); + if (list != null) + Preferences.updateServerListLastUpdate(updateServerList, list); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.pref_network_settings); + } + + @Override + protected void setupPreferences() { + // disable push notifications if GCM is not available on the device + if (!PushServiceManager.getInstance(getActivity()).isServiceAvailable()) { + final CheckBoxPreference push = (CheckBoxPreference) findPreference("pref_push_notifications"); + push.setEnabled(false); + push.setChecked(false); + push.setSummary(R.string.pref_title_disabled_push_notifications); + } + + final Preference manualServer = findPreference("pref_network_uri"); + manualServer.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newValue) { + String value = newValue.toString().trim(); + if (value.length() > 0 && !EndpointServer.validate(value)) { + new AlertDialogWrapper.Builder(getActivity()) + .setTitle(R.string.pref_network_uri) + .setMessage(R.string.err_server_invalid_format) + .setPositiveButton(android.R.string.ok, null) + .show(); + return false; + } + return true; + } + }); + } + + private void cancelServerlistUpdater() { + if (mServerlistUpdater != null) { + mServerlistUpdater.cancel(); + mServerlistUpdater = null; + } + } + + public static final class DialogHelperFragment extends DialogFragment { + public static final int DIALOG_SERVERLIST_UPDATER = 1; + + public static DialogHelperFragment newInstance(int dialogId) { + DialogHelperFragment f = new DialogHelperFragment(); + Bundle args = new Bundle(); + args.putInt("id", dialogId); + f.setArguments(args); + return f; + } + + public DialogHelperFragment() { + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onDestroyView() { + if (getDialog() != null && getRetainInstance()) + getDialog().setOnDismissListener(null); + super.onDestroyView(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + int id = args.getInt("id"); + + switch (id) { + case DIALOG_SERVERLIST_UPDATER: + return new MaterialDialog.Builder(getContext()) + .cancelable(true) + .content(R.string.serverlist_updating) + .progress(true, 0) + .cancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + ((NetworkFragment) getTargetFragment()) + .cancelServerlistUpdater(); + } + }) + .build(); + } + + return super.onCreateDialog(savedInstanceState); + } + } + +} diff --git a/app/src/main/java/org/kontalk/ui/prefs/NotificationFragment.java b/app/src/main/java/org/kontalk/ui/prefs/NotificationFragment.java new file mode 100644 index 000000000..8f182fa46 --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/NotificationFragment.java @@ -0,0 +1,127 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.text.TextUtils; + +import com.afollestad.materialdialogs.color.ColorChooserDialog; + +import org.kontalk.R; +import org.kontalk.util.Preferences; + +/** + * Notification settings fragment + */ +public class NotificationFragment extends RootPreferenceFragment { + + private static final int REQUEST_PICK_RINGTONE = Activity.RESULT_FIRST_USER + 2; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences_notification); + + // set ringtone + final Preference setRingtone = findPreference("pref_ringtone"); + setRingtone.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(preference.getContext()); + + String _currentRingtone = prefs.getString(preference.getKey(), + getString(R.string.pref_default_ringtone)); + Uri currentRingtone = !TextUtils.isEmpty(_currentRingtone) ? Uri.parse(_currentRingtone) : null; + + final Intent i = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + i.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentRingtone); + i.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + i.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); + + i.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); + i.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + i.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, preference.getTitle()); + + startActivityForResult(i, REQUEST_PICK_RINGTONE); + return true; + } + }); + + // notification LED color + final Preference notificationLed = findPreference("pref_notification_led_color"); + notificationLed.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Resources res = getResources(); + int[] ledColors = new int[]{ + res.getColor(android.R.color.white), res.getColor(R.color.blue_light), + res.getColor(R.color.purple_light), res.getColor(R.color.green_light), + res.getColor(R.color.yellow_light), res.getColor(R.color.red_light), + }; + + new ColorChooserDialog.Builder((PreferencesActivity) getActivity(), + R.string.pref_notification_led_color) + .customColors(ledColors, null) + .preselect(Preferences.getNotificationLEDColor(getContext())) + .allowUserColorInput(false) + .dynamicButtonColor(false) + .show(); + return true; + } + }); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.pref_notification_settings); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_PICK_RINGTONE) { + if (resultCode == Activity.RESULT_OK) { + Context ctx = getActivity(); + if (ctx != null) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + Preferences.setRingtone(ctx, uri != null ? uri.toString() : ""); + } + } + } + else { + super.onActivityResult(requestCode, resultCode, data); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/kontalk/ui/PreferencesActivity.java b/app/src/main/java/org/kontalk/ui/prefs/PreferencesActivity.java similarity index 81% rename from app/src/main/java/org/kontalk/ui/PreferencesActivity.java rename to app/src/main/java/org/kontalk/ui/prefs/PreferencesActivity.java index 3d931bd60..1d4fc8d83 100644 --- a/app/src/main/java/org/kontalk/ui/PreferencesActivity.java +++ b/app/src/main/java/org/kontalk/ui/prefs/PreferencesActivity.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.ui; +package org.kontalk.ui.prefs; import java.io.File; import java.io.FileNotFoundException; @@ -38,6 +38,7 @@ import org.kontalk.R; import org.kontalk.authenticator.Authenticator; import org.kontalk.crypto.PersonalKeyPack; +import org.kontalk.ui.ToolbarActivity; import org.kontalk.util.Preferences; @@ -47,7 +48,7 @@ * @author Daniele Ricci */ public final class PreferencesActivity extends ToolbarActivity - implements PreferencesFragment.Callback, FolderChooserDialog.FolderCallback, ColorChooserDialog.ColorCallback { + implements RootPreferenceFragment.Callback, FolderChooserDialog.FolderCallback, ColorChooserDialog.ColorCallback { private static final String TAG_NESTED = "nested"; @Override @@ -99,9 +100,27 @@ public void onBackPressed() { public void onNestedPreferenceSelected(int key) { Fragment fragment; switch (key) { + case R.xml.preferences_network: + fragment = new NetworkFragment(); + break; + case R.xml.preferences_messaging: + fragment = new MessagingFragment(); + break; case R.xml.privacy_preferences: fragment = new PrivacyPreferences(); break; + case R.xml.preferences_appearance: + fragment = new AppearanceFragment(); + break; + case R.xml.preferences_media: + fragment = new MediaFragment(); + break; + case R.xml.preferences_notification: + fragment = new NotificationFragment(); + break; + case R.xml.preferences_maintenance: + fragment = new MaintenanceFragment(); + break; default: throw new IllegalArgumentException("unknown preference screen: " + key); } @@ -114,8 +133,8 @@ public void onNestedPreferenceSelected(int key) { @Override public void onFolderSelection(@NonNull FolderChooserDialog folderChooserDialog, @NonNull File folder) { try { - PreferencesFragment f = (PreferencesFragment) getSupportFragmentManager() - .findFragmentById(R.id.container); + MaintenanceFragment f = (MaintenanceFragment) getSupportFragmentManager() + .findFragmentById(R.id.container); f.exportPersonalKey(this, new FileOutputStream(new File(folder, PersonalKeyPack.KEYPACK_FILENAME))); diff --git a/app/src/main/java/org/kontalk/ui/prefs/PreferencesFragment.java b/app/src/main/java/org/kontalk/ui/prefs/PreferencesFragment.java new file mode 100644 index 000000000..0d5df02dc --- /dev/null +++ b/app/src/main/java/org/kontalk/ui/prefs/PreferencesFragment.java @@ -0,0 +1,105 @@ +/* + * Kontalk Android client + * Copyright (C) 2016 Kontalk Devteam + + * 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 . + */ + +package org.kontalk.ui.prefs; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.view.MenuItem; + +import org.kontalk.Kontalk; +import org.kontalk.R; +import org.kontalk.ui.ConversationsActivity; + + +/** + * Preferences overview fragment. + * @author Daniele Ricci + * @author Andrea Cappelli + */ +public final class PreferencesFragment extends RootPreferenceFragment { + static final String TAG = Kontalk.TAG; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // upgrade from old version: pref_text_enter becomes string + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + try { + prefs.getString("pref_text_enter", null); + } + catch (ClassCastException e) { + // legacy mode + prefs.edit() + .putString("pref_text_enter", + prefs.getBoolean("pref_text_enter", false) ? + "newline" : "default") + .commit(); + } + + addPreferencesFromResource(R.xml.preferences); + } + + @Override + public void onResume() { + super.onResume(); + + ((PreferencesActivity) getActivity()).getSupportActionBar() + .setTitle(R.string.menu_settings); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Activity ctx = getActivity(); + ctx.finish(); + startActivity(new Intent(ctx, ConversationsActivity.class)); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + protected void setupPreferences() { + setupPreferences("pref_network_settings", R.xml.preferences_network); + setupPreferences("pref_messaging_settings", R.xml.preferences_messaging); + setupPreferences("pref_appearance_settings", R.xml.preferences_appearance); + setupPreferences("pref_media_settings", R.xml.preferences_media); + setupPreferences("pref_notification_settings", R.xml.preferences_notification); + setupPreferences("pref_maintenance_settings", R.xml.preferences_maintenance); + } + + private void setupPreferences(String pref, final int xml) { + final Preference preference = findPreference(pref); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + invokeCallback(xml); + return true; + } + }); + } + +} diff --git a/app/src/main/java/org/kontalk/ui/PrivacyPreferences.java b/app/src/main/java/org/kontalk/ui/prefs/PrivacyPreferences.java similarity index 98% rename from app/src/main/java/org/kontalk/ui/PrivacyPreferences.java rename to app/src/main/java/org/kontalk/ui/prefs/PrivacyPreferences.java index eb0cf18cb..c76dc398b 100644 --- a/app/src/main/java/org/kontalk/ui/PrivacyPreferences.java +++ b/app/src/main/java/org/kontalk/ui/prefs/PrivacyPreferences.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.ui; +package org.kontalk.ui.prefs; import com.github.machinarius.preferencefragment.PreferenceFragment; diff --git a/app/src/main/java/org/kontalk/ui/RootPreferenceFragment.java b/app/src/main/java/org/kontalk/ui/prefs/RootPreferenceFragment.java similarity index 81% rename from app/src/main/java/org/kontalk/ui/RootPreferenceFragment.java rename to app/src/main/java/org/kontalk/ui/prefs/RootPreferenceFragment.java index d9bbc1881..8d5f62427 100644 --- a/app/src/main/java/org/kontalk/ui/RootPreferenceFragment.java +++ b/app/src/main/java/org/kontalk/ui/prefs/RootPreferenceFragment.java @@ -16,15 +16,18 @@ * along with this program. If not, see . */ -package org.kontalk.ui; +package org.kontalk.ui.prefs; import com.github.machinarius.preferencefragment.PreferenceFragment; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.preference.Preference; +import android.view.MenuItem; import org.kontalk.R; +import org.kontalk.ui.ConversationsActivity; /** @@ -58,22 +61,23 @@ public void onDetach() { mCallback = null; } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + getActivity().onBackPressed(); + return true; + } + + return super.onOptionsItemSelected(item); + } + void invokeCallback(int key) { if (mCallback != null) mCallback.onNestedPreferenceSelected(key); } - protected void setupPreferences() { - // privacy section - final Preference privacy = findPreference("pref_privacy_settings"); - privacy.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - invokeCallback(R.xml.privacy_preferences); - return true; - } - }); - } + protected void setupPreferences() {} public interface Callback { public void onNestedPreferenceSelected(int key); diff --git a/app/src/main/res/xml/preference_headers.xml b/app/src/main/res/xml/preference_headers.xml new file mode 100644 index 000000000..89cf66129 --- /dev/null +++ b/app/src/main/res/xml/preference_headers.xml @@ -0,0 +1,27 @@ + + + + +
+
+ + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 37c8fcf5d..4977b4203 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -18,188 +18,35 @@ - - - - - + - - - - - - - + - - - - - - - + - - - - - + - - - - - - - + - - - - - - - - - - + diff --git a/app/src/main/res/xml/preferences_appearance.xml b/app/src/main/res/xml/preferences_appearance.xml new file mode 100644 index 000000000..e98132cfb --- /dev/null +++ b/app/src/main/res/xml/preferences_appearance.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_maintenance.xml b/app/src/main/res/xml/preferences_maintenance.xml new file mode 100644 index 000000000..ecc84ed32 --- /dev/null +++ b/app/src/main/res/xml/preferences_maintenance.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_media.xml b/app/src/main/res/xml/preferences_media.xml new file mode 100644 index 000000000..63993ad30 --- /dev/null +++ b/app/src/main/res/xml/preferences_media.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_messaging.xml b/app/src/main/res/xml/preferences_messaging.xml new file mode 100644 index 000000000..b18d8b14c --- /dev/null +++ b/app/src/main/res/xml/preferences_messaging.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_network.xml b/app/src/main/res/xml/preferences_network.xml new file mode 100644 index 000000000..d5ba3879d --- /dev/null +++ b/app/src/main/res/xml/preferences_network.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_notification.xml b/app/src/main/res/xml/preferences_notification.xml new file mode 100644 index 000000000..52ce9d180 --- /dev/null +++ b/app/src/main/res/xml/preferences_notification.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + +