diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 493dd4ef213..41fed89ecc1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -337,12 +337,12 @@ android:label="@string/AndroidManifest__linked_devices" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> - - getLogs() { - final SettableFuture future = new SettableFuture<>(); + public ListenableFuture getLogs() { + final SettableFuture future = new SettableFuture<>(); executor.execute(() -> { StringBuilder builder = new StringBuilder(); @@ -118,7 +118,7 @@ public ListenableFuture getLogs() { } } - future.set(builder.toString()); + future.set(builder); } catch (NoExternalStorageException e) { future.setException(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/CompleteLogLine.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/CompleteLogLine.java new file mode 100644 index 00000000000..bbdc61f654f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/CompleteLogLine.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.logsubmit; + +import androidx.annotation.NonNull; + +/** + * A {@link LogLine} with proper IDs. + */ +public class CompleteLogLine implements LogLine { + + private final long id; + private final LogLine line; + + public CompleteLogLine(long id, @NonNull LogLine line) { + this.id = id; + this.line = line; + } + + @Override + public long getId() { + return id; + } + + @Override + public @NonNull String getText() { + return line.getText(); + } + + @Override + public @NonNull Style getStyle() { + return line.getStyle(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java new file mode 100644 index 00000000000..8b4d8126f59 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.logsubmit; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import java.util.List; +import java.util.regex.Pattern; + +interface LogLine { + + long getId(); + @NonNull String getText(); + @NonNull Style getStyle(); + + static List fromText(@NonNull CharSequence text) { + return Stream.of(Pattern.compile("\\n").split(text)) + .map(s -> new SimpleLogLine(s, Style.NONE)) + .map(line -> (LogLine) line) + .toList(); + } + + enum Style { + NONE, VERBOSE, DEBUG, INFO, WARNING, ERROR + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSection.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSection.java new file mode 100644 index 00000000000..54c45f7103d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSection.java @@ -0,0 +1,21 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import java.util.List; + +interface LogSection { + /** + * The title to show at the top of the log section. + */ + @NonNull String getTitle(); + + /** + * The full content of your log section. We use a {@link CharSequence} instead of a + * {@link List } for performance reasons. Scrubbing large swaths of text is faster than + * one line at a time. + */ + @NonNull CharSequence getContent(@NonNull Context context); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java new file mode 100644 index 00000000000..8f0f7dee1eb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.Util; + +import java.util.Map; + +public class LogSectionFeatureFlags implements LogSection { + + @Override + public @NonNull String getTitle() { + return "FEATURE FLAGS"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + StringBuilder out = new StringBuilder(); + Map memory = FeatureFlags.getMemoryValues(); + Map disk = FeatureFlags.getDiskValues(); + Map forced = FeatureFlags.getForcedValues(); + int remoteLength = Stream.of(memory.keySet()).map(String::length).max(Integer::compareTo).orElse(0); + int diskLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0); + int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0); + + out.append("-- Memory\n"); + for (Map.Entry entry : memory.entrySet()) { + out.append(Util.rightPad(entry.getKey(), remoteLength)).append(": ").append(entry.getValue()).append("\n"); + } + out.append("\n"); + + out.append("-- Disk\n"); + for (Map.Entry entry : disk.entrySet()) { + out.append(Util.rightPad(entry.getKey(), diskLength)).append(": ").append(entry.getValue()).append("\n"); + } + out.append("\n"); + + out.append("-- Forced\n"); + if (forced.isEmpty()) { + out.append("None\n"); + } else { + for (Map.Entry entry : forced.entrySet()) { + out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n"); + } + } + + return out; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionJobs.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionJobs.java new file mode 100644 index 00000000000..ed3c3b321ad --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionJobs.java @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; + +import java.util.List; + +public class LogSectionJobs implements LogSection { + + @Override + public @NonNull String getTitle() { + return "JOBS"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + return ApplicationDependencies.getJobManager().getDebugInfo(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogcat.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogcat.java new file mode 100644 index 00000000000..e1a2f26eb80 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogcat.java @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.logging.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class LogSectionLogcat implements LogSection { + + @Override + public @NonNull String getTitle() { + return "LOGCAT"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + 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) { + return "Failed to retrieve."; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java new file mode 100644 index 00000000000..4e40d2668de --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.ApplicationContext; + +import java.util.concurrent.ExecutionException; + +public class LogSectionLogger implements LogSection { + + @Override + public @NonNull String getTitle() { + return "LOGGER"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + try { + return ApplicationContext.getInstance(context).getPersistentLogger().getLogs().get(); + } catch (ExecutionException | InterruptedException e) { + return "Failed to retrieve."; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java new file mode 100644 index 00000000000..873da276bf1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPermissions.java @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import androidx.annotation.NonNull; + +import org.whispersystems.libsignal.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LogSectionPermissions implements LogSection { + @Override + public @NonNull String getTitle() { + return "PERMISSIONS"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + StringBuilder out = new StringBuilder(); + List> status = new ArrayList<>(); + + try { + PackageInfo info = context.getPackageManager().getPackageInfo("org.thoughtcrime.securesms", PackageManager.GET_PERMISSIONS); + + for (int i = 0; i < info.requestedPermissions.length; i++) { + status.add(new Pair<>(info.requestedPermissions[i], + (info.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0)); + } + } catch (PackageManager.NameNotFoundException e) { + return "Unable to retrieve."; + } + + Collections.sort(status, (o1, o2) -> o1.first().compareTo(o2.first())); + + for (Pair pair : status) { + out.append(pair.first()).append(": "); + out.append(pair.second() ? "YES" : "NO"); + out.append("\n"); + } + + return out; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPower.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPower.java new file mode 100644 index 00000000000..9d42ab3a6f7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPower.java @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.app.usage.UsageStatsManager; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import org.thoughtcrime.securesms.util.BucketInfo; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@RequiresApi(28) +public class LogSectionPower implements LogSection { + + @Override + public @NonNull String getTitle() { + return "POWER"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + final UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); + + if (usageStatsManager == null) { + return "UsageStatsManager not available"; + } + + BucketInfo info = BucketInfo.getInfo(usageStatsManager, TimeUnit.DAYS.toMillis(3)); + + return new StringBuilder().append("Current bucket: ").append(BucketInfo.bucketToString(info.getCurrentBucket())).append('\n') + .append("Highest bucket: ").append(BucketInfo.bucketToString(info.getBestBucket())).append('\n') + .append("Lowest bucket : ").append(BucketInfo.bucketToString(info.getWorstBucket())).append("\n\n") + .append(info.getHistory()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java new file mode 100644 index 00000000000..29dbefac192 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionSystemInfo.java @@ -0,0 +1,94 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.util.ByteUnit; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +public class LogSectionSystemInfo implements LogSection { + + @Override + public @NonNull String getTitle() { + return "SYSINFO"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull 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(Build.VERSION.RELEASE).append(" (") + .append(Build.VERSION.INCREMENTAL).append(", ") + .append(Build.DISPLAY).append(")\n"); + builder.append("ABIs : ").append(TextUtils.join(", ", getSupportedAbis())).append("\n"); + builder.append("Memory : ").append(getMemoryUsage()).append("\n"); + builder.append("Memclass : ").append(getMemoryClass(context)).append("\n"); + builder.append("OS Host : ").append(Build.HOST).append("\n"); + builder.append("First Version: ").append(TextSecurePreferences.getFirstInstallVersion(context)).append("\n"); + builder.append("App : "); + try { + builder.append(pm.getApplicationLabel(pm.getApplicationInfo(context.getPackageName(), 0))) + .append(" ") + .append(pm.getPackageInfo(context.getPackageName(), 0).versionName) + .append(" (") + .append(Util.getManifestApkVersion(context)) + .append(")\n"); + } catch (PackageManager.NameNotFoundException nnfe) { + builder.append("Unknown\n"); + } + + return builder; + } + + private static @NonNull String getMemoryUsage() { + Runtime info = Runtime.getRuntime(); + long totalMemory = info.totalMemory(); + + return String.format(Locale.ENGLISH, + "%dM (%.2f%% free, %dM max)", + ByteUnit.BYTES.toMegabytes(totalMemory), + (float) info.freeMemory() / totalMemory * 100f, + ByteUnit.BYTES.toMegabytes(info.maxMemory())); + } + + private static @NonNull String getMemoryClass(Context context) { + ActivityManager activityManager = ServiceUtil.getActivityManager(context); + String lowMem = ""; + + if (activityManager.isLowRamDevice()) { + lowMem = ", low-mem device"; + } + + return activityManager.getMemoryClass() + lowMem; + } + + private static @NonNull Iterable getSupportedAbis() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Arrays.asList(Build.SUPPORTED_ABIS); + } else { + LinkedList 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; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionThreads.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionThreads.java new file mode 100644 index 00000000000..e1ce2a60e98 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionThreads.java @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Map; + +public class LogSectionThreads implements LogSection { + + @Override + public @NonNull String getTitle() { + return "BLOCKED THREADS"; + } + + @Override + public @NonNull CharSequence getContent(@NonNull Context context) { + Map traces = Thread.getAllStackTraces(); + StringBuilder out = new StringBuilder(); + + for (Map.Entry entry : traces.entrySet()) { + if (entry.getKey().getState() == Thread.State.BLOCKED) { + Thread thread = entry.getKey(); + out.append("-- [").append(thread.getId()).append("] ") + .append(thread.getName()).append(" (").append(thread.getState().toString()).append(")\n"); + + for (StackTraceElement element : entry.getValue()) { + out.append(element.toString()).append("\n"); + } + + out.append("\n"); + } + } + + return out.length() == 0 ? "None" : out; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogStyleParser.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogStyleParser.java new file mode 100644 index 00000000000..56ab1a96ab0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogStyleParser.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.logsubmit; + +import androidx.annotation.NonNull; + +import java.util.HashMap; +import java.util.Map; + +public class LogStyleParser { + + private static final Map STYLE_MARKERS = new HashMap() {{ + put(" V ", LogLine.Style.VERBOSE); + put(" D ", LogLine.Style.DEBUG); + put(" I ", LogLine.Style.INFO); + put(" W ", LogLine.Style.WARNING); + put(" E ", LogLine.Style.ERROR); + }}; + + public static LogLine.Style parseStyle(@NonNull String text) { + for (Map.Entry entry : STYLE_MARKERS.entrySet()) { + if (text.contains(entry.getKey())) { + return entry.getValue(); + } + } + return LogLine.Style.NONE; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/ShareIntentListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/ShareIntentListAdapter.java deleted file mode 100644 index 2180e5f0a4a..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/ShareIntentListAdapter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * * - * 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 . - * / - */ - -package org.thoughtcrime.securesms.logsubmit; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.ResolveInfo; -import androidx.annotation.NonNull; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import org.thoughtcrime.securesms.R; - -import java.util.List; - -/** - * rhodey - */ -public class ShareIntentListAdapter extends ArrayAdapter { - - public static ShareIntentListAdapter getAdapterForIntent(Context context, Intent shareIntent) { - List activities = context.getPackageManager().queryIntentActivities(shareIntent, 0); - return new ShareIntentListAdapter(context, activities.toArray(new ResolveInfo[activities.size()])); - } - - public ShareIntentListAdapter(Context context, ResolveInfo[] items) { - super(context, R.layout.share_intent_list, items); - } - - @Override - public @NonNull View getView(int position, View convertView, @NonNull ViewGroup parent) { - LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - View rowView = inflater.inflate(R.layout.share_intent_row, parent, false); - ImageView intentImage = (ImageView) rowView.findViewById(R.id.share_intent_image); - TextView intentLabel = (TextView) rowView.findViewById(R.id.share_intent_label); - - ApplicationInfo intentInfo = getItem(position).activityInfo.applicationInfo; - - intentImage.setImageDrawable(intentInfo.loadIcon(getContext().getPackageManager())); - intentLabel.setText(intentInfo.loadLabel(getContext().getPackageManager())); - - return rowView; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SimpleLogLine.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SimpleLogLine.java new file mode 100644 index 00000000000..90c6017fcee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SimpleLogLine.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.logsubmit; + +import androidx.annotation.NonNull; + +/** + * A {@link LogLine} that doesn't worry about IDs. + */ +class SimpleLogLine implements LogLine { + + static final SimpleLogLine EMPTY = new SimpleLogLine("", Style.NONE); + + private final String text; + private final Style style; + + SimpleLogLine(@NonNull String text, @NonNull Style style) { + this.text = text; + this.style = style; + } + + @Override + public long getId() { + return -1; + } + + public @NonNull String getText() { + return text; + } + + public @NonNull Style getStyle() { + return style; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java new file mode 100644 index 00000000000..203514b9779 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java @@ -0,0 +1,273 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.text.util.Linkify; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.SearchView; +import androidx.core.app.ShareCompat; +import androidx.core.text.util.LinkifyCompat; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.dd.CircularProgressButton; + +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.ThemeUtil; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; + +import java.util.List; + +public class SubmitDebugLogActivity extends PassphraseRequiredActionBarActivity implements SubmitDebugLogAdapter.Listener { + + private RecyclerView lineList; + private SubmitDebugLogAdapter adapter; + private SubmitDebugLogViewModel viewModel; + + private View warningBanner; + private View editBanner; + private CircularProgressButton submitButton; + private AlertDialog loadingDialog; + private View scrollToBottomButton; + private View scrollToTopButton; + + private MenuItem editMenuItem; + private MenuItem doneMenuItem; + private MenuItem searchMenuItem; + + private final DynamicTheme dynamicTheme = new DynamicTheme(); + + @Override + protected void onPreCreate() { + dynamicTheme.onCreate(this); + } + + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + setContentView(R.layout.submit_debug_log_activity); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + initView(); + initViewModel(); + } + + @Override + protected void onResume() { + super.onResume(); + dynamicTheme.onResume(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.submit_debug_log_normal, menu); + + this.editMenuItem = menu.findItem(R.id.menu_edit_log); + this.doneMenuItem = menu.findItem(R.id.menu_done_editing_log); + this.searchMenuItem = menu.findItem(R.id.menu_search); + + SearchView searchView = (SearchView) searchMenuItem.getActionView(); + SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + viewModel.onQueryUpdated(query); + return true; + } + + @Override + public boolean onQueryTextChange(String query) { + viewModel.onQueryUpdated(query); + return true; + } + }; + + searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + searchView.setOnQueryTextListener(queryListener); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + searchView.setOnQueryTextListener(null); + viewModel.onSearchClosed(); + return true; + } + }); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.menu_edit_log: + viewModel.onEditButtonPressed(); + break; + case R.id.menu_done_editing_log: + viewModel.onDoneEditingButtonPressed(); + break; + } + + return false; + } + + @Override + public void onBackPressed() { + if (!viewModel.onBackPressed()) { + super.onBackPressed(); + } + } + + @Override + public void onLogDeleted(@NonNull LogLine logLine) { + viewModel.onLogDeleted(logLine); + } + + private void initView() { + this.lineList = findViewById(R.id.debug_log_lines); + this.warningBanner = findViewById(R.id.debug_log_warning_banner); + this.editBanner = findViewById(R.id.debug_log_edit_banner); + this.submitButton = findViewById(R.id.debug_log_submit_button); + this.scrollToBottomButton = findViewById(R.id.debug_log_scroll_to_bottom); + this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top); + + this.adapter = new SubmitDebugLogAdapter(this); + + this.lineList.setLayoutManager(new LinearLayoutManager(this)); + this.lineList.setAdapter(adapter); + + submitButton.setOnClickListener(v -> onSubmitClicked()); + + scrollToBottomButton.setOnClickListener(v -> lineList.scrollToPosition(adapter.getItemCount() - 1)); + scrollToTopButton.setOnClickListener(v -> lineList.scrollToPosition(0)); + + lineList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() < adapter.getItemCount() - 10) { + scrollToBottomButton.setVisibility(View.VISIBLE); + } else { + scrollToBottomButton.setVisibility(View.GONE); + } + + if (((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() > 10) { + scrollToTopButton.setVisibility(View.VISIBLE); + } else { + scrollToTopButton.setVisibility(View.GONE); + } + } + }); + + this.loadingDialog = SimpleProgressDialog.show(this); + } + + private void initViewModel() { + this.viewModel = ViewModelProviders.of(this, new SubmitDebugLogViewModel.Factory()).get(SubmitDebugLogViewModel.class); + + viewModel.getLines().observe(this, this::presentLines); + viewModel.getMode().observe(this, this::presentMode); + } + + private void presentLines(@NonNull List lines) { + if (loadingDialog != null) { + loadingDialog.dismiss(); + loadingDialog = null; + + warningBanner.setVisibility(View.VISIBLE); + submitButton.setVisibility(View.VISIBLE); + } + + adapter.setLines(lines); + } + + private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) { + switch (mode) { + case NORMAL: + editBanner.setVisibility(View.GONE); + adapter.setEditing(false); + editMenuItem.setVisible(true); + doneMenuItem.setVisible(false); + searchMenuItem.setVisible(true); + break; + case SUBMITTING: + editBanner.setVisibility(View.GONE); + adapter.setEditing(false); + editMenuItem.setVisible(false); + doneMenuItem.setVisible(false); + searchMenuItem.setVisible(false); + break; + case EDIT: + editBanner.setVisibility(View.VISIBLE); + adapter.setEditing(true); + editMenuItem.setVisible(false); + doneMenuItem.setVisible(true); + searchMenuItem.setVisible(true); + break; + } + } + + private void presentResultDialog(@NonNull String url) { + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(R.string.SubmitDebugLogActivity_success) + .setCancelable(false) + .setNeutralButton(R.string.SubmitDebugLogActivity_ok, (d, w) -> finish()) + .setPositiveButton(R.string.SubmitDebugLogActivity_share, (d, w) -> { + ShareCompat.IntentBuilder.from(this) + .setText(url) + .setType("text/plain") + .setEmailTo(new String[] { "support@signal.org" }) + .startChooser(); + }); + + TextView textView = new TextView(builder.getContext()); + textView.setText(getResources().getString(R.string.SubmitDebugLogActivity_copy_this_url_and_add_it_to_your_issue, url)); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + textView.setOnLongClickListener(v -> { + Util.copyToClipboard(this, url); + Toast.makeText(this, R.string.SubmitDebugLogActivity_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + return true; + }); + + LinkifyCompat.addLinks(textView, Linkify.WEB_URLS); + ViewUtil.setPadding(textView, (int) ThemeUtil.getThemedDimen(this, R.attr.dialogPreferredPadding)); + + builder.setView(textView); + builder.show(); + } + + private void onSubmitClicked() { + submitButton.setClickable(false); + submitButton.setIndeterminateProgressMode(true); + submitButton.setProgress(50); + + viewModel.onSubmitClicked().observe(this, result -> { + if (result.isPresent()) { + presentResultDialog(result.get()); + } else { + Toast.makeText(this, R.string.SubmitDebugLogActivity_failed_to_submit_logs, Toast.LENGTH_LONG).show(); + } + + submitButton.setClickable(true); + submitButton.setIndeterminateProgressMode(false); + submitButton.setProgress(0); + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java new file mode 100644 index 00000000000..26a34e69a50 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java @@ -0,0 +1,168 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.ListenableHorizontalScrollView; +import org.thoughtcrime.securesms.util.ThemeUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class SubmitDebugLogAdapter extends RecyclerView.Adapter { + + private final List lines; + private final ScrollManager scrollManager; + private final Listener listener; + + private boolean editing; + private int longestLine; + + public SubmitDebugLogAdapter(@NonNull Listener listener) { + this.listener = listener; + this.lines = new ArrayList<>(); + this.scrollManager = new ScrollManager(); + + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return lines.get(position).getId(); + } + + @Override + public @NonNull LineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new LineViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.submit_debug_log_line_item, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull LineViewHolder holder, int position) { + holder.bind(lines.get(position), longestLine, editing, scrollManager, listener); + } + + @Override + public void onViewRecycled(@NonNull LineViewHolder holder) { + holder.unbind(scrollManager); + } + + @Override + public int getItemCount() { + return lines.size(); + } + + public void setLines(@NonNull List lines) { + this.lines.clear(); + this.lines.addAll(lines); + + this.longestLine = Stream.of(lines).reduce(0, (currentMax, line) -> Math.max(currentMax, line.getText().length())); + + notifyDataSetChanged(); + } + + public void setEditing(boolean editing) { + this.editing = editing; + notifyDataSetChanged(); + } + + private static class ScrollManager { + private final List listeners = new CopyOnWriteArrayList<>(); + + private int currentPosition; + + void subscribe(@NonNull ScrollObserver observer) { + listeners.add(observer); + observer.onScrollChanged(currentPosition); + } + + void unsubscribe(@NonNull ScrollObserver observer) { + listeners.remove(observer); + } + + void notify(int position) { + currentPosition = position; + + for (ScrollObserver listener : listeners) { + listener.onScrollChanged(position); + } + } + } + + private interface ScrollObserver { + void onScrollChanged(int position); + } + + interface Listener { + void onLogDeleted(@NonNull LogLine logLine); + } + + static class LineViewHolder extends RecyclerView.ViewHolder implements ScrollObserver { + + private final TextView text; + private final ListenableHorizontalScrollView scrollView; + + LineViewHolder(@NonNull View itemView) { + super(itemView); + this.text = itemView.findViewById(R.id.log_item_text); + this.scrollView = itemView.findViewById(R.id.log_item_scroll); + } + + void bind(@NonNull LogLine line, int longestLine, boolean editing, @NonNull ScrollManager scrollManager, @NonNull Listener listener) { + Context context = itemView.getContext(); + + if (line.getText().length() < longestLine) { + text.setText(padRight(line.getText(), longestLine)); + } else { + text.setText(line.getText()); + } + + switch (line.getStyle()) { + case NONE: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_none)); break; + case VERBOSE: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_verbose)); break; + case DEBUG: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_debug)); break; + case INFO: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_info)); break; + case WARNING: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_warn)); break; + case ERROR: text.setTextColor(ThemeUtil.getThemedColor(context, R.attr.debuglog_color_error)); break; + } + + scrollView.setOnScrollListener((newLeft, oldLeft) -> { + if (oldLeft - newLeft != 0) { + scrollManager.notify(newLeft); + } + }); + + scrollManager.subscribe(this); + + if (editing) { + text.setOnClickListener(v -> listener.onLogDeleted(line)); + } else { + text.setOnClickListener(null); + } + } + + void unbind(@NonNull ScrollManager scrollManager) { + text.setOnClickListener(null); + scrollManager.unsubscribe(this); + } + + @Override + public void onScrollChanged(int position) { + scrollView.scrollTo(position, 0); + } + + private static String padRight(String s, int n) { + return String.format("%-" + n + "s", s); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java new file mode 100644 index 00000000000..6c6184fdafc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java @@ -0,0 +1,215 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import com.annimon.stream.Stream; + +import org.json.JSONException; +import org.json.JSONObject; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.logsubmit.util.Scrubber; +import org.thoughtcrime.securesms.net.UserAgentInterceptor; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.regex.Pattern; + +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * Handles retrieving, scrubbing, and uploading of all debug logs. + * + * Adding a new log section: + * - Create a new {@link LogSection}. + * - Add it to {@link #SECTIONS}. The order of the list is the order the sections are displayed. + */ +class SubmitDebugLogRepository { + + private static final String TAG = Log.tag(SubmitDebugLogRepository.class); + + private static final char TITLE_DECORATION = '='; + private static final int MIN_DECORATIONS = 5; + private static final int SECTION_SPACING = 3; + private static final String API_ENDPOINT = "https://debuglogs.org"; + + /** Ordered list of log sections. */ + private static final List SECTIONS = new ArrayList() {{ + add(new LogSectionSystemInfo()); + add(new LogSectionJobs()); + if (Build.VERSION.SDK_INT >= 28) { + add(new LogSectionPower()); + } + add(new LogSectionThreads()); + add(new LogSectionFeatureFlags()); + add(new LogSectionPermissions()); + add(new LogSectionLogcat()); + add(new LogSectionLogger()); + }}; + + private final Context context; + private final ExecutorService executor; + + SubmitDebugLogRepository() { + this.context = ApplicationDependencies.getApplication(); + this.executor = SignalExecutors.SERIAL; + } + + void getLogLines(@NonNull Callback> callback) { + executor.execute(() -> callback.onResult(getLogLinesInternal())); + } + + void submitLog(@NonNull List lines, Callback> callback) { + SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines))); + } + + @WorkerThread + private @NonNull Optional submitLogInternal(@NonNull List lines) { + StringBuilder bodyBuilder = new StringBuilder(); + for (LogLine line : lines) { + bodyBuilder.append(line.getText()).append('\n'); + } + + try { + OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new UserAgentInterceptor()).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 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"), bodyBuilder.toString())); + + Response postResponse = client.newCall(new Request.Builder().url(url).post(post.build()).build()).execute(); + + if (!postResponse.isSuccessful()) { + throw new IOException("Bad response: " + postResponse); + } + + return Optional.of(API_ENDPOINT + "/" + item); + } catch (IOException | JSONException e) { + Log.w(TAG, "Error during upload.", e); + return Optional.absent(); + } + } + + @WorkerThread + private @NonNull List getLogLinesInternal() { + long startTime = System.currentTimeMillis(); + + int maxTitleLength = Stream.of(SECTIONS).reduce(0, (max, section) -> Math.max(max, section.getTitle().length())); + + List>> futures = new ArrayList<>(); + + for (LogSection section : SECTIONS) { + futures.add(SignalExecutors.BOUNDED.submit(() -> { + List lines = getLinesForSection(context, section, maxTitleLength); + + if (SECTIONS.indexOf(section) != SECTIONS.size() - 1) { + for (int i = 0; i < SECTION_SPACING; i++) { + lines.add(SimpleLogLine.EMPTY); + } + } + + return lines; + })); + } + + List allLines = new ArrayList<>(); + + for (Future> future : futures) { + try { + allLines.addAll(future.get()); + } catch (ExecutionException | InterruptedException e) { + throw new AssertionError(e); + } + } + + List withIds = new ArrayList<>(allLines.size()); + + for (int i = 0; i < allLines.size(); i++) { + withIds.add(new CompleteLogLine(i, allLines.get(i))); + } + + Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms"); + + return withIds; + } + + @WorkerThread + private static @NonNull List getLinesForSection(@NonNull Context context, @NonNull LogSection section, int maxTitleLength) { + long startTime = System.currentTimeMillis(); + + List out = new ArrayList<>(); + out.add(new SimpleLogLine(formatTitle(section.getTitle(), maxTitleLength), LogLine.Style.NONE)); + + CharSequence content = Scrubber.scrub(section.getContent(context)); + + List lines = Stream.of(Pattern.compile("\\n").split(content)) + .map(s -> new SimpleLogLine(s, LogStyleParser.parseStyle(s))) + .map(line -> (LogLine) line) + .toList(); + + out.addAll(lines); + + Log.d(TAG, "[" + section.getTitle() + "] Took " + (System.currentTimeMillis() - startTime) + " ms"); + + return out; + } + + private static @NonNull String formatTitle(@NonNull String title, int maxTitleLength) { + int neededPadding = maxTitleLength - title.length(); + int leftPadding = neededPadding / 2; + int rightPadding = neededPadding - leftPadding; + + StringBuilder out = new StringBuilder(); + + for (int i = 0; i < leftPadding + MIN_DECORATIONS; i++) { + out.append(TITLE_DECORATION); + } + + out.append(' ').append(title).append(' '); + + for (int i = 0; i < rightPadding + MIN_DECORATIONS; i++) { + out.append(TITLE_DECORATION); + } + + return out.toString(); + } + + interface Callback { + void onResult(E result); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java new file mode 100644 index 00000000000..fc12dedea99 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.util.DefaultValueLiveData; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.util.Collections; +import java.util.List; + +public class SubmitDebugLogViewModel extends ViewModel { + + private final SubmitDebugLogRepository repo; + private final DefaultValueLiveData> lines; + private final DefaultValueLiveData mode; + + private List sourceLines; + + private SubmitDebugLogViewModel() { + this.repo = new SubmitDebugLogRepository(); + this.lines = new DefaultValueLiveData<>(Collections.emptyList()); + this.mode = new DefaultValueLiveData<>(Mode.NORMAL); + + repo.getLogLines(result -> { + sourceLines = result; + mode.postValue(Mode.NORMAL); + lines.postValue(sourceLines); + }); + } + + @NonNull LiveData> getLines() { + return lines; + } + + boolean hasLines() { + return lines.getValue().size() > 0; + } + + @NonNull LiveData getMode() { + return mode; + } + + @NonNull LiveData> onSubmitClicked() { + mode.postValue(Mode.SUBMITTING); + + MutableLiveData> result = new MutableLiveData<>(); + + repo.submitLog(lines.getValue(), value -> { + mode.postValue(Mode.NORMAL); + result.postValue(value); + }); + + return result; + } + + void onQueryUpdated(@NonNull String query) { + if (TextUtils.isEmpty(query)) { + lines.postValue(sourceLines); + } else { + List filtered = Stream.of(sourceLines) + .filter(l -> l.getText().toLowerCase().contains(query.toLowerCase())) + .toList(); + lines.postValue(filtered); + } + } + + void onSearchClosed() { + lines.postValue(sourceLines); + } + + void onEditButtonPressed() { + mode.setValue(Mode.EDIT); + } + + void onDoneEditingButtonPressed() { + mode.setValue(Mode.NORMAL); + } + + void onLogDeleted(@NonNull LogLine line) { + sourceLines.remove(line); + + List logs = lines.getValue(); + logs.remove(line); + + lines.postValue(logs); + } + + boolean onBackPressed() { + if (mode.getValue().equals(Mode.EDIT)) { + mode.setValue(Mode.NORMAL); + return true; + } else { + return false; + } + } + + enum Mode { + NORMAL, EDIT, SUBMITTING + } + + public static class Factory extends ViewModelProvider.NewInstanceFactory { + @Override + public @NonNull T create(@NonNull Class modelClass) { + //noinspection ConstantConditions + return modelClass.cast(new SubmitDebugLogViewModel()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java deleted file mode 100644 index 84ce1818420..00000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitLogFragment.java +++ /dev/null @@ -1,759 +0,0 @@ -/* - * 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 . - */ - -package org.thoughtcrime.securesms.logsubmit; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.ActivityManager; -import android.app.AlertDialog; -import android.app.usage.UsageStatsManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -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.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 androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.annimon.stream.Stream; - -import org.json.JSONException; -import org.json.JSONObject; -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.logsubmit.util.Scrubber; -import org.thoughtcrime.securesms.util.BucketInfo; -import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.FrameRateTracker; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; -import org.whispersystems.libsignal.util.Pair; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -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_POWER = "========== POWER =========="; - private static final String HEADER_THREADS = "===== BLOCKED THREADS ====="; - private static final String HEADER_PERMISSIONS = "======= PERMISSIONS ======="; - private static final String HEADER_FLAGS = "====== FEATURE FLAGS ======"; - 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(@NonNull 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(@NonNull 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(@NonNull 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 { - private WeakReference 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; - - CharSequence 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(); - CharSequence scrubbedLogcat = Scrubber.scrub(logcat); - Log.i(TAG, "Scrub logcat: " + (System.currentTimeMillis() - t4) + " ms"); - - - StringBuilder stringBuilder = new StringBuilder(); - - stringBuilder.append(HEADER_SYSINFO) - .append("\n\n") - .append(buildDescription(context)) - .append("\n\n\n") - .append(HEADER_JOBS) - .append("\n\n") - .append(Scrubber.scrub(ApplicationDependencies.getJobManager().getDebugInfo())) - .append("\n\n\n"); - - if (VERSION.SDK_INT >= 28) { - stringBuilder.append(HEADER_POWER) - .append("\n\n") - .append(buildPower(context)) - .append("\n\n\n"); - } - - stringBuilder.append(HEADER_THREADS) - .append("\n\n") - .append(buildBlockedThreads()) - .append("\n\n\n"); - - stringBuilder.append(HEADER_FLAGS) - .append("\n\n") - .append(buildFlags()) - .append("\n\n\n"); - - stringBuilder.append(HEADER_PERMISSIONS) - .append("\n\n") - .append(buildPermissions(context)) - .append("\n\n\n"); - - stringBuilder.append(HEADER_LOGCAT) - .append("\n\n") - .append(scrubbedLogcat) - .append("\n\n\n") - .append(HEADER_LOGGER) - .append("\n\n") - .append(newLogs); - - return stringBuilder.toString(); - } - - @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 { - 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 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(); - long totalMemory = info.totalMemory(); - return String.format(Locale.ENGLISH, "%dM (%.2f%% free, %dM max)", - asMegs(totalMemory), - (float)info.freeMemory() / 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 CharSequence 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("Refresh Rate : ").append(String.format(Locale.ENGLISH, "%.2f", FrameRateTracker.getDisplayRefreshRate(context))).append(" hz").append("\n"); - builder.append("Average FPS : ").append(String.format(Locale.ENGLISH, "%.2f", ApplicationDependencies.getFrameRateTracker().getRunningAverageFps())).append("\n"); - builder.append("First Version: ").append(TextSecurePreferences.getFirstInstallVersion(context)).append("\n"); - builder.append("App : ").append(BuildConfig.VERSION_NAME); - - return builder; - } - - @RequiresApi(28) - private static CharSequence buildPower(@NonNull Context context) { - final UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - - if (usageStatsManager == null) { - return "UsageStatsManager not available"; - } - - BucketInfo info = BucketInfo.getInfo(usageStatsManager, TimeUnit.DAYS.toMillis(3)); - - return new StringBuilder().append("Current bucket: ").append(BucketInfo.bucketToString(info.getCurrentBucket())).append('\n') - .append("Highest bucket: ").append(BucketInfo.bucketToString(info.getBestBucket())).append('\n') - .append("Lowest bucket : ").append(BucketInfo.bucketToString(info.getWorstBucket())).append("\n\n") - .append(info.getHistory()); - } - - private static CharSequence buildBlockedThreads() { - Map traces = Thread.getAllStackTraces(); - StringBuilder out = new StringBuilder(); - - for (Map.Entry entry : traces.entrySet()) { - if (entry.getKey().getState() == Thread.State.BLOCKED) { - Thread thread = entry.getKey(); - out.append("-- [").append(thread.getId()).append("] ") - .append(thread.getName()).append(" (").append(thread.getState().toString()).append(")\n"); - - for (StackTraceElement element : entry.getValue()) { - out.append(element.toString()).append("\n"); - } - - out.append("\n"); - } - } - - return out.length() == 0 ? "None" : out; - } - - private static CharSequence buildPermissions(@NonNull Context context) { - StringBuilder out = new StringBuilder(); - - List> status = new ArrayList<>(); - - try { - PackageInfo info = context.getPackageManager().getPackageInfo("org.thoughtcrime.securesms", PackageManager.GET_PERMISSIONS); - - for (int i = 0; i < info.requestedPermissions.length; i++) { - status.add(new Pair<>(info.requestedPermissions[i], - (info.requestedPermissionsFlags[i] & PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0)); - } - } catch (PackageManager.NameNotFoundException e) { - return "Unable to retrieve."; - } - - Collections.sort(status, (o1, o2) -> o1.first().compareTo(o2.first())); - - for (Pair pair : status) { - out.append(pair.first()).append(": "); - out.append(pair.second() ? "YES" : "NO"); - out.append("\n"); - } - - return out; - } - - private static CharSequence buildFlags() { - StringBuilder out = new StringBuilder(); - Map memory = FeatureFlags.getMemoryValues(); - Map disk = FeatureFlags.getDiskValues(); - Map forced = FeatureFlags.getForcedValues(); - int remoteLength = Stream.of(memory.keySet()).map(String::length).max(Integer::compareTo).orElse(0); - int diskLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0); - int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0); - - out.append("-- Memory\n"); - for (Map.Entry entry : memory.entrySet()) { - out.append(Util.rightPad(entry.getKey(), remoteLength)).append(": ").append(entry.getValue()).append("\n"); - } - out.append("\n"); - - out.append("-- Disk\n"); - for (Map.Entry entry : disk.entrySet()) { - out.append(Util.rightPad(entry.getKey(), diskLength)).append(": ").append(entry.getValue()).append("\n"); - } - out.append("\n"); - - out.append("-- Forced\n"); - if (forced.isEmpty()) { - out.append("None\n"); - } else { - for (Map.Entry entry : forced.entrySet()) { - out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n"); - } - } - - return out; - } - - - private static Iterable getSupportedAbis() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return Arrays.asList(Build.SUPPORTED_ABIS); - } else { - LinkedList 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. - *

- * See the Android Training lesson Communicating with Other Fragments for more information. - */ - public interface OnLogSubmittedListener { - public void onSuccess(); - public void onFailure(); - public void onCancel(); - } - - private static final class LogPreviewAdapter extends RecyclerView.Adapter { - - 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; - } - } - }; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java index 8ebfc686850..3780edb1313 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java @@ -4,10 +4,8 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; -import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.widget.Toast; @@ -26,10 +24,10 @@ import com.google.firebase.iid.FirebaseInstanceId; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; -import org.thoughtcrime.securesms.LogSubmitActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactIdentityManager; +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity; import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; @@ -146,7 +144,7 @@ private void handleIdentitySelection(Intent data) { private class SubmitDebugLogListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { - final Intent intent = new Intent(getActivity(), LogSubmitActivity.class); + final Intent intent = new Intent(getActivity(), SubmitDebugLogActivity.class); startActivity(intent); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java index 6828144a381..7ebc2d0df3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java @@ -17,8 +17,8 @@ import com.dd.CircularProgressButton; -import org.thoughtcrime.securesms.LogSubmitActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA; @@ -95,7 +95,7 @@ public void onClick(View v) { debugTapCounter++; if (debugTapCounter >= DEBUG_TAP_TARGET) { - context.startActivity(new Intent(context, LogSubmitActivity.class)); + context.startActivity(new Intent(context, SubmitDebugLogActivity.class)); } else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) { int remaining = DEBUG_TAP_TARGET - debugTapCounter; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ByteUnit.java b/app/src/main/java/org/thoughtcrime/securesms/util/ByteUnit.java new file mode 100644 index 00000000000..739419d98ce --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ByteUnit.java @@ -0,0 +1,40 @@ +package org.thoughtcrime.securesms.util; + +/** + * Just like {@link java.util.concurrent.TimeUnit}, but for bytes. + */ +public enum ByteUnit { + + BYTES { + public long toBytes(long d) { return d; } + public long toKilobytes(long d) { return d/1024; } + public long toMegabytes(long d) { return toKilobytes(d)/1024; } + public long toGigabytes(long d) { return toMegabytes(d)/1024; } + }, + + KILOBYTES { + public long toBytes(long d) { return d * 1024; } + public long toKilobytes(long d) { return d; } + public long toMegabytes(long d) { return d/1024; } + public long toGigabytes(long d) { return toMegabytes(d)/1024; } + }, + + MEGABYTES { + public long toBytes(long d) { return toKilobytes(d) * 1024; } + public long toKilobytes(long d) { return d * 1024; } + public long toMegabytes(long d) { return d; } + public long toGigabytes(long d) { return d/1024; } + }, + + GIGABYTES { + public long toBytes(long d) { return toKilobytes(d) * 1024; } + public long toKilobytes(long d) { return toMegabytes(d) * 1024; } + public long toMegabytes(long d) { return d * 1024; } + public long toGigabytes(long d) { return d; } + }; + + public long toBytes(long d) { throw new AbstractMethodError(); } + public long toKilobytes(long d) { throw new AbstractMethodError(); } + public long toMegabytes(long d) { throw new AbstractMethodError(); } + public long toGigabytes(long d) { throw new AbstractMethodError(); } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DefaultValueLiveData.java b/app/src/main/java/org/thoughtcrime/securesms/util/DefaultValueLiveData.java new file mode 100644 index 00000000000..68b9623558b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DefaultValueLiveData.java @@ -0,0 +1,19 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.lifecycle.MutableLiveData; + +public class DefaultValueLiveData extends MutableLiveData { + + private final T defaultValue; + + public DefaultValueLiveData(@NonNull T defaultValue) { + this.defaultValue = defaultValue; + } + + @Override + public @NonNull T getValue() { + T value = super.getValue(); + return value != null ? value : defaultValue; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java index 3782dc3f0ed..cf438a23aa4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtil.java @@ -266,6 +266,10 @@ public static void setPaddingBottom(@NonNull View view, int padding) { view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding); } + public static void setPadding(@NonNull View view, int padding) { + view.setPadding(padding, padding, padding, padding); + } + public static boolean isPointInsideView(@NonNull View view, float x, float y) { int[] location = new int[2]; diff --git a/app/src/main/res/layout/fragment_submit_log.xml b/app/src/main/res/layout/fragment_submit_log.xml deleted file mode 100644 index e7eafcc9ee4..00000000000 --- a/app/src/main/res/layout/fragment_submit_log.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - -