Improve debuglog submission.
This commit is contained in:
parent
1faf196f82
commit
0c254c9621
38 changed files with 1575 additions and 1004 deletions
|
@ -337,7 +337,7 @@
|
|||
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"/>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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>
|
105
app/src/main/res/layout/submit_debug_log_activity.xml
Normal file
105
app/src/main/res/layout/submit_debug_log_activity.xml
Normal 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>
|
15
app/src/main/res/layout/submit_debug_log_line_item.xml
Normal file
15
app/src/main/res/layout/submit_debug_log_line_item.xml
Normal 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>
|
25
app/src/main/res/menu/submit_debug_log_normal.xml
Normal file
25
app/src/main/res/menu/submit_debug_log_normal.xml
Normal 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>
|
|
@ -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" />
|
||||
|
|
16
app/src/main/res/values/debuglog_colors.xml
Normal file
16
app/src/main/res/values/debuglog_colors.xml
Normal 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>
|
|
@ -149,4 +149,6 @@
|
|||
|
||||
<dimen name="storage_legend_circle_size">8dp</dimen>
|
||||
|
||||
<dimen name="debug_log_text_size">12sp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue