Java tutorial
/*************************************************************************************** * Copyright (c) 2011 Kostas Spyropoulos <inigo.aldana@gmail.com> * * * * 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 com.ichi2.anki; import android.app.Application; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.SimpleAdapter; import com.ichi2.anim.ActivityTransitionAnimation; import com.ichi2.async.Connection; import com.ichi2.async.Connection.Payload; import com.ichi2.libanki.Utils; import com.ichi2.themes.StyledDialog; import com.ichi2.themes.Themes; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.TimeZone; import java.util.UUID; public class Feedback extends AnkiActivity { public static String REPORT_ASK = "2"; public static String REPORT_NEVER = "1"; public static String REPORT_ALWAYS = "0"; public static String STATE_WAITING = "0"; public static String STATE_UPLOADING = "1"; public static String STATE_SUCCESSFUL = "2"; public static String STATE_FAILED = "3"; public static String TYPE_STACKTRACE = "crash-stacktrace"; public static String TYPE_FEEDBACK = "feedback"; public static String TYPE_ERROR_FEEDBACK = "error-feedback"; public static String TYPE_OTHER_ERROR = "other-error"; protected static SimpleDateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US); protected static SimpleDateFormat df2 = new SimpleDateFormat("Z", Locale.US); protected static TimeZone localTz = TimeZone.getDefault(); // This is used to group the batch of bugs and notes sent on the server side protected long mNonce; protected List<HashMap<String, String>> mErrorReports; protected SimpleAdapter mErrorAdapter; protected ListView mLvErrorList; protected EditText mEtFeedbackText; protected boolean mPostingFeedback; protected InputMethodManager mImm = null; protected StyledDialog mNoConnectionAlert = null; protected String mReportErrorMode; protected String mFeedbackUrl; protected String mErrorUrl; private boolean mAllowFeedback; private boolean mErrorsSent = false; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { deleteFiles(true, false); closeFeedback(); } return super.onKeyDown(keyCode, event); } /** * Create AlertDialogs used on all the activity */ private void initAllAlertDialogs() { Resources res = getResources(); StyledDialog.Builder builder = new StyledDialog.Builder(this); // builder.setTitle(res.getString(R.string.connection_error_title)); builder.setIcon(R.drawable.ic_dialog_alert); builder.setMessage(res.getString(R.string.youre_offline)); builder.setPositiveButton(res.getString(R.string.dialog_ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mPostingFeedback = false; refreshInterface(); } }); mNoConnectionAlert = builder.create(); } private void closeFeedback() { if (getIntent().getIntExtra("request", 0) == DeckPicker.RESULT_DB_ERROR) { setResult(DeckPicker.RESULT_DB_ERROR); } else { setResult(RESULT_OK); } finish(); ActivityTransitionAnimation.slide(Feedback.this, ActivityTransitionAnimation.LEFT); } private void refreshInterface() { if (mAllowFeedback) { Resources res = getResources(); Button btnSend = (Button) findViewById(R.id.btnFeedbackSend); Button btnKeepLatest = (Button) findViewById(R.id.btnFeedbackKeepLatest); Button btnClearAll = (Button) findViewById(R.id.btnFeedbackClearAll); ProgressBar pbSpinner = (ProgressBar) findViewById(R.id.pbFeedbackSpinner); int numErrors = mErrorReports.size(); if (numErrors == 0 || mErrorsSent) { if (!mErrorsSent) { mLvErrorList.setVisibility(View.GONE); } btnKeepLatest.setVisibility(View.GONE); btnClearAll.setVisibility(View.GONE); btnSend.setText(res.getString(R.string.feedback_send_feedback)); } else { mLvErrorList.setVisibility(View.VISIBLE); btnKeepLatest.setVisibility(View.VISIBLE); btnClearAll.setVisibility(View.VISIBLE); btnSend.setText(res.getString(R.string.feedback_send_feedback_and_errors)); refreshErrorListView(); if (numErrors == 1) { btnKeepLatest.setEnabled(false); } else { btnKeepLatest.setEnabled(true); } } if (mPostingFeedback) { int buttonHeight = btnSend.getHeight(); btnSend.setVisibility(View.GONE); pbSpinner.setVisibility(View.VISIBLE); LinearLayout topLine = (LinearLayout) findViewById(R.id.llFeedbackTopLine); topLine.setMinimumHeight(buttonHeight); mEtFeedbackText.setEnabled(false); mImm.hideSoftInputFromWindow(mEtFeedbackText.getWindowToken(), 0); } else { btnSend.setVisibility(View.VISIBLE); pbSpinner.setVisibility(View.GONE); mEtFeedbackText.setEnabled(true); } } } @Override protected void onCreate(Bundle savedInstanceState) { Themes.applyTheme(this); super.onCreate(savedInstanceState); Resources res = getResources(); Context context = getBaseContext(); SharedPreferences sharedPreferences = AnkiDroidApp.getSharedPrefs(context); mReportErrorMode = sharedPreferences.getString("reportErrorMode", REPORT_ASK); mNonce = UUID.randomUUID().getMostSignificantBits(); mFeedbackUrl = res.getString(R.string.feedback_post_url); mErrorUrl = res.getString(R.string.error_post_url); mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); mPostingFeedback = false; initAllAlertDialogs(); getErrorFiles(); Intent i = getIntent(); mAllowFeedback = (i.hasExtra("request") && (i.getIntExtra("request", 0) == DeckPicker.REPORT_FEEDBACK || i.getIntExtra("request", 0) == DeckPicker.RESULT_DB_ERROR)) || mReportErrorMode.equals(REPORT_ASK); if (!mAllowFeedback) { if (mReportErrorMode.equals(REPORT_ALWAYS)) { // Always report try { String feedback = "Automatically sent"; Connection.sendFeedback(mSendListener, new Payload(new Object[] { mFeedbackUrl, mErrorUrl, feedback, mErrorReports, mNonce, getApplication(), true })); if (mErrorReports.size() > 0) { mPostingFeedback = true; } if (feedback.length() > 0) { mPostingFeedback = true; } } catch (Exception e) { Log.e(AnkiDroidApp.TAG, e.toString()); } finish(); ActivityTransitionAnimation.slide(Feedback.this, ActivityTransitionAnimation.NONE); return; } else if (mReportErrorMode.equals(REPORT_NEVER)) { // Never report deleteFiles(false, false); finish(); ActivityTransitionAnimation.slide(Feedback.this, ActivityTransitionAnimation.NONE); return; } } View mainView = getLayoutInflater().inflate(R.layout.feedback, null); setContentView(mainView); Themes.setWallpaper(mainView); Themes.setTextViewStyle(findViewById(R.id.tvFeedbackDisclaimer)); Themes.setTextViewStyle(findViewById(R.id.lvFeedbackErrorList)); Button btnSend = (Button) findViewById(R.id.btnFeedbackSend); Button btnKeepLatest = (Button) findViewById(R.id.btnFeedbackKeepLatest); Button btnClearAll = (Button) findViewById(R.id.btnFeedbackClearAll); mEtFeedbackText = (EditText) findViewById(R.id.etFeedbackText); mLvErrorList = (ListView) findViewById(R.id.lvFeedbackErrorList); mErrorAdapter = new SimpleAdapter(this, mErrorReports, R.layout.error_item, new String[] { "name", "state", "result" }, new int[] { R.id.error_item_text, R.id.error_item_progress, R.id.error_item_status }); mErrorAdapter.setViewBinder(new SimpleAdapter.ViewBinder() { @Override public boolean setViewValue(View view, Object arg1, String text) { switch (view.getId()) { case R.id.error_item_progress: if (text.equals(STATE_UPLOADING)) { view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } return true; case R.id.error_item_status: if (text.length() == 0) { view.setVisibility(View.GONE); return true; } else { view.setVisibility(View.VISIBLE); return false; } } return false; } }); btnClearAll.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { deleteFiles(false, false); refreshErrorListView(); refreshInterface(); } }); mLvErrorList.setAdapter(mErrorAdapter); btnSend.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!mPostingFeedback) { String feedback = mEtFeedbackText.getText().toString(); Connection.sendFeedback(mSendListener, new Payload(new Object[] { mFeedbackUrl, mErrorUrl, feedback, mErrorReports, mNonce, getApplication(), false })); if (mErrorReports.size() > 0) { mPostingFeedback = true; } if (feedback.length() > 0) { mPostingFeedback = true; } refreshInterface(); } } }); btnKeepLatest.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { deleteFiles(false, true); refreshErrorListView(); refreshInterface(); } }); refreshInterface(); getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: closeFeedback(); return true; default: return super.onOptionsItemSelected(item); } } private void refreshErrorListView() { if (mAllowFeedback) { mErrorAdapter.notifyDataSetChanged(); } } private void getErrorFiles() { mErrorReports = new ArrayList<HashMap<String, String>>(); String[] errors = fileList(); for (String file : errors) { if (file.endsWith(".stacktrace")) { HashMap<String, String> error = new HashMap<String, String>(); error.put("filename", file); error.put("name", file); error.put("state", STATE_WAITING); error.put("result", ""); mErrorReports.add(error); } } } /** * Delete the crash log files. * * @param onlyProcessed only delete the log files that have been sent. * @param keepLatest keep the latest log file. If the file has not been sent yet, it is not deleted even if this * value is set to false. */ private void deleteFiles(boolean onlyProcessed, boolean keepLatest) { for (int i = (keepLatest ? 1 : 0); i < mErrorReports.size();) { try { String errorState = mErrorReports.get(i).get("state"); if (!onlyProcessed || errorState.equals(STATE_SUCCESSFUL)) { deleteFile(mErrorReports.get(i).get("filename")); mErrorReports.remove(i); } else { i++; } } catch (Exception e) { Log.e(AnkiDroidApp.TAG, String.format("Could not delete file: %s", mErrorReports.get(i))); } } } public static boolean isErrorType(String postType) { return !(postType.equals(TYPE_FEEDBACK) || postType.equals(TYPE_ERROR_FEEDBACK)); } Connection.TaskListener mSendListener = new Connection.TaskListener() { @Override public void onDisconnected() { if (mNoConnectionAlert != null) { mNoConnectionAlert.show(); } } @Override public void onPostExecute(Payload data) { mPostingFeedback = false; mErrorsSent = true; refreshInterface(); } @Override public void onPreExecute() { // pass } @Override public void onProgressUpdate(Object... values) { Resources res = getResources(); String postType = (String) values[0]; int errorIndex = (Integer) values[1]; String state = (String) values[2]; if (isErrorType(postType) && mErrorReports.size() > errorIndex) { mErrorReports.get(errorIndex).put("state", state); if (!state.equals(Feedback.STATE_UPLOADING)) { int returnCode = (Integer) values[3]; if (returnCode == 200) { // The result is either: "new" (for first encountered bug), "known" (for existing bugs) or // ("issue:xxx:<status>" for known and linked) String result = (String) values[4]; if (result.equalsIgnoreCase("new")) { mErrorReports.get(errorIndex).put("name", res.getString(R.string.feedback_error_reply_new)); } else if (result.equalsIgnoreCase("known")) { mErrorReports.get(errorIndex).put("name", res.getString(R.string.feedback_error_reply_known)); } else if (result.startsWith("issue:")) { String[] resultPieces = result.split(":"); int issue = Integer.parseInt(resultPieces[1]); String status = ""; if (resultPieces.length > 1) { if (resultPieces.length > 2) { status = resultPieces[2]; } if (status.length() == 0) { mErrorReports.get(errorIndex).put("name", res.getString(R.string.feedback_error_reply_issue_unknown, issue)); } else if (status.equalsIgnoreCase("fixed")) { mErrorReports.get(errorIndex).put("name", res.getString(R.string.feedback_error_reply_issue_fixed_prod, issue)); } else if (status.equalsIgnoreCase("fixedindev")) { mErrorReports.get(errorIndex).put("name", res.getString(R.string.feedback_error_reply_issue_fixed_dev, issue)); } else { mErrorReports.get(errorIndex).put("name", res .getString(R.string.feedback_error_reply_issue_status, issue, status)); } } else { mErrorReports.get(errorIndex).put("result", res.getString(R.string.feedback_error_reply_malformed)); } } else { mErrorReports.get(errorIndex).put("result", res.getString(R.string.feedback_error_reply_malformed)); } } else { mErrorReports.get(errorIndex).put("result", res.getString(R.string.feedback_error_reply_failed)); } } refreshErrorListView(); } else { if (mAllowFeedback) { if (state.equals(STATE_SUCCESSFUL)) { mEtFeedbackText.setText(""); Themes.showThemedToast(Feedback.this, res.getString(R.string.feedback_message_sent_success), false); } else if (state.equals(STATE_FAILED)) { int respCode = (Integer) values[3]; if (respCode == 0) { onDisconnected(); } else { Themes.showThemedToast(Feedback.this, res.getString(R.string.feedback_message_sent_failure, respCode), false); } } } } } }; // Run in AsyncTask private static void addTimestamp(List<NameValuePair> pairs) { Date ts = new Date(); df1.setTimeZone(TimeZone.getTimeZone("UTC")); String reportsentutc = String.format("%s", df1.format(ts)); String reportsenttzoffset = String.format("%s", df2.format(ts)); String reportsenttz = String.format("%s", localTz.getID()); pairs.add(new BasicNameValuePair("reportsentutc", reportsentutc)); pairs.add(new BasicNameValuePair("reportsenttzoffset", reportsenttzoffset)); pairs.add(new BasicNameValuePair("reportsenttz", reportsenttz)); } private static List<NameValuePair> extractPairsFromError(String type, String errorFile, String groupId, int index, Application app) { List<NameValuePair> pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("type", "crash-stacktrace")); pairs.add(new BasicNameValuePair("groupid", groupId)); pairs.add(new BasicNameValuePair("index", String.valueOf(index))); addTimestamp(pairs); String singleLine = null; try { BufferedReader br = new BufferedReader(new InputStreamReader(app.openFileInput(errorFile))); while ((singleLine = br.readLine()) != null) { int indexOfEquals = singleLine.indexOf('='); if (indexOfEquals == -1) { continue; } String key = singleLine.substring(0, indexOfEquals).toLowerCase(Locale.US); String value = singleLine.substring(indexOfEquals + 1, singleLine.length()); if (key.equals("stacktrace")) { StringBuilder sb = new StringBuilder(value); while ((singleLine = br.readLine()) != null) { sb.append(singleLine); sb.append("\n"); } value = sb.toString(); } pairs.add(new BasicNameValuePair(key, value)); } br.close(); } catch (FileNotFoundException e) { Log.w(AnkiDroidApp.TAG, "Couldn't open crash report " + errorFile); return null; } catch (IOException e) { Log.w(AnkiDroidApp.TAG, "Couldn't read crash report " + errorFile); return null; } return pairs; } /** * Posting feedback or error info to the server. This is called from the AsyncTask. * * @param url The url to post the feedback to. * @param type The type of the info, eg Feedback.TYPE_CRASH_STACKTRACE. * @param feedback For feedback types this is the message. For error/crash types this is the path to the error file. * @param groupId A single time generated ID, so that errors/feedback send together can be grouped together. * @param index The index of the error in the list * @return A Payload file showing success, response code and response message. */ public static Payload postFeedback(String url, String type, String feedback, String groupId, int index, Application app) { Payload result = new Payload(null); List<NameValuePair> pairs = null; if (!isErrorType(type)) { pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("type", type)); pairs.add(new BasicNameValuePair("groupid", groupId)); pairs.add(new BasicNameValuePair("index", "0")); pairs.add(new BasicNameValuePair("message", feedback)); addTimestamp(pairs); } else { pairs = Feedback.extractPairsFromError(type, feedback, groupId, index, app); if (pairs == null) { result.success = false; result.result = null; } } HttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader("User-Agent", "AnkiDroid"); try { httpPost.setEntity(new UrlEncodedFormEntity(pairs)); HttpResponse response = httpClient.execute(httpPost); Log.e(AnkiDroidApp.TAG, String.format("Bug report posted to %s", url)); int respCode = response.getStatusLine().getStatusCode(); switch (respCode) { case 200: result.success = true; result.returnType = respCode; result.result = Utils.convertStreamToString(response.getEntity().getContent()); // Log.i(AnkiDroidApp.TAG, String.format("postFeedback OK: %s", result.result)); break; default: Log.e(AnkiDroidApp.TAG, String.format("postFeedback failure: %d - %s", response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase())); result.success = false; result.returnType = respCode; result.result = response.getStatusLine().getReasonPhrase(); break; } } catch (ClientProtocolException ex) { Log.e(AnkiDroidApp.TAG, "ClientProtocolException: " + ex.toString()); result.success = false; result.result = ex.toString(); } catch (IOException ex) { Log.e(AnkiDroidApp.TAG, "IOException: " + ex.toString()); result.success = false; result.result = ex.toString(); } return result; } }