diff --git a/android/app/src/main/java/app/organicmaps/MwmActivity.java b/android/app/src/main/java/app/organicmaps/MwmActivity.java index 9c9bdfcc10..974e7dfeb9 100644 --- a/android/app/src/main/java/app/organicmaps/MwmActivity.java +++ b/android/app/src/main/java/app/organicmaps/MwmActivity.java @@ -208,6 +208,8 @@ public class MwmActivity extends BaseMwmFragmentActivity @SuppressWarnings("NotNullFieldNotInitialized") @NonNull private ActivityResultLauncher mLocationResolutionRequest; + @NonNull + private ActivityResultLauncher 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(); diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkCategoriesFragment.java b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkCategoriesFragment.java index 9c75410633..4154f9009d 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkCategoriesFragment.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarkCategoriesFragment.java @@ -62,7 +62,7 @@ public class BookmarkCategoriesFragment extends BaseMwmRecyclerFragment shareLauncher; + private ActivityResultLauncher shareLauncher; @Nullable private BookmarkCategory mSelectedCategory; diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java index 2aeee02165..6a5eebc332 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksListFragment.java @@ -65,7 +65,7 @@ public class BookmarksListFragment extends BaseMwmRecyclerFragment shareLauncher; + private ActivityResultLauncher shareLauncher; @SuppressWarnings("NotNullFieldNotInitialized") @NonNull diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksSharingHelper.java b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksSharingHelper.java index 733b71fd2d..168b062b1c 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksSharingHelper.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/BookmarksSharingHelper.java @@ -47,7 +47,7 @@ public enum BookmarksSharingHelper } public void onPreparedFileForSharing(@NonNull FragmentActivity context, - @NonNull ActivityResultLauncher launcher, + @NonNull ActivityResultLauncher launcher, @NonNull BookmarkSharingResult result) { if (mProgressDialog != null && mProgressDialog.isShowing()) diff --git a/android/app/src/main/java/app/organicmaps/help/FaqFragment.java b/android/app/src/main/java/app/organicmaps/help/FaqFragment.java index de0524e7a4..91952718e1 100644 --- a/android/app/src/main/java/app/organicmaps/help/FaqFragment.java +++ b/android/app/src/main/java/app/organicmaps/help/FaqFragment.java @@ -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 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; } } diff --git a/android/app/src/main/java/app/organicmaps/help/HelpFragment.java b/android/app/src/main/java/app/organicmaps/help/HelpFragment.java index c6c128537e..5f503a9ece 100644 --- a/android/app/src/main/java/app/organicmaps/help/HelpFragment.java +++ b/android/app/src/main/java/app/organicmaps/help/HelpFragment.java @@ -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 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) diff --git a/android/app/src/main/java/app/organicmaps/settings/StoragePathFragment.java b/android/app/src/main/java/app/organicmaps/settings/StoragePathFragment.java index 3f6a36d618..5559966147 100644 --- a/android/app/src/main/java/app/organicmaps/settings/StoragePathFragment.java +++ b/android/app/src/main/java/app/organicmaps/settings/StoragePathFragment.java @@ -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 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); diff --git a/android/app/src/main/java/app/organicmaps/util/SharingUtils.java b/android/app/src/main/java/app/organicmaps/util/SharingUtils.java index b90251ba6a..abf400c366 100644 --- a/android/app/src/main/java/app/organicmaps/util/SharingUtils.java +++ b/android/app/src/main/java/app/organicmaps/util/SharingUtils.java @@ -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> + { + static private Uri sourceUri; + + @NonNull + @Override + public Intent createIntent(@NonNull Context context, SharingIntent input) + { + sourceUri = input.GetSourceFile(); + return input.GetIntent(); + } + + @Override + public Pair 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 RegisterLauncher(@NonNull Fragment fragment) + private static void ProcessShareResult(@NonNull ContentResolver resolver, Pair 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 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 launcher, String fileName, String fileMimeType) + public static ActivityResultLauncher RegisterLauncher(@NonNull AppCompatActivity activity) + { + return activity.registerForActivityResult( + new SharingContract(), + result -> ProcessShareResult(activity.getContentResolver(), result) + ); + } + + public static void shareFile(Context context, ActivityResultLauncher 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 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); } } diff --git a/android/app/src/main/java/app/organicmaps/util/Utils.java b/android/app/src/main/java/app/organicmaps/util/Utils.java index dbdf109188..3b4a58068d 100644 --- a/android/app/src/main/java/app/organicmaps/util/Utils.java +++ b/android/app/src/main/java/app/organicmaps/util/Utils.java @@ -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 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 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 mLauncher; @NonNull private final WeakReference 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 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); + }); } }