mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 14:48:35 +00:00
Improve debuglog submission.
This commit is contained in:
parent
1faf196f82
commit
0c254c9621
@ -337,12 +337,12 @@
|
|||||||
android:label="@string/AndroidManifest__linked_devices"
|
android:label="@string/AndroidManifest__linked_devices"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".LogSubmitActivity"
|
<activity android:name=".logsubmit.SubmitDebugLogActivity"
|
||||||
android:label="@string/AndroidManifest__log_submit"
|
android:label="@string/AndroidManifest__log_submit"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".MediaPreviewActivity"
|
<activity android:name=".MediaPreviewActivity"
|
||||||
android:label="@string/AndroidManifest__media_preview"
|
android:label="@string/AndroidManifest__media_preview"
|
||||||
android:windowSoftInputMode="stateHidden"
|
android:windowSoftInputMode="stateHidden"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
@ -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.InvalidPassphraseException;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||||
|
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||||
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
|
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
@ -164,7 +165,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogSubmit() {
|
private void handleLogSubmit() {
|
||||||
Intent intent = new Intent(this, LogSubmitActivity.class);
|
Intent intent = new Intent(this, SubmitDebugLogActivity.class);
|
||||||
startActivity(intent);
|
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
|
@WorkerThread
|
||||||
public ListenableFuture<String> getLogs() {
|
public ListenableFuture<CharSequence> getLogs() {
|
||||||
final SettableFuture<String> future = new SettableFuture<>();
|
final SettableFuture<CharSequence> future = new SettableFuture<>();
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
@ -118,7 +118,7 @@ public class PersistentLogger extends Log.Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
future.set(builder.toString());
|
future.set(builder);
|
||||||
} catch (NoExternalStorageException e) {
|
} catch (NoExternalStorageException e) {
|
||||||
future.setException(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.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@ -26,10 +24,10 @@ import org.thoughtcrime.securesms.logging.Log;
|
|||||||
import com.google.firebase.iid.FirebaseInstanceId;
|
import com.google.firebase.iid.FirebaseInstanceId;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||||
import org.thoughtcrime.securesms.LogSubmitActivity;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
|
import org.thoughtcrime.securesms.contacts.ContactIdentityManager;
|
||||||
|
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||||
@ -146,7 +144,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
|
|||||||
private class SubmitDebugLogListener implements Preference.OnPreferenceClickListener {
|
private class SubmitDebugLogListener implements Preference.OnPreferenceClickListener {
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
final Intent intent = new Intent(getActivity(), LogSubmitActivity.class);
|
final Intent intent = new Intent(getActivity(), SubmitDebugLogActivity.class);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@ import androidx.lifecycle.ViewModelProviders;
|
|||||||
|
|
||||||
import com.dd.CircularProgressButton;
|
import com.dd.CircularProgressButton;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.LogSubmitActivity;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
|
||||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA;
|
import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA;
|
||||||
@ -95,7 +95,7 @@ abstract class BaseRegistrationFragment extends Fragment {
|
|||||||
debugTapCounter++;
|
debugTapCounter++;
|
||||||
|
|
||||||
if (debugTapCounter >= DEBUG_TAP_TARGET) {
|
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) {
|
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
|
||||||
int remaining = DEBUG_TAP_TARGET - debugTapCounter;
|
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);
|
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) {
|
public static boolean isPointInsideView(@NonNull View view, float x, float y) {
|
||||||
int[] location = new int[2];
|
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_shade" format="color"/>
|
||||||
<attr name="megaphone_reactions_close_tint" 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">
|
<declare-styleable name="ColorPreference">
|
||||||
<attr name="itemLayout" format="reference" />
|
<attr name="itemLayout" format="reference" />
|
||||||
<attr name="choices" 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="storage_legend_circle_size">8dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="debug_log_text_size">12sp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -870,6 +870,18 @@
|
|||||||
<string name="StickerPackPreviewActivity_stickers">Stickers</string>
|
<string name="StickerPackPreviewActivity_stickers">Stickers</string>
|
||||||
<string name="StickerPackPreviewActivity_failed_to_load_sticker_pack">Failed to load sticker pack</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 -->
|
<!-- ThreadRecord -->
|
||||||
<string name="ThreadRecord_group_updated">Group updated</string>
|
<string name="ThreadRecord_group_updated">Group updated</string>
|
||||||
<string name="ThreadRecord_left_the_group">Left the group</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__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__loading_logs">Loading logs…</string>
|
||||||
<string name="log_submit_activity__uploading_logs">Uploading 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__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__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>
|
<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="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="verification_background">@color/core_grey_05</item>
|
||||||
|
|
||||||
<item name="emoji_tab_strip_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="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="verification_background">@color/core_grey_95</item>
|
||||||
|
|
||||||
<item name="dialog_info_icon">@drawable/ic_info_outline_dark</item>
|
<item name="dialog_info_icon">@drawable/ic_info_outline_dark</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user