Package net.tawacentral.roger.secrets

Source Code of net.tawacentral.roger.secrets.SecretsListActivity

// Copyright (c) 2009, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package net.tawacentral.roger.secrets;

import java.io.File;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import javax.crypto.Cipher;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.KeyguardManager;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.ClipboardManager;
import android.util.Log;
import android.view.ContextMenu;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnTouchListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ScrollView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;

/**
* An activity that handles two main functions: displaying the list of all
* secrets, and modifying an existing secret.  The reason that these two
* activities are combined into one is to take advantage of the 3d page flip
* effect, which basically happens inside one View.
*
* If the 3d transition could be done while transferring from the view of one
* activity to the view of another, I would do that instead, since its more
* natural for an android app.  Because of this hack, I need to override the
* behaviour of the back button to restore the "natural feel" of the back
* button to the user.
*
* @author rogerta
*/
@SuppressWarnings("deprecation")
public class SecretsListActivity extends ListActivity {
  private static final int DIALOG_DELETE_SECRET = 1;
  private static final int DIALOG_CONFIRM_RESTORE = 2;
  private static final int DIALOG_IMPORT_SUCCESS = 3;
  private static final int DIALOG_CHANGE_PASSWORD = 4;
  private static final int DIALOG_ENTER_RESTORE_PASSWORD = 5;

  private static final int PROGRESS_ROUNDS_OFFSET = 4;

  private static final String EMPTY_STRING = "";

  public static final String EXTRA_ACCESS_LOG =
      "net.tawacentreal.secrets.accesslog";

  /** Tag for logging purposes. */
  public static final String LOG_TAG = "SecretsListActivity";

  public static final String STATE_IS_EDITING = "is_editing";
  public static final String STATE_EDITING_POSITION = "editing_position";
  public static final String STATE_EDITING_DESCRIPTION = "editing_description";
  public static final String STATE_EDITING_USERNAME = "editing_username";
  public static final String STATE_EDITING_PASSWORD = "editing_password";
  public static final String STATE_EDITING_EMAIL = "editing_email";
  public static final String STATE_EDITING_NOTES = "editing_notes";

  private SecretsListAdapter secretsList;  // list of secrets
  private Toast toast;  // toast used to show password
  private GestureDetector detector;  // detects taps and double taps
  private boolean isEditing;  // true if changing a secret
  private int editingPosition;  // position of item being edited
  private int cmenuPosition;  // position of item for cmenu
  private View root;  // root of the layout for this activity
  private View edit;  // root view for the editing layout
  private File importedFile;  // File that was imported
  private boolean isConfigChange;  // being destroyed for config change?
  private String restorePoint;  // That file that should be restored from

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle state) {
    Log.d(LOG_TAG, "SecretsListActivity.onCreate");
    super.onCreate(state);
    setContentView(R.layout.list);

    // We should not get a search intent upon launch.
    if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
      Log.e(LOG_TAG, "onCreate: not expecting a search intent");
      finish();
      return;
    }

    // If for any reason we get here and there is no secrets list, then we
    // cannot continue.  Finish the activity and return.
    if (null == LoginActivity.getSecrets()) {
      finish();
      return;
    }

    secretsList = new SecretsListAdapter(this, LoginActivity.getSecrets());
    setTitle();

    setListAdapter(secretsList);
    getListView().setTextFilterEnabled(true);

    // Setup the auto complete adapters for the username and email views.
    AutoCompleteTextView username = (AutoCompleteTextView)
        findViewById(R.id.list_username);
    AutoCompleteTextView email = (AutoCompleteTextView)
        findViewById(R.id.list_email);

    username.setAdapter(secretsList.getUsernameAutoCompleteAdapter());
    email.setAdapter(secretsList.getEmailAutoCompleteAdapter());

    // The 3d flip animation will be done on the root view of this activity.
    // Also get the edit group of views for use as the second view in the
    // animation.
    root = findViewById(R.id.list_container);
    edit = findViewById(R.id.edit_layout);

    // When the SEARCH key is pressed, make sure the global search dialog
    // is displayed.
    setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);

    // If there is state information, use it to initialize the activity.
    if (null != state) {
      isEditing = state.getBoolean(STATE_IS_EDITING);
      if (isEditing) {
        EditText description = (EditText) findViewById(R.id.list_description);
        EditText password = (EditText) findViewById(R.id.list_password);
        EditText notes = (EditText) findViewById(R.id.list_notes);

        editingPosition = state.getInt(STATE_EDITING_POSITION);
        description.setText(state.getCharSequence(STATE_EDITING_DESCRIPTION));
        username.setText(state.getCharSequence(STATE_EDITING_USERNAME));
        password.setText(state.getCharSequence(STATE_EDITING_PASSWORD));
        email.setText(state.getCharSequence(STATE_EDITING_EMAIL));
        notes.setText(state.getCharSequence(STATE_EDITING_NOTES));

        getListView().setVisibility(View.GONE);
        edit.setVisibility(View.VISIBLE);
      }
    }

    // This listener handles click using the scroll wheel.
    if (OS.supportsScrollWheel()) {
      getListView().setOnItemClickListener(new OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position,
            long id) {
          onItemClicked(position);
        }
      });
    }

    // The gesture detector is used to handle taps and double taps.  Its
    // important to handle simple taps here and not just defer to the
    // onItemClickListener, otherwise a double tap will trigger both the
    // onItemClicked() behaviour as well as the double tap behaviour.  By
    // handling simple taps here, we can use the onSingleTapConfirmed() to
    // only do the simple tap behaviour when we are sure there is no double
    // tap as well.
    //
    // However, in order to support device with a scroll wheel, I still need to
    // handle onItemClicked(), causing both behaviours.  This can be
    // fixed for device that do not have a scroll wheel, in which case the
    // we do not need to handle onItemClicked().  However, the API to check
    // if the device has a scroll where was only introduced in Android 2.3.
    GestureDetector.SimpleOnGestureListener listener =
        new GestureDetector.SimpleOnGestureListener() {
          @Override
          public boolean onSingleTapConfirmed(MotionEvent e) {
            int position = getListView().pointToPosition((int)e.getX(),
                                                         (int)e.getY());
            onItemClicked(position);
            return true;
          }
          @Override
          public boolean onDoubleTap(MotionEvent e) {
            int position = getListView().pointToPosition((int)e.getX(),
                                                         (int)e.getY());
            if (AdapterView.INVALID_POSITION != position) {
              SetEditViews(position);
              animateToEditView();
              hideToast();
            }
            return true;
          }
        };
    detector = new GestureDetector(this, listener);
    detector.setOnDoubleTapListener(listener);

    getListView().setOnTouchListener(new OnTouchListener() {
      @Override
      public boolean onTouch(View arg0, MotionEvent event) {
        return detector.onTouchEvent(event);
      }
    });

    registerForContextMenu(getListView());
  }

  private void onItemClicked(int position) {
    if (AdapterView.INVALID_POSITION != position) {
      Secret secret = getSecret(position);
      CharSequence password = secret.getPassword(false);
      if (password.length() == 0)
      password = getText(R.string.no_password);

      showToast(password);
      // TODO(rogerta): to reliably record "view" access, we would want
      // to checkpoint the secrets and save them here.  But doing so
      // causes unacceptable delays is displaying the toast.
      //FileUtils.saveSecrets(SecretsListActivity.this,
      //                      secretsList_.getAllSecrets());
    }
  }

  @Override
  protected void onNewIntent(Intent intent) {
    // This method is invoked when the user performs a search from the global
    // search dialog.

    // Get the search string.  Make it a full text search by ensuring the
    // string begins with a dot.
    setIntent(intent);
    String filter = intent.getStringExtra(SearchManager.QUERY);
    if (filter.charAt(0) != SecretsListAdapter.DOT)
      filter = SecretsListAdapter.DOT + filter;

    getListView().setFilterText(filter);
    getListView().requestFocus();
  }

  @Override
  public boolean onSearchRequested() {
    // Don't allow search in edit mode.
    if (isEditing)
      return true;

    return super.onSearchRequested();
  }

  /**
   * Check to see if the key guard is enabled.  If so, its means the device
   * probably went to sleep due to inactivity.  If this is the case, this
   * activity is finished().
   *
   * @return True if the activity is finished, false otherwise.
   */
  private boolean checkKeyguard() {
    // If the key guard has been displayed, exit this activity.  This returns
    // us to the login page requiring the user to enter his password again
    // before getting access again to his secrets.
    KeyguardManager key_guard = (KeyguardManager) getSystemService(
        KEYGUARD_SERVICE);
    boolean isInputRestricted = key_guard.inKeyguardRestrictedInputMode();
    if (isInputRestricted) {
      Log.d(LOG_TAG, "SecretsListActivity.checkKeyguard finishing");
      finish();
    }

    return isInputRestricted;
  }

  /** Set the title for this activity. */
  public void setTitle() {
    CharSequence title;
    int allCount = secretsList.getAllSecrets().size();
    int count = secretsList.getCount();
    if (allCount > 0) {
      if (allCount != count) {
        String template = getText(R.string.list_title_filtered).toString();
        title = MessageFormat.format(template, count, allCount);
      } else {
        String template = getText(R.string.list_title).toString();
        title = MessageFormat.format(template, allCount);
      }
    } else {
      title = getText(R.string.list_no_data);
    }

    setTitle(title);
  }

  @Override
  protected void onResume() {
    Log.d(LOG_TAG, "SecretsListActivity.onResume");
    super.onResume();

    // If checkKeyguard() returns true, then this activity has been finished.
    // We don't want to execute any more in this function.
    if (checkKeyguard())
      return;

    // Show instruction toast auto popup options menu if there are no secrets
    // in the list.  This check used to be done in the onCreate() method above,
    // that could occasionally cause a crash when changing layout from
    // portrait to landscape, or back.  Not sure why exactly, but I suspect
    // its because the UI elements are not actually ready to be rendered until
    // onResume() is called.
    if (0 == secretsList.getAllSecrets().size() && !isEditing) {
      showToast(getText(R.string.list_no_data));
      getListView().post(new Runnable() {
        @Override
        public void run() {
          openOptionsMenu();
        }
      });
    } else if (FileUtils.isRestoreFileTooOld()) {
      getListView().post(new Runnable() {
        @Override
        public void run() {
          showToast(getText(R.string.restore_file_too_old));
          openOptionsMenu();
        }
      });
    }
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.list_menu, menu);

    OS.configureSearchView(this, menu);
    return true;
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    // We must always set the state of all the buttons, since we don't know
    // their states before this method is called.

    boolean secretsListEmpty = (secretsList == null) || secretsList.isEmpty();
    menu.findItem(R.id.list_add).setVisible(!isEditing);
    menu.findItem(R.id.list_backup).setVisible(!isEditing && !secretsListEmpty);
    menu.findItem(R.id.list_search).setVisible(!isEditing);
    menu.findItem(R.id.list_restore).setVisible(!isEditing);
    menu.findItem(R.id.list_import).setVisible(!isEditing);
    menu.findItem(R.id.list_export).setVisible(!isEditing && !secretsListEmpty);
    menu.findItem(R.id.list_menu_change_password).setVisible(!isEditing);

    menu.findItem(R.id.list_save).setVisible(isEditing);
    menu.findItem(R.id.list_generate_password).setVisible(isEditing);
    menu.findItem(R.id.list_discard).setVisible(isEditing);

    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    boolean handled = false;

    // TODO(rogerta): when using this menu to finish the editing activity, for
    // some reason the selected item in the list view is not highlighted.  Need
    // to figure out what the interaction with the menu is.  This does not
    // happen when using the back button to finish the editing activity.
    switch (item.getItemId()) {
      case R.id.list_add:
          SetEditViews(AdapterView.INVALID_POSITION);
          animateToEditView();
          break;
      case R.id.list_search:
        onSearchRequested();
        break;
      case R.id.list_save:
        saveSecret();
        // NO BREAK
      case R.id.list_discard:
        animateFromEditView();
        break;
      case R.id.list_generate_password: {
        String pwd = generatePassword();
        EditText password = (EditText) findViewById(R.id.list_password);
        password.setText(pwd);
        break;
      }
      case R.id.list_backup:
        backupSecrets();
        break;
      case R.id.list_restore:
        showDialog(DIALOG_CONFIRM_RESTORE);
        break;
      case R.id.list_export:
        exportSecrets();
        break;
      case R.id.list_import:
        importSecrets();
        break;
      case R.id.list_menu_change_password:
        showDialog(DIALOG_CHANGE_PASSWORD);
        break;
      default:
        break;
    }

    return handled;
  }

  @Override
  public void onCreateContextMenu(ContextMenu menu, View v,
      ContextMenuInfo menuInfo) {
    hideToast();
    AdapterView.AdapterContextMenuInfo  info =
        (AdapterContextMenuInfo) menuInfo;
    cmenuPosition = info.position;
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.list_cmenu, menu);
    Secret secret = secretsList.getSecret(cmenuPosition);
    menu.setHeaderTitle(secret.getDescription());
  }

  @Override
  public boolean onContextItemSelected(MenuItem item) {
    boolean handled = false;
    switch (item.getItemId()) {
      case R.id.list_edit:
        SetEditViews(cmenuPosition);
        animateToEditView();
        break;
      case R.id.list_delete:
        if (AdapterView.INVALID_POSITION != cmenuPosition) {
          showDialog(DIALOG_DELETE_SECRET);
        }
        break;
      case R.id.list_access: {
        // TODO(rogerta): maybe just stuff the index into the intent instead
        // of serializing the whole secret, it seems to be slow.
        Secret secret = secretsList.getSecret(cmenuPosition);
        Intent intent = new Intent(this, AccessLogActivity.class);
        intent.putExtra(EXTRA_ACCESS_LOG, secret);
        startActivity(intent);
        break;
      }
      case R.id.list_copy_password_to_clipboard:
      case R.id.list_copy_username_to_clipboard: {
        Secret secret = secretsList.getSecret(cmenuPosition);
        ClipboardManager cm = (ClipboardManager) getSystemService(
            CLIPBOARD_SERVICE);
        int typeId;
        if (item.getItemId() == R.id.list_copy_password_to_clipboard) {
          cm.setText(secret.getPassword(false));
          typeId = R.string.password_copied_to_clipboard;
        } else {
          cm.setText(secret.getUsername());
          typeId = R.string.username_copied_to_clipboard;
        }
        String template = getText(R.string.copied_to_clipboard).toString();
        String typeOfCopy = getText(typeId).toString();
        String msg = MessageFormat.format(template, secret.getDescription(),
                                          typeOfCopy);
        showToast(msg);
        break;
      }
    }

    return handled;
  }

  /** Import from a CSV file on the SD card. */
  private void importSecrets() {
    importedFile = FileUtils.getFileToImport();
    if (null == importedFile) {
      String template = getText(R.string.import_not_found).toString();
      String msg = MessageFormat.format(template, FileUtils.getCsvFileNames());
      showToast(msg);
      return;
    }

    ArrayList<Secret> secrets = new ArrayList<Secret>();
    boolean allSucceeded = FileUtils.importSecrets(this, importedFile, secrets);

    if (!secrets.isEmpty()) {
      for (Secret secret : secrets) {
        secretsList.insert(secret);
      }

      secretsList.notifyDataSetChanged();
      setTitle();

      if (allSucceeded) {
        showDialog(DIALOG_IMPORT_SUCCESS);
      } else {
        String template = getText(R.string.import_partial).toString();
        String msg = MessageFormat.format(template, importedFile.getName());
        showToast(msg);
      }
    } else {
      String template = getText(R.string.import_failed).toString();
      String msg = MessageFormat.format(template, importedFile.getName());
      showToast(msg);
    }
  }

  private void deleteImportedFile() {
    if (null != importedFile) {
      importedFile.delete();
      importedFile = null;
    }
  }

  private void exportSecrets() {
    // Export everything to the SD card.
    if (FileUtils.exportSecrets(this, secretsList.getAllSecrets())) {
      showToast(R.string.export_succeeded);
    } else {
      showToast(R.string.export_failed);
    }
  }

  /** Restore secrets from the given restore point.
   *
   * @param rp The name of the restore point to restore from.
   * @param info A CipherInfo structure describing the decryption cipher to use.
   *
   * @return True if the restore succeeded, false otherwise.
   */
  private boolean restoreSecrets(String rp, SecurityUtils.CipherInfo info) {
    // Restore everything to the SD card.
    ArrayList<Secret> secrets = FileUtils.restoreSecrets(this, rp, info);
    if (null == secrets) {
      restorePoint = rp;
      showDialog(DIALOG_ENTER_RESTORE_PASSWORD);
      return false;
    }

    LoginActivity.restoreSecrets(secrets);
    secretsList.notifyDataSetChanged();
    setTitle();
    return true;
  }

  private void backupSecrets() {
    // Backup everything to the SD card.
    Cipher cipher = SecurityUtils.getEncryptionCipher();
    byte[] salt = SecurityUtils.getSalt();
    int rounds = SecurityUtils.getRounds();
    if (FileUtils.backupSecrets(this, cipher, salt, rounds,
                                secretsList.getAllSecrets())) {
      showToast(R.string.backup_succeeded);
    } else {
      showToast(R.string.error_save_secrets);
    }
  }

  /** Holds the currently chosen item in the restore dialog. */
  private class RestoreDialogState {
    public int selected = 0;
    private List<String> restorePoints;

    /** Get an array of choices for the restore dialog. */
    public CharSequence[] getRestoreChoices() {
      restorePoints = FileUtils.getRestorePoints(SecretsListActivity.this);
      return restorePoints.toArray(new CharSequence[restorePoints.size()]);
    }

    public String getSelectedRestorePoint() {
      return restorePoints.get(selected);
    }
  }

  @Override
  public Dialog onCreateDialog(int id) {
    Dialog dialog = null;

    switch (id) {
      case DIALOG_DELETE_SECRET: {
        // NOTE: the assumption at this point is that position is valid,
        // otherwise we would never get here because of the check done
        // in onOptionsItemSelected().
        DialogInterface.OnClickListener listener =
            new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                if (DialogInterface.BUTTON1 == which) {
                  deleteSecret(cmenuPosition);
                }
              }
            };

        // NOTE: the message part of this dialog is dynamic, so its value is
        // set in onPrepareDialog() below.  However, its important to set it
        // to something here, even the empty string, so that the setMessage()
        // call done later actually has an effect.
        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.list_menu_delete)
            .setIcon(android.R.drawable.ic_dialog_alert)
            .setMessage(EMPTY_STRING)
            .setPositiveButton(R.string.login_reset_password_pos, listener)
            .setNegativeButton(R.string.login_reset_password_neg, null)
            .create();
        break;
      }
      case DIALOG_CONFIRM_RESTORE: {
        final RestoreDialogState state = new RestoreDialogState();

        DialogInterface.OnClickListener itemListener =
          new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
              state.selected = which;
              dialog.dismiss();
              SecurityUtils.CipherInfo info = SecurityUtils.getCipherInfo();
              if (restoreSecrets(state.getSelectedRestorePoint(), info))
                showToast(R.string.restore_succeeded);
            }
          };

        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.dialog_restore_title)
            .setIcon(android.R.drawable.ic_dialog_alert)
            .setSingleChoiceItems(state.getRestoreChoices(),
                                  state.selected,
                                  itemListener)
            .create();
        break;
      }
      case DIALOG_IMPORT_SUCCESS: {
        DialogInterface.OnClickListener listener =
          new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
              if (DialogInterface.BUTTON1 == which) {
                deleteImportedFile();
              }

              importedFile = null;
            }
          };

        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.list_menu_import)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setMessage(EMPTY_STRING)
            .setPositiveButton(R.string.login_reset_password_pos, listener)
            .setNegativeButton(R.string.login_reset_password_neg, null)
            .create();
        break;
      }
      case DIALOG_CHANGE_PASSWORD: {
        DialogInterface.OnClickListener listener =
          new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogi, int which) {
              AlertDialog dialog = (AlertDialog) dialogi;
              TextView password1 = (TextView) dialog.findViewById(
                  R.id.password);
              TextView password2 = (TextView) dialog.findViewById(
                  R.id.password_validation);
              String password = password1.getText().toString();
              String p2 = password2.getText().toString();
              if (!password.equals(p2) || password.length() == 0) {
                showToast(R.string.invalid_password);
                return;
              }

              SeekBar bar = (SeekBar) dialog.findViewById(R.id.cipher_strength);
              byte[] salt = SecurityUtils.getSalt();
              int rounds = bar.getProgress() + PROGRESS_ROUNDS_OFFSET;

              SecurityUtils.CipherInfo info = SecurityUtils.createCiphers(
                  password, salt, rounds);
              if (null != info) {
                SecurityUtils.saveCiphers(info);
                showToast(R.string.password_changed);
              } else {
                showToast(R.string.error_reset_password);
              }
            }
          };

        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(R.layout.change_password, getListView(),
                                     false);

        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.list_menu_change_password)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setView(view)
            .setPositiveButton(R.string.list_menu_change_password, listener)
            .create();
        final Dialog dialogFinal = dialog;
       
        SeekBar bar = (SeekBar) view.findViewById(R.id.cipher_strength);
        bar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
          @Override
          public void onProgressChanged(SeekBar seekBar, int progress,
              boolean fromUser) {
            setCipherStrengthLabel(dialogFinal, progress +
                                   PROGRESS_ROUNDS_OFFSET);
          }
          @Override
          public void onStartTrackingTouch(SeekBar seekBar) {
          }
          @Override
          public void onStopTrackingTouch(SeekBar seekBar) {
          }});
        break;
      }
      case DIALOG_ENTER_RESTORE_PASSWORD: {
        DialogInterface.OnClickListener listener =
          new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogi, int which) {
              AlertDialog dialog = (AlertDialog) dialogi;
              TextView password1 = (TextView) dialog.findViewById(
                  R.id.password);

              String password = password1.getText().toString();
              FileUtils.SaltAndRounds saltAndRounds =
                  FileUtils.getSaltAndRounds(null, restorePoint);
             
              SecurityUtils.CipherInfo info =  SecurityUtils.createCiphers(
                  password, saltAndRounds.salt, saltAndRounds.rounds);
              if (restoreSecrets(restorePoint, info)) {
                SecurityUtils.clearCiphers();
                SecurityUtils.saveCiphers(info);

                String message = getText(R.string.password_changed).toString();
                message += '\n';
                message += getText(R.string.restore_succeeded).toString();
                showToast(message);
              } else {
                showToast(R.string.restore_failed);
              }
            }
          };

        LayoutInflater inflater = getLayoutInflater();
        View view = inflater.inflate(R.layout.change_password, getListView(),
                                     false);

        // For this dialog, we don't want to show the seek bar nor the
        // confirmation password field.
        view.findViewById(R.id.cipher_strength).setVisibility(View.GONE);
        view.findViewById(R.id.cipher_strength_label).setVisibility(View.GONE);
        view.findViewById(R.id.password_validation).setVisibility(View.GONE);
            view.findViewById(R.id.password_validation_label)
            .setVisibility(View.GONE);

        dialog = new AlertDialog.Builder(this)
            .setTitle(R.string.login_enter_password)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setView(view)
            .setPositiveButton(R.string.list_menu_restore, listener)
            .create();
        break;
      }
      default:
        break;
    }

    return dialog;
  }

  private void setCipherStrengthLabel(Dialog dialog, int rounds) {
    String template =
      getText(R.string.cipher_strength_label).toString();
    String msg = MessageFormat.format(template, rounds);
    TextView text = (TextView) dialog.findViewById(
        R.id.cipher_strength_label);
    text.setText(msg);
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

    switch(id) {
      case DIALOG_DELETE_SECRET: {
        AlertDialog alert = (AlertDialog) dialog;
        Secret secret = secretsList.getSecret(cmenuPosition);
        String template = getText(R.string.edit_menu_delete_secret_message).
            toString();
        String msg = MessageFormat.format(template, secret.getDescription());
        alert.setMessage(msg);
        break;
      }
      case DIALOG_IMPORT_SUCCESS: {
        AlertDialog alert = (AlertDialog) dialog;
        String template =
            getText(R.string.edit_menu_import_secrets_message).toString();
        String msg = MessageFormat.format(template, importedFile.getName());
        alert.setMessage(msg);
        break;
      }
      case DIALOG_CHANGE_PASSWORD: {
        SeekBar bar = (SeekBar) dialog.findViewById(R.id.cipher_strength);
        int rounds = SecurityUtils.getRounds();
        bar.setProgress(rounds - PROGRESS_ROUNDS_OFFSET);
        setCipherStrengthLabel(dialog, rounds);
        TextView password1 = (TextView) dialog.findViewById(
            R.id.password);
        password1.setText("");
        TextView password2 = (TextView) dialog.findViewById(
            R.id.password_validation);
        password2.setText("");
        password1.requestFocus();
        break;
      }
      case DIALOG_ENTER_RESTORE_PASSWORD: {
        TextView password1 = (TextView) dialog.findViewById(R.id.password);
        password1.setText("");
        break;
      }
    }
  }

  /**
   * Trap the "back" button to simulate going back from the secret edit
   * view to the list view.  Note this needs to be done in key-down and not
   * key-up, since the system's default action for "back" happen on key-down.
   */
  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (isEditing && KeyEvent.KEYCODE_BACK == keyCode) {
      saveSecret();
      animateFromEditView();
      return true;
    }

    return super.onKeyDown(keyCode, event);
  }

  /**
   * Called before the view is to be destroyed, so we can save state.  Its
   * important to use this method for saving state if the user happens to
   * open/close the keyboard while the view is displayed.
   */
  @Override
  protected void onSaveInstanceState(Bundle state) {
    Log.d(LOG_TAG, "SecretsListActivity.onSaveInstanceState");
    super.onSaveInstanceState(state);

    // Save our state for later.
    state.putBoolean(STATE_IS_EDITING, isEditing);

    if (isEditing) {
      saveSecret();

      EditText description = (EditText) findViewById(R.id.list_description);
      EditText username = (EditText) findViewById(R.id.list_username);
      EditText password = (EditText) findViewById(R.id.list_password);
      EditText email = (EditText) findViewById(R.id.list_email);
      EditText notes = (EditText) findViewById(R.id.list_notes);

      state.putInt(STATE_EDITING_POSITION, editingPosition);
      state.putCharSequence(STATE_EDITING_DESCRIPTION, description.getText());
      state.putCharSequence(STATE_EDITING_USERNAME, username.getText());
      state.putCharSequence(STATE_EDITING_PASSWORD, password.getText());
      state.putCharSequence(STATE_EDITING_EMAIL, email.getText());
      state.putCharSequence(STATE_EDITING_NOTES, notes.getText());
    }

    Log.d(LOG_TAG, "SecretsListActivity.onSaveInstanceState");
  }

  /** Called when the activity is no longer visible. */
  @Override
  protected void onPause() {
    Log.d(LOG_TAG, "SecretsListActivity.onPause");

    // Cancel any toast that may currently be displayed.
    if (null != toast)
      toast.cancel();

    // Do the save in the background, so that we don't block the UI thread.
    // For people with lots and lots of secrets, it can take a long time to
    // save, and they may get a "force close" dialog if the save was done in
    // the UI thread.
    //
    // The issue is that we cannot give the user feedback about the save,
    // unless I use a notification (need to look into that).  Also, because
    // the process hangs around, this thread should continue running until
    // completion even if the user switches to another task/application.
    List<Secret> secrets = secretsList.getAllSecrets();
    Cipher cipher = SecurityUtils.getEncryptionCipher();
    byte[] salt = SecurityUtils.getSalt();
    int rounds = SecurityUtils.getRounds();
    SaveService.execute(this, secrets, cipher, salt, rounds);
    super.onPause();
  }

  /**
   * This method is called when the activity is being destroyed and recreated
   * due to a configuration change, such as the keyboard being opened or closed.
   * Its called after onPause() and onSaveInstanceState(), but before
   * onDestroy().
   *
   * When this is called, I will set a boolean value so that onDestroy() will
   * not clear the secrets data.
   */
  @Override
  public Object onRetainNonConfigurationInstance() {
    Log.d(LOG_TAG, "SecretsListActivity.onRetainNonConfigurationInstance");
    isConfigChange = true;
    return super.onRetainNonConfigurationInstance();
  }

  /** Called before activity is destroyed. */
  @Override
  protected void onDestroy() {
    // Don't clear the secrets if this is a configuration change, since we
    // are going to need it immediately anyway.  We do want to clear it in
    // other circumstances, otherwise the login activity will ignore attempt
    // to login again.
    if (!isConfigChange) {
      Log.d(LOG_TAG, "SecretsListActivity.onDestroy");
      LoginActivity.clearSecrets();
    }

    super.onDestroy();
  }

  /**
   * Set the secret specified by the given position in the list into the
   * edit fields used to modify the secret.  Position 0 means "add secret".
   *
   * @param position Position of secret to edit.
   */
  private void SetEditViews(int position) {
    editingPosition = position;

    EditText description = (EditText) findViewById(R.id.list_description);
    EditText username = (EditText) findViewById(R.id.list_username);
    EditText password = (EditText) findViewById(R.id.list_password);
    EditText email = (EditText) findViewById(R.id.list_email);
    EditText notes = (EditText) findViewById(R.id.list_notes);

    if (AdapterView.INVALID_POSITION == position) {
      description.setText(EMPTY_STRING);
      username.setText(EMPTY_STRING);
      password.setText(EMPTY_STRING);
      email.setText(EMPTY_STRING);
      notes.setText(EMPTY_STRING);

      description.requestFocus();
    } else {
      Secret secret = secretsList.getSecret(position);

      description.setText(secret.getDescription());
      username.setText(secret.getUsername());
      password.setText(secret.getPassword(false));
      email.setText(secret.getEmail());
      notes.setText(secret.getNote());

      password.requestFocus();
    }

    ScrollView scroll = (ScrollView) findViewById(R.id.edit_layout);
    scroll.scrollTo(0, 0);
  }

  /**
   * Save the current values in the edit views into the current secret being
   * edited.  If the current secret is at position 0, this means add a new
   * secret.
   *
   * Secrets will be added in alphabetical order by description.
   *
   * All secrets are flushed to persistent storage.
   */
  private void saveSecret() {
    EditText description = (EditText) findViewById(R.id.list_description);
    EditText username = (EditText) findViewById(R.id.list_username);
    EditText password = (EditText) findViewById(R.id.list_password);
    EditText email = (EditText) findViewById(R.id.list_email);
    EditText notes = (EditText) findViewById(R.id.list_notes);

    // If all the text views are blank, then don't do anything if we are
    // supposed to be adding a secret.  Also, if all the views are
    // the same as the current secret, don't do anything either.
    Secret secret;
    String description_text = description.getText().toString();
    String username_text = username.getText().toString();
    String password_text = password.getText().toString();
    String email_text = email.getText().toString();
    String note_text = notes.getText().toString();

    if (AdapterView.INVALID_POSITION == editingPosition) {
      if (0 == description.getText().length() &&
          0 == username.getText().length() &&
          0 == password.getText().length() &&
          0 == email.getText().length() &&
          0 == notes.getText().length())
        return;

      secret = new Secret();
    } else {
      secret = secretsList.getSecret(editingPosition);

      if (description_text.equals(secret.getDescription()) &&
          username_text.equals(secret.getUsername()) &&
          password_text.equals(secret.getPassword(false)) &&
          email_text.equals(secret.getEmail()) &&
          note_text.equals(secret.getNote()))
        return;

      secretsList.remove(editingPosition);
    }

    secret.setDescription(description.getText().toString());
    secret.setUsername(username.getText().toString());
    secret.setPassword(password.getText().toString());
    secret.setEmail(email.getText().toString());
    secret.setNote(notes.getText().toString());

    editingPosition = secretsList.insert(secret);
    secretsList.notifyDataSetChanged();
  }

  /**
   * Delete the secret at the given position. If the user is currently editing
   * a secret, he is returned to the list. */
  public void deleteSecret(int position) {
    if (AdapterView.INVALID_POSITION != position) {
      secretsList.remove(position);
      secretsList.notifyDataSetChanged();

      // TODO(rogerta): is this is really a performance issue to save here?
      //if (!FileUtils.saveSecrets(this, secretsList_.getAllSecrets()))
      //  showToast(R.string.error_save_secrets);
      if (isEditing) {
        // We need to clear the edit position so that when the animation is
        // done, the code does not try to make visible a secret that no longer
        // exists.  This was causing a crash (issue 16).
        editingPosition = AdapterView.INVALID_POSITION;
        animateFromEditView();
      } else {
        setTitle();
      }
    }
  }

  /**
   * Show a toast on the screen with the given message.  If a toast is already
   * being displayed, the message is replaced and timer is restarted.
   *
   * @param message Resource id of the text to display in the toast.
   */
  private void showToast(int message) {
    showToast(getText(message));
  }

  /**
   * Show a toast on the screen with the given message.  If a toast is already
   * being displayed, the message is replaced and timer is restarted.
   *
   * @param message Text to display in the toast.
   */
  private void showToast(CharSequence message) {
    if (null == toast) {
      toast = Toast.makeText(SecretsListActivity.this, message,
          Toast.LENGTH_LONG);
      toast.setGravity(Gravity.CENTER, 0, 0);
    } else {
      toast.setText(message);
    }

    toast.show();
  }

  /** Hide the toast, if any. */
  private void hideToast() {
    if (null != toast) {
      toast.cancel();
    }
  }

  /** Get the secret at the specified position in the list. */
  private Secret getSecret(int position) {
    return (Secret) getListAdapter().getItem(position);
  }

  /**
   * Start the view animation that transitions from the list of secrets to
   * the secret edit view.
   */
  private void animateToEditView() {
    assert(!isEditing);
    isEditing = true;

    // Cancel any toast and soft keyboard that may currently be displayed.
    if (null != toast)
      toast.cancel();

    OS.hideSoftKeyboard(this, getListView());
    OS.invalidateOptionsMenu(this);

    View list = getListView();
    int cx = root.getWidth() / 2;
    int cy = root.getHeight() / 2;
    Animation animation = new Flip3dAnimation(list, edit, cx, cy, true);
    animation.setAnimationListener(new AnimationListener() {
      @Override
      public void onAnimationEnd(Animation animation) {
        hideToast();
        if (0 == secretsList.getCount()) {
          showToast(getText(R.string.edit_instructions));
        }
      }
      @Override
      public void onAnimationRepeat(Animation animation) {
      }
      @Override
      public void onAnimationStart(Animation animation) {
      }
    });

    root.startAnimation(animation);
  }

  /**
   * Start the view animation that transitions from the secret edit view to
   * the list of secrets.
   */
  private void animateFromEditView() {
    assert(isEditing);
    isEditing = false;

    OS.hideSoftKeyboard(this, getListView());
    OS.invalidateOptionsMenu(this);

    View list = getListView();
    int cx = root.getWidth() / 2;
    int cy = root.getHeight() / 2;
    Animation animation = new Flip3dAnimation(list, edit, cx, cy, false);
    animation.setAnimationListener(new AnimationListener() {
      @Override
      public void onAnimationEnd(Animation animation) {
        if (AdapterView.INVALID_POSITION != editingPosition) {
          ListView listView = getListView();
          listView.requestFocus();
         
          int first = listView.getFirstVisiblePosition();
          int last = listView.getLastVisiblePosition();
          if (editingPosition < first || editingPosition > last) {
            listView.setSelection(editingPosition);
          }
        }

        setTitle();

        if (1 == secretsList.getAllSecrets().size()) {
          showToast(getText(R.string.list_instructions));
        }
      }
      @Override
      public void onAnimationRepeat(Animation animation) {
      }
      @Override
      public void onAnimationStart(Animation animation) {
      }
    });
    root.startAnimation(animation);
  }

  /** Generate and return a difficult to guess password. */
  private String generatePassword() {
    StringBuilder builder = new StringBuilder(8);
    try {
      SecureRandom r = SecureRandom.getInstance("SHA1PRNG");
      final String p = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                       "abcdefghijklmnopqrstuvwxyz" +
                       "abcdefghijklmnopqrstuvwxyz" +
                       "0123456789" +
                       "0123456789" +
                       "~!@#$%^&*()_+`-=[]{}|;':,./<>?";

      for (int i = 0; i < 8; ++i)
        builder.append(p.charAt(r.nextInt(128)));
    } catch (NoSuchAlgorithmException ex) {
      Log.e(LOG_TAG, "generatePassword", ex);
    }
    return builder.toString();
  }
}
TOP

Related Classes of net.tawacentral.roger.secrets.SecretsListActivity

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.