Java tutorial
/* * Copyright (C) 2014 Open Whisper Systems * * 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 org.thoughtcrime.securesms.logsubmit; import android.annotation.TargetApi; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.ClipboardManager; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.util.Linkify; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import org.json.JSONException; import org.json.JSONObject; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logsubmit.util.Scrubber; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.Locale; import java.util.concurrent.ExecutionException; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; /** * A helper {@link Fragment} to preview and submit logcat information to a public pastebin. * Activities that contain this fragment must implement the * {@link SubmitLogFragment.OnLogSubmittedListener} interface * to handle interaction events. * Use the {@link SubmitLogFragment#newInstance} factory method to * create an instance of this fragment. * */ public class SubmitLogFragment extends Fragment { private static final String TAG = SubmitLogFragment.class.getSimpleName(); private static final String API_ENDPOINT = "https://debuglogs.org"; private static final String HEADER_SYSINFO = "========== SYSINFO ========"; private static final String HEADER_JOBS = "=========== JOBS ========="; private static final String HEADER_LOGCAT = "========== LOGCAT ========"; private static final String HEADER_LOGGER = "========== LOGGER ========"; private Button okButton; private Button cancelButton; private View scrollButton; private String supportEmailAddress; private String supportEmailSubject; private String hackSavedLogUrl; private boolean emailActivityWasStarted = false; private RecyclerView logPreview; private LogPreviewAdapter logPreviewAdapter; private OnLogSubmittedListener mListener; /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @return A new instance of fragment SubmitLogFragment. */ public static SubmitLogFragment newInstance(String supportEmailAddress, String supportEmailSubject) { SubmitLogFragment fragment = new SubmitLogFragment(); fragment.supportEmailAddress = supportEmailAddress; fragment.supportEmailSubject = supportEmailSubject; return fragment; } public static SubmitLogFragment newInstance() { return newInstance(null, null); } public SubmitLogFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_submit_log, container, false); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); initializeResources(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (OnLogSubmittedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onResume() { super.onResume(); if (emailActivityWasStarted && mListener != null) mListener.onSuccess(); } @Override public void onDetach() { super.onDetach(); mListener = null; } private void initializeResources() { okButton = getView().findViewById(R.id.ok); cancelButton = getView().findViewById(R.id.cancel); logPreview = getView().findViewById(R.id.log_preview); scrollButton = getView().findViewById(R.id.scroll_to_bottom_button); okButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { new SubmitToPastebinAsyncTask(logPreviewAdapter.getText()).execute(); } }); cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mListener != null) mListener.onCancel(); } }); scrollButton.setOnClickListener(v -> logPreview.scrollToPosition(logPreviewAdapter.getItemCount() - 1)); logPreview.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (((LinearLayoutManager) recyclerView.getLayoutManager()) .findLastVisibleItemPosition() < logPreviewAdapter.getItemCount() - 10) { scrollButton.setVisibility(View.VISIBLE); } else { scrollButton.setVisibility(View.GONE); } } }); logPreviewAdapter = new LogPreviewAdapter(); logPreview.setLayoutManager(new LinearLayoutManager(getContext())); logPreview.setAdapter(logPreviewAdapter); new PopulateLogcatAsyncTask(getActivity()).execute(); } private static String grabLogcat() { try { final Process process = Runtime.getRuntime().exec("logcat -d"); final BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(process.getInputStream())); final StringBuilder log = new StringBuilder(); final String separator = System.getProperty("line.separator"); String line; while ((line = bufferedReader.readLine()) != null) { log.append(line); log.append(separator); } return log.toString(); } catch (IOException ioe) { Log.w(TAG, "IOException when trying to read logcat.", ioe); return null; } } private Intent getIntentForSupportEmail(String logUrl) { Intent emailSendIntent = new Intent(Intent.ACTION_SEND); emailSendIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { supportEmailAddress }); emailSendIntent.putExtra(Intent.EXTRA_SUBJECT, supportEmailSubject); emailSendIntent.putExtra(Intent.EXTRA_TEXT, getString(R.string.log_submit_activity__please_review_this_log_from_my_app, logUrl)); emailSendIntent.setType("message/rfc822"); return emailSendIntent; } private void handleShowChooserForIntent(final Intent intent, String chooserTitle) { final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); final ShareIntentListAdapter adapter = ShareIntentListAdapter.getAdapterForIntent(getActivity(), intent); builder.setTitle(chooserTitle).setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ActivityInfo info = adapter.getItem(which).activityInfo; intent.setClassName(info.packageName, info.name); startActivity(intent); emailActivityWasStarted = true; } }).setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialogInterface) { if (hackSavedLogUrl != null) handleShowSuccessDialog(hackSavedLogUrl); } }).create().show(); } private TextView handleBuildSuccessTextView(final String logUrl) { TextView showText = new TextView(getActivity()); showText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); showText.setPadding(15, 30, 15, 30); showText.setText(getString(R.string.log_submit_activity__copy_this_url_and_add_it_to_your_issue, logUrl)); showText.setAutoLinkMask(Activity.RESULT_OK); showText.setMovementMethod(LinkMovementMethod.getInstance()); showText.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { @SuppressWarnings("deprecation") ClipboardManager manager = (ClipboardManager) getActivity() .getSystemService(Activity.CLIPBOARD_SERVICE); manager.setText(logUrl); Toast.makeText(getActivity(), R.string.log_submit_activity__copied_to_clipboard, Toast.LENGTH_SHORT) .show(); return true; } }); Linkify.addLinks(showText, Linkify.WEB_URLS); return showText; } private void handleShowSuccessDialog(final String logUrl) { TextView showText = handleBuildSuccessTextView(logUrl); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(R.string.log_submit_activity__success).setView(showText).setCancelable(false) .setNeutralButton(R.string.log_submit_activity__button_got_it, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dialogInterface.dismiss(); if (mListener != null) mListener.onSuccess(); } }); if (supportEmailAddress != null) { builder.setPositiveButton(R.string.log_submit_activity__button_compose_email, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { handleShowChooserForIntent(getIntentForSupportEmail(logUrl), getString(R.string.log_submit_activity__choose_email_app)); } }); } builder.create().show(); hackSavedLogUrl = logUrl; } private class PopulateLogcatAsyncTask extends AsyncTask<Void, Void, String> { private WeakReference<Context> weakContext; public PopulateLogcatAsyncTask(Context context) { this.weakContext = new WeakReference<>(context); } @Override protected String doInBackground(Void... voids) { Context context = weakContext.get(); if (context == null) return null; Scrubber scrubber = new Scrubber(); String newLogs; try { long t1 = System.currentTimeMillis(); String logs = ApplicationContext.getInstance(context).getPersistentLogger().getLogs().get(); Log.i(TAG, "Fetch our logs : " + (System.currentTimeMillis() - t1) + " ms"); long t2 = System.currentTimeMillis(); newLogs = scrubber.scrub(logs); Log.i(TAG, "Scrub our logs: " + (System.currentTimeMillis() - t2) + " ms"); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, "Failed to retrieve new logs.", e); newLogs = "Failed to retrieve logs."; } long t3 = System.currentTimeMillis(); String logcat = grabLogcat(); Log.i(TAG, "Fetch logcat: " + (System.currentTimeMillis() - t3) + " ms"); long t4 = System.currentTimeMillis(); String scrubbedLogcat = scrubber.scrub(logcat); Log.i(TAG, "Scrub logcat: " + (System.currentTimeMillis() - t4) + " ms"); return HEADER_SYSINFO + "\n\n" + buildDescription(context) + "\n\n\n" + HEADER_JOBS + "\n\n" + scrubber.scrub(ApplicationContext.getInstance(context).getJobManager().getDebugInfo()) + "\n\n" + HEADER_LOGCAT + "\n\n" + scrubbedLogcat + "\n\n\n" + HEADER_LOGGER + "\n\n" + newLogs; } @Override protected void onPreExecute() { super.onPreExecute(); logPreviewAdapter.setText(getString(R.string.log_submit_activity__loading_logs)); okButton.setEnabled(false); } @Override protected void onPostExecute(String logcat) { super.onPostExecute(logcat); if (TextUtils.isEmpty(logcat)) { if (mListener != null) mListener.onFailure(); return; } logPreviewAdapter.setText(logcat); okButton.setEnabled(true); } } private class SubmitToPastebinAsyncTask extends ProgressDialogAsyncTask<Void, Void, String> { private final String paste; public SubmitToPastebinAsyncTask(String paste) { super(getActivity(), R.string.log_submit_activity__submitting, R.string.log_submit_activity__uploading_logs); this.paste = paste; } @Override protected String doInBackground(Void... voids) { try { OkHttpClient client = new OkHttpClient.Builder().build(); Response response = client.newCall(new Request.Builder().url(API_ENDPOINT).get().build()).execute(); ResponseBody body = response.body(); if (!response.isSuccessful() || body == null) { throw new IOException("Unsuccessful response: " + response); } JSONObject json = new JSONObject(body.string()); String url = json.getString("url"); JSONObject fields = json.getJSONObject("fields"); String item = fields.getString("key"); MultipartBody.Builder post = new MultipartBody.Builder(); Iterator<String> keys = fields.keys(); post.addFormDataPart("Content-Type", "text/plain"); while (keys.hasNext()) { String key = keys.next(); post.addFormDataPart(key, fields.getString(key)); } post.addFormDataPart("file", "file", RequestBody.create(MediaType.parse("text/plain"), paste)); Response postResponse = client.newCall(new Request.Builder().url(url).post(post.build()).build()) .execute(); if (!postResponse.isSuccessful()) { throw new IOException("Bad response: " + postResponse); } return API_ENDPOINT + "/" + item; } catch (IOException | JSONException e) { Log.w("ImageActivity", e); } return null; } @Override protected void onPostExecute(final String response) { super.onPostExecute(response); if (response != null) handleShowSuccessDialog(response); else { Log.w(TAG, "Response was null from Gist API."); Toast.makeText(getActivity(), R.string.log_submit_activity__network_failure, Toast.LENGTH_LONG) .show(); } } } private static long asMegs(long bytes) { return bytes / 1048576L; } public static String getMemoryUsage(Context context) { Runtime info = Runtime.getRuntime(); info.totalMemory(); return String.format(Locale.ENGLISH, "%dM (%.2f%% free, %dM max)", asMegs(info.totalMemory()), (float) info.freeMemory() / info.totalMemory() * 100f, asMegs(info.maxMemory())); } @TargetApi(VERSION_CODES.KITKAT) public static String getMemoryClass(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); String lowMem = ""; if (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice()) { lowMem = ", low-mem device"; } return activityManager.getMemoryClass() + lowMem; } private static String buildDescription(Context context) { final PackageManager pm = context.getPackageManager(); final StringBuilder builder = new StringBuilder(); builder.append("Time : ").append(System.currentTimeMillis()).append('\n'); builder.append("Device : ").append(Build.MANUFACTURER).append(" ").append(Build.MODEL).append(" (") .append(Build.PRODUCT).append(")\n"); builder.append("Android : ").append(VERSION.RELEASE).append(" (").append(VERSION.INCREMENTAL).append(", ") .append(Build.DISPLAY).append(")\n"); builder.append("ABIs : ").append(TextUtils.join(", ", getSupportedAbis())).append("\n"); builder.append("Memory : ").append(getMemoryUsage(context)).append("\n"); builder.append("Memclass: ").append(getMemoryClass(context)).append("\n"); builder.append("OS Host : ").append(Build.HOST).append("\n"); builder.append("App : "); try { builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0))).append(" ") .append(pm.getPackageInfo(context.getPackageName(), 0).versionName).append("\n"); } catch (PackageManager.NameNotFoundException nnfe) { builder.append("Unknown\n"); } return builder.toString(); } private static Iterable<String> getSupportedAbis() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return Arrays.asList(Build.SUPPORTED_ABIS); } else { LinkedList<String> abis = new LinkedList<>(); abis.add(Build.CPU_ABI); if (Build.CPU_ABI2 != null && !"unknown".equals(Build.CPU_ABI2)) { abis.add(Build.CPU_ABI2); } return abis; } } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. * <p> * See the Android Training lesson <a href= * "http://developer.android.com/training/basics/fragments/communicating.html" * >Communicating with Other Fragments</a> for more information. */ public interface OnLogSubmittedListener { public void onSuccess(); public void onFailure(); public void onCancel(); } private static final class LogPreviewAdapter extends RecyclerView.Adapter<LogPreviewViewHolder> { private String[] lines = new String[0]; @Override public LogPreviewViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new LogPreviewViewHolder( LayoutInflater.from(parent.getContext()).inflate(R.layout.item_log_preview, parent, false)); } @Override public void onBindViewHolder(LogPreviewViewHolder holder, int position) { holder.bind(lines, position); } @Override public void onViewRecycled(LogPreviewViewHolder holder) { holder.unbind(); } @Override public int getItemCount() { return lines.length; } void setText(@NonNull String text) { lines = text.split("\n"); notifyDataSetChanged(); } String getText() { return Util.join(lines, "\n"); } } private static final class LogPreviewViewHolder extends RecyclerView.ViewHolder { private EditText text; private String[] lines; private int index; LogPreviewViewHolder(View itemView) { super(itemView); text = (EditText) itemView; } void bind(String[] lines, int index) { this.lines = lines; this.index = index; text.setText(lines[index]); text.addTextChangedListener(textWatcher); } void unbind() { text.removeTextChangedListener(textWatcher); } private final SimpleTextWatcher textWatcher = new SimpleTextWatcher() { @Override public void onTextChanged(String text) { if (lines != null) { lines[index] = text; } } }; } }