/*
* Copyright (c) 2013, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package ca.psiphon.ploggy;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
/**
* User interface for (re-)generating self identity.
*
* An AsyncTask is used to generate TLS and Hidden Service key material. A progress
* spinner is displayed while generation occurs, then the user may type their
* nickname. The resulting identity fingerprint and Robohash avatar is updated
* after brief pauses in typing.
*/
public class ActivityGenerateSelf extends ActivitySendIdentityByNfc implements View.OnClickListener {
private static final String LOG_TAG = "Generate Self";
private ImageView mAvatarImage;
private EditText mNicknameEdit;
private TextView mFingerprintText;
private Button mRegenerateButton;
private Button mSaveButton;
private ProgressDialog mProgressDialog;
private GenerateTask mGenerateTask;
private GenerateResult mGenerateResult;
private Timer mAvatarTimer;
private TimerTask mAvatarTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_generate_self);
mAvatarImage = (ImageView)findViewById(R.id.generate_self_avatar_image);
mAvatarImage.setImageResource(R.drawable.ic_unknown_avatar);
mNicknameEdit = (EditText)findViewById(R.id.generate_self_nickname_edit);
mNicknameEdit.addTextChangedListener(getNicknameTextChangedListener());
mNicknameEdit.setEnabled(false);
mFingerprintText = (TextView)findViewById(R.id.generate_self_fingerprint_text);
mRegenerateButton = (Button)findViewById(R.id.generate_self_regenerate_button);
mRegenerateButton.setEnabled(false);
mRegenerateButton.setVisibility(View.GONE);
mRegenerateButton.setOnClickListener(this);
mSaveButton = (Button)findViewById(R.id.generate_self_save_button);
mSaveButton.setEnabled(false);
mSaveButton.setVisibility(View.GONE);
mSaveButton.setOnClickListener(this);
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setMessage(getText(R.string.prompt_generate_self_progress));
mProgressDialog.setCancelable(false);
mAvatarTimer = new Timer();
}
private void showAvatarAndFingerprint(Identity.PublicIdentity publicIdentity) {
try {
if (publicIdentity.mNickname.length() > 0) {
Robohash.setRobohashImage(this, mAvatarImage, false, publicIdentity);
} else {
Robohash.setRobohashImage(this, mAvatarImage, false, null);
}
mFingerprintText.setText(Utils.formatFingerprint(publicIdentity.getFingerprint()));
} catch (Utils.ApplicationError e) {
Log.addEntry(LOG_TAG, "failed to show self");
}
}
@Override
public void onResume() {
super.onResume();
Data.Self self = getSelf();
if (self == null) {
startGenerating();
} else {
showAvatarAndFingerprint(self.mPublicIdentity);
mNicknameEdit.setText(self.mPublicIdentity.mNickname);
mRegenerateButton.setEnabled(true);
mRegenerateButton.setVisibility(View.VISIBLE);
mSaveButton.setEnabled(false);
mSaveButton.setVisibility(View.GONE);
}
// Don't show the keyboard until edit selected
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
}
private void startGenerating() {
Robohash.setRobohashImage(this, mAvatarImage, false, null);
mNicknameEdit.setText("");
mFingerprintText.setText("");
mRegenerateButton.setEnabled(false);
mRegenerateButton.setVisibility(View.GONE);
mSaveButton.setEnabled(false);
mSaveButton.setVisibility(View.VISIBLE);
mGenerateTask = new GenerateTask();
mGenerateTask.execute();
}
@Override
public void onPause() {
super.onPause();
// TODO: http://stackoverflow.com/questions/1875670/what-to-do-with-asynctask-in-onpause
if (mGenerateTask != null) {
mGenerateTask.cancel(true);
mGenerateTask = null;
}
}
@Override
public void onBackPressed() {
if (getSelf() != null) {
super.onBackPressed();
}
}
@Override
public void onClick(View view) {
if (view.equals(mRegenerateButton)) {
startGenerating();
} else if (view.equals(mSaveButton)) {
String nickname = mNicknameEdit.getText().toString();
if (mGenerateResult == null || !Protocol.isValidNickname(nickname)) {
return;
}
try {
Data.getInstance().updateSelf(
new Data.Self(
Identity.makeSignedPublicIdentity(
nickname,
mGenerateResult.mX509KeyMaterial,
mGenerateResult.mHiddenServiceKeyMaterial),
Identity.makePrivateIdentity(
mGenerateResult.mX509KeyMaterial,
mGenerateResult.mHiddenServiceKeyMaterial),
new Date()));
Utils.hideKeyboard(this);
finish();
} catch (Utils.ApplicationError e) {
Log.addEntry(LOG_TAG, "failed to update self");
}
}
}
private static class GenerateResult {
public final X509.KeyMaterial mX509KeyMaterial;
public final HiddenService.KeyMaterial mHiddenServiceKeyMaterial;
public GenerateResult(
X509.KeyMaterial x509KeyMaterial,
HiddenService.KeyMaterial hiddenServiceKeyMaterial) {
mX509KeyMaterial = x509KeyMaterial;
mHiddenServiceKeyMaterial = hiddenServiceKeyMaterial;
}
}
private class GenerateTask extends AsyncTask<Void, Void, GenerateResult> {
@Override
protected GenerateResult doInBackground(Void... params) {
try {
HiddenService.KeyMaterial hiddenServiceKeyMaterial = HiddenService.generateKeyMaterial();
Log.addEntry(LOG_TAG, "generated Tor hidden service key material");
// TODO: possible to check isCancelled within the key generation?
if (isCancelled()) {
return null;
}
X509.KeyMaterial x509KeyMaterial = X509.generateKeyMaterial(hiddenServiceKeyMaterial.mHostname);
Log.addEntry(LOG_TAG, "generated X.509 key material");
return new GenerateResult(x509KeyMaterial, hiddenServiceKeyMaterial);
} catch (Utils.ApplicationError e) {
Log.addEntry(LOG_TAG, "failed to generate key material");
return null;
}
}
@Override
protected void onPreExecute() {
mProgressDialog.show();
}
@Override
protected void onPostExecute(GenerateResult result) {
if (mProgressDialog.isShowing()) {
mProgressDialog.dismiss();
}
if (result == null) {
// Failed, so dismiss the activity.
// For now, this will simply restart this activity.
finish();
} else {
mGenerateResult = result;
// Display fingerprint/avatar for blank nickname
showAvatarAndFingerprint(
new Identity.PublicIdentity(
mNicknameEdit.getText().toString(),
mGenerateResult.mX509KeyMaterial.mCertificate,
mGenerateResult.mHiddenServiceKeyMaterial.mHostname,
mGenerateResult.mHiddenServiceKeyMaterial.mAuthCookie,
null));
mNicknameEdit.setEnabled(true);
}
}
}
private TextWatcher getNicknameTextChangedListener() {
// Refresh identity fingerprint and Robohash avatar 1 second after user stops typing nickname
return new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
final String nickname = s.toString();
mSaveButton.setEnabled(mGenerateResult != null && Protocol.isValidNickname(nickname));
// TODO: use Handler instead of Timer
if (mAvatarTimerTask != null) {
mAvatarTimerTask.cancel();
}
mAvatarTimerTask = new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mGenerateResult != null) {
// Display fingerprint/avatar for updated nickname
showAvatarAndFingerprint(
new Identity.PublicIdentity(
nickname,
mGenerateResult.mX509KeyMaterial.mCertificate,
mGenerateResult.mHiddenServiceKeyMaterial.mHostname,
mGenerateResult.mHiddenServiceKeyMaterial.mAuthCookie,
null));
}
}
});
}
};
mAvatarTimer.schedule(mAvatarTimerTask, 1000);
}
};
}
static private Data.Self getSelf() {
Data.Self self = null;
try {
self = Data.getInstance().getSelf();
} catch (Utils.ApplicationError e) {
// Treat as no self
}
return self;
}
static public void checkLaunchGenerateSelf(Context context) {
// Helper to ensure Self is generated. Called from other Activities to jump to this one first.
// When Self is generated, ensure the background Service is started.
if (getSelf() == null) {
context.startActivity(new Intent(context, ActivityGenerateSelf.class));
} else {
context.startService(new Intent(context, PloggyService.class));
}
}
}