[android] Add possibility to save bug reports to the local storage

Fixes #8287

Signed-off-by: Kiryl Razhdzestvenski <kirill.rozh@gmail.com>
This commit is contained in:
krozhdestvenski 2024-07-30 13:00:17 +02:00 committed by GitHub
parent 96608e08ac
commit bf3ae3ee42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 207 additions and 95 deletions

View file

@ -208,6 +208,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private ActivityResultLauncher<IntentSenderRequest> mLocationResolutionRequest;
@NonNull
private ActivityResultLauncher<SharingUtils.SharingIntent> mShareLauncher;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
@ -517,6 +519,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
mPostNotificationPermissionRequest = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
this::onPostNotificationPermissionResult);
mShareLauncher = SharingUtils.RegisterLauncher(this);
mDisplayManager = DisplayManager.from(this);
if (mDisplayManager.isCarDisplayUsed())
{
@ -2040,7 +2044,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
.setTitle(R.string.load_kmz_title)
.setMessage(getString(R.string.unknown_file_type, uri))
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.report_a_bug, (dialog, which) -> Utils.sendBugReport(this,
.setNegativeButton(R.string.report_a_bug, (dialog, which) -> Utils.sendBugReport(mShareLauncher, this,
getString(R.string.load_kmz_title), getString(R.string.unknown_file_type, uri)))
.setOnDismissListener(dialog -> mAlertDialog = null)
.show();
@ -2054,7 +2058,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
.setTitle(R.string.load_kmz_title)
.setMessage(getString(R.string.failed_to_open_file, uri, error))
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.report_a_bug, (dialog, which) -> Utils.sendBugReport(this,
.setNegativeButton(R.string.report_a_bug, (dialog, which) -> Utils.sendBugReport(mShareLauncher, this,
getString(R.string.load_kmz_title), getString(R.string.failed_to_open_file, uri, error)))
.setOnDismissListener(dialog -> mAlertDialog = null)
.show();

View file

@ -62,7 +62,7 @@ public class BookmarkCategoriesFragment extends BaseMwmRecyclerFragment<Bookmark
public static final String BOOKMARKS_CATEGORIES_MENU_ID = "BOOKMARKS_CATEGORIES_BOTTOM_SHEET";
private ActivityResultLauncher<Intent> shareLauncher;
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
@Nullable
private BookmarkCategory mSelectedCategory;

View file

@ -65,7 +65,7 @@ public class BookmarksListFragment extends BaseMwmRecyclerFragment<ConcatAdapter
private static final String TRACK_MENU_ID = "TRACK_MENU_BOTTOM_SHEET";
private static final String OPTIONS_MENU_ID = "OPTIONS_MENU_BOTTOM_SHEET";
private ActivityResultLauncher<Intent> shareLauncher;
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull

View file

@ -47,7 +47,7 @@ public enum BookmarksSharingHelper
}
public void onPreparedFileForSharing(@NonNull FragmentActivity context,
@NonNull ActivityResultLauncher launcher,
@NonNull ActivityResultLauncher<SharingUtils.SharingIntent> launcher,
@NonNull BookmarkSharingResult result)
{
if (mProgressDialog != null && mProgressDialog.isShowing())

View file

@ -8,29 +8,33 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.R;
import app.organicmaps.WebContainerDelegate;
import app.organicmaps.base.BaseMwmFragment;
import app.organicmaps.util.Constants;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.Utils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class FaqFragment extends BaseMwmFragment
{
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
@NonNull
private final DialogInterface.OnClickListener mDialogClickListener = new DialogInterface.OnClickListener()
{
private void sendGeneralFeedback()
{
Utils.sendFeedback(requireActivity());
Utils.sendFeedback(shareLauncher, requireActivity());
}
private void reportBug()
{
Utils.sendBugReport(requireActivity(), "", "");
Utils.sendBugReport(shareLauncher, requireActivity(), "", "");
}
@Override
@ -76,6 +80,8 @@ public class FaqFragment extends BaseMwmFragment
});
}
shareLauncher = SharingUtils.RegisterLauncher(this);
return root;
}
}

View file

@ -8,6 +8,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -20,11 +21,13 @@ import app.organicmaps.util.Config;
import app.organicmaps.util.Constants;
import app.organicmaps.util.DateUtils;
import app.organicmaps.util.Graphics;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.Utils;
public class HelpFragment extends BaseMwmFragment implements View.OnClickListener
{
private String mDonateUrl;
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
private TextView setupItem(@IdRes int id, boolean tint, @NonNull View frame)
{
@ -93,6 +96,8 @@ public class HelpFragment extends BaseMwmFragment implements View.OnClickListene
termOfUseView.setOnClickListener(v -> Utils.openUrl(requireActivity(), getResources().getString(R.string.translated_om_site_url) + "terms/"));
privacyPolicyView.setOnClickListener(v -> Utils.openUrl(requireActivity(), getResources().getString(R.string.translated_om_site_url) + "privacy/"));
shareLauncher = SharingUtils.RegisterLauncher(this);
return root;
}
@ -125,7 +130,7 @@ public class HelpFragment extends BaseMwmFragment implements View.OnClickListene
else if (id == R.id.faq)
((HelpActivity) requireActivity()).stackFragment(FaqFragment.class, getString(R.string.faq), null);
else if (id == R.id.report)
Utils.sendBugReport(requireActivity(), "", "");
Utils.sendBugReport(shareLauncher, requireActivity(), "", "");
else if (id == R.id.support_us)
Utils.openUrl(requireActivity(), getResources().getString(R.string.translated_om_site_url) + "support-us/");
else if (id == R.id.donate)

View file

@ -10,11 +10,13 @@ import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.util.Config;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.StorageUtils;
import app.organicmaps.util.Utils;
import app.organicmaps.util.concurrency.ThreadPool;
@ -31,6 +33,7 @@ public class StoragePathFragment extends BaseSettingsFragment
private StoragePathAdapter mAdapter;
private StoragePathManager mPathManager;
private ActivityResultLauncher<SharingUtils.SharingIntent> shareLauncher;
@Override
protected int getLayoutRes()
{
@ -49,6 +52,8 @@ public class StoragePathFragment extends BaseSettingsFragment
list.setOnItemClickListener((parent, view, position, id) -> changeStorage(position));
list.setAdapter(mAdapter);
shareLauncher = SharingUtils.RegisterLauncher(this);
return root;
}
@ -131,7 +136,7 @@ public class StoragePathFragment extends BaseSettingsFragment
new MaterialAlertDialogBuilder(requireActivity(), R.style.MwmTheme_AlertDialog)
.setTitle(R.string.move_maps_error)
.setPositiveButton(R.string.report_a_bug,
(dlg, which) -> Utils.sendBugReport(requireActivity(), "Error moving map files", ""))
(dlg, which) -> Utils.sendBugReport(shareLauncher, requireActivity(), "Error moving map files", ""))
.show();
}
Framework.nativeChangeWritableDir(newPath);

View file

@ -2,23 +2,27 @@ package app.organicmaps.util;
import android.app.Activity;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ComponentName;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import java.io.IOException;
import app.organicmaps.Framework;
import app.organicmaps.R;
import app.organicmaps.SplashActivity;
import app.organicmaps.bookmarks.data.BookmarkInfo;
import app.organicmaps.bookmarks.data.MapObject;
@ -27,8 +31,76 @@ public class SharingUtils
private static final String KMZ_MIME_TYPE = "application/vnd.google-earth.kmz";
private static final String GPX_MIME_TYPE = "application/gpx";
private static final String TEXT_MIME_TYPE = "text/plain";
public static class ShareInfo
{
public String mMimeType = "";
public String mSubject = "";
public String mText = "";
public String mMail = "";
public String mFileName = "";
private static Uri sourceFileUri;
ShareInfo()
{
}
ShareInfo(@NonNull String mimeType, String subject, String text, String mail, String fileName)
{
mMimeType = mimeType;
mSubject = subject;
mText = text;
mMail = mail;
mFileName = fileName;
}
}
public static class SharingIntent
{
private final Intent mIntent;
private Uri mSource;
SharingIntent(@NonNull Intent intent, Uri source)
{
mIntent = intent;
mSource = source;
}
SharingIntent(@NonNull Intent intent)
{
mIntent = intent;
}
public void SetSourceFile(@NonNull Uri source)
{
mSource = source;
}
Intent GetIntent() {return mIntent;}
Uri GetSourceFile() {return mSource;}
}
public static class SharingContract extends ActivityResultContract<SharingIntent, Pair<Uri,Uri>>
{
static private Uri sourceUri;
@NonNull
@Override
public Intent createIntent(@NonNull Context context, SharingIntent input)
{
sourceUri = input.GetSourceFile();
return input.GetIntent();
}
@Override
public Pair<Uri,Uri> parseResult(int resultCode, Intent intent)
{
if (resultCode == Activity.RESULT_OK && intent != null)
{
Uri dest = intent.getData();
return new Pair<>(sourceUri, dest);
}
return null;
}
}
// This utility class has only static methods
private SharingUtils()
@ -102,69 +174,96 @@ public class SharingUtils
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share)));
}
public static ActivityResultLauncher<Intent> RegisterLauncher(@NonNull Fragment fragment)
private static void ProcessShareResult(@NonNull ContentResolver resolver, Pair<Uri, Uri> result)
{
if (resolver!=null && result != null)
{
Uri sourceUri = result.first;
Uri destinationUri = result.second;
try
{
if (sourceUri != null && destinationUri != null)
StorageUtils.copyFile(resolver, sourceUri, destinationUri);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
}
public static ActivityResultLauncher<SharingIntent> RegisterLauncher(@NonNull Fragment fragment)
{
return fragment.registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result ->
{
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null)
{
Uri destinationUri = result.getData().getData();
Uri sourceUri = sourceFileUri;
sourceFileUri = null;
try
{
if (sourceUri != null && destinationUri != null)
StorageUtils.copyFile(fragment.requireContext().getContentResolver(), sourceUri, destinationUri);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
});
new SharingContract(),
result -> ProcessShareResult(fragment.requireContext().getContentResolver(), result)
);
}
public static void shareBookmarkFile(Context context, ActivityResultLauncher<Intent> launcher, String fileName, String fileMimeType)
public static ActivityResultLauncher<SharingIntent> RegisterLauncher(@NonNull AppCompatActivity activity)
{
return activity.registerForActivityResult(
new SharingContract(),
result -> ProcessShareResult(activity.getContentResolver(), result)
);
}
public static void shareFile(Context context, ActivityResultLauncher<SharingIntent> launcher, ShareInfo info)
{
Intent intent = new Intent(Intent.ACTION_SEND);
final String subject = context.getString(R.string.share_bookmarks_email_subject);
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
final String text = context.getString(R.string.share_bookmarks_email_body);
intent.putExtra(Intent.EXTRA_TEXT, text);
final Uri fileUri = StorageUtils.getUriForFilePath(context, fileName);
intent.putExtra(android.content.Intent.EXTRA_STREAM, fileUri);
// Properly set permissions for intent, see
// https://developer.android.com/reference/androidx/core/content/FileProvider#include-the-permission-in-an-intent
intent.setDataAndType(fileUri, fileMimeType);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
intent.setClipData(ClipData.newRawUri("", fileUri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
Intent saveIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
saveIntent.setType(fileMimeType);
DocumentFile documentFile = DocumentFile.fromSingleUri(context, fileUri);
if (documentFile != null)
saveIntent.putExtra(Intent.EXTRA_TITLE, documentFile.getName());
sourceFileUri = fileUri;
Intent[] extraIntents = {saveIntent};
if (!info.mSubject.isEmpty())
intent.putExtra(Intent.EXTRA_SUBJECT, info.mSubject);
if (!info.mMail.isEmpty())
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{info.mMail});
if (!info.mText.isEmpty())
intent.putExtra(Intent.EXTRA_TEXT, info.mText);
Intent chooser = Intent.createChooser(intent, context.getString(R.string.share));
SharingIntent sharingIntent = new SharingIntent(chooser);
// Prevent sharing to ourselves (supported from API Level 24).
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N)
if (!info.mFileName.isEmpty())
{
ComponentName[] excludeSelf = { new ComponentName(context, app.organicmaps.SplashActivity.class) };
chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludeSelf);
final Uri fileUri = StorageUtils.getUriForFilePath(context, info.mFileName);
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
intent.setDataAndType(fileUri, info.mMimeType);
// Properly set permissions for intent, see
// https://developer.android.com/reference/androidx/core/content/FileProvider#include-the-permission-in-an-intent
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
intent.setClipData(ClipData.newRawUri("", fileUri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
Intent saveIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
saveIntent.setType(info.mMimeType);
final String fileName = fileUri.getPathSegments().get(fileUri.getPathSegments().size()-1);
saveIntent.putExtra(Intent.EXTRA_TITLE, fileName);
Intent[] extraIntents = {saveIntent};
// Prevent sharing to ourselves (supported from API Level 24).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
ComponentName[] excludeSelf = { new ComponentName(context, SplashActivity.class) };
chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludeSelf);
}
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents);
sharingIntent.SetSourceFile(fileUri);
}
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents);
launcher.launch(sharingIntent);
}
launcher.launch(chooser);
public static void shareBookmarkFile(Context context, ActivityResultLauncher<SharingIntent> launcher, String fileName, String mimeType)
{
final String subject = context.getString(R.string.share_bookmarks_email_subject);
final String text = context.getString(R.string.share_bookmarks_email_body);
ShareInfo info = new ShareInfo(mimeType, subject, text, "", fileName);
shareFile(context, launcher, info);
}
}

View file

@ -28,6 +28,7 @@ import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.DimenRes;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
@ -65,6 +66,9 @@ public class Utils
public static final int INVALID_ID = 0;
public static final String UTF_8 = "utf-8";
public static final String TEXT_HTML = "text/html; charset=utf-8";
public static final String ZIP_MIME_TYPE = "application/x-zip";
public static final String EMAIL_MIME_TYPE = "message/rfc822";
private Utils()
{
@ -315,16 +319,16 @@ public class Utils
/**
* @param subject could be an empty string
*/
public static void sendBugReport(@NonNull Activity activity, @NonNull String subject, @NonNull String body)
public static void sendBugReport(@NonNull ActivityResultLauncher<SharingUtils.SharingIntent> launcher, @NonNull Activity activity, @NonNull String subject, @NonNull String body)
{
subject = "Organic Maps Bugreport" + (TextUtils.isEmpty(subject) ? "" : ": " + subject);
LogsManager.INSTANCE.zipLogs(new SupportInfoWithLogsCallback(activity, subject, body, Constants.Email.SUPPORT));
LogsManager.INSTANCE.zipLogs(new SupportInfoWithLogsCallback(launcher, activity, subject, body, Constants.Email.SUPPORT));
}
// TODO: Don't send logs with general feedback, send system information only (version, device name, connectivity, etc.)
public static void sendFeedback(@NonNull Activity activity)
public static void sendFeedback(@NonNull ActivityResultLauncher<SharingUtils.SharingIntent> launcher, @NonNull Activity activity)
{
LogsManager.INSTANCE.zipLogs(new SupportInfoWithLogsCallback(activity, "Organic Maps Feedback", "",
LogsManager.INSTANCE.zipLogs(new SupportInfoWithLogsCallback(launcher, activity, "Organic Maps Feedback", "",
Constants.Email.SUPPORT));
}
@ -671,6 +675,8 @@ public class Utils
private static class SupportInfoWithLogsCallback implements LogsManager.OnZipCompletedListener
{
@NonNull
ActivityResultLauncher<SharingUtils.SharingIntent> mLauncher;
@NonNull
private final WeakReference<Activity> mActivityRef;
@NonNull
@ -680,13 +686,14 @@ public class Utils
@NonNull
private final String mEmail;
private SupportInfoWithLogsCallback(@NonNull Activity activity, @NonNull String subject,
@NonNull String body, @NonNull String email)
private SupportInfoWithLogsCallback(@NonNull ActivityResultLauncher<SharingUtils.SharingIntent> launcher, @NonNull Activity activity, @NonNull String subject,
@NonNull String body, @NonNull String email)
{
mActivityRef = new WeakReference<>(activity);
mSubject = subject;
mBody = body;
mEmail = email;
mLauncher = launcher;
}
@Override
@ -698,38 +705,24 @@ public class Utils
if (activity == null)
return;
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_EMAIL, new String[] { mEmail });
intent.putExtra(Intent.EXTRA_SUBJECT, "[" + BuildConfig.VERSION_NAME + "] " + mSubject);
// TODO: Send a short text attachment with system info and logs if zipping logs failed
SharingUtils.ShareInfo info = new SharingUtils.ShareInfo();
info.mMail = mEmail;
info.mSubject = "[" + BuildConfig.VERSION_NAME + "] " + mSubject;
info.mText = mBody;
if (success)
{
final Uri uri = StorageUtils.getUriForFilePath(activity, zipPath);
intent.putExtra(Intent.EXTRA_STREAM, uri);
// Properly set permissions for intent, see
// https://developer.android.com/reference/androidx/core/content/FileProvider#include-the-permission-in-an-intent
intent.setDataAndType(uri, "message/rfc822");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
intent.setClipData(ClipData.newRawUri("", uri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
info.mFileName = zipPath;
info.mMimeType = ZIP_MIME_TYPE;
}
else
{
intent.setType("message/rfc822");
}
// Do this so some email clients don't complain about empty body.
intent.putExtra(Intent.EXTRA_TEXT, mBody);
try
{
activity.startActivity(intent);
}
catch (ActivityNotFoundException e)
{
Logger.w(TAG, "No activities found which can handle sending a support message.", e);
info.mMimeType = EMAIL_MIME_TYPE;
}
SharingUtils.shareFile(activity.getApplicationContext(), mLauncher, info);
});
}
}