Better permission handling, refactor common operations

This commit is contained in:
Matthieu 2021-11-26 16:57:19 +01:00
parent cb0e928352
commit 9c3bd5f7ed
5 changed files with 195 additions and 270 deletions

View File

@ -6,7 +6,6 @@ import android.content.ClipData
import android.content.ContentUris import android.content.ContentUris
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
@ -17,14 +16,12 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.camera.core.* import androidx.camera.core.*
import androidx.camera.core.ImageCapture.Metadata import androidx.camera.core.ImageCapture.Metadata
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.setPadding import androidx.core.view.setPadding
@ -32,10 +29,10 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import org.pixeldroid.app.R
import org.pixeldroid.app.postCreation.PostCreationActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity
import java.io.File import java.io.File
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -43,30 +40,22 @@ import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
import org.pixeldroid.app.R
// This is an arbitrary number we are using to keep track of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts.
private const val REQUEST_CODE_PERMISSIONS = 10
private const val ANIMATION_FAST_MILLIS = 50L private const val ANIMATION_FAST_MILLIS = 50L
private const val ANIMATION_SLOW_MILLIS = 100L private const val ANIMATION_SLOW_MILLIS = 100L
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE
)
/** /**
* Camera fragment * Camera fragment
*/ */
class CameraFragment : Fragment() { class CameraFragment : Fragment() {
private lateinit var container: ConstraintLayout private lateinit var container: ConstraintLayout
private lateinit var viewFinder: PreviewView
private val cameraLifecycleOwner = CameraLifecycleOwner() private val cameraLifecycleOwner = CameraLifecycleOwner()
private lateinit var binding: FragmentCameraBinding
private var displayId: Int = -1 private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null private var preview: Preview? = null
@ -75,34 +64,11 @@ class CameraFragment : Fragment() {
private var inActivity by Delegates.notNull<Boolean>() private var inActivity by Delegates.notNull<Boolean>()
private var filePermissionDialogLaunched: Boolean = false
/** Blocking camera operations are performed using this executor */ /** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService private lateinit var cameraExecutor: ExecutorService
override fun onResume() {
super.onResume()
// Make sure that all permissions are still present on resume,
// since they could have been removed while away.
if (!allPermissionsGranted()) {
ActivityCompat.requestPermissions(
requireActivity(),
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
} else {
// Build UI controls
updateCameraUi()
}
cameraLifecycleOwner.resume()
}
/**
* Check if all permission specified in the manifest have been granted
*/
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
requireContext(), it
) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
@ -114,15 +80,16 @@ class CameraFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
inActivity = arguments?.getBoolean("CameraActivity") ?: false inActivity = arguments?.getBoolean("CameraActivity") ?: false
return inflater.inflate(R.layout.fragment_camera, container, false) binding = FragmentCameraBinding.inflate(layoutInflater)
return binding.root
} }
private fun setGalleryThumbnail(uri: Uri) { private fun setGalleryThumbnail(uri: Uri) {
// Reference of the view that holds the gallery thumbnail val thumbnail = binding.photoViewButton
val thumbnail = container.findViewById<ImageButton>(R.id.photo_view_button)
// Run the operations in the view's thread // Run the operations in the view's thread
thumbnail.post { thumbnail.post {
@ -141,45 +108,43 @@ class CameraFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
container = view as ConstraintLayout container = view as ConstraintLayout
viewFinder = container.findViewById(R.id.view_finder)
// Initialize our background executor // Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor() cameraExecutor = Executors.newSingleThreadExecutor()
bindCameraUseCases() if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
bindCameraUseCases()
}
else {
// Ask for Camera permission.
bindCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
// Every time the orientation of device changes, update rotation for use cases setupUploadImage()
setupFlipCameras()
setupImageCapture()
// Wait for the views to be properly laid out // Wait for the views to be properly laid out
viewFinder.post { binding.viewFinder.post {
// Keep track of the display in which this view is attached // Keep track of the display in which this view is attached
displayId = viewFinder.display?.displayId ?: -1 displayId = binding.viewFinder.display?.displayId ?: -1
} }
} }
/** /** Declare and bind preview and capture use cases */
* Inflate camera controls and update the UI manually upon config changes to avoid removing
* and re-adding the view finder from the view hierarchy; this provides a seamless rotation
* transition on devices that support it.
*
* NOTE: The flag is supported starting in Android 8 but there still is a small flash on the
* screen for devices that run Android 9 or below.
*/
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateCameraUi()
}
/** Declare and bind preview, capture and analysis use cases */
private fun bindCameraUseCases() { private fun bindCameraUseCases() {
// Get screen metrics used to setup camera for full screen resolution // Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics().also { viewFinder.display?.getRealMetrics(it) } val metrics = DisplayMetrics()
val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
val rotation = viewFinder.display?.rotation ?: 0 val rotation = binding.viewFinder.display?.rotation ?: 0
// Bind the CameraProvider to the LifeCycleOwner // Bind the CameraProvider to the LifeCycleOwner
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
@ -220,7 +185,7 @@ class CameraFragment : Fragment() {
) )
// Attach the viewfinder's surface provider to preview use case // Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(viewFinder.surfaceProvider) preview?.setSurfaceProvider(binding.viewFinder.surfaceProvider)
} catch (exc: Exception) { } catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc) Log.e(TAG, "Use case binding failed", exc)
} }
@ -232,6 +197,25 @@ class CameraFragment : Fragment() {
cameraLifecycleOwner.pause() cameraLifecycleOwner.pause()
} }
override fun onResume() {
super.onResume()
// Update gallery thumbnail
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
updateGalleryThumbnail()
}
else if (!filePermissionDialogLaunched) {
// Ask for external storage permission.
updateGalleryThumbnailPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
cameraLifecycleOwner.resume()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
cameraLifecycleOwner.destroy() cameraLifecycleOwner.destroy()
@ -267,16 +251,21 @@ class CameraFragment : Fragment() {
return AspectRatio.RATIO_16_9 return AspectRatio.RATIO_16_9
} }
/** Method used to re-draw the camera UI controls, called every time configuration changes. */ private val updateGalleryThumbnailPermissionLauncher =
private fun updateCameraUi() { registerForActivityResult(ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
// Remove previous UI if any if (isGranted) {
container.findViewById<ConstraintLayout>(R.id.camera_ui_container)?.let { updateGalleryThumbnail()
container.removeView(it) } else if(!filePermissionDialogLaunched){
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.no_storage_permission))
.setPositiveButton(android.R.string.ok) { _, _ ->}.show()
filePermissionDialogLaunched = true
}
} }
// Inflate a new view containing all UI for controlling the camera /** Method used to re-draw the camera UI controls, called every time configuration changes. */
val controls = View.inflate(requireContext(), R.layout.camera_ui_container, container) private fun updateGalleryThumbnail() {
// In the background, load latest photo taken (if any) for gallery thumbnail // In the background, load latest photo taken (if any) for gallery thumbnail
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@ -302,14 +291,9 @@ class CameraFragment : Fragment() {
cursor.close() cursor.close()
} }
} }
setupImageCapture(controls)
setupFlipCameras(controls)
setupUploadImage(controls)
} }
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val data: Intent? = result.data val data: Intent? = result.data
if (result.resultCode == Activity.RESULT_OK && data != null) { if (result.resultCode == Activity.RESULT_OK && data != null) {
@ -329,9 +313,9 @@ class CameraFragment : Fragment() {
} }
} }
private fun setupUploadImage(controls: View) { private fun setupUploadImage() {
// Listener for button used to view the most recent photo // Listener for button used to view the most recent photo
controls.findViewById<ImageButton>(R.id.photo_view_button)?.setOnClickListener { binding.photoViewButton.setOnClickListener {
Intent().apply { Intent().apply {
type = "image/*" type = "image/*"
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
@ -345,75 +329,105 @@ class CameraFragment : Fragment() {
} }
} }
private fun setupFlipCameras(controls: View) {
private val bindCameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
bindCameraUseCases()
} else {
AlertDialog.Builder(requireContext())
.setMessage(R.string.no_camera_permission)
.setPositiveButton(android.R.string.ok) { _, _ ->}.show()
}
}
private fun setupFlipCameras() {
// Listener for button used to switch cameras // Listener for button used to switch cameras
controls.findViewById<ImageButton>(R.id.camera_switch_button)?.setOnClickListener { binding.cameraSwitchButton.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) { lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
CameraSelector.LENS_FACING_BACK CameraSelector.LENS_FACING_BACK
} else { } else {
CameraSelector.LENS_FACING_FRONT CameraSelector.LENS_FACING_FRONT
} }
// Re-bind use cases to update selected camera, being careful about permissions. // Re-bind use cases to update selected camera, being careful about permissions.
if (!allPermissionsGranted()) { if (ContextCompat.checkSelfPermission(
ActivityCompat.requestPermissions( requireContext(),
requireActivity(), Manifest.permission.CAMERA
REQUIRED_PERMISSIONS, ) == PackageManager.PERMISSION_GRANTED
REQUEST_CODE_PERMISSIONS ) {
)
} else {
bindCameraUseCases() bindCameraUseCases()
} }
else {
// Ask for Camera permission.
bindCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
} }
} }
private fun setupImageCapture(controls: View) { private fun setupImageCapture() {
// Listener for button used to capture photo // Listener for button used to capture photo
controls.findViewById<ImageButton>(R.id.camera_capture_button)?.setOnClickListener { binding.cameraCaptureButton.setOnClickListener {
// Get a stable reference of the modifiable image capture use case
imageCapture?.let { imageCapture ->
// Create output file to hold the image
val photoFile = File.createTempFile(
"cachedPhoto", ".png", context?.cacheDir
)
// Setup image capture metadata
val metadata = Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.build()
// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
val uri: ArrayList<String> = ArrayList()
uri.add(savedUri.toString())
startAlbumCreation(uri)
}
})
// Display flash animation to indicate that photo was captured
container.postDelayed({
container.foreground = ColorDrawable(Color.WHITE)
container.postDelayed(
{ container.foreground = null }, ANIMATION_FAST_MILLIS
)
}, ANIMATION_SLOW_MILLIS)
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
takePhoto()
} }
else {
// Ask for Camera permission.
// Use the same permission launcher as bind camera
// (taking a photo after the permission prompt is going to be useless anyways)
bindCameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
imageCapture?.let { imageCapture ->
// Create output file to hold the image
val photoFile = File.createTempFile(
"cachedPhoto", ".png", context?.cacheDir
)
// Setup image capture metadata
val metadata = Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
.setMetadata(metadata)
.build()
// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
val uri: ArrayList<String> = ArrayList()
uri.add(savedUri.toString())
startAlbumCreation(uri)
}
})
// Display flash animation to indicate that photo was captured
container.postDelayed({
container.foreground = ColorDrawable(Color.WHITE)
container.postDelayed(
{ container.foreground = null }, ANIMATION_FAST_MILLIS
)
}, ANIMATION_SLOW_MILLIS)
} }
} }

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera_ui_container"
android:layoutDirection="ltr"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Camera control and gallery buttons -->
<ImageButton
android:id="@id/camera_switch_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginEnd="@dimen/margin_xlarge"
android:layout_marginBottom="@dimen/margin_small"
android:padding="@dimen/spacing_small"
android:scaleType="fitXY"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_switch"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/gallery_button_alt" />
<ImageButton
android:id="@id/camera_capture_button"
android:layout_width="@dimen/round_button_large"
android:layout_height="@dimen/round_button_large"
android:layout_marginEnd="@dimen/shutter_button_margin"
android:background="@drawable/ic_shutter"
android:contentDescription="@string/capture_button_alt"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@id/photo_view_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginEnd="@dimen/margin_xlarge"
android:layout_marginTop="@dimen/margin_small"
android:padding="@dimen/spacing_large"
android:scaleType="fitXY"
android:background="@drawable/ic_outer_circle"
app:srcCompat="@drawable/ic_photo"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/switch_camera_button_alt" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/camera_ui_container"
android:layoutDirection="ltr"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Camera control and gallery buttons -->
<ImageButton
android:id="@+id/camera_switch_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginBottom="@dimen/margin_xlarge"
android:layout_marginStart="@dimen/margin_small"
android:padding="@dimen/spacing_small"
android:scaleType="fitCenter"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_switch"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/switch_camera_button_alt" />
<ImageButton
android:id="@+id/camera_capture_button"
android:layout_width="@dimen/round_button_large"
android:layout_height="@dimen/round_button_large"
android:layout_marginBottom="@dimen/shutter_button_margin"
android:scaleType="fitCenter"
android:background="@drawable/ic_shutter"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/capture_button_alt" />
<ImageButton
android:id="@+id/photo_view_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginBottom="@dimen/margin_xlarge"
android:layout_marginEnd="@dimen/margin_small"
android:padding="@dimen/spacing_large"
android:scaleType="fitCenter"
android:background="@drawable/ic_outer_circle"
app:srcCompat="@drawable/ic_photo"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/gallery_button_alt" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -29,4 +29,45 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />
<!-- Camera control and gallery buttons -->
<ImageButton
android:id="@+id/camera_switch_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginBottom="@dimen/margin_xlarge"
android:layout_marginStart="@dimen/margin_small"
android:padding="@dimen/spacing_small"
android:scaleType="fitCenter"
android:background="@android:color/transparent"
app:srcCompat="@drawable/ic_switch"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/switch_camera_button_alt" />
<ImageButton
android:id="@+id/camera_capture_button"
android:layout_width="@dimen/round_button_large"
android:layout_height="@dimen/round_button_large"
android:layout_marginBottom="@dimen/shutter_button_margin"
android:scaleType="fitCenter"
android:background="@drawable/ic_shutter"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/capture_button_alt" />
<ImageButton
android:id="@+id/photo_view_button"
android:layout_width="@dimen/round_button_medium"
android:layout_height="@dimen/round_button_medium"
android:layout_marginBottom="@dimen/margin_xlarge"
android:layout_marginEnd="@dimen/margin_small"
android:padding="@dimen/spacing_large"
android:scaleType="fitCenter"
android:background="@drawable/ic_outer_circle"
app:srcCompat="@drawable/ic_photo"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/gallery_button_alt" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -248,4 +248,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="notifications_settings">Notification settings</string> <string name="notifications_settings">Notification settings</string>
<string name="notifications_settings_summary">Manage what notifications you want to receive</string> <string name="notifications_settings_summary">Manage what notifications you want to receive</string>
<string name="login_notifications">Couldn\'t fetch latest notifications</string> <string name="login_notifications">Couldn\'t fetch latest notifications</string>
<string name="no_camera_permission">Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera</string>
<string name="no_storage_permission">Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail</string>
</resources> </resources>