org.thoughtcrime.securesms.logsubmit.SubmitLogFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.thoughtcrime.securesms.logsubmit.SubmitLogFragment.java

Source

/*
 * 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;
                }
            }
        };
    }
}