From bc8c0e580d62b1f383a4764cfecffdedf7abb4fb Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Tue, 23 Apr 2024 13:51:52 +0200 Subject: [PATCH 1/4] update permission request code in ViewMediaActivity --- .../keylesspalace/tusky/ViewMediaActivity.kt | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 18654ec5c..7e9340580 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -23,7 +23,6 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.Color import android.net.Uri @@ -38,6 +37,7 @@ import android.view.View import android.view.WindowManager import android.webkit.MimeTypeMap import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import androidx.core.content.IntentCompat @@ -92,6 +92,19 @@ class ViewMediaActivity : private val toolbarVisibilityListeners = mutableListOf() private var imageUrl: String? = null + private val requestDownloadMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + downloadMedia() + } else { + showErrorDialog( + binding.toolbar, + R.string.error_media_download_permission, + R.string.action_retry + ) { requestDownloadMedia() } + } + } + fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0 { this.toolbarVisibilityListeners.add(listener) listener(isToolbarVisible) @@ -235,22 +248,7 @@ class ViewMediaActivity : private fun requestDownloadMedia() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - requestPermissions( - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) - ) { _, grantResults -> - if ( - grantResults.isNotEmpty() && - grantResults[0] == PackageManager.PERMISSION_GRANTED - ) { - downloadMedia() - } else { - showErrorDialog( - binding.toolbar, - R.string.error_media_download_permission, - R.string.action_retry - ) { requestDownloadMedia() } - } - } + requestDownloadMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { downloadMedia() } From 99b4629b8cdcbd384b28887b6735790d053ac449 Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Tue, 23 Apr 2024 14:05:16 +0200 Subject: [PATCH 2/4] update permission request code in ComposeActivity --- .../components/compose/ComposeActivity.kt | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 303c6cbf3..65de2bed2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -21,7 +21,6 @@ import android.content.ClipData import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter @@ -50,8 +49,6 @@ import androidx.annotation.ColorInt import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.IntentCompat import androidx.core.content.res.use @@ -164,13 +161,30 @@ class ComposeActivity : private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS - private val takePicture = + private val takePictureLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> if (success) { pickMedia(photoUploadUri!!) } } - private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris -> + private val pickMediaFilePermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pickMediaFileLauncher.launch(true) + } else { + Snackbar.make( + binding.activityCompose, + R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT + ).apply { + setAction(R.string.action_retry) { onMediaPick() } + // necessary so snackbar is shown over everything + view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + show() + } + } + } + private val pickMediaFileLauncher = registerForActivityResult(PickMediaFiles()) { uris -> if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) { Toast.makeText( this, @@ -971,14 +985,10 @@ class ComposeActivity : // Wait until bottom sheet is not collapsed and show next screen after if (newState == BottomSheetBehavior.STATE_COLLAPSED) { addMediaBehavior.removeBottomSheetCallback(this) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions( - this@ComposeActivity, - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE - ) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + pickMediaFilePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } else { - pickMediaFile.launch(true) + pickMediaFileLauncher.launch(true) } } } @@ -1130,31 +1140,6 @@ class ComposeActivity : } } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - pickMediaFile.launch(true) - } else { - Snackbar.make( - binding.activityCompose, - R.string.error_media_upload_permission, - Snackbar.LENGTH_SHORT - ).apply { - setAction(R.string.action_retry) { onMediaPick() } - // necessary so snackbar is shown over everything - view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) - show() - } - } - } - } - private fun initiateCameraApp() { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED @@ -1171,7 +1156,7 @@ class ComposeActivity : BuildConfig.APPLICATION_ID + ".fileprovider", photoFile ) - takePicture.launch(photoUploadUri) + takePictureLauncher.launch(photoUploadUri) } private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { @@ -1550,7 +1535,6 @@ class ComposeActivity : companion object { private const val TAG = "ComposeActivity" // logging tag - private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" From bb6673db59ee1e1a3418651e8b91a405630cf90c Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Tue, 23 Apr 2024 15:24:36 +0200 Subject: [PATCH 3/4] update permission request code in SearchStatusesFragment and SFragment, store the argument in a field saved as part of instance state. --- .../fragments/SearchStatusesFragment.kt | 79 ++++++++++++------- .../keylesspalace/tusky/fragment/SFragment.kt | 62 ++++++++++----- 2 files changed, 95 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index abbfea9d0..0a87c8589 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -21,16 +21,18 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Environment import android.util.Log import android.view.View import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.getSystemService import androidx.core.view.ViewCompat import androidx.lifecycle.lifecycleScope import androidx.paging.PagingData @@ -41,7 +43,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.onFailure import com.google.android.material.snackbar.Snackbar -import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -79,6 +80,34 @@ class SearchStatusesFragment : SearchFragment(), Status private val searchAdapter get() = super.adapter as SearchStatusesAdapter + private var pendingMediaDownloads: List? = null + + private val downloadAllMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pendingMediaDownloads?.let { downloadAllMedia(it) } + } else { + Toast.makeText( + context, + R.string.error_media_download_permission, + Toast.LENGTH_SHORT + ).show() + } + pendingMediaDownloads = null + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pendingMediaDownloads = savedInstanceState?.getStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingMediaDownloads?.let { + outState.putStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY, ArrayList(it)) + } + } + override fun createAdapter(): PagingDataAdapter { val preferences = PreferenceManager.getDefaultSharedPreferences( binding.searchRecyclerView.context @@ -236,10 +265,6 @@ class SearchStatusesFragment : SearchFragment(), Status } } - companion object { - fun newInstance() = SearchStatusesFragment() - } - private fun reply(status: StatusViewData.Concrete) { val actionableStatus = status.actionable val mentionedUsernames = actionableStatus.mentions.map { it.username } @@ -499,37 +524,31 @@ class SearchStatusesFragment : SearchFragment(), Status ) } - private fun downloadAllMedia(status: Status) { + private fun downloadAllMedia(mediaUrls: List) { Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() - for ((_, url) in status.attachments) { - val uri = Uri.parse(url) - val filename = uri.lastPathSegment + val downloadManager: DownloadManager = requireContext().getSystemService()!! - val downloadManager = requireActivity().getSystemService( - Context.DOWNLOAD_SERVICE - ) as DownloadManager + for (url in mediaUrls) { + val uri = Uri.parse(url) val request = DownloadManager.Request(uri) - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, + uri.lastPathSegment + ) downloadManager.enqueue(request) } } private fun requestDownloadAllMedia(status: Status) { + if (status.attachments.isEmpty()) { + return + } + val mediaUrls = status.attachments.map { it.url } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) - (activity as BaseActivity).requestPermissions(permissions) { _, grantResults -> - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - downloadAllMedia(status) - } else { - Toast.makeText( - context, - R.string.error_media_download_permission, - Toast.LENGTH_SHORT - ).show() - } - } + pendingMediaDownloads = mediaUrls + downloadAllMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { - downloadAllMedia(status) + downloadAllMedia(mediaUrls) } } @@ -628,4 +647,10 @@ class SearchStatusesFragment : SearchFragment(), Status ) } } + + companion object { + private const val PENDING_MEDIA_DOWNLOADS_STATE_KEY = "pending_media_downloads" + + fun newInstance() = SearchStatusesFragment() + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt index 110a99e95..9b67aeab1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt @@ -21,17 +21,19 @@ import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Environment import android.util.Log import android.view.MenuItem import android.view.View import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.getSystemService import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import at.connyduck.calladapter.networkresult.fold @@ -93,6 +95,22 @@ abstract class SFragment : Fragment(), Injectable { @Inject lateinit var instanceInfoRepository: InstanceInfoRepository + private var pendingMediaDownloads: List? = null + + private val downloadAllMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pendingMediaDownloads?.let { downloadAllMedia(it) } + } else { + Toast.makeText( + context, + R.string.error_media_download_permission, + Toast.LENGTH_SHORT + ).show() + } + pendingMediaDownloads = null + } + override fun startActivity(intent: Intent) { requireActivity().startActivityWithSlideInAnimation(intent) } @@ -106,6 +124,18 @@ abstract class SFragment : Fragment(), Injectable { } } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pendingMediaDownloads = savedInstanceState?.getStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingMediaDownloads?.let { + outState.putStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY, ArrayList(it)) + } + } + override fun onResume() { super.onResume() @@ -522,13 +552,11 @@ abstract class SFragment : Fragment(), Injectable { } } - private fun downloadAllMedia(status: Status) { + private fun downloadAllMedia(mediaUrls: List) { Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() - val downloadManager = requireActivity().getSystemService( - Context.DOWNLOAD_SERVICE - ) as DownloadManager + val downloadManager: DownloadManager = requireContext().getSystemService()!! - for ((_, url) in status.attachments) { + for (url in mediaUrls) { val uri = Uri.parse(url) downloadManager.enqueue( DownloadManager.Request(uri).apply { @@ -542,26 +570,22 @@ abstract class SFragment : Fragment(), Injectable { } private fun requestDownloadAllMedia(status: Status) { + if (status.attachments.isEmpty()) { + return + } + val mediaUrls = status.attachments.map { it.url } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) - (activity as BaseActivity).requestPermissions(permissions) { _, grantResults -> - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - downloadAllMedia(status) - } else { - Toast.makeText( - context, - R.string.error_media_download_permission, - Toast.LENGTH_SHORT - ).show() - } - } + pendingMediaDownloads = mediaUrls + downloadAllMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) } else { - downloadAllMedia(status) + downloadAllMedia(mediaUrls) } } companion object { private const val TAG = "SFragment" + private const val PENDING_MEDIA_DOWNLOADS_STATE_KEY = "pending_media_downloads" + private fun accountIsInMentions( account: AccountEntity?, mentions: List From efcaf949042a0503323bf3838314e427378dd9cd Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Tue, 23 Apr 2024 15:26:01 +0200 Subject: [PATCH 4/4] remove legacy code from BaseActivity --- .../com/keylesspalace/tusky/BaseActivity.java | 43 ------------------- .../tusky/interfaces/PermissionRequester.kt | 5 --- 2 files changed, 48 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index a5bc0b04a..4c1712bc3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -19,7 +19,6 @@ import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -34,8 +33,6 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import com.google.android.material.color.MaterialColors; @@ -46,14 +43,11 @@ import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.interfaces.AccountSelectionListener; -import com.keylesspalace.tusky.interfaces.PermissionRequester; import com.keylesspalace.tusky.settings.AppTheme; import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.ActivityExtensions; import com.keylesspalace.tusky.util.ThemeUtils; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import javax.inject.Inject; @@ -71,9 +65,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab @NonNull public AccountManager accountManager; - private static final int REQUESTER_NONE = Integer.MAX_VALUE; - private HashMap requesters; - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -107,8 +98,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab if(requiresLogin()) { redirectIfNotLoggedIn(); } - - requesters = new HashMap<>(); } private boolean activityTransitionWasRequested() { @@ -273,36 +262,4 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab startActivity(intent); finish(); } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requesters.containsKey(requestCode)) { - PermissionRequester requester = requesters.remove(requestCode); - requester.onRequestPermissionsResult(permissions, grantResults); - } - } - - public void requestPermissions(@NonNull String[] permissions, @NonNull PermissionRequester requester) { - ArrayList permissionsToRequest = new ArrayList<>(); - for(String permission: permissions) { - if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { - permissionsToRequest.add(permission); - } - } - if (permissionsToRequest.isEmpty()) { - int[] permissionsAlreadyGranted = new int[permissions.length]; - requester.onRequestPermissionsResult(permissions, permissionsAlreadyGranted); - return; - } - - int newKey = requester == null ? REQUESTER_NONE : requesters.size(); - if (newKey != REQUESTER_NONE) { - requesters.put(newKey, requester); - } - String[] permissionsCopy = new String[permissionsToRequest.size()]; - permissionsToRequest.toArray(permissionsCopy); - ActivityCompat.requestPermissions(this, permissionsCopy, newKey); - - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.kt deleted file mode 100644 index d31bd1feb..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/PermissionRequester.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.keylesspalace.tusky.interfaces - -fun interface PermissionRequester { - fun onRequestPermissionsResult(permissions: Array, grantResults: IntArray) -}