From f9de131017a5e220efdd48ee9d02d3e09bc21ba1 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 5 Mar 2020 19:05:06 -0400 Subject: [PATCH] Add new contact us flow. --- .../ApplicationPreferencesActivity.java | 7 + .../securesms/help/HelpFragment.java | 279 ++++++++++++++++++ .../securesms/help/HelpViewModel.java | 79 +++++ .../securesms/logsubmit/LogLine.java | 2 +- .../logsubmit/SubmitDebugLogRepository.java | 10 +- .../securesms/util/IntentUtils.java | 21 ++ .../thoughtcrime/securesms/util/SpanUtil.java | 7 + .../res/color/help_fragment_next_dark.xml | 5 + .../res/color/help_fragment_next_light.xml | 5 + .../cta_button_background_disabled.xml | 9 + ...p_fragment_emoji_radio_background_dark.xml | 13 + ..._fragment_emoji_radio_background_light.xml | 13 + .../help_fragment_problem_background_dark.xml | 20 ++ ...help_fragment_problem_background_light.xml | 20 ++ .../main/res/drawable/ic_help_outline_24.xml | 5 + .../main/res/drawable/ic_help_solid_24.xml | 5 + app/src/main/res/layout/help_fragment.xml | 221 ++++++++++++++ app/src/main/res/values/attrs.xml | 5 + app/src/main/res/values/strings.xml | 30 ++ app/src/main/res/values/themes.xml | 10 + app/src/main/res/xml/preferences.xml | 4 + 21 files changed, 764 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java create mode 100644 app/src/main/res/color/help_fragment_next_dark.xml create mode 100644 app/src/main/res/color/help_fragment_next_light.xml create mode 100644 app/src/main/res/drawable/cta_button_background_disabled.xml create mode 100644 app/src/main/res/drawable/help_fragment_emoji_radio_background_dark.xml create mode 100644 app/src/main/res/drawable/help_fragment_emoji_radio_background_light.xml create mode 100644 app/src/main/res/drawable/help_fragment_problem_background_dark.xml create mode 100644 app/src/main/res/drawable/help_fragment_problem_background_light.xml create mode 100644 app/src/main/res/drawable/ic_help_outline_24.xml create mode 100644 app/src/main/res/drawable/ic_help_solid_24.xml create mode 100644 app/src/main/res/layout/help_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 60fe7f7abf..ccdd269dbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -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(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java new file mode 100644 index 0000000000..078a02c469 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpFragment.java @@ -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 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(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java new file mode 100644 index 0000000000..a526ab23b3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java @@ -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 problemMeetsLengthRequirements = new MutableLiveData<>(); + private MutableLiveData hasLines = new MutableLiveData<>(false); + private LiveData isFormValid = Transformations.map(new LiveDataPair<>(problemMeetsLengthRequirements, hasLines), this::transformValidationData); + + private final SubmitDebugLogRepository submitDebugLogRepository; + + private List logLines; + + public HelpViewModel() { + submitDebugLogRepository = new SubmitDebugLogRepository(); + + submitDebugLogRepository.getLogLines(lines -> { + logLines = lines; + hasLines.postValue(true); + }); + } + + LiveData isFormValid() { + return isFormValid; + } + + void onProblemChanged(@NonNull String problem) { + problemMeetsLengthRequirements.setValue(problem.length() >= MINIMUM_PROBLEM_CHARS); + } + + LiveData onSubmitClicked(boolean includeDebugLogs) { + MutableLiveData 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 validationData) { + return validationData.first() == Boolean.TRUE && validationData.second() == Boolean.TRUE; + } + + static class SubmitResult { + private final Optional debugLogUrl; + private final boolean isError; + + private SubmitResult(@NonNull Optional debugLogUrl, boolean isError) { + this.debugLogUrl = debugLogUrl; + this.isError = isError; + } + + @NonNull Optional getDebugLogUrl() { + return debugLogUrl; + } + + boolean isError() { + return isError; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java index 8b4d8126f5..304071ab8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogLine.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java index 6c6184fdaf..b0c8aa6310 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java @@ -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> callback) { + public void getLogLines(@NonNull Callback> callback) { executor.execute(() -> callback.onResult(getLogLinesInternal())); } - void submitLog(@NonNull List lines, Callback> callback) { + public void submitLog(@NonNull List lines, Callback> callback) { SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines))); } @@ -209,7 +209,7 @@ class SubmitDebugLogRepository { return out.toString(); } - interface Callback { + public interface Callback { void onResult(E result); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java index 0eb110ebdc..c4a6465561 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IntentUtils.java @@ -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: https://stackoverflow.com/a/12328282 + */ + 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; + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java index 8890b6efb5..1b01da1ab2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java @@ -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 { diff --git a/app/src/main/res/color/help_fragment_next_dark.xml b/app/src/main/res/color/help_fragment_next_dark.xml new file mode 100644 index 0000000000..d210aa9226 --- /dev/null +++ b/app/src/main/res/color/help_fragment_next_dark.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/help_fragment_next_light.xml b/app/src/main/res/color/help_fragment_next_light.xml new file mode 100644 index 0000000000..6ecdc9eb3c --- /dev/null +++ b/app/src/main/res/color/help_fragment_next_light.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/cta_button_background_disabled.xml b/app/src/main/res/drawable/cta_button_background_disabled.xml new file mode 100644 index 0000000000..d6e861c1e0 --- /dev/null +++ b/app/src/main/res/drawable/cta_button_background_disabled.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/help_fragment_emoji_radio_background_dark.xml b/app/src/main/res/drawable/help_fragment_emoji_radio_background_dark.xml new file mode 100644 index 0000000000..d6f235e65f --- /dev/null +++ b/app/src/main/res/drawable/help_fragment_emoji_radio_background_dark.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/help_fragment_emoji_radio_background_light.xml b/app/src/main/res/drawable/help_fragment_emoji_radio_background_light.xml new file mode 100644 index 0000000000..c648a10f86 --- /dev/null +++ b/app/src/main/res/drawable/help_fragment_emoji_radio_background_light.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/help_fragment_problem_background_dark.xml b/app/src/main/res/drawable/help_fragment_problem_background_dark.xml new file mode 100644 index 0000000000..db351dad23 --- /dev/null +++ b/app/src/main/res/drawable/help_fragment_problem_background_dark.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/help_fragment_problem_background_light.xml b/app/src/main/res/drawable/help_fragment_problem_background_light.xml new file mode 100644 index 0000000000..2abc1a166b --- /dev/null +++ b/app/src/main/res/drawable/help_fragment_problem_background_light.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_help_outline_24.xml b/app/src/main/res/drawable/ic_help_outline_24.xml new file mode 100644 index 0000000000..5865c28c9c --- /dev/null +++ b/app/src/main/res/drawable/ic_help_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_help_solid_24.xml b/app/src/main/res/drawable/ic_help_solid_24.xml new file mode 100644 index 0000000000..156c057914 --- /dev/null +++ b/app/src/main/res/drawable/ic_help_solid_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml new file mode 100644 index 0000000000..c0e4715d0d --- /dev/null +++ b/app/src/main/res/layout/help_fragment.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + +