diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 512342fd0..80a912178 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -58,6 +58,7 @@ import org.briarproject.briar.android.privategroup.conversation.GroupActivity; import org.briarproject.briar.android.removabledrive.RemovableDriveActivity; import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced; import org.briarproject.briar.android.util.ActivityLaunchers.GetMultipleImagesAdvanced; +import org.briarproject.briar.android.util.ActivityLaunchers.OpenMultipleImageDocumentsAdvanced; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.ImagePreview; @@ -143,6 +144,7 @@ import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHME import static org.briarproject.briar.android.conversation.ImageActivity.DATE; import static org.briarproject.briar.android.conversation.ImageActivity.ITEM_ID; import static org.briarproject.briar.android.conversation.ImageActivity.NAME; +import static org.briarproject.briar.android.util.UiUtils.launchActivityToOpenFile; import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.android.view.AuthorView.setAvatar; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE; @@ -196,12 +198,18 @@ public class ConversationActivity extends BriarActivity requireNonNull(name); loadMessages(); }; - private final ActivityResultLauncher launcher = SDK_INT >= 18 ? - registerForActivityResult(new GetMultipleImagesAdvanced(), + @Nullable + private final ActivityResultLauncher docLauncher = SDK_INT >= 19 ? + registerForActivityResult(new OpenMultipleImageDocumentsAdvanced(), this::onImagesChosen) : - registerForActivityResult(new GetImageAdvanced(), uri -> { - if (uri != null) onImagesChosen(singletonList(uri)); - }); + null; + private final ActivityResultLauncher contentLauncher = + SDK_INT >= 18 ? + registerForActivityResult(new GetMultipleImagesAdvanced(), + this::onImagesChosen) : + registerForActivityResult(new GetImageAdvanced(), uri -> { + if (uri != null) onImagesChosen(singletonList(uri)); + }); private AttachmentRetriever attachmentRetriever; private ConversationViewModel viewModel; @@ -424,9 +432,11 @@ public class ConversationActivity extends BriarActivity startActivity(intent); return true; } else if (itemId == R.id.action_transfer_data) { - Intent intent = new Intent(this, RemovableDriveActivity.class); - intent.putExtra(CONTACT_ID, contactId.getInt()); - startActivity(intent); + if (SDK_INT >= 19) { + Intent intent = new Intent(this, RemovableDriveActivity.class); + intent.putExtra(CONTACT_ID, contactId.getInt()); + startActivity(intent); + } return true; } else if (itemId == R.id.action_delete_all_messages) { askToDeleteAllMessages(); @@ -774,7 +784,7 @@ public class ConversationActivity extends BriarActivity @Override public void onAttachImageClicked() { - launcher.launch("image/*"); + launchActivityToOpenFile(this, docLauncher, contentLauncher, "image/*"); } private void onImagesChosen(@Nullable List uris) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 641b1cf90..67db6804a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -33,7 +33,6 @@ import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -65,7 +64,6 @@ public class ImageActivity extends BriarActivity final static String DATE = "date"; final static String ITEM_ID = "itemId"; - @RequiresApi(api = 16) private final static int UI_FLAGS_DEFAULT = SYSTEM_UI_FLAG_LAYOUT_STABLE | SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; @@ -79,9 +77,11 @@ public class ImageActivity extends BriarActivity private List attachments; private MessageId conversationMessageId; - private final ActivityResultLauncher launcher = + @Nullable + private final ActivityResultLauncher launcher = SDK_INT >= 19 ? registerForActivityResult(new CreateDocumentAdvanced(), - this::onImageUriSelected); + this::onImageUriSelected) : + null; @Override public void injectActivity(ActivityComponent component) { @@ -209,14 +209,12 @@ public class ImageActivity extends BriarActivity super.onBackPressed(); } - @RequiresApi(api = 16) private void onImageClicked(@Nullable Boolean clicked) { if (clicked != null && clicked) { toggleSystemUi(); } } - @RequiresApi(api = 16) private void toggleSystemUi() { View decorView = getWindow().getDecorView(); if (appBarLayout.getVisibility() == VISIBLE) { @@ -226,7 +224,6 @@ public class ImageActivity extends BriarActivity } } - @RequiresApi(api = 16) private void hideSystemUi(View decorView) { decorView.setSystemUiVisibility( SYSTEM_UI_FLAG_FULLSCREEN | UI_FLAGS_DEFAULT); @@ -237,7 +234,6 @@ public class ImageActivity extends BriarActivity .start(); } - @RequiresApi(api = 16) private void showSystemUi(View decorView) { decorView.setSystemUiVisibility(UI_FLAGS_DEFAULT); appBarLayout.animate() @@ -265,7 +261,7 @@ public class ImageActivity extends BriarActivity String name = viewModel.getFileName() + "." + getVisibleAttachment().getExtension(); try { - launcher.launch(name); + requireNonNull(launcher).launch(name); } catch (ActivityNotFoundException e) { viewModel.onSaveImageError(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/FallbackFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/FallbackFragment.java index 56c88f061..3b8d57bc8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/hotspot/FallbackFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/hotspot/FallbackFragment.java @@ -34,6 +34,7 @@ import static android.os.Build.VERSION.SDK_INT; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static androidx.transition.TransitionManager.beginDelayedTransition; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.briar.android.AppModule.getAndroidComponent; import static org.briarproject.briar.android.hotspot.HotspotViewModel.getApkFileName; @@ -47,9 +48,11 @@ public class FallbackFragment extends BaseFragment { ViewModelProvider.Factory viewModelFactory; private HotspotViewModel viewModel; - private final ActivityResultLauncher launcher = + @Nullable + private final ActivityResultLauncher launcher = SDK_INT >= 19 ? registerForActivityResult(new CreateDocumentAdvanced(), - this::onDocumentCreated); + this::onDocumentCreated) : + null; private Button fallbackButton; private ProgressBar progressBar; @@ -87,8 +90,11 @@ public class FallbackFragment extends BaseFragment { fallbackButton.setVisibility(INVISIBLE); progressBar.setVisibility(VISIBLE); - if (SDK_INT >= 19) launcher.launch(getApkFileName()); - else viewModel.exportApk(); + if (SDK_INT >= 19) { + requireNonNull(launcher).launch(getApkFileName()); + } else { + viewModel.exportApk(); + } }); viewModel.getSavedApkToUri().observeEvent(this, this::shareUri); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java index 14e6cdb1a..1e7f5e308 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/ReceiveFragment.java @@ -15,11 +15,13 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.util.ActivityLaunchers.GetContentAdvanced; +import org.briarproject.briar.android.util.ActivityLaunchers.OpenDocumentAdvanced; import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; @@ -28,14 +30,19 @@ import static android.view.View.FOCUS_DOWN; import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; import static org.briarproject.briar.android.AppModule.getAndroidComponent; +import static org.briarproject.briar.android.util.UiUtils.launchActivityToOpenFile; +@RequiresApi(19) @MethodsNotNullByDefault @ParametersNotNullByDefault public class ReceiveFragment extends Fragment { final static String TAG = ReceiveFragment.class.getName(); - private final ActivityResultLauncher launcher = + private final ActivityResultLauncher docLauncher = + registerForActivityResult(new OpenDocumentAdvanced(), + this::onDocumentChosen); + private final ActivityResultLauncher contentLauncher = registerForActivityResult(new GetContentAdvanced(), this::onDocumentChosen); @@ -69,8 +76,8 @@ public class ReceiveFragment extends Fragment { progressBar = v.findViewById(R.id.progressBar); button = v.findViewById(R.id.fileButton); button.setOnClickListener(view -> - launcher.launch("*/*") - ); + launchActivityToOpenFile(requireContext(), + docLauncher, contentLauncher, "*/*")); viewModel.getOldTaskResumedEvent() .observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed); viewModel.getState() diff --git a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java index ff5166290..1acab3a93 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/removabledrive/SendFragment.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.removabledrive; +import android.content.ActivityNotFoundException; import android.content.Context; import android.net.Uri; import android.os.Bundle; @@ -18,10 +19,13 @@ import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; import org.briarproject.briar.R; import org.briarproject.briar.android.util.ActivityLaunchers.CreateDocumentAdvanced; +import java.util.logging.Logger; + import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; @@ -30,13 +34,18 @@ import static android.os.Build.VERSION.SDK_INT; import static android.view.View.FOCUS_DOWN; import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.android.AppModule.getAndroidComponent; +@RequiresApi(19) @MethodsNotNullByDefault @ParametersNotNullByDefault public class SendFragment extends Fragment { final static String TAG = SendFragment.class.getName(); + private static final Logger LOG = getLogger(TAG); private final ActivityResultLauncher launcher = registerForActivityResult(new CreateDocumentAdvanced(), @@ -73,9 +82,15 @@ public class SendFragment extends Fragment { introTextView = v.findViewById(R.id.introTextView); progressBar = v.findViewById(R.id.progressBar); button = v.findViewById(R.id.fileButton); - button.setOnClickListener(view -> - launcher.launch(viewModel.getFileName()) - ); + button.setOnClickListener(view -> { + try { + launcher.launch(viewModel.getFileName()); + } catch (ActivityNotFoundException e) { + logException(LOG, WARNING, e); + Toast.makeText(requireContext(), + R.string.error_start_activity, LENGTH_LONG).show(); + } + }); viewModel.getOldTaskResumedEvent() .observeEvent(getViewLifecycleOwner(), this::onOldTaskResumed); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index 219c43891..348fcc4ac 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.mailbox.MailboxActivity; import org.briarproject.briar.android.util.ActivityLaunchers.GetImageAdvanced; +import org.briarproject.briar.android.util.ActivityLaunchers.OpenImageDocumentAdvanced; import javax.inject.Inject; @@ -23,9 +24,11 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceGroup; +import static android.os.Build.VERSION.SDK_INT; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.AppModule.getAndroidComponent; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; +import static org.briarproject.briar.android.util.UiUtils.launchActivityToOpenFile; import static org.briarproject.briar.android.util.UiUtils.triggerFeedback; @MethodsNotNullByDefault @@ -46,7 +49,12 @@ public class SettingsFragment extends PreferenceFragmentCompat { private SettingsViewModel viewModel; private AvatarPreference prefAvatar; - private final ActivityResultLauncher launcher = + @Nullable + private final ActivityResultLauncher docLauncher = SDK_INT >= 19 ? + registerForActivityResult(new OpenImageDocumentAdvanced(), + this::onImageSelected) : + null; + private final ActivityResultLauncher contentLauncher = registerForActivityResult(new GetImageAdvanced(), this::onImageSelected); @@ -65,7 +73,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { prefAvatar = requireNonNull(findPreference(PREF_KEY_AVATAR)); if (viewModel.shouldEnableProfilePictures()) { prefAvatar.setOnPreferenceClickListener(preference -> { - launcher.launch("image/*"); + launchActivityToOpenFile(requireContext(), + docLauncher, contentLauncher, "image/*"); return true; }); } else { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java b/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java index 7798e6659..f71755596 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/ActivityLaunchers.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.util; -import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; @@ -10,8 +9,11 @@ import androidx.activity.result.contract.ActivityResultContract; import androidx.activity.result.contract.ActivityResultContracts.CreateDocument; import androidx.activity.result.contract.ActivityResultContracts.GetContent; import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents; +import androidx.activity.result.contract.ActivityResultContracts.OpenDocument; +import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import static android.app.Activity.RESULT_CANCELED; import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; @@ -24,6 +26,7 @@ import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageConten @NotNullByDefault public class ActivityLaunchers { + @RequiresApi(19) public static class CreateDocumentAdvanced extends CreateDocument { @NonNull @Override @@ -45,6 +48,19 @@ public class ActivityLaunchers { } } + @RequiresApi(19) + public static class OpenDocumentAdvanced extends OpenDocument { + @NonNull + @Override + public Intent createIntent(Context context, String[] input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + i.setType("/*"); + i.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + return i; + } + } + public static class GetImageAdvanced extends GetContent { @NonNull @Override @@ -59,7 +75,7 @@ public class ActivityLaunchers { } } - @TargetApi(18) + @RequiresApi(18) public static class GetMultipleImagesAdvanced extends GetMultipleContents { @NonNull @Override @@ -74,6 +90,37 @@ public class ActivityLaunchers { } } + @RequiresApi(19) + public static class OpenImageDocumentAdvanced extends OpenDocument { + @NonNull + @Override + public Intent createIntent(Context context, String[] input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + i.setType("image/*"); + i.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + if (SDK_INT >= 19) + i.putExtra(EXTRA_MIME_TYPES, getSupportedImageContentTypes()); + return i; + } + } + + @RequiresApi(19) + public static class OpenMultipleImageDocumentsAdvanced + extends OpenMultipleDocuments { + @NonNull + @Override + public Intent createIntent(Context context, String[] input) { + Intent i = super.createIntent(context, input); + putShowAdvancedExtra(i); + i.setType("image/*"); + i.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + if (SDK_INT >= 19) + i.putExtra(EXTRA_MIME_TYPES, getSupportedImageContentTypes()); + return i; + } + } + public static class RequestBluetoothDiscoverable extends ActivityResultContract { @NonNull diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index 81d7d355a..040ca0025 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -42,6 +42,7 @@ import org.briarproject.briar.android.view.ArticleMovementMethod; import java.util.Locale; import java.util.logging.Logger; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.AnyThread; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; @@ -103,6 +104,7 @@ import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.briar.BuildConfig.APPLICATION_ID; import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; @@ -117,6 +119,8 @@ import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA @ParametersNotNullByDefault public class UiUtils { + private static final Logger LOG = getLogger(UiUtils.class.getName()); + public static final long MIN_DATE_RESOLUTION = MINUTE_IN_MILLIS; public static final int TEASER_LENGTH = 320; public static final float GREY_OUT = 0.5f; @@ -579,4 +583,26 @@ public class UiUtils { (dialog, which) -> requestPermissions.run()); builder.show(); } + + public static void launchActivityToOpenFile(Context ctx, + @Nullable ActivityResultLauncher docLauncher, + ActivityResultLauncher contentLauncher, + String contentType) { + // Try GET_CONTENT, fall back to OPEN_DOCUMENT if available + try { + contentLauncher.launch(contentType); + return; + } catch (ActivityNotFoundException e) { + logException(LOG, WARNING, e); + } + if (docLauncher != null) { + try { + docLauncher.launch(new String[] {contentType}); + return; + } catch (ActivityNotFoundException e) { + logException(LOG, WARNING, e); + } + } + Toast.makeText(ctx, R.string.error_start_activity, LENGTH_LONG).show(); + } }