package com.stardust.scriptdroid.ui.error;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringDef;
import android.support.annotation.StringRes;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TextInputEditText;
import android.support.design.widget.TextInputLayout;
import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.github.aakira.expandablelayout.ExpandableRelativeLayout;
import com.heinrichreimersoftware.androidissuereporter.model.DeviceInfo;
import com.heinrichreimersoftware.androidissuereporter.model.Report;
import com.heinrichreimersoftware.androidissuereporter.model.github.ExtraInfo;
import com.heinrichreimersoftware.androidissuereporter.model.github.GithubLogin;
import com.heinrichreimersoftware.androidissuereporter.model.github.GithubTarget;
import com.heinrichreimersoftware.androidissuereporter.util.ColorUtils;
import com.heinrichreimersoftware.androidissuereporter.util.ThemeUtils;
import org.eclipse.egit.github.core.Issue;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.client.RequestException;
import org.eclipse.egit.github.core.service.IssueService;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import static android.util.Patterns.EMAIL_ADDRESS;
import com.heinrichreimersoftware.androidissuereporter.R;
import com.stardust.theme.ThemeColorManager;
/**
* Created by Stardust on 2017/4/3.
*/
public abstract class AbstractIssueReporterActivity extends AppCompatActivity {
private static final String TAG = AbstractIssueReporterActivity.class.getSimpleName();
private static final int STATUS_BAD_CREDENTIALS = 401;
private static final int STATUS_ISSUES_NOT_ENABLED = 410;
private boolean mCrash = false;
private boolean mReportFailed = true;
@StringDef({RESULT_OK, RESULT_BAD_CREDENTIALS, RESULT_INVALID_TOKEN, RESULT_ISSUES_NOT_ENABLED,
RESULT_UNKNOWN})
@Retention(RetentionPolicy.SOURCE)
private @interface Result {
}
private static final String RESULT_OK = "RESULT_OK";
private static final String RESULT_BAD_CREDENTIALS = "RESULT_BAD_CREDENTIALS";
private static final String RESULT_INVALID_TOKEN = "RESULT_INVALID_TOKEN";
private static final String RESULT_ISSUES_NOT_ENABLED = "RESULT_ISSUES_NOT_ENABLED";
private static final String RESULT_UNKNOWN = "RESULT_UNKNOWN";
private boolean emailRequired = false;
private int bodyMinChar = 0;
private Toolbar toolbar;
private TextInputEditText inputTitle;
private TextInputEditText inputDescription;
private TextView textDeviceInfo;
private ImageButton buttonDeviceInfo;
private ExpandableRelativeLayout layoutDeviceInfo;
private ExpandableRelativeLayout layoutAnonymous;
private TextInputEditText inputUsername;
private TextInputEditText inputPassword;
private TextInputEditText inputEmail;
private RadioButton optionUseAccount;
private RadioButton optionAnonymous;
private ExpandableRelativeLayout layoutLogin;
private FloatingActionButton buttonSend;
private Drawable optionUseAccountButtonDrawable = null;
private String token;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (TextUtils.isEmpty(getTitle()))
setTitle(R.string.air_title_report_issue);
setContentView(com.stardust.scriptdroid.R.layout.air_activity_issue_reporter);
findViews();
//noinspection deprecation
token = getGuestToken();
initViews();
DeviceInfo deviceInfo = new DeviceInfo(this);
textDeviceInfo.setText(deviceInfo.toString());
handleIntent();
optionAnonymous.post(new Runnable() {
@Override
public void run() {
optionAnonymous.performClick();
}
});
}
private void handleIntent() {
final String errorDetail = getIntent().getStringExtra("error");
if (errorDetail != null) {
inputDescription.setText(errorDetail);
String title = getFirstLine(errorDetail);
inputTitle.setText(title);
mCrash = true;
}
}
private String getFirstLine(String str) {
int i = str.indexOf('\n');
if (i < 0)
return str;
return str.substring(0, i);
}
private void findViews() {
toolbar = (Toolbar) findViewById(R.id.air_toolbar);
inputTitle = (TextInputEditText) findViewById(R.id.air_inputTitle);
inputDescription = (TextInputEditText) findViewById(R.id.air_inputDescription);
textDeviceInfo = (TextView) findViewById(R.id.air_textDeviceInfo);
buttonDeviceInfo = (ImageButton) findViewById(R.id.air_buttonDeviceInfo);
layoutDeviceInfo = (ExpandableRelativeLayout) findViewById(R.id.air_layoutDeviceInfo);
inputUsername = (TextInputEditText) findViewById(R.id.air_inputUsername);
inputPassword = (TextInputEditText) findViewById(R.id.air_inputPassword);
inputEmail = (TextInputEditText) findViewById(R.id.air_inputEmail);
optionUseAccount = (RadioButton) findViewById(R.id.air_optionUseAccount);
optionAnonymous = (RadioButton) findViewById(R.id.air_optionAnonymous);
layoutLogin = (ExpandableRelativeLayout) findViewById(R.id.air_layoutLogin);
layoutAnonymous = (ExpandableRelativeLayout) findViewById(R.id.air_layoutGuest);
buttonSend = (FloatingActionButton) findViewById(R.id.air_buttonSend);
}
private void initViews() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(ThemeColorManager.getColorPrimary());
}
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(com.stardust.scriptdroid.R.string.text_issue_report);
}
toolbar.setBackgroundColor(ThemeColorManager.getColorPrimary());
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
buttonDeviceInfo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layoutDeviceInfo.toggle();
}
});
inputPassword.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
reportIssue();
return true;
}
return false;
}
});
updateGuestTokenViews();
buttonSend.setImageResource(ColorUtils.isDark(ThemeUtils.getColorAccent(this)) ?
R.drawable.air_ic_send_dark : R.drawable.air_ic_send_light);
buttonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
reportIssue();
} catch (Exception e) {
e.printStackTrace();
mReportFailed = true;
finish();
}
}
});
}
private void setOptionUseAccountMarginStart(int marginStart) {
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) optionUseAccount.getLayoutParams();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
layoutParams.setMarginStart(marginStart);
} else {
layoutParams.leftMargin = marginStart;
}
optionUseAccount.setLayoutParams(layoutParams);
}
private void updateGuestTokenViews() {
if (TextUtils.isEmpty(token)) {
int baseline = getResources().getDimensionPixelSize(R.dimen.air_baseline);
int radioButtonPaddingStart = getResources().getDimensionPixelSize(R.dimen.air_radio_button_padding_start);
setOptionUseAccountMarginStart(-2 * baseline - radioButtonPaddingStart);
optionUseAccount.setEnabled(false);
optionAnonymous.setVisibility(View.GONE);
} else {
setOptionUseAccountMarginStart(0);
optionUseAccount.setEnabled(true);
optionUseAccount.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layoutLogin.expand();
layoutAnonymous.collapse();
inputUsername.setEnabled(true);
inputPassword.setEnabled(true);
}
});
optionAnonymous.setVisibility(View.VISIBLE);
optionAnonymous.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
layoutLogin.collapse();
layoutAnonymous.expand();
inputUsername.setEnabled(false);
inputPassword.setEnabled(false);
}
});
}
}
private void reportIssue() {
if (!validateInput()) return;
if (optionUseAccount.isChecked()) {
String username = inputUsername.getText().toString();
String password = inputPassword.getText().toString();
sendBugReport(new GithubLogin(username, password), null);
} else {
if (TextUtils.isEmpty(token))
throw new IllegalStateException("You must provide a GitHub API Token.");
String email = null;
if (!TextUtils.isEmpty(inputEmail.getText()) &&
EMAIL_ADDRESS.matcher(inputEmail.getText().toString()).matches()) {
email = inputEmail.getText().toString();
}
sendBugReport(new GithubLogin(token), email);
}
}
private boolean validateInput() {
boolean hasErrors = false;
if (optionUseAccount.isChecked()) {
if (TextUtils.isEmpty(inputUsername.getText())) {
setError(inputUsername, R.string.air_error_no_username);
hasErrors = true;
} else {
removeError(inputUsername);
}
if (TextUtils.isEmpty(inputPassword.getText())) {
setError(inputPassword, R.string.air_error_no_password);
hasErrors = true;
} else {
removeError(inputPassword);
}
} else {
if (emailRequired) {
if (TextUtils.isEmpty(inputEmail.getText()) ||
!EMAIL_ADDRESS.matcher(inputEmail.getText().toString()).matches()) {
setError(inputEmail, R.string.air_error_no_email);
hasErrors = true;
} else {
removeError(inputEmail);
}
}
}
if (TextUtils.isEmpty(inputTitle.getText())) {
setError(inputTitle, R.string.air_error_no_title);
hasErrors = true;
} else {
removeError(inputTitle);
}
if (TextUtils.isEmpty(inputDescription.getText())) {
setError(inputDescription, R.string.air_error_no_description);
hasErrors = true;
} else {
if (bodyMinChar > 0) {
if (inputDescription.getText().toString().length() < bodyMinChar) {
setError(inputDescription, getResources().getQuantityString(R.plurals.air_error_short_description, bodyMinChar, bodyMinChar));
hasErrors = true;
} else {
removeError(inputDescription);
}
} else
removeError(inputDescription);
}
return !hasErrors;
}
private void setError(TextInputEditText editText, @StringRes int errorRes) {
try {
View layout = (View) editText.getParent();
while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
layout = (View) layout.getParent();
TextInputLayout realLayout = (TextInputLayout) layout;
realLayout.setError(getString(errorRes));
} catch (ClassCastException | NullPointerException e) {
Log.e(TAG, "Issue while setting error UI.", e);
}
}
private void setError(TextInputEditText editText, String error) {
try {
View layout = (View) editText.getParent();
while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
layout = (View) layout.getParent();
TextInputLayout realLayout = (TextInputLayout) layout;
realLayout.setError(error);
} catch (ClassCastException | NullPointerException e) {
Log.e(TAG, "Issue while setting error UI.", e);
}
}
private void removeError(TextInputEditText editText) {
try {
View layout = (View) editText.getParent();
while (!layout.getClass().getSimpleName().equals(TextInputLayout.class.getSimpleName()))
layout = (View) layout.getParent();
TextInputLayout realLayout = (TextInputLayout) layout;
realLayout.setError(null);
} catch (ClassCastException | NullPointerException e) {
Log.e(TAG, "Issue while removing error UI.", e);
}
}
private void sendBugReport(GithubLogin login, String email) {
if (!validateInput()) return;
String bugTitle = inputTitle.getText().toString();
String bugDescription = inputDescription.getText().toString();
DeviceInfo deviceInfo = new DeviceInfo(this);
ExtraInfo extraInfo = new ExtraInfo();
onSaveExtraInfo(extraInfo);
Report report = new Report(bugTitle, bugDescription, deviceInfo, extraInfo, email);
GithubTarget target = getTarget();
report(this, report, target, login);
}
protected final void setGuestEmailRequired(boolean required) {
this.emailRequired = required;
if (required) {
optionAnonymous.setText(R.string.air_label_use_email);
((TextInputLayout) findViewById(R.id.air_inputEmailParent)).setHint(getString(R.string.air_label_email));
} else {
optionAnonymous.setText(R.string.air_label_use_guest);
((TextInputLayout) findViewById(R.id.air_inputEmailParent)).setHint(getString(R.string.air_label_email_optional));
}
}
protected final void setMinimumDescriptionLength(int length) {
this.bodyMinChar = length;
}
protected void onSaveExtraInfo(ExtraInfo extraInfo) {
}
protected abstract GithubTarget getTarget();
@Deprecated
protected String getGuestToken() {
return null;
}
protected final void setGuestToken(String token) {
this.token = token;
Log.d(TAG, "GuestToken: " + token);
updateGuestTokenViews();
}
@Override
public void finish() {
if (mCrash) {
if (!mReportFailed) {
Toast.makeText(this, com.stardust.scriptdroid.R.string.text_report_succeed, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, com.stardust.scriptdroid.R.string.text_report_fail, Toast.LENGTH_SHORT).show();
}
finishAffinity();
} else {
super.finish();
}
}
private void report(Activity activity, Report report, GithubTarget target,
GithubLogin login) {
new ReportIssueTask(activity, report, target, login).execute();
}
private class ReportIssueTask extends DialogAsyncTask<Void, Void, String> {
private final Report report;
private final GithubTarget target;
private final GithubLogin login;
private ReportIssueTask(Activity activity, Report report, GithubTarget target,
GithubLogin login) {
super(activity);
this.report = report;
this.target = target;
this.login = login;
}
@Override
protected Dialog createDialog(@NonNull Context context) {
return new MaterialDialog.Builder(context)
.progress(true, 0)
.progressIndeterminateStyle(true)
.title(R.string.air_dialog_title_loading)
.show();
}
@Override
@Result
protected String doInBackground(Void... params) {
GitHubClient client;
if (login.shouldUseApiToken()) {
client = new GitHubClient().setOAuth2Token(login.getApiToken());
} else {
client = new GitHubClient().setCredentials(login.getUsername(), login.getPassword());
}
Issue issue = new Issue().setTitle(report.getTitle()).setBody(report.getDescription());
try {
new IssueService(client).createIssue(target.getUsername(), target.getRepository(), issue);
return RESULT_OK;
} catch (RequestException e) {
switch (e.getStatus()) {
case STATUS_BAD_CREDENTIALS:
if (login.shouldUseApiToken())
return RESULT_INVALID_TOKEN;
return RESULT_BAD_CREDENTIALS;
case STATUS_ISSUES_NOT_ENABLED:
return RESULT_ISSUES_NOT_ENABLED;
default:
e.printStackTrace();
return RESULT_UNKNOWN;
}
} catch (IOException e) {
e.printStackTrace();
return RESULT_UNKNOWN;
}
}
@Override
protected void onPostExecute(@Result String result) {
super.onPostExecute(result);
Context context = getContext();
if (context == null) return;
switch (result) {
case RESULT_OK:
mReportFailed = false;
tryToFinishActivity();
break;
case RESULT_BAD_CREDENTIALS:
new MaterialDialog.Builder(context)
.title(R.string.air_dialog_title_failed)
.content(R.string.air_dialog_description_failed_wrong_credentials)
.positiveText(R.string.air_dialog_action_failed)
.show();
break;
case RESULT_INVALID_TOKEN:
new MaterialDialog.Builder(context)
.title(R.string.air_dialog_title_failed)
.content(R.string.air_dialog_description_failed_invalid_token)
.positiveText(R.string.air_dialog_action_failed)
.show();
break;
case RESULT_ISSUES_NOT_ENABLED:
new MaterialDialog.Builder(context)
.title(R.string.air_dialog_title_failed)
.content(R.string.air_dialog_description_failed_issues_not_available)
.positiveText(R.string.air_dialog_action_failed)
.show();
break;
default:
new MaterialDialog.Builder(context)
.title(R.string.air_dialog_title_failed)
.content(R.string.air_dialog_description_failed_unknown)
.positiveText(R.string.air_dialog_action_failed)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog,
@NonNull DialogAction which) {
tryToFinishActivity();
}
})
.cancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
tryToFinishActivity();
}
})
.show();
break;
}
}
private void tryToFinishActivity() {
Context context = getContext();
if (context instanceof Activity && !((Activity) context).isFinishing()) {
((Activity) context).finish();
}
}
}
private static abstract class DialogAsyncTask<Pa, Pr, Re> extends AsyncTask<Pa, Pr, Re> {
private WeakReference<Context> contextWeakReference;
private WeakReference<Dialog> dialogWeakReference;
private boolean supposedToBeDismissed;
private DialogAsyncTask(Context context) {
contextWeakReference = new WeakReference<>(context);
dialogWeakReference = new WeakReference<>(null);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
Context context = getContext();
if (!supposedToBeDismissed && context != null) {
Dialog dialog = createDialog(context);
dialogWeakReference = new WeakReference<>(dialog);
dialog.show();
}
}
@SuppressWarnings("unchecked")
@Override
protected final void onProgressUpdate(Pr... values) {
super.onProgressUpdate(values);
Dialog dialog = getDialog();
if (dialog != null) {
onProgressUpdate(dialog, values);
}
}
@SuppressWarnings("unchecked")
private void onProgressUpdate(@NonNull Dialog dialog, Pr... values) {
}
@Nullable
Context getContext() {
return contextWeakReference.get();
}
@Nullable
Dialog getDialog() {
return dialogWeakReference.get();
}
@Override
protected void onCancelled(Re result) {
super.onCancelled(result);
tryToDismiss();
}
@Override
protected void onPostExecute(Re result) {
super.onPostExecute(result);
tryToDismiss();
}
private void tryToDismiss() {
supposedToBeDismissed = true;
try {
Dialog dialog = getDialog();
if (dialog != null)
dialog.dismiss();
} catch (Exception e) {
e.printStackTrace();
}
}
protected abstract Dialog createDialog(@NonNull Context context);
}
}