mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-30 15:26:11 +00:00
Add new contact us flow.
This commit is contained in:
committed by
Greyson Parrelli
parent
f1f505d41c
commit
f9de131017
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.help.HelpFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
|
||||
@@ -66,6 +67,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
@@ -154,6 +156,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
this.findPreference(PREFERENCE_CATEGORY_HELP)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
|
||||
@@ -240,6 +244,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
case PREFERENCE_CATEGORY_ADVANCED:
|
||||
fragment = new AdvancedPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_HELP:
|
||||
fragment = new HelpFragment();
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package org.thoughtcrime.securesms.help;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.LabeledIntent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.util.IntentUtils;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class HelpFragment extends Fragment {
|
||||
|
||||
private EditText problem;
|
||||
private CheckBox includeDebugLogs;
|
||||
private View debugLogInfo;
|
||||
private View faq;
|
||||
private CircularProgressButton next;
|
||||
private View toaster;
|
||||
private List<EmojiImageView> emoji;
|
||||
private HelpViewModel helpViewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.help_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViewModels();
|
||||
initializeViews(view);
|
||||
initializeListeners();
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__help);
|
||||
|
||||
cancelSpinning(next);
|
||||
problem.setEnabled(true);
|
||||
}
|
||||
|
||||
private void initializeViewModels() {
|
||||
helpViewModel = ViewModelProviders.of(this).get(HelpViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
problem = view.findViewById(R.id.help_fragment_problem);
|
||||
includeDebugLogs = view.findViewById(R.id.help_fragment_debug);
|
||||
debugLogInfo = view.findViewById(R.id.help_fragment_debug_info);
|
||||
faq = view.findViewById(R.id.help_fragment_faq);
|
||||
next = view.findViewById(R.id.help_fragment_next);
|
||||
toaster = view.findViewById(R.id.help_fragment_next_toaster);
|
||||
emoji = new ArrayList<>(Feeling.values().length);
|
||||
|
||||
for (Feeling feeling : Feeling.values()) {
|
||||
EmojiImageView emojiView = view.findViewById(feeling.getViewId());
|
||||
emojiView.setImageEmoji(feeling.getEmojiCode());
|
||||
emoji.add(view.findViewById(feeling.getViewId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeListeners() {
|
||||
problem.addTextChangedListener(new AfterTextChanged(e -> helpViewModel.onProblemChanged(e.toString())));
|
||||
Stream.of(emoji).forEach(view -> view.setOnClickListener(this::handleEmojiClicked));
|
||||
faq.setOnClickListener(v -> launchFaq());
|
||||
debugLogInfo.setOnClickListener(v -> launchDebugLogInfo());
|
||||
next.setOnClickListener(v -> submitForm());
|
||||
toaster.setOnClickListener(v -> Toast.makeText(requireContext(), R.string.HelpFragment__please_be_as_descriptive_as_possible, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
//noinspection CodeBlock2Expr
|
||||
helpViewModel.isFormValid().observe(getViewLifecycleOwner(), isValid -> {
|
||||
next.setEnabled(isValid);
|
||||
toaster.setVisibility(isValid ? View.GONE : View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleEmojiClicked(@NonNull View clicked) {
|
||||
if (clicked.isSelected()) {
|
||||
clicked.setSelected(false);
|
||||
} else {
|
||||
Stream.of(emoji).forEach(view -> view.setSelected(false));
|
||||
clicked.setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void launchFaq() {
|
||||
Uri data = Uri.parse(getString(R.string.HelpFragment__link__faq));
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, data);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void launchDebugLogInfo() {
|
||||
Uri data = Uri.parse(getString(R.string.HelpFragment__link__debug_info));
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, data);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void submitForm() {
|
||||
setSpinning(next);
|
||||
problem.setEnabled(false);
|
||||
|
||||
helpViewModel.onSubmitClicked(includeDebugLogs.isChecked()).observe(this, result -> {
|
||||
if (result.getDebugLogUrl().isPresent()) {
|
||||
submitFormWithDebugLog(result.getDebugLogUrl().get());
|
||||
} else if (result.isError()) {
|
||||
submitFormWithDebugLog(getString(R.string.HelpFragment__could_not_upload_logs));
|
||||
} else {
|
||||
submitFormWithDebugLog(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void submitFormWithDebugLog(@Nullable String debugLog) {
|
||||
Feeling feeling = Stream.of(emoji)
|
||||
.filter(View::isSelected)
|
||||
.map(view -> Feeling.getByViewId(view.getId()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
Spanned body = getEmailBody(debugLog, feeling);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_SENDTO);
|
||||
intent.setData(Uri.parse("mailto:"));
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{getString(R.string.RegistrationActivity_support_email)});
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, getEmailSubject());
|
||||
intent.putExtra(Intent.EXTRA_TEXT, body.toString());
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private String getEmailSubject() {
|
||||
return getString(R.string.HelpFragment__signal_android_support_request);
|
||||
}
|
||||
|
||||
private Spanned getEmailBody(@Nullable String debugLog, @Nullable Feeling feeling) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
builder.append(problem.getText().toString());
|
||||
builder.append("\n\n");
|
||||
builder.append("--- ");
|
||||
builder.append(getString(R.string.HelpFragment__support_info));
|
||||
builder.append(" ---\n");
|
||||
builder.append(getString(R.string.HelpFragment__subject));
|
||||
builder.append(" ");
|
||||
builder.append(getString(R.string.HelpFragment__signal_android_support_request));
|
||||
builder.append("\n");
|
||||
builder.append(getString(R.string.HelpFragment__device_info));
|
||||
builder.append(" ");
|
||||
builder.append(getDeviceInfo());
|
||||
builder.append("\n");
|
||||
builder.append(getString(R.string.HelpFragment__android_version));
|
||||
builder.append(" ");
|
||||
builder.append(getAndroidVersion());
|
||||
builder.append("\n");
|
||||
builder.append(getString(R.string.HelpFragment__signal_version));
|
||||
builder.append(" ");
|
||||
builder.append(getSignalVersion());
|
||||
builder.append("\n");
|
||||
builder.append(getString(R.string.HelpFragment__locale));
|
||||
builder.append(" ");
|
||||
builder.append(Locale.getDefault().toString());
|
||||
|
||||
if (debugLog != null) {
|
||||
builder.append("\n");
|
||||
builder.append(getString(R.string.HelpFragment__debug_log));
|
||||
builder.append(" ");
|
||||
builder.append(debugLog);
|
||||
}
|
||||
|
||||
if (feeling != null) {
|
||||
builder.append("\n\n");
|
||||
builder.append(feeling.getEmojiCode());
|
||||
builder.append("\n");
|
||||
builder.append(getString(feeling.getStringId()));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static CharSequence getDeviceInfo() {
|
||||
return String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.PRODUCT);
|
||||
}
|
||||
|
||||
private static CharSequence getAndroidVersion() {
|
||||
return String.format("%s (%s, %s)", Build.VERSION.RELEASE, Build.VERSION.INCREMENTAL, Build.DISPLAY);
|
||||
}
|
||||
|
||||
private static CharSequence getSignalVersion() {
|
||||
return BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
private static void setSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setClickable(false);
|
||||
button.setIndeterminateProgressMode(true);
|
||||
button.setProgress(50);
|
||||
}
|
||||
}
|
||||
|
||||
private static void cancelSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setProgress(0);
|
||||
button.setIndeterminateProgressMode(false);
|
||||
button.setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Feeling {
|
||||
ECSTATIC(R.id.help_fragment_emoji_5, R.string.HelpFragment__emoji_5, "\ud83d\ude00"),
|
||||
HAPPY(R.id.help_fragment_emoji_4, R.string.HelpFragment__emoji_4, "\ud83d\ude42"),
|
||||
AMBIVALENT(R.id.help_fragment_emoji_3, R.string.HelpFragment__emoji_3, "\ud83d\ude10"),
|
||||
UNHAPPY(R.id.help_fragment_emoji_2, R.string.HelpFragment__emoji_2, "\ud83d\ude41"),
|
||||
ANGRY(R.id.help_fragment_emoji_1, R.string.HelpFragment__emoji_1, "\ud83d\ude20");
|
||||
|
||||
private final @IdRes int viewId;
|
||||
private final @StringRes int stringId;
|
||||
private final CharSequence emojiCode;
|
||||
|
||||
Feeling(@IdRes int viewId, @StringRes int stringId, @NonNull CharSequence emojiCode) {
|
||||
this.viewId = viewId;
|
||||
this.stringId = stringId;
|
||||
this.emojiCode = emojiCode;
|
||||
}
|
||||
|
||||
public @IdRes int getViewId() {
|
||||
return viewId;
|
||||
}
|
||||
|
||||
public @StringRes int getStringId() {
|
||||
return stringId;
|
||||
}
|
||||
|
||||
public @NonNull CharSequence getEmojiCode() {
|
||||
return emojiCode;
|
||||
}
|
||||
|
||||
static Feeling getByViewId(@IdRes int viewId) {
|
||||
for (Feeling feeling : values()) {
|
||||
if (feeling.viewId == viewId) {
|
||||
return feeling;
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.thoughtcrime.securesms.help;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.logsubmit.LogLine;
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HelpViewModel extends ViewModel {
|
||||
|
||||
private static final int MINIMUM_PROBLEM_CHARS = 10;
|
||||
|
||||
private MutableLiveData<Boolean> problemMeetsLengthRequirements = new MutableLiveData<>();
|
||||
private MutableLiveData<Boolean> hasLines = new MutableLiveData<>(false);
|
||||
private LiveData<Boolean> isFormValid = Transformations.map(new LiveDataPair<>(problemMeetsLengthRequirements, hasLines), this::transformValidationData);
|
||||
|
||||
private final SubmitDebugLogRepository submitDebugLogRepository;
|
||||
|
||||
private List<LogLine> logLines;
|
||||
|
||||
public HelpViewModel() {
|
||||
submitDebugLogRepository = new SubmitDebugLogRepository();
|
||||
|
||||
submitDebugLogRepository.getLogLines(lines -> {
|
||||
logLines = lines;
|
||||
hasLines.postValue(true);
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<Boolean> isFormValid() {
|
||||
return isFormValid;
|
||||
}
|
||||
|
||||
void onProblemChanged(@NonNull String problem) {
|
||||
problemMeetsLengthRequirements.setValue(problem.length() >= MINIMUM_PROBLEM_CHARS);
|
||||
}
|
||||
|
||||
LiveData<SubmitResult> onSubmitClicked(boolean includeDebugLogs) {
|
||||
MutableLiveData<SubmitResult> resultLiveData = new MutableLiveData<>();
|
||||
|
||||
if (includeDebugLogs) {
|
||||
submitDebugLogRepository.submitLog(logLines, result -> resultLiveData.postValue(new SubmitResult(result, result.isPresent())));
|
||||
} else {
|
||||
resultLiveData.postValue(new SubmitResult(Optional.absent(), false));
|
||||
}
|
||||
|
||||
return resultLiveData;
|
||||
}
|
||||
|
||||
private boolean transformValidationData(Pair<Boolean, Boolean> validationData) {
|
||||
return validationData.first() == Boolean.TRUE && validationData.second() == Boolean.TRUE;
|
||||
}
|
||||
|
||||
static class SubmitResult {
|
||||
private final Optional<String> debugLogUrl;
|
||||
private final boolean isError;
|
||||
|
||||
private SubmitResult(@NonNull Optional<String> debugLogUrl, boolean isError) {
|
||||
this.debugLogUrl = debugLogUrl;
|
||||
this.isError = isError;
|
||||
}
|
||||
|
||||
@NonNull Optional<String> getDebugLogUrl() {
|
||||
return debugLogUrl;
|
||||
}
|
||||
|
||||
boolean isError() {
|
||||
return isError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import com.annimon.stream.Stream;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
interface LogLine {
|
||||
public interface LogLine {
|
||||
|
||||
long getId();
|
||||
@NonNull String getText();
|
||||
|
||||
@@ -41,7 +41,7 @@ import okhttp3.ResponseBody;
|
||||
* - Create a new {@link LogSection}.
|
||||
* - Add it to {@link #SECTIONS}. The order of the list is the order the sections are displayed.
|
||||
*/
|
||||
class SubmitDebugLogRepository {
|
||||
public class SubmitDebugLogRepository {
|
||||
|
||||
private static final String TAG = Log.tag(SubmitDebugLogRepository.class);
|
||||
|
||||
@@ -67,16 +67,16 @@ class SubmitDebugLogRepository {
|
||||
private final Context context;
|
||||
private final ExecutorService executor;
|
||||
|
||||
SubmitDebugLogRepository() {
|
||||
public SubmitDebugLogRepository() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.executor = SignalExecutors.SERIAL;
|
||||
}
|
||||
|
||||
void getLogLines(@NonNull Callback<List<LogLine>> callback) {
|
||||
public void getLogLines(@NonNull Callback<List<LogLine>> callback) {
|
||||
executor.execute(() -> callback.onResult(getLogLinesInternal()));
|
||||
}
|
||||
|
||||
void submitLog(@NonNull List<LogLine> lines, Callback<Optional<String>> callback) {
|
||||
public void submitLog(@NonNull List<LogLine> lines, Callback<Optional<String>> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines)));
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ class SubmitDebugLogRepository {
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
public interface Callback<E> {
|
||||
void onResult(E result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.LabeledIntent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -15,4 +19,21 @@ public class IntentUtils {
|
||||
return resolveInfoList != null && resolveInfoList.size() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* From: <a href="https://stackoverflow.com/a/12328282">https://stackoverflow.com/a/12328282</a>
|
||||
*/
|
||||
public static @Nullable LabeledIntent getLabelintent(@NonNull Context context, @NonNull Intent origIntent, int name, int drawable) {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
ComponentName launchName = origIntent.resolveActivity(pm);
|
||||
|
||||
if (launchName != null) {
|
||||
Intent resolved = new Intent();
|
||||
resolved.setComponent(launchName);
|
||||
resolved.setData(origIntent.getData());
|
||||
|
||||
return new LabeledIntent(resolved, context.getPackageName(), name, drawable);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.URLSpan;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SpanUtil {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user