package org.bitseal.activities;
import info.guardianproject.cacheword.CacheWordHandler;
import info.guardianproject.cacheword.ICacheWordSubscriber;
import java.security.GeneralSecurityException;
import java.util.Timer;
import java.util.TimerTask;
import org.bitseal.R;
import org.bitseal.services.NotificationsService;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
/**
* The Activity class for the app's 'Lock' screen.
*
* @author Jonathan Coe
*/
public class LockScreenActivity extends Activity implements ICacheWordSubscriber
{
private EditText enterPassphraseEditText;
private ImageView unlockIcon;
private Context mActivityContext;
private CacheWordHandler mCacheWordHandler;
private TimerTask mUnlockAttemptTimerTask;
/** Records whether onCacheWordOpened() has been called during the lifetime of this activity */
private boolean mOnCacheWordOpenedCalled;
/** This variable is used as a flag to detect times when attempts to unlock the database hang without giving any result */
private boolean mLastUnlockAttemptCompleted;
/** The minimum length we will allow for a database encryption passphrase */
private static final int MINIMUM_PASSPHRASE_LENGTH = 8;
/** Signals to the InboxActivity that the database has just been unlocked, so it should not redirect
* the user to the lock screen. */
public static final String EXTRA_DATABASE_UNLOCKED = "lockScreenActivity.DATABASE_UNLOCKED";
private static final String TAG = "LOCK_SCREEN_ACTIVITY";
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lock_screen);
mActivityContext = this;
mCacheWordHandler = new CacheWordHandler(this);
mCacheWordHandler.connectToService();
enterPassphraseEditText = (EditText) findViewById(R.id.lock_screen_enter_passphrase_edittext);
unlockIcon = (ImageView) findViewById(R.id.lock_screen_unlock_icon_imageview);
unlockIcon.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
Log.i(TAG, "Lock screen unlock button clicked");
unlockIcon.setClickable(false);
String enteredPassphrase = enterPassphraseEditText.getText().toString();
// Validate the passphrase entered by the user
if (validatePassphrase(enteredPassphrase))
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
{
// Post Jelly Bean devices should generally be fast enough that the app will either unlock or show an 'invalid' message
// almost instantly, so showing this message is only useful on older, slower devices.
Toast.makeText(getBaseContext(), R.string.lock_screen_toast_checking_passphrase, Toast.LENGTH_SHORT).show();
}
new AttemptUnlockTask().execute(new String[]{enteredPassphrase}); // Attempt to unlock the app using the passphrase entered by the user
}
else
{
Toast.makeText(getBaseContext(), R.string.lock_screen_toast_invalid_passphrase, Toast.LENGTH_SHORT).show();
unlockIcon.setClickable(true);
}
}
});
}
/**
* Validates a passphrase entered by the user
*
* @param passphrase - The passphrase
*
* @return A boolean indicating whether or not the passphrase is valid
*/
private boolean validatePassphrase(String passphrase)
{
// Check the length of the passphrase
if (passphrase.length() < MINIMUM_PASSPHRASE_LENGTH)
{
Log.i(TAG, "The passphrase entered is too short - only " + passphrase.length() + " characters in length.\n" +
"The passphrase must be at least " + MINIMUM_PASSPHRASE_LENGTH + " characters in length");
return false;
}
return true;
}
/**
* Attempts to unlock the encrypted database using the passphrase provided.
*/
class AttemptUnlockTask extends AsyncTask<String, Void, Boolean>
{
@Override
protected Boolean doInBackground(String... enteredPassphrase)
{
Log.i(TAG, "LockScreenActivity.AttemptUnlockTask.execute() called");
// Sometimes the call to mCacheWordHandler.setPassphrase() can hang indefinitely, with no result or exception.
// Therefore we will set a timer task to detect when this happens and handle it.
mLastUnlockAttemptCompleted = false;
mUnlockAttemptTimerTask = new TimerTask()
{
@Override
public void run()
{
if (mLastUnlockAttemptCompleted == false)
{
Log.e(TAG, "We detected that the last unlock attempt hung, without giving any result. We will now attempt to handle this error.");
unlockIcon.setClickable(true);
mCacheWordHandler = new CacheWordHandler(mActivityContext);
mCacheWordHandler.connectToService();
}
}
};
new Timer().schedule(mUnlockAttemptTimerTask, 4000); // Run after 4 seconds
try
{
mCacheWordHandler.setPassphrase(enteredPassphrase[0].toCharArray());
}
catch (GeneralSecurityException e) // If the passphrase is invalid, a GeneralSecurityException will be thrown
{
Log.i(TAG, "The passphrase entered by the user was invalid, resulting in a GeneralSecurityException");
mLastUnlockAttemptCompleted = true;
return false;
}
catch (Exception e)
{
Log.e(TAG, "Cacheword passphrase verification failed, throwing an unknown exception. The exception message was:\n"
+ e.getMessage());
mLastUnlockAttemptCompleted = true;
return false;
}
mLastUnlockAttemptCompleted = true;
return true;
}
@Override
protected void onPostExecute(Boolean passphraseCorrect)
{
unlockIcon.setClickable(true);
if (passphraseCorrect)
{
openBitseal();
}
else
{
Toast.makeText(getBaseContext(), R.string.lock_screen_toast_invalid_passphrase, Toast.LENGTH_SHORT).show();
}
}
}
/**
* Checks whether the correct conditions to open the app have been met and, if so,
* opens the app.
*/
@SuppressLint("InlinedApi")
private void openBitseal()
{
// Check whether onCacheWordOpened() has been called
if (mOnCacheWordOpenedCalled)
{
// Reset this flag
mOnCacheWordOpenedCalled = false;
// Cancel the 'handle hung unlock attempts' timer task
mUnlockAttemptTimerTask.cancel();
// Clear any 'unlock' notifications
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NotificationsService.getUnlockNotificationId());
// Open the Inbox Activity
Intent intent = new Intent(getBaseContext(), InboxActivity.class);
intent.putExtra(EXTRA_DATABASE_UNLOCKED, true);
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);
}
else
{
Log.e(TAG, "LockScreenActivity.openBitseal() was called, but onCacheWordOpened() has not yet been called!");
// Attempt to handle this failure
mCacheWordHandler = new CacheWordHandler(mActivityContext);
mCacheWordHandler.connectToService();
new AttemptUnlockTask().execute(new String[]{enterPassphraseEditText.getText().toString()});
}
}
@Override
protected void onStop()
{
super.onStop();
if (mCacheWordHandler != null)
{
mCacheWordHandler.disconnectFromService();
}
}
@Override
public void onCacheWordLocked()
{
Log.i(TAG, "LockScreenActivity.onCacheWordLocked() called.");
// We are already at the lock screen activity, so there's nothing to do here
}
@SuppressLint("InlinedApi")
@Override
public void onCacheWordOpened()
{
Log.i(TAG, "LockScreenActivity.onCacheWordOpened() called.");
mOnCacheWordOpenedCalled = true;
}
@Override
public void onCacheWordUninitialized()
{
Log.i(TAG, "LockScreenActivity.onCacheWordUninitialized() called.");
// Nothing to do here
}
}