package info.guardianproject.securereaderinterface; import info.guardianproject.securereader.Settings; import info.guardianproject.securereaderinterface.uiutil.UIHelpers; import info.guardianproject.securereaderinterface.widgets.GroupView; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.GeneralSecurityException; import java.util.ArrayList; import org.holoeverywhere.app.Dialog; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Bundle; import android.text.TextUtils; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.widget.Checkable; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.RadioButton; import android.widget.Toast; import info.guardianproject.cacheword.CacheWordHandler; import info.guardianproject.cacheword.PassphraseSecrets; import info.guardianproject.securereaderinterface.R; public class SettingsActivity extends FragmentActivityWithMenu { private static final String TAG = "Settings"; public static final String EXTRA_GO_TO_GROUP = "go_to_group"; Settings mSettings; private ViewGroup rootView; private RadioButton mRbUseKillPassphraseOn; private RadioButton mRbUseKillPassphraseOff; private boolean mLanguageBeingUpdated; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); setMenuIdentifier(R.menu.activity_settings); mSettings = App.getSettings(); rootView = (ViewGroup) findViewById(R.id.root); TypedArray array = this.obtainStyledAttributes(R.style.SettingsRadioButtonSubStyle, new int[] { android.R.attr.textColor }); if (array != null) { int color = array.getColor(0, 0x999999); CharacterStyle colored = new ForegroundColorSpan(color); CharacterStyle small = new RelativeSizeSpan(0.85f); applySpanToAllRadioButtons(rootView, small, colored); array.recycle(); } } private void applySpanToAllRadioButtons(ViewGroup parent, CharacterStyle... cs) { for (int i = 0; i < parent.getChildCount(); i++) { View view = parent.getChildAt(i); if (view instanceof RadioButton) { RadioButton rb = (RadioButton) view; rb.setText(setSpanOnMultilineText(rb.getText(), cs)); } else if (view instanceof ViewGroup) { applySpanToAllRadioButtons((ViewGroup) view, cs); } } } private CharSequence setSpanOnMultilineText(CharSequence text, CharacterStyle... cs) { int idxBreak = text.toString().indexOf('\n'); if (idxBreak > -1) { StringBuilder sb = new StringBuilder(text); sb.insert(idxBreak, "##"); sb.append("##"); return UIHelpers.setSpanBetweenTokens(sb.toString(), "##", cs); } return text; } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); } @Override public void onResume() { super.onResume(); populateProfileTab(); if (getIntent().hasExtra(EXTRA_GO_TO_GROUP)) { handleGoToGroup(getIntent().getIntExtra(EXTRA_GO_TO_GROUP, 0)); getIntent().removeExtra(EXTRA_GO_TO_GROUP); } if (getIntent().hasExtra("savedInstance")) { this.onRestoreInstanceState(getIntent().getBundleExtra("savedInstance")); getIntent().removeExtra("savedInstance"); } } private void handleGoToGroup(int goToSection) { if (goToSection != 0) { final View view = rootView.findViewById(goToSection); if (view != null) { if (view instanceof GroupView) { ((GroupView) view).setExpanded(true, false); } rootView.post(new Runnable() { @Override public void run() { int top = view.getTop(); rootView.scrollTo(0, top - 5); } }); } } } private void populateProfileTab() { View tabView = rootView; this.hookupCheckbox(tabView, R.id.chkRequireTor, "requireTor"); mRbUseKillPassphraseOn = (RadioButton) tabView.findViewById(R.id.rbKillPassphraseOn); mRbUseKillPassphraseOff = (RadioButton) tabView.findViewById(R.id.rbKillPassphraseOff); if (mSettings.useKillPassphrase()) mRbUseKillPassphraseOn.setChecked(true); else mRbUseKillPassphraseOff.setChecked(true); mRbUseKillPassphraseOn.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked != mSettings.useKillPassphrase()) { if (isChecked && TextUtils.isEmpty(mSettings.killPassphrase())) { promptForKillPassphrase(true); } else { mSettings.setUseKillPassphrase(isChecked); } } } }); this.hookupBinaryRadioButton(tabView, R.id.rbWipeApp, R.id.rbWipeContent, "wipeApp"); // Immediate, 1 minute, 1 hour, 1 day, 1 week this.hookupRadioButtonWithArray(tabView, "passphraseTimeout", int.class, new ResourceValueMapping[] { new ResourceValueMapping(R.id.rbPassphraseTimeout1, 0), new ResourceValueMapping(R.id.rbPassphraseTimeout2, 1), new ResourceValueMapping(R.id.rbPassphraseTimeout3, 60), new ResourceValueMapping(R.id.rbPassphraseTimeout4, 1440), new ResourceValueMapping(R.id.rbPassphraseTimeout5, 10080),});//Integer.MAX_VALUE / 60000), }); //MAX_INT milliseconds given in minutes this.hookupRadioButton(tabView, "articleExpiration", Settings.ArticleExpiration.class, R.id.rbExpirationNever, R.id.rbExpiration1Day, R.id.rbExpiration1Week, R.id.rbExpiration1Month); this.hookupRadioButton(tabView, "syncFrequency", Settings.SyncFrequency.class, R.id.rbSyncManual, R.id.rbSyncWhenRunning, R.id.rbSyncInBackground); this.hookupRadioButton(tabView, "syncMode", Settings.SyncMode.class, R.id.rbSyncModeBitwise, R.id.rbSyncModeFlow); this.hookupRadioButton(tabView, "syncNetwork", Settings.SyncNetwork.class, R.id.rbSyncNetworkWifiAndMobile, R.id.rbSyncNetworkWifiOnly); this.hookupRadioButton(tabView, "readerSwipeDirection", Settings.ReaderSwipeDirection.class, R.id.rbSwipeDirectionRtl, R.id.rbSwipeDirectionLtr, R.id.rbSwipeDirectionAutomatic); this.hookupRadioButtonWithArray(tabView, "uiLanguage", Settings.UiLanguage.class, new ResourceValueMapping[] { new ResourceValueMapping(R.id.rbUiLanguageEnglish, Settings.UiLanguage.English), new ResourceValueMapping(R.id.rbUiLanguageTibetan, Settings.UiLanguage.Tibetan), new ResourceValueMapping(R.id.rbUiLanguageChinese, Settings.UiLanguage.Chinese), new ResourceValueMapping(R.id.rbUiLanguageUkrainian, Settings.UiLanguage.Ukrainian), new ResourceValueMapping(R.id.rbUiLanguageRussian, Settings.UiLanguage.Russian) }); this.hookupRadioButtonWithArray(tabView, "numberOfPasswordAttempts", int.class, new ResourceValueMapping[] { new ResourceValueMapping(R.id.rbNumberOfPasswordAttempts1, 2), new ResourceValueMapping(R.id.rbNumberOfPasswordAttempts2, 3), new ResourceValueMapping(R.id.rbNumberOfPasswordAttempts3, 0), }); // this.hookupBinaryRadioButton(tabView, R.id.rbKillPassphraseOn, // R.id.rbKillPassphraseOff, "useKillPassphrase"); tabView.findViewById(R.id.btnSetLaunchPassphrase).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { promptForNewPassphrase(); } }); tabView.findViewById(R.id.btnSetKillPassphrase).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { promptForKillPassphrase(false); } }); } private class ResourceValueMapping { private final int mResId; private final Object mValue; public ResourceValueMapping(int resId, Object value) { mResId = resId; mValue = value; } public int getResId() { return mResId; } public Object getValue() { return mValue; } } private void hookupCheckbox(View parentView, int resIdCheckbox, String methodNameOfGetter) { Log.v(TAG, methodNameOfGetter); Checkable cb = (Checkable) parentView.findViewById(resIdCheckbox); if (cb == null) { Log.v(TAG, "Failed to find checkbox: " + resIdCheckbox); return; } try { String methodNameOfSetter = "set" + String.valueOf(methodNameOfGetter.charAt(0)).toUpperCase() + methodNameOfGetter.substring(1); final Method getter = mSettings.getClass().getMethod(methodNameOfGetter, (Class[]) null); final Method setter = mSettings.getClass().getMethod(methodNameOfSetter, new Class<?>[] { boolean.class }); if (getter == null || setter == null) { Log.v(TAG, "Failed to find propety getter/setter for: " + methodNameOfGetter); return; } // Set initial value cb.setChecked((Boolean) getter.invoke(mSettings, (Object[]) null)); // Set listener ((View) cb).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { boolean checked; try { checked = (Boolean) getter.invoke(mSettings, (Object[]) null); boolean newState = !checked; setter.invoke(mSettings, newState); ((Checkable) v).setChecked(newState); } catch (Exception e) { Log.v(TAG, "Failed checked change listener: " + e.toString()); } } }); } catch (NoSuchMethodException e) { Log.v(TAG, "Failed to find propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (IllegalArgumentException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (IllegalAccessException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (InvocationTargetException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } } private void hookupBinaryRadioButton(View parentView, int resIdRadioTrue, int resIdRadioFalse, String methodNameOfGetter) { hookupRadioButtonWithArray(parentView, methodNameOfGetter, boolean.class, new ResourceValueMapping[] { new ResourceValueMapping(resIdRadioTrue, true), new ResourceValueMapping(resIdRadioFalse, false) }); } private void hookupRadioButton(View parentView, String methodNameOfGetter, Class<?> enumClass, int... resIds) { Object[] constants = enumClass.getEnumConstants(); if (constants.length != resIds.length) { Log.w(TAG, "hookupRadioButton: mismatched classes!"); return; } ArrayList<ResourceValueMapping> mappings = new ArrayList<ResourceValueMapping>(); int idx = 0; for (int resId : resIds) { mappings.add(new ResourceValueMapping(Integer.valueOf(resId), constants[idx++])); } hookupRadioButtonWithArray(parentView, methodNameOfGetter, enumClass, mappings.toArray(new ResourceValueMapping[] {})); } private void hookupRadioButtonWithArray(View parentView, String methodNameOfGetter, Class<?> valueType, ResourceValueMapping[] values) { try { String methodNameOfSetter = "set" + String.valueOf(methodNameOfGetter.charAt(0)).toUpperCase() + methodNameOfGetter.substring(1); final Method getter = mSettings.getClass().getMethod(methodNameOfGetter, (Class[]) null); final Method setter = mSettings.getClass().getMethod(methodNameOfSetter, new Class<?>[] { valueType }); if (getter == null || setter == null) { Log.w(TAG, "Failed to find propety getter/setter for: " + methodNameOfGetter); return; } RadioButtonChangeListener listener = new RadioButtonChangeListener(mSettings, getter, setter); Object currentValueInSettings = getter.invoke(mSettings, (Object[]) null); for (ResourceValueMapping value : values) { int resId = value.getResId(); if (resId == 0) continue; // Ignore this value, cant be set in the ui RadioButton rb = (RadioButton) parentView.findViewById(resId); if (rb == null) { Log.w(TAG, "Failed to find checkbox: " + resId); return; } if (currentValueInSettings.equals(value.getValue())) rb.setChecked(true); rb.setTag(value.getValue()); rb.setOnCheckedChangeListener(listener); } } catch (NoSuchMethodException e) { Log.v(TAG, "Failed to find propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (IllegalArgumentException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (IllegalAccessException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } catch (InvocationTargetException e) { Log.v(TAG, "Failed to invoke propety getter/setter for: " + methodNameOfGetter + " error: " + e.toString()); } } private class RadioButtonChangeListener implements RadioButton.OnCheckedChangeListener { private final Settings mSettings; private final Method mGetter; private final Method mSetter; public RadioButtonChangeListener(Settings settings, Method getter, Method setter) { mSettings = settings; mGetter = getter; mSetter = setter; } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { try { if (isChecked) { Object currentValueInSettings = mGetter.invoke(mSettings, (Object[]) null); Object valueOfThisRB = ((RadioButton) buttonView).getTag(); if (!currentValueInSettings.equals(valueOfThisRB)) mSetter.invoke(mSettings, valueOfThisRB); } } catch (Exception e) { Log.v(TAG, "Failed checked change listener: " + e.toString()); } } } private void promptForNewPassphrase() { final Dialog alert = new Dialog(this); alert.requestWindowFeature(Window.FEATURE_NO_TITLE); alert.setContentView(R.layout.settings_change_passphrase); final EditText editEnterPassphrase = (EditText) alert.findViewById(R.id.editEnterPassphrase); final EditText editNewPassphrase = (EditText) alert.findViewById(R.id.editNewPassphrase); final EditText editConfirmNewPassphrase = (EditText) alert.findViewById(R.id.editConfirmNewPassphrase); alert.findViewById(R.id.btnOk).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (editNewPassphrase.getText().length() == 0 && editConfirmNewPassphrase.getText().length() == 0) return; // Both empty, ignore click if (!(editNewPassphrase.getText().toString().equals(editConfirmNewPassphrase.getText().toString()))) { Toast.makeText(SettingsActivity.this, getString(R.string.change_passphrase_not_matching), Toast.LENGTH_LONG).show(); alert.dismiss(); promptForNewPassphrase(); return; // Try again... } CacheWordHandler cwh = new CacheWordHandler((Context)SettingsActivity.this, null, null); char[] passwd = editEnterPassphrase.getText().toString().toCharArray(); PassphraseSecrets secrets; try { secrets = PassphraseSecrets.fetchSecrets(SettingsActivity.this, passwd); cwh.changePassphrase(secrets, editNewPassphrase.getText().toString().toCharArray()); Toast.makeText(SettingsActivity.this, getString(R.string.change_passphrase_changed), Toast.LENGTH_LONG).show(); } catch (Exception e) { // Invalid password or the secret key has been Log.e(TAG, e.getMessage()); Toast.makeText(SettingsActivity.this, getString(R.string.change_passphrase_incorrect), Toast.LENGTH_LONG).show(); alert.dismiss(); promptForNewPassphrase(); return; // Try again... } alert.dismiss(); } }); alert.findViewById(R.id.btnCancel).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { alert.cancel(); } }); alert.show(); } /** * Lets the user input a kill passphrase * * @param setToOnIfSuccessful * If true, update the settings if we manage to set the * passphrase. */ private void promptForKillPassphrase(final boolean setToOnIfSuccessful) { final Dialog alert = new Dialog(this); alert.requestWindowFeature(Window.FEATURE_NO_TITLE); alert.setContentView(R.layout.settings_set_kill_passphrase); final EditText editNewPassphrase = (EditText) alert.findViewById(R.id.editNewPassphrase); final EditText editConfirmNewPassphrase = (EditText) alert.findViewById(R.id.editConfirmNewPassphrase); alert.findViewById(R.id.btnOk).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (editNewPassphrase.getText().length() == 0 && editConfirmNewPassphrase.getText().length() == 0) return; // Both empty, ignore click // Check old boolean matching = (editNewPassphrase.getText().toString().equals(editConfirmNewPassphrase.getText().toString())); boolean sameAsPassphrase = false; CacheWordHandler cwh = new CacheWordHandler((Context)SettingsActivity.this, null, null); try { cwh.setPassphrase(editNewPassphrase.getText().toString().toCharArray()); sameAsPassphrase = true; } catch (GeneralSecurityException e) { Log.e(TAG, "Cacheword initialization failed: " + e.getMessage()); } if (!matching || sameAsPassphrase) { editNewPassphrase.setText(""); editConfirmNewPassphrase.setText(""); editNewPassphrase.requestFocus(); if (!matching) Toast.makeText(SettingsActivity.this, getString(R.string.lock_screen_passphrases_not_matching), Toast.LENGTH_LONG).show(); else Toast.makeText(SettingsActivity.this, getString(R.string.settings_security_kill_passphrase_same_as_login), Toast.LENGTH_LONG).show(); alert.dismiss(); promptForKillPassphrase(setToOnIfSuccessful); return; // Try again... } // Store App.getSettings().setKillPassphrase(editNewPassphrase.getText().toString()); if (setToOnIfSuccessful) updateUseKillPassphrase(); alert.dismiss(); } }); alert.findViewById(R.id.btnCancel).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { alert.cancel(); } }); alert.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { if (setToOnIfSuccessful) updateUseKillPassphrase(); } }); alert.show(); } private void updateUseKillPassphrase() { if (!TextUtils.isEmpty(mSettings.killPassphrase())) { mRbUseKillPassphraseOn.setChecked(true); mSettings.setUseKillPassphrase(true); } else { mRbUseKillPassphraseOff.setChecked(true); mSettings.setUseKillPassphrase(false); } } @Override protected void onUiLanguageChanged() { mLanguageBeingUpdated = true; super.onUiLanguageChanged(); } private void collectExpandedGroupViews(View current, ArrayList<Integer> expandedViews) { if (current instanceof ViewGroup) { for (int child = 0; child < ((ViewGroup) current).getChildCount(); child++) collectExpandedGroupViews(((ViewGroup) current).getChildAt(child), expandedViews); } if (current instanceof GroupView) { if (((GroupView) current).getExpanded()) expandedViews.add(Integer.valueOf(current.getId())); } } @Override protected void onSaveInstanceState(Bundle outState) { // Dont call base, see http://stackoverflow.com/questions/4504024/android-localization-problem-not-all-items-in-the-layout-update-properly-when-s //super.onSaveInstanceState(outState); if (mLanguageBeingUpdated) { ArrayList<Integer> expandedViews = new ArrayList<Integer>(); collectExpandedGroupViews(rootView, expandedViews); outState.putIntegerArrayList("expandedViews", expandedViews); } } private void expandSelectedGroupViews(View current, ArrayList<Integer> expandedViews) { if (current instanceof ViewGroup) { for (int child = 0; child < ((ViewGroup) current).getChildCount(); child++) expandSelectedGroupViews(((ViewGroup) current).getChildAt(child), expandedViews); } if (current instanceof GroupView) { if (expandedViews.contains(Integer.valueOf(current.getId()))) ((GroupView) current).setExpanded(true, false); } } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { //super.onRestoreInstanceState(savedInstanceState); if (savedInstanceState.containsKey("expandedViews")) { expandSelectedGroupViews(rootView, savedInstanceState.getIntegerArrayList("expandedViews")); handleGoToGroup(R.id.groupLanguage); } } }