Improve debuglog submission.

master
Greyson Parrelli 2019-12-13 00:18:46 -05:00
parent 1faf196f82
commit 0c254c9621
38 changed files with 1575 additions and 1004 deletions

View File

@ -337,12 +337,12 @@
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".LogSubmitActivity"
<activity android:name=".logsubmit.SubmitDebugLogActivity"
android:label="@string/AndroidManifest__log_submit"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"

View File

@ -1,78 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentTransaction;
import org.thoughtcrime.securesms.logging.Log;
import android.view.MenuItem;
import android.widget.Toast;
import org.thoughtcrime.securesms.logsubmit.SubmitLogFragment;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Activity for submitting logcat logs to a pastebin service.
*/
public class LogSubmitActivity extends BaseActionBarActivity implements SubmitLogFragment.OnLogSubmittedListener {
private static final String TAG = LogSubmitActivity.class.getSimpleName();
private DynamicTheme dynamicTheme = new DynamicTheme();
@Override
protected void onCreate(Bundle icicle) {
dynamicTheme.onCreate(this);
super.onCreate(icicle);
setContentView(R.layout.log_submit_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
SubmitLogFragment fragment = SubmitLogFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
@Override
protected void onResume() {
dynamicTheme.onResume(this);
super.onResume();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return false;
}
@Override
public void onSuccess() {
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__thanks, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onFailure() {
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__log_fetch_failed, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onCancel() {
finish();
}
@Override
public void startActivity(Intent intent) {
try {
super.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w(TAG, e);
Toast.makeText(this, R.string.log_submit_activity__no_browser_installed, Toast.LENGTH_LONG).show();
}
}
}

View File

@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -164,7 +165,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
}
private void handleLogSubmit() {
Intent intent = new Intent(this, LogSubmitActivity.class);
Intent intent = new Intent(this, SubmitDebugLogActivity.class);
startActivity(intent);
}

View File

@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.HorizontalScrollView;
import androidx.annotation.Nullable;
/**
* Unfortunately {@link HorizontalScrollView#setOnScrollChangeListener(OnScrollChangeListener)}
* wasn't added until API 23, so now we have to do this ourselves.
*/
public class ListenableHorizontalScrollView extends HorizontalScrollView {
private OnScrollListener listener;
public ListenableHorizontalScrollView(Context context) {
super(context);
}
public ListenableHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnScrollListener(@Nullable OnScrollListener listener) {
this.listener = listener;
}
@Override
protected void onScrollChanged(int newLeft, int newTop, int oldLeft, int oldTop) {
if (listener != null) {
listener.onScroll(newLeft, oldLeft);
}
super.onScrollChanged(newLeft, newTop, oldLeft, oldTop);
}
public interface OnScrollListener {
void onScroll(int newLeft, int oldLeft);
}
}

View File

@ -100,8 +100,8 @@ public class PersistentLogger extends Log.Logger {
}
@WorkerThread
public ListenableFuture<String> getLogs() {
final SettableFuture<String> future = new SettableFuture<>();
public ListenableFuture<CharSequence> getLogs() {
final SettableFuture<CharSequence> future = new SettableFuture<>();
executor.execute(() -> {
StringBuilder builder = new StringBuilder();
@ -118,7 +118,7 @@ public class PersistentLogger extends Log.Logger {
}
}
future.set(builder.toString());
future.set(builder);
} catch (NoExternalStorageException e) {
future.setException(e);
}

View File

@ -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();
}
}

View File

@ -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<LogLine> 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
}
}

View File

@ -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<LogLine> } for performance reasons. Scrubbing large swaths of text is faster than
* one line at a time.
*/
@NonNull CharSequence getContent(@NonNull Context context);
}

View File

@ -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<String, Boolean> memory = FeatureFlags.getMemoryValues();
Map<String, Boolean> disk = FeatureFlags.getDiskValues();
Map<String, Boolean> 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<String, Boolean> 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<String, Boolean> 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<String, Boolean> entry : forced.entrySet()) {
out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n");
}
}
return out;
}
}

View File

@ -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();
}
}

View File

@ -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.";
}
}
}

View File

@ -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.";
}
}
}

View File

@ -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<Pair<String, Boolean>> 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<String, Boolean> pair : status) {
out.append(pair.first()).append(": ");
out.append(pair.second() ? "YES" : "NO");
out.append("\n");
}
return out;
}
}

View File

@ -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());
}
}

View File

@ -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<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;
}
}
}

View File

@ -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<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
StringBuilder out = new StringBuilder();
for (Map.Entry<Thread, StackTraceElement[]> 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;
}
}

View File

@ -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<String, LogLine.Style> STYLE_MARKERS = new HashMap<String, LogLine.Style>() {{
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<String, LogLine.Style> entry : STYLE_MARKERS.entrySet()) {
if (text.contains(entry.getKey())) {
return entry.getValue();
}
}
return LogLine.Style.NONE;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
* /
*/
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<ResolveInfo> {
public static ShareIntentListAdapter getAdapterForIntent(Context context, Intent shareIntent) {
List<ResolveInfo> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<LogLine> 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);
});
}
}

View File

@ -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<SubmitDebugLogAdapter.LineViewHolder> {
private final List<LogLine> 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<LogLine> 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<ScrollObserver> 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);
}
}
}

View File

@ -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<LogSection> SECTIONS = new ArrayList<LogSection>() {{
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<List<LogLine>> callback) {
executor.execute(() -> callback.onResult(getLogLinesInternal()));
}
void submitLog(@NonNull List<LogLine> lines, Callback<Optional<String>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines)));
}
@WorkerThread
private @NonNull Optional<String> submitLogInternal(@NonNull List<LogLine> 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<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"), 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<LogLine> getLogLinesInternal() {
long startTime = System.currentTimeMillis();
int maxTitleLength = Stream.of(SECTIONS).reduce(0, (max, section) -> Math.max(max, section.getTitle().length()));
List<Future<List<LogLine>>> futures = new ArrayList<>();
for (LogSection section : SECTIONS) {
futures.add(SignalExecutors.BOUNDED.submit(() -> {
List<LogLine> 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<LogLine> allLines = new ArrayList<>();
for (Future<List<LogLine>> future : futures) {
try {
allLines.addAll(future.get());
} catch (ExecutionException | InterruptedException e) {
throw new AssertionError(e);
}
}
List<LogLine> 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<LogLine> getLinesForSection(@NonNull Context context, @NonNull LogSection section, int maxTitleLength) {
long startTime = System.currentTimeMillis();
List<LogLine> out = new ArrayList<>();
out.add(new SimpleLogLine(formatTitle(section.getTitle(), maxTitleLength), LogLine.Style.NONE));
CharSequence content = Scrubber.scrub(section.getContent(context));
List<LogLine> 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<E> {
void onResult(E result);
}
}

View File

@ -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<List<LogLine>> lines;
private final DefaultValueLiveData<Mode> mode;
private List<LogLine> 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<List<LogLine>> getLines() {
return lines;
}
boolean hasLines() {
return lines.getValue().size() > 0;
}
@NonNull LiveData<Mode> getMode() {
return mode;
}
@NonNull LiveData<Optional<String>> onSubmitClicked() {
mode.postValue(Mode.SUBMITTING);
MutableLiveData<Optional<String>> 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<LogLine> 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<LogLine> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new SubmitDebugLogViewModel());
}
}
}

View File

@ -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 <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.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<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;
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<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();
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<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
StringBuilder out = new StringBuilder();
for (Map.Entry<Thread, StackTraceElement[]> 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<Pair<String, Boolean>> 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<String, Boolean> 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<String, Boolean> memory = FeatureFlags.getMemoryValues();
Map<String, Boolean> disk = FeatureFlags.getDiskValues();
Map<String, Boolean> 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<String, Boolean> 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<String, Boolean> 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<String, Boolean> entry : forced.entrySet()) {
out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n");
}
}
return out;
}
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;
}
}
};
}
}

View File

@ -4,10 +4,8 @@ import android.app.Activity;
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 org.thoughtcrime.securesms.logging.Log;
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 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
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;
}

View File

@ -17,8 +17,8 @@ import androidx.lifecycle.ViewModelProviders;
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 @@ abstract class BaseRegistrationFragment extends Fragment {
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;

View File

@ -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(); }
}

View File

@ -0,0 +1,19 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
public class DefaultValueLiveData<T> extends MutableLiveData<T> {
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;
}
}

View File

@ -266,6 +266,10 @@ public class ViewUtil {
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];

View File

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ /**
~ * Copyright (C) 2014 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/>.
~ */
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:id="@+id/log_submit_confirmation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="@string/log_submit_activity__this_log_will_be_posted_online"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:background="@color/logsubmit_confirmation_background"
android:fontFamily="sans-serif-light"
tools:ignore="UnusedAttribute"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"/>
<ImageButton
android:id="@+id/scroll_to_bottom_button"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:padding="5dp"
android:layout_gravity="bottom|end"
android:background="@drawable/circle_tintable"
android:tint="@color/grey_600"
android:elevation="1dp"
android:alpha="0.9"
android:contentDescription="@string/conversation_fragment__scroll_to_the_bottom_content_description"
android:src="@drawable/ic_scroll_down"/>
</FrameLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button android:id="@+id/cancel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/log_submit_activity__button_dont_submit"
android:layout_weight="1"/>
<Button android:id="@+id/ok"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/log_submit_activity__button_submit"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/debug_log_warning_banner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/log_submit_activity__this_log_will_be_posted_online"
android:textColor="@color/core_black"
android:background="@color/core_yellow"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
<TextView
android:id="@+id/debug_log_edit_banner"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
android:gravity="center"
android:text="@string/SubmitDebugLogActivity_tap_a_line_to_delete_it"
android:textColor="@color/core_white"
android:fontFamily="sans-serif-medium"
android:background="@color/core_blue"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/debug_log_warning_banner"
app:layout_constraintBottom_toBottomOf="@id/debug_log_warning_banner"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/debug_log_header_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/debug_log_lines"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toBottomOf="@id/debug_log_header_barrier"
app:layout_constraintBottom_toTopOf="@id/debug_log_submit_button"/>
<ImageButton
android:id="@+id/debug_log_scroll_to_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
android:padding="5dp"
android:background="@drawable/circle_tintable"
android:tint="@color/grey_600"
android:elevation="1dp"
android:src="@drawable/ic_scroll_down"
android:scaleY="-1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/debug_log_warning_banner"/>
<ImageButton
android:id="@+id/debug_log_scroll_to_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:padding="5dp"
android:background="@drawable/circle_tintable"
android:tint="@color/grey_600"
android:elevation="1dp"
android:src="@drawable/ic_scroll_down"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/debug_log_submit_button"/>
<com.dd.CircularProgressButton
android:id="@+id/debug_log_submit_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:background="@color/signal_primary"
android:textAllCaps="true"
android:textColor="@color/white"
android:visibility="gone"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/SubmitDebugLogActivity_submit"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,15 @@
<org.thoughtcrime.securesms.components.ListenableHorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/log_item_scroll"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:scrollbars="none">
<TextView
android:id="@+id/log_item_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textSize="@dimen/debug_log_text_size"/>
</org.thoughtcrime.securesms.components.ListenableHorizontalScrollView>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_search_24"
android:title="@string/CameraContacts__menu_search"
android:visible="false"
app:actionViewClass="org.thoughtcrime.securesms.components.SearchView"
app:showAsAction="collapseActionView|always" />
<item
android:id="@+id/menu_edit_log"
android:title="@string/SubmitDebugLogActivity_edit"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/menu_done_editing_log"
android:title="@string/SubmitDebugLogActivity_done"
android:visible="false"
app:showAsAction="always" />
</menu>

View File

@ -261,6 +261,13 @@
<attr name="megaphone_reactions_shade" format="color"/>
<attr name="megaphone_reactions_close_tint" format="color"/>
<attr name="debuglog_color_none" format="color" />
<attr name="debuglog_color_verbose" format="color" />
<attr name="debuglog_color_debug" format="color" />
<attr name="debuglog_color_info" format="color" />
<attr name="debuglog_color_warn" format="color" />
<attr name="debuglog_color_error" format="color" />
<declare-styleable name="ColorPreference">
<attr name="itemLayout" format="reference" />
<attr name="choices" format="reference" />

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="debuglog_light_none">@color/core_black</color>
<color name="debuglog_light_verbose">#515151</color>
<color name="debuglog_light_debug">#089314</color>
<color name="debuglog_light_info">#0a7087</color>
<color name="debuglog_light_warn">#b58c12</color>
<color name="debuglog_light_error">#af0d0a</color>
<color name="debuglog_dark_none">@color/core_white</color>
<color name="debuglog_dark_verbose">#8a8a8a</color>
<color name="debuglog_dark_debug">#5ca72b</color>
<color name="debuglog_dark_info">#46bbb9</color>
<color name="debuglog_dark_warn">#cdd637</color>
<color name="debuglog_dark_error">#ff6b68</color>
</resources>

View File

@ -149,4 +149,6 @@
<dimen name="storage_legend_circle_size">8dp</dimen>
<dimen name="debug_log_text_size">12sp</dimen>
</resources>

View File

@ -870,6 +870,18 @@
<string name="StickerPackPreviewActivity_stickers">Stickers</string>
<string name="StickerPackPreviewActivity_failed_to_load_sticker_pack">Failed to load sticker pack</string>
<!-- SubmitDebugLogActivity -->
<string name="SubmitDebugLogActivity_edit">Edit</string>
<string name="SubmitDebugLogActivity_done">Done</string>
<string name="SubmitDebugLogActivity_tap_a_line_to_delete_it">Tap a line to delete it</string>
<string name="SubmitDebugLogActivity_submit">Submit</string>
<string name="SubmitDebugLogActivity_failed_to_submit_logs">Failed to submit logs</string>
<string name="SubmitDebugLogActivity_success">Success!</string>
<string name="SubmitDebugLogActivity_copy_this_url_and_add_it_to_your_issue">Copy this URL and add it to your issue report or support email:\n\n<b>%1$s</b></string>
<string name="SubmitDebugLogActivity_copied_to_clipboard">Copied to clipboard</string>
<string name="SubmitDebugLogActivity_ok">Ok</string>
<string name="SubmitDebugLogActivity_share">Share</string>
<!-- ThreadRecord -->
<string name="ThreadRecord_group_updated">Group updated</string>
<string name="ThreadRecord_left_the_group">Left the group</string>
@ -1282,9 +1294,6 @@
<string name="log_submit_activity__this_log_will_be_posted_online">This log will be posted publicly online for contributors to view, you may examine and edit it before submitting.</string>
<string name="log_submit_activity__loading_logs">Loading logs…</string>
<string name="log_submit_activity__uploading_logs">Uploading logs…</string>
<string name="log_submit_activity__success">Success!</string>
<string name="log_submit_activity__copy_this_url_and_add_it_to_your_issue">Copy this URL and add it to your issue report or support email:\n\n<b>%1$s</b>\n</string>
<string name="log_submit_activity__copied_to_clipboard">Copied to clipboard</string>
<string name="log_submit_activity__choose_email_app">Choose email app</string>
<string name="log_submit_activity__please_review_this_log_from_my_app">Please review this log from my app: %1$s</string>
<string name="log_submit_activity__network_failure">Network failure. Please try again.</string>

View File

@ -249,6 +249,13 @@
<item name="contact_list_divider">@drawable/contact_list_divider_light</item>
<item name="debuglog_color_none">@color/debuglog_light_none</item>
<item name="debuglog_color_verbose">@color/debuglog_light_verbose</item>
<item name="debuglog_color_debug">@color/debuglog_light_debug</item>
<item name="debuglog_color_info">@color/debuglog_light_info</item>
<item name="debuglog_color_warn">@color/debuglog_light_warn</item>
<item name="debuglog_color_error">@color/debuglog_light_error</item>
<item name="verification_background">@color/core_grey_05</item>
<item name="emoji_tab_strip_background">@color/core_grey_05</item>
@ -496,6 +503,13 @@
<item name="contact_list_divider">@drawable/contact_list_divider_dark</item>
<item name="debuglog_color_none">@color/debuglog_dark_none</item>
<item name="debuglog_color_verbose">@color/debuglog_dark_verbose</item>
<item name="debuglog_color_debug">@color/debuglog_dark_debug</item>
<item name="debuglog_color_info">@color/debuglog_dark_info</item>
<item name="debuglog_color_warn">@color/debuglog_dark_warn</item>
<item name="debuglog_color_error">@color/debuglog_dark_error</item>
<item name="verification_background">@color/core_grey_95</item>
<item name="dialog_info_icon">@drawable/ic_info_outline_dark</item>