-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 업로드 화질 저하 이슈 해결 #498
Changes from 29 commits
dd3dc34
695d4f4
36378be
3c02085
d7a1d86
45fdef8
58fd874
69b99a0
6b0c5d3
efa55dd
b544482
bade642
56c6a7d
1706851
e72ad60
387553c
cc8dad6
cbaf08e
7cf4cce
e93124a
47999ed
35dd08e
423c032
ff5cd1c
bed0971
086cf88
30b1f09
989f4ea
340d9a8
f16ea16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,12 +6,12 @@ import android.content.Context | |
import android.content.Intent | ||
import android.content.pm.PackageManager | ||
import android.graphics.Bitmap | ||
import android.graphics.BitmapFactory | ||
import android.location.Location | ||
import android.net.Uri | ||
import android.os.Build | ||
import android.os.Bundle | ||
import android.provider.MediaStore | ||
import android.provider.Settings | ||
import android.view.View | ||
import androidx.activity.result.contract.ActivityResultContracts | ||
import androidx.activity.viewModels | ||
|
@@ -20,14 +20,15 @@ import com.google.android.gms.location.LocationServices | |
import com.google.android.gms.tasks.CancellationToken | ||
import com.google.android.gms.tasks.CancellationTokenSource | ||
import com.google.android.gms.tasks.OnTokenCanceledListener | ||
import com.google.android.material.snackbar.Snackbar | ||
import com.now.domain.model.Coordinate | ||
import com.now.naaga.R | ||
import com.now.naaga.data.firebase.analytics.AnalyticsDelegate | ||
import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate | ||
import com.now.naaga.data.firebase.analytics.UPLOAD_OPEN_CAMERA | ||
import com.now.naaga.data.throwable.DataThrowable | ||
import com.now.naaga.databinding.ActivityUploadBinding | ||
import com.now.naaga.presentation.upload.UploadViewModel.Companion.FILE_EMPTY | ||
import com.now.naaga.util.BitmapBuilder | ||
import com.now.naaga.util.extension.openSetting | ||
import com.now.naaga.util.extension.showSnackbarWithEvent | ||
import com.now.naaga.util.extension.showToast | ||
|
@@ -40,43 +41,57 @@ import java.time.LocalDateTime | |
class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { | ||
private lateinit var binding: ActivityUploadBinding | ||
private val viewModel: UploadViewModel by viewModels() | ||
|
||
private val cameraLauncher = registerForActivityResult( | ||
ActivityResultContracts.TakePicturePreview(), | ||
) { bitmap -> | ||
if (bitmap != null) { | ||
setImage(bitmap) | ||
private var imageUri: Uri? = null | ||
private val cameraLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> | ||
if (!success) return@registerForActivityResult | ||
if (imageUri == null) { | ||
showToast(getString(R.string.upload_image_orientation_error)) | ||
return@registerForActivityResult | ||
} | ||
setImage(requireNotNull(imageUri) { "imageUri가 null입니다" }) | ||
} | ||
|
||
private val requestPermissionLauncher = registerForActivityResult( | ||
ActivityResultContracts.RequestMultiplePermissions(), | ||
) { permission: Map<String, Boolean> -> | ||
private val storagePermissionLauncher = | ||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> | ||
val isGranted: Boolean = permissions.values.all { it } | ||
if (!isGranted) { | ||
showStoragePermissionSnackBar() | ||
return@registerForActivityResult | ||
} | ||
openCamera() | ||
} | ||
|
||
val keys = permission.entries.map { it.key } | ||
val isStorageRequest = storagePermissions.any { keys.contains(it) } | ||
if (isStorageRequest) { | ||
if (permission.entries.map { it.value }.contains(false)) { | ||
showPermissionSnackbar(getString(R.string.snackbar_storage_message)) | ||
} else { | ||
openCamera() | ||
private val locationPermissionLauncher = | ||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> | ||
val isGranted: Boolean = permissions.values.all { true } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P4] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it이 맞습니다. 잘못 작성했네요 수정했습니다! |
||
if (!isGranted) { | ||
showLocationPermissionSnackBar() | ||
return@registerForActivityResult | ||
} | ||
return@registerForActivityResult | ||
setCoordinate() | ||
} | ||
showPermissionSnackbar(getString(R.string.snackbar_location_message)) | ||
|
||
private val isStoragePermissionGranted: Boolean | ||
get() = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { | ||
storagePermissionsBelow28.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED } | ||
} else { | ||
true | ||
} | ||
|
||
private val locationSettingLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { | ||
setCoordinate() | ||
} | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
binding = ActivityUploadBinding.inflate(layoutInflater) | ||
setContentView(binding.root) | ||
|
||
initViewModel() | ||
subscribe() | ||
registerAnalytics(this.lifecycle) | ||
setCoordinate() | ||
setClickListeners() | ||
locationPermissionLauncher.launch(locationPermissions) | ||
registerAnalytics(this.lifecycle) | ||
} | ||
|
||
private fun initViewModel() { | ||
|
@@ -115,29 +130,88 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic | |
} | ||
} | ||
|
||
private fun setClickListeners() { | ||
binding.ivUploadCameraIcon.setOnClickListener { | ||
logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) | ||
openCameraWithPermission() | ||
} | ||
binding.ivUploadPhoto.setOnClickListener { | ||
logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) | ||
openCameraWithPermission() | ||
} | ||
binding.ivUploadBack.setOnClickListener { | ||
finish() | ||
} | ||
binding.btnUploadSubmit.setOnClickListener { | ||
if (isFormValid().not()) { | ||
showToast(getString(R.string.upload_error_insufficient_info_message)) | ||
} else { | ||
viewModel.postPlace() | ||
} | ||
} | ||
} | ||
|
||
private fun changeVisibility(view: View, status: Int) { | ||
view.visibility = status | ||
} | ||
|
||
private fun showPermissionSnackbar(message: String) { | ||
private fun showStoragePermissionSnackBar() { | ||
binding.root.showSnackbarWithEvent( | ||
message = message, | ||
message = getString(R.string.snackbar_storage_message), | ||
actionTitle = getString(R.string.snackbar_action_title), | ||
length = Snackbar.LENGTH_LONG, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확실히 길어지니까 좋네요👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분은 PR 첫 게시글에 적혀있습니다 |
||
action = { openSetting() }, | ||
) | ||
} | ||
|
||
private fun showLocationPermissionSnackBar() { | ||
binding.root.showSnackbarWithEvent( | ||
message = getString(R.string.snackbar_location_message), | ||
actionTitle = getString(R.string.snackbar_action_title), | ||
length = Snackbar.LENGTH_INDEFINITE, | ||
action = { | ||
val appDetailsIntent = getSettingIntent() | ||
locationSettingLauncher.launch(appDetailsIntent) | ||
}, | ||
Comment on lines
+172
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] 그리고 그렇게 된다면 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 확장함수는 확인 했지만 위에서 말씀하셨듯이 보여주는 길이가 달라져서 사용하지 못했습니다. |
||
) | ||
} | ||
|
||
private fun getSettingIntent() = Intent( | ||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS, | ||
Uri.parse("package:$packageName"), | ||
).addCategory(Intent.CATEGORY_DEFAULT) | ||
|
||
private fun setImage(uri: Uri) { | ||
binding.ivUploadCameraIcon.visibility = View.GONE | ||
val bitmap = getBitmap(uri) | ||
binding.ivUploadPhoto.setImageBitmap(bitmap) | ||
val file = saveFile(bitmap) | ||
viewModel.setFile(file) | ||
} | ||
|
||
private fun getBitmap(uri: Uri) = BitmapBuilder(uri, contentResolver) | ||
.addScaling(RESIZE) | ||
.setProperRotate() | ||
.build() | ||
|
||
private fun saveFile(bitmap: Bitmap): File { | ||
val tempFile = File.createTempFile(System.currentTimeMillis().toString(), ".jpeg", cacheDir) | ||
FileOutputStream(tempFile).use { | ||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) | ||
} | ||
return tempFile | ||
} | ||
|
||
private fun setCoordinate() { | ||
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { | ||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) | ||
fusedLocationClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, createCancellationToken()) | ||
.addOnSuccessListener { location -> | ||
location.let { viewModel.setCoordinate(getCoordinate(location)) } | ||
} | ||
.addOnFailureListener { } | ||
} else { | ||
requestPermissionLauncher.launch(locationPermissions) | ||
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { | ||
showLocationPermissionSnackBar() | ||
return | ||
} | ||
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) | ||
fusedLocationClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, createCancellationToken()) | ||
.addOnSuccessListener { location -> | ||
location.let { viewModel.setCoordinate(getCoordinate(location)) } | ||
}.addOnFailureListener { } | ||
} | ||
|
||
private fun createCancellationToken(): CancellationToken { | ||
|
@@ -163,80 +237,39 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic | |
return (number * 10_000).toLong().toDouble() / 10_000 | ||
} | ||
|
||
private fun setClickListeners() { | ||
binding.ivUploadCameraIcon.setOnClickListener { | ||
logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) | ||
requestStoragePermission() | ||
} | ||
binding.ivUploadPhoto.setOnClickListener { | ||
logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) | ||
requestStoragePermission() | ||
} | ||
binding.ivUploadBack.setOnClickListener { | ||
finish() | ||
} | ||
binding.btnUploadSubmit.setOnClickListener { | ||
if (isFormValid().not()) { | ||
showToast(getString(R.string.upload_error_insufficient_info_message)) | ||
} else { | ||
viewModel.postPlace() | ||
} | ||
private fun openCameraWithPermission() { | ||
if (!isStoragePermissionGranted) { | ||
storagePermissionLauncher.launch(storagePermissionsBelow28) | ||
return | ||
} | ||
} | ||
|
||
private fun requestStoragePermission() { | ||
val permissionToRequest = storagePermissions.toMutableList() | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||
return openCamera() | ||
} | ||
|
||
requestPermissionLauncher.launch(permissionToRequest.toTypedArray()) | ||
openCamera() | ||
} | ||
|
||
private fun openCamera() { | ||
cameraLauncher.launch(null) | ||
} | ||
|
||
private fun setImage(bitmap: Bitmap) { | ||
binding.ivUploadCameraIcon.visibility = View.GONE | ||
binding.ivUploadPhoto.setImageBitmap(bitmap) | ||
val uri = getImageUri(bitmap) ?: Uri.EMPTY | ||
val file = makeImageFile(uri) | ||
viewModel.setFile(file) | ||
} | ||
|
||
private fun makeImageFile(uri: Uri): File { | ||
val bitmap = contentResolver.openInputStream(uri).use { | ||
BitmapFactory.decodeStream(it) | ||
imageUri = createImageUri().getOrElse { | ||
Snackbar.make(binding.root, getString(R.string.upload_retry), Snackbar.LENGTH_SHORT).show() | ||
return | ||
} | ||
val tempFile = File.createTempFile("image", ".jpeg", cacheDir) ?: FILE_EMPTY | ||
|
||
FileOutputStream(tempFile).use { | ||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) | ||
} | ||
return tempFile | ||
cameraLauncher.launch(imageUri) | ||
} | ||
|
||
private fun getImageUri(bitmap: Bitmap): Uri? { | ||
val resolver = applicationContext.contentResolver | ||
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) | ||
?.let { imageUri -> | ||
val outputStream = resolver.openOutputStream(imageUri) | ||
outputStream?.use { stream -> | ||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) | ||
} | ||
return imageUri | ||
} | ||
return null | ||
private fun createImageUri(): Result<Uri> { | ||
val imageUri: Uri = contentResolver.insert( | ||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, | ||
contentValues, | ||
) ?: return Result.failure(IllegalStateException("이미지 uri를 가져오지 못했습니다.")) | ||
return Result.success(imageUri) | ||
} | ||
|
||
private fun isFormValid(): Boolean { | ||
return viewModel.isFormValid() | ||
} | ||
|
||
companion object { | ||
private val storagePermissions = arrayOf( | ||
private const val PRIORITY_HIGH_ACCURACY = 100 | ||
private const val RESIZE = 500 | ||
private val storagePermissionsBelow28 = arrayOf( | ||
Manifest.permission.WRITE_EXTERNAL_STORAGE, | ||
Manifest.permission.READ_EXTERNAL_STORAGE, | ||
) | ||
|
||
|
@@ -250,8 +283,6 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic | |
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") | ||
} | ||
|
||
const val PRIORITY_HIGH_ACCURACY = 100 | ||
|
||
fun getIntent(context: Context): Intent { | ||
return Intent(context, UploadActivity::class.java) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ import javax.inject.Inject | |
class UploadViewModel @Inject constructor( | ||
private val placeRepository: PlaceRepository, | ||
) : ViewModel() { | ||
private var file = FILE_EMPTY | ||
private var file: File? = null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 상황에 따라 다르지만 현재 상황에서는 빈 파일이 서버에 업로드 되는 것 보단 에러를 발생시키고 실패했다는 피드백을 유저에게 주는 것이 좋을 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 상황에서는 isFormValid로 체크하므로 터질일은 없다고 생각됩니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 안전하게 처리하는 방법을 선택했던 것인데요. 이것을 선택한 이유는 서버에서 한 번 검수를 한 뒤에 데이터를 넣는 작업을 진행하고 있고, File이 없는 경우 사용자에게 피드백을 주어도 어떠한 대처를 할 수 없다고 판단했기 때문입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 대처를 할 수 없더라도 유저가 등록이 되었는지 되지 않았는지를 아는게 좋다고 생각합니다. |
||
|
||
val name = MutableLiveData<String>() | ||
|
||
|
@@ -43,7 +43,7 @@ class UploadViewModel @Inject constructor( | |
} | ||
|
||
fun isFormValid(): Boolean { | ||
return (file != FILE_EMPTY) && (_coordinate.value != null) && (name.value != null) | ||
return (file != null) && (_coordinate.value != null) && (name.value != null) | ||
} | ||
|
||
fun postPlace() { | ||
|
@@ -55,7 +55,7 @@ class UploadViewModel @Inject constructor( | |
name = name.value.toString(), | ||
description = "", | ||
coordinate = coordinate, | ||
file = file, | ||
file = file!!, | ||
) | ||
}.onSuccess { | ||
_successUpload.setValue(UploadStatus.SUCCESS) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1]
string을 상수화 해도 좋겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 문구를 상수화해야 할 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러문구를 상수화 해놓으면 동반객체를 보았을 때 어떤 오류들이 발생할 수 있겠구나 한 번에 볼 수 있을 것 같아요.
사실 현재 코드가 길기 때문에 더 보기 힘든 것도 있지만 한 번에 관리하는 것이 코드의 가독성도, 리뷰에도 편한 것 같습니다. 만약 정말 에러가 발생한다고 가정했을 때 해당 문구를 어디서 사용하는지 바로가기를 통해 확인할 수도 있구요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 사실 에러 문구를 굳이 상수화 해야하는가? 라고 생각합니다.
보통 에러를 확인할 때 이 객체에서 어떤 에러가 일어날 수 있을까?를 생각하며 한눈에 모든 에러를 살펴보기 보다는 에러가 발생했을 때 어떤 에러가 발생했는 지를 확인한다고 생각합니다.
그렇기 때문에 굳이 한 곳에 모아놓아야하나? 라는 의문점이 듭니다.
또한 에러가 정말로 발생했을 때도 해당 에러가 발생한 곳으로 바로 이동이 가능하기 때문에 거기서 변수를 한 번 더 눌러서 확인하는 것 보다는 해당 코드에서 어떤 에러인지 바로 인지할 수 있는 것이 더 좋다고 생각합니다.