package org.bitseal.activities;
import info.guardianproject.cacheword.CacheWordHandler;
import info.guardianproject.cacheword.ICacheWordSubscriber;
import org.bitseal.R;
import org.bitseal.database.DatabaseContentProvider;
import org.bitseal.services.AppLockHandler;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
/**
* The Activity class for the app's 'Security Settings' screen.
*
* @author Jonathan Coe
*/
public class SecurityActivity extends Activity implements ICacheWordSubscriber
{
private CheckBox databaseEncryptionCheckbox;
private TextView enterPassphraseLabelTextView;
private TextView confirmPassphraseLabelTextView;
private EditText enterPassphraseEditText;
private EditText confirmPassphraseEditText;
private TextWatcher passphraseEditTextsWatcher;
private Button changePassphraseButton;
private Button savePassphraseButton;
private Button cancelPassphraseButton;
private Menu mMenu;
private CacheWordHandler mCacheWordHandler;
/** The minimum length we will allow for a database encryption passphrase */
private static final int MINIMUM_PASSPHRASE_LENGTH = 8;
/** A placeholder String that we use to fill out the passphrase edit texts when a user-defined passphrase
* has previously been set. This means that we avoid leaking the length of the existing passphrase through the UI. */
private static final String PLACEHOLDER_PASSPHRASE = "thisIsARelativelyLongString";
/** The key for a boolean variable that records whether or not the "enable database encryption" checkbox has been selected
* by the user */
private static final String KEY_DATABASE_ENCRYPTION_SELECTED = "databaseEncryptionSelected";
/** The key for a boolean variable that records whether or not a user-defined database encryption passphrase has been saved */
private static final String KEY_DATABASE_PASSPHRASE_SAVED = "databasePassphraseSaved";
private static final String TAG = "SECURITY_ACTIVITY";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_security);
// Connect to the CacheWordService
mCacheWordHandler = new CacheWordHandler(this);
mCacheWordHandler.connectToService();
enterPassphraseLabelTextView = (TextView) findViewById(R.id.security_enter_passphrase_label_textview);
confirmPassphraseLabelTextView = (TextView) findViewById(R.id.security_confirm_passphrase_label_textview);
enterPassphraseEditText = (EditText) findViewById(R.id.security_enter_passphrase_edittext);
confirmPassphraseEditText = (EditText) findViewById(R.id.security_confirm_passphrase_edittext);
passphraseEditTextsWatcher = new TextWatcher()
{
boolean runningInTextWatcher = false; // Used to prevent infinite loops with the TextWatcher causes itself to be called
public void afterTextChanged(Editable s)
{
// Nothing to do here
}
public void beforeTextChanged(CharSequence s, int start, int count, int after)
{
if (runningInTextWatcher)
{
return;
}
runningInTextWatcher = true;
if (enterPassphraseEditText.getText().toString().equals(PLACEHOLDER_PASSPHRASE) && confirmPassphraseEditText.getText().toString().equals(PLACEHOLDER_PASSPHRASE))
{
enterPassphraseEditText.setText("");
confirmPassphraseEditText.setText("");
}
runningInTextWatcher = false;
}
public void onTextChanged(CharSequence s, int start, int before, int count)
{
savePassphraseButton.setVisibility(View.VISIBLE);
cancelPassphraseButton.setVisibility(View.VISIBLE);
savePassphraseButton.setEnabled(true);
cancelPassphraseButton.setEnabled(true);
}
};
enterPassphraseEditText.addTextChangedListener(passphraseEditTextsWatcher);
confirmPassphraseEditText.addTextChangedListener(passphraseEditTextsWatcher);
changePassphraseButton = (Button) findViewById(R.id.security_change_passphrase_button);
changePassphraseButton.setVisibility(View.GONE);
changePassphraseButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.i(TAG, "Security settings change passphrase button clicked");
enterPassphraseEditText.setText(PLACEHOLDER_PASSPHRASE);
confirmPassphraseEditText.setText(PLACEHOLDER_PASSPHRASE);
enterPassphraseEditText.addTextChangedListener(passphraseEditTextsWatcher);
confirmPassphraseEditText.addTextChangedListener(passphraseEditTextsWatcher);
showDatabaseEncryptionUI();
changePassphraseButton.setVisibility(View.GONE);
}
});
savePassphraseButton = (Button) findViewById(R.id.security_save_passphrase_button);
savePassphraseButton.setOnClickListener(new View.OnClickListener()
{
@SuppressLint("InlinedApi")
@Override
public void onClick(View v)
{
Log.i(TAG, "Security settings save passphrase button clicked");
String enteredPassphrase = enterPassphraseEditText.getText().toString();
String confirmedPassphrase = confirmPassphraseEditText.getText().toString();
if (validateDatabasePassphrase(enteredPassphrase, confirmedPassphrase))
{
// Check whether this is the first time that the user has set a database passphrase
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean databasePassphraseSaved= prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false);
if (databasePassphraseSaved)
{
Toast.makeText(getBaseContext(), R.string.security_toast_changing_passphrase, Toast.LENGTH_LONG).show();
savePassphraseButton = (Button) v;
savePassphraseButton.setEnabled(false);
cancelPassphraseButton = (Button) v;
cancelPassphraseButton.setEnabled(false);
// Change the database passphrase
new ChangePassphraseTask().execute(enteredPassphrase);
}
else
{
Toast.makeText(getBaseContext(), R.string.security_toast_encrypting_database, Toast.LENGTH_LONG).show();
// Set the database passphrase for the first time
new SetPassphraseTask().execute(enteredPassphrase);
}
}
}
});
cancelPassphraseButton = (Button) findViewById(R.id.security_cancel_passphrase_button);
cancelPassphraseButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.i(TAG, "Security settings cancel passphrase button clicked");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean databasePassphraseSaved = prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false);
if (databasePassphraseSaved)
{
hideDatabaseEncryptionUI();
changePassphraseButton.setVisibility(View.VISIBLE);
closeKeyboardIfOpen();
}
else
{
clearPassphraseEditTexts();
hideDatabaseEncryptionUI();
databaseEncryptionCheckbox.setChecked(false);
closeKeyboardIfOpen();
}
}
});
databaseEncryptionCheckbox = (CheckBox) findViewById(R.id.security_database_encryption_checkbox);
databaseEncryptionCheckbox.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
if (databaseEncryptionCheckbox.isChecked()) // If the user has just checked the checkbox
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(KEY_DATABASE_ENCRYPTION_SELECTED, true);
editor.commit();
showDatabaseEncryptionUI();
enterPassphraseEditText.requestFocus();
}
else // If the user has just unchecked the checkbox
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean databasePassphraseSaved = prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false);
if (databasePassphraseSaved)
{
openDisableEncryptionConfirmDialog();
}
else
{
clearPassphraseEditTexts();
hideDatabaseEncryptionUI();
databaseEncryptionCheckbox.setChecked(false);
closeKeyboardIfOpen();
}
}
}
});
// Read the Shared Preferences to determine whether or not the database encryption settings should be visible
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
boolean databasePassphraseSaved = prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false);
Log.i(TAG, "Database passphrase saved is set to " + databasePassphraseSaved);
if (databasePassphraseSaved)
{
changePassphraseButton.setVisibility(View.VISIBLE);
databaseEncryptionCheckbox.setChecked(true);
databaseEncryptionCheckbox.setText("Database encryption enabled");
enterPassphraseEditText.setText(PLACEHOLDER_PASSPHRASE);
confirmPassphraseEditText.setText(PLACEHOLDER_PASSPHRASE);
}
else
{
hideDatabaseEncryptionUI();
databaseEncryptionCheckbox.setChecked(false);
}
hideDatabaseEncryptionUI();
}
/**
* Opens a dialog box to confirm that the user wants to disable database encryption
*/
private void openDisableEncryptionConfirmDialog()
{
// Open a dialog to confirm or cancel the creation of a new address
final Dialog disableEncryptionConfirmDialog = new Dialog(SecurityActivity.this);
LinearLayout dialogLayout = (LinearLayout) View.inflate(SecurityActivity.this, R.layout.dialog_security_disable_database_encryption_confirm, null);
disableEncryptionConfirmDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
disableEncryptionConfirmDialog.setContentView(dialogLayout);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
lp.copyFrom(disableEncryptionConfirmDialog.getWindow().getAttributes());
lp.width = WindowManager.LayoutParams.MATCH_PARENT;
disableEncryptionConfirmDialog.show();
disableEncryptionConfirmDialog.getWindow().setAttributes(lp);
Button confirmButton = (Button) dialogLayout.findViewById(R.id.security_disable_database_encryption_confirm_confirm_button);
confirmButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.i(TAG, "Security settings screen enable database encryption confirm button pressed");
Toast.makeText(getBaseContext(), R.string.security_toast_disabling_encryption, Toast.LENGTH_LONG).show();
new RestoreDefaultPassphraseTask().execute();
disableEncryptionConfirmDialog.dismiss();
}
});
Button cancelButton = (Button) dialogLayout.findViewById(R.id.security_disable_database_encryption_confirm_cancel_button);
cancelButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.i(TAG, "Security settings screen enable database encryption cancel button pressed");
databaseEncryptionCheckbox.setChecked(true);
disableEncryptionConfirmDialog.dismiss();
}
});
}
private void showDatabaseEncryptionUI()
{
enterPassphraseLabelTextView.setVisibility(View.VISIBLE);
confirmPassphraseLabelTextView.setVisibility(View.VISIBLE);
enterPassphraseEditText.setVisibility(View.VISIBLE);
confirmPassphraseEditText.setVisibility(View.VISIBLE);
savePassphraseButton.setVisibility(View.VISIBLE);
cancelPassphraseButton.setVisibility(View.VISIBLE);
}
private void hideDatabaseEncryptionUI()
{
enterPassphraseLabelTextView.setVisibility(View.GONE);
confirmPassphraseLabelTextView.setVisibility(View.GONE);
enterPassphraseEditText.setVisibility(View.GONE);
confirmPassphraseEditText.setVisibility(View.GONE);
savePassphraseButton.setVisibility(View.GONE);
cancelPassphraseButton.setVisibility(View.GONE);
}
/**
* Takes a pair Strings and validates them, determining whether or not
* they contain a valid database passphrase.
*
* @param enteredPassphrase - The entered passphrase
* @param confirmedPassphrase - The confirmed passphrase
*
* @return A boolean indicating whether the given Strings are a valid passphrase
*/
private boolean validateDatabasePassphrase(String enteredPassphrase, String confirmedPassphrase)
{
// Check whether the passphrases matched
if (enteredPassphrase.equals(confirmedPassphrase) == false)
{
Toast.makeText(this, R.string.security_toast_passphrases_not_match, Toast.LENGTH_LONG).show();
Log.e(TAG, "The passphrases do not match");
return false;
}
// Check the length of the passphrase
if (enteredPassphrase.length() < MINIMUM_PASSPHRASE_LENGTH)
{
Toast.makeText(this, R.string.security_toast_passphrase_at_least + MINIMUM_PASSPHRASE_LENGTH + R.string.security_toast_characters_long, Toast.LENGTH_LONG).show();
Log.e(TAG, "The passphrase entered is too short - only " + enteredPassphrase.length() + " characters in length.\n" +
"The passphrase must be at least " + MINIMUM_PASSPHRASE_LENGTH + " characters in length");
return false;
}
if (enteredPassphrase.equals(PLACEHOLDER_PASSPHRASE))
{
Toast.makeText(this, R.string.security_toast_passphrase_not_changed, Toast.LENGTH_LONG).show();
Log.e(TAG, "Prevented the user from saving the placeholder string as the database passphrase");
return false;
}
// The passphrases entered appear to match and be valid
return true;
}
/**
* Sets the database passphrase to one specified by the user.
*/
class SetPassphraseTask extends AsyncTask<String, Void, Boolean>
{
@Override
protected Boolean doInBackground(String... enteredPassphrase)
{
return DatabaseContentProvider.changeDatabasePassphrase(enteredPassphrase[0]);
}
@Override
protected void onPostExecute(Boolean success)
{
if (success)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(KEY_DATABASE_PASSPHRASE_SAVED, true);
editor.commit();
databaseEncryptionCheckbox.setChecked(true);
databaseEncryptionCheckbox.setText("Database encryption enabled");
mMenu.add(Menu.NONE, R.id.menu_item_lock, 7, R.string.menu_item_label_lock);
Toast.makeText(getBaseContext(), R.string.security_toast_database_passphrase_set, Toast.LENGTH_LONG).show();
}
onPassphraseModificationResult(success);
}
}
/**
* Attempts change the database encryption passphrase.
*/
class ChangePassphraseTask extends AsyncTask<String, Void, Boolean>
{
@Override
protected Boolean doInBackground(String... enteredPassphrase)
{
return DatabaseContentProvider.changeDatabasePassphrase(enteredPassphrase[0]);
}
@Override
protected void onPostExecute(Boolean success)
{
if (success)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(KEY_DATABASE_PASSPHRASE_SAVED, true);
editor.commit();
databaseEncryptionCheckbox.setChecked(true);
databaseEncryptionCheckbox.setText("Database encryption enabled");
Toast.makeText(getBaseContext(), R.string.security_toast_database_passphrase_changed, Toast.LENGTH_LONG).show();
}
onPassphraseModificationResult(success);
}
}
/**
* Sets the database passphrase to its default value
*/
class RestoreDefaultPassphraseTask extends AsyncTask<Void, Void, Boolean>
{
@Override
protected Boolean doInBackground(Void... params)
{
return DatabaseContentProvider.changeDatabasePassphrase(DatabaseContentProvider.DEFAULT_DATABASE_PASSPHRASE);
}
@Override
protected void onPostExecute(Boolean success)
{
if (success)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(KEY_DATABASE_ENCRYPTION_SELECTED, false);
editor.putBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false);
editor.commit();
hideDatabaseEncryptionUI();
clearPassphraseEditTexts();
changePassphraseButton.setVisibility(View.GONE);
savePassphraseButton.setVisibility(View.GONE);
cancelPassphraseButton.setVisibility(View.GONE);
databaseEncryptionCheckbox.setText("Enable database encryption");
databaseEncryptionCheckbox.setChecked(false);
Toast.makeText(getBaseContext(), R.string.security_toast_database_encryption_disabled, Toast.LENGTH_LONG).show();
}
onPassphraseModificationResult(success);
}
}
/**
* A routine to be run after we have attempted to set or change the database
* encryption passphrase
*
* @param success - Whether the attempt to modify the passphrase was successful
*/
private void onPassphraseModificationResult(boolean success)
{
if (success)
{
closeKeyboardIfOpen();
savePassphraseButton.setVisibility(View.GONE);
cancelPassphraseButton.setVisibility(View.GONE);
// For some reason the cancel button was failing to disappear. Finding it again seems to fix this
cancelPassphraseButton = (Button) findViewById(R.id.security_cancel_passphrase_button);
cancelPassphraseButton.setVisibility(View.GONE);
}
else
{
Toast.makeText(getBaseContext(), R.string.security_toast_error_occurred, Toast.LENGTH_LONG).show();
}
}
/**
* Clears the 'enter passphrase' and 'confirm passphrase' edit texts.
*/
private void clearPassphraseEditTexts()
{
enterPassphraseEditText.setText("");
confirmPassphraseEditText.setText("");
}
/**
* If the soft keyboard is open, this method will close it. Currently only
* works for API 16 and above.
*/
private void closeKeyboardIfOpen()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
{
final View activityRootView = getWindow().getDecorView().getRootView();
final OnGlobalLayoutListener globalListener = new OnGlobalLayoutListener()
{
@Override
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public void onGlobalLayout()
{
Rect rect = new Rect();
// rect will be populated with the coordinates of your view that area still visible.
activityRootView.getWindowVisibleDisplayFrame(rect);
int heightDiff = activityRootView.getRootView().getHeight() - (rect.bottom - rect.top);
if (heightDiff > 100)
{
// If the difference is more than 100 pixels, it's probably caused by the soft keyboard being open. Now we want to close it.
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); // Toggle the soft keyboard.
}
// Now we have to remove the OnGlobalLayoutListener, otherwise we will experience errors
activityRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
};
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(globalListener);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
mMenu = menu;
getMenuInflater().inflate(R.menu.options_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (prefs.getBoolean(KEY_DATABASE_PASSPHRASE_SAVED, false) == false)
{
menu.removeItem(R.id.menu_item_lock);
}
return super.onPrepareOptionsMenu(menu);
}
@SuppressLint("InlinedApi")
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch(item.getItemId())
{
case R.id.menu_item_inbox:
Intent intent1 = new Intent(this, InboxActivity.class);
intent1.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent1);
break;
case R.id.menu_item_sent:
Intent intent2 = new Intent(this, SentActivity.class);
intent2.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent2);
break;
case R.id.menu_item_compose:
Intent intent3 = new Intent(this, ComposeActivity.class);
intent3.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent3);
break;
case R.id.menu_item_identities:
Intent intent4 = new Intent(this, IdentitiesActivity.class);
intent4.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent4);
break;
case R.id.menu_item_addressBook:
Intent intent5 = new Intent(this, AddressBookActivity.class);
intent5.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent5);
break;
case R.id.menu_item_settings:
Intent intent6 = new Intent(this, SettingsActivity.class);
intent6.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent6);
break;
case R.id.menu_item_lock:
AppLockHandler.runLockRoutine(mCacheWordHandler);
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
protected void onStop()
{
super.onStop();
if (mCacheWordHandler != null)
{
mCacheWordHandler.disconnectFromService();
}
}
@SuppressLint("InlinedApi")
@Override
public void onCacheWordLocked()
{
Log.i(TAG, "SecurityActivity.onCacheWordLocked() called.");
// Redirect to the lock screen activity
Intent intent = new Intent(getBaseContext(), LockScreenActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) // FLAG_ACTIVITY_CLEAR_TASK only exists in API 11 and later
{
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);// Clear the stack of activities
}
else
{
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
startActivity(intent);
}
@Override
public void onCacheWordOpened()
{
// Nothing to do here currently
}
@Override
public void onCacheWordUninitialized()
{
// Database encryption is currently not enabled by default, so there is nothing to do here
}
}