From baf7a1983568982bd505a9a56429ebea784ef2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A8=D0=B0=D0=BC=D0=B8=D0=BB=D1=8C?= Date: Wed, 25 Mar 2026 00:20:39 +0300 Subject: [PATCH] done --- app/build.gradle | 28 ++-- app/src/main/AndroidManifest.xml | 23 ++- .../sample/otuslocationmapshw/MapsActivity.kt | 51 +++--- .../camera/CameraActivity.kt | 146 ++++++++++-------- gradle.properties | 3 +- 5 files changed, 132 insertions(+), 119 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3e76c3d..17c8724 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,12 +6,12 @@ plugins { android { namespace 'com.sample.otuslocationmapshw' - compileSdk 35 + compileSdk 36 defaultConfig { applicationId "com.sample.otuslocationmapshw" minSdk 23 - targetSdk 35 + targetSdk 36 versionCode 1 versionName "1.0" @@ -38,18 +38,18 @@ android { dependencies { - implementation 'androidx.core:core-ktx:1.15.0' - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'com.google.android.material:material:1.12.0' - implementation 'com.google.android.gms:play-services-maps:19.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.2.0' - implementation 'androidx.camera:camera-core:1.4.1' - implementation 'androidx.camera:camera-lifecycle:1.4.1' - implementation 'androidx.camera:camera-camera2:1.4.1' - implementation 'androidx.camera:camera-view:1.4.1' + implementation 'androidx.core:core-ktx:1.17.0' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'com.google.android.material:material:1.13.0' + implementation 'com.google.android.gms:play-services-maps:20.0.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' + implementation 'androidx.camera:camera-core:1.5.3' + implementation 'androidx.camera:camera-lifecycle:1.5.3' + implementation 'androidx.camera:camera-camera2:1.5.3' + implementation 'androidx.camera:camera-view:1.5.3' implementation 'com.google.android.gms:play-services-location:21.3.0' - implementation 'androidx.exifinterface:exifinterface:1.3.7' + implementation 'androidx.exifinterface:exifinterface:1.4.2' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a1883f..75bba66 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,13 @@ + + + + + - + android:theme="@style/Theme.OTUSLocationMapsHW" > + android:value="${MAPS_API_KEY}" /> { cameraForResultLauncher.launch(Intent(this, CameraActivity::class.java)) true } - else -> { - super.onOptionsItemSelected(item) - } + else -> super.onOptionsItemSelected(item) } - } override fun onMapReady(googleMap: GoogleMap) { map = googleMap - showPreviewsOnMap() } private fun showPreviewsOnMap() { map.clear() + val folder = File("${filesDir.absolutePath}/photos/") - folder.listFiles()?.forEach { - val exifInterface = ExifInterface(it) - val location = locationDataUtils.getLocationFromExif(exifInterface) + + folder.listFiles()?.forEach { file -> + + val exif = ExifInterface(file) + val location = locationDataUtils.getLocationFromExif(exif) val point = LatLng(location.latitude, location.longitude) - val pinBitmap = Bitmap.createScaledBitmap( + + val bitmap = Bitmap.createScaledBitmap( BitmapFactory.decodeFile( - it.path, + file.path, BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 - }), 64, 64, false + } + ), + 64, + 64, + false ) - // TODO("Указать pinBitmap как иконку для маркера") + map.addMarker( MarkerOptions() .position(point) + .icon(BitmapDescriptorFactory.fromBitmap(bitmap)) ) - // TODO("Передвинуть карту к местоположению последнего фото") + + map.moveCamera(CameraUpdateFactory.newLatLngZoom(point, 10f)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/sample/otuslocationmapshw/camera/CameraActivity.kt b/app/src/main/java/com/sample/otuslocationmapshw/camera/CameraActivity.kt index 076ade5..c818a70 100644 --- a/app/src/main/java/com/sample/otuslocationmapshw/camera/CameraActivity.kt +++ b/app/src/main/java/com/sample/otuslocationmapshw/camera/CameraActivity.kt @@ -12,9 +12,7 @@ import android.util.Log import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.camera.core.CameraSelector -import androidx.camera.core.ImageCapture -import androidx.camera.core.Preview +import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -24,8 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.sample.otuslocationmapshw.databinding.ActivityCameraBinding import java.io.File import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale +import java.util.* import kotlin.math.abs class CameraActivity : AppCompatActivity() { @@ -44,23 +41,19 @@ class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityCameraBinding.inflate(layoutInflater) setContentView(binding.root) - if (allPermissionsGranted()) { - startCamera() - } else { - ActivityCompat.requestPermissions( - this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS - ) - } + if (allPermissionsGranted()) startCamera() + else ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) cameraProviderFuture = ProcessCameraProvider.getInstance(this) - // TODO("Получить экземпляр SensorManager") - // TODO("Добавить проверку на наличие датчика акселерометра и присвоить значение tiltSensor") - tiltSensor = TODO("Get tilt sensor") + sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager + tiltSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + cameraProviderFuture.addListener({ cameraProvider = cameraProviderFuture.get() }, ContextCompat.getMainExecutor(this)) @@ -71,19 +64,23 @@ class CameraActivity : AppCompatActivity() { binding.errorTextView.visibility = if (abs(tilt) > 2) View.VISIBLE else View.GONE } - override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { - //nothing to do - } + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} } - binding.takePhotoButton.setOnClickListener { - takePhoto() - } + binding.takePhotoButton.setOnClickListener { takePhoto() } } - // TODO("Подписаться на получение событий обновления датчика") + override fun onResume() { + super.onResume() + tiltSensor?.let { + sensorManager.registerListener(sensorEventListener, it, SensorManager.SENSOR_DELAY_NORMAL) + } + } - // TODO("Остановить получение событий от датчика") + override fun onPause() { + super.onPause() + sensorManager.unregisterListener(sensorEventListener) + } override fun onRequestPermissionsResult( requestCode: Int, @@ -91,15 +88,11 @@ class CameraActivity : AppCompatActivity() { grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQUEST_CODE_PERMISSIONS) { - if (allPermissionsGranted()) { - startCamera() - } else { - Toast.makeText( - this, - "Permissions not granted by the user.", - Toast.LENGTH_SHORT - ).show() + if (allPermissionsGranted()) startCamera() + else { + Toast.makeText(this, "Permissions not granted by the user.", Toast.LENGTH_SHORT).show() finish() } } @@ -108,70 +101,91 @@ class CameraActivity : AppCompatActivity() { @SuppressLint("MissingPermission") private fun takePhoto() { getLastLocation { location -> + Log.d("LOCATION", location.toString()) - val folderPath = "${filesDir.absolutePath}/photos/" - val folder = File(folderPath) - if (!folder.exists()) { - folder.mkdirs() + val folder = File("${filesDir.absolutePath}/photos/") + if (!folder.exists()) folder.mkdirs() + + val filePath = folder.absolutePath + "/" + + SimpleDateFormat(FILENAME_FORMAT, Locale.getDefault()).format(Date()) + + val metadata = ImageCapture.Metadata().apply { + location?.let { this.location = it } } - val filePath = folderPath + SimpleDateFormat(FILENAME_FORMAT, Locale.getDefault()).format(Date()) - // TODO("4. Добавить установку местоположения в метаданные фото") - val outputFileOptions = ImageCapture.OutputFileOptions.Builder(File(filePath)) + val outputOptions = ImageCapture.OutputFileOptions.Builder(File(filePath)) + .setMetadata(metadata) .build() - // TODO("Добавить вызов CameraX для фото") - // TODO("Вывести Toast о том, что фото успешно сохранено и закрыть текущее активити c указанием кода результата SUCCESS_RESULT_CODE") - // imageCapture... + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(this), + object : ImageCapture.OnImageSavedCallback { + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + Toast.makeText(this@CameraActivity, "Фото успешно сохранено!", Toast.LENGTH_SHORT).show() + setResult(SUCCESS_RESULT_CODE) + finish() + } + + override fun onError(exception: ImageCaptureException) { + Log.e(TAG, "Photo capture failed: ${exception.message}", exception) + Toast.makeText( + this@CameraActivity, + "Ошибка при сохранении фото: ${exception.message}", + Toast.LENGTH_LONG + ).show() + } + } + ) } } @SuppressLint("MissingPermission") private fun getLastLocation(callback: (location: Location?) -> Unit) { - // TODO("Добавить получение местоположения от fusedLocationClient и передать результат в callback после получения") + fusedLocationClient.lastLocation + .addOnSuccessListener { callback.invoke(it) } + .addOnFailureListener { callback.invoke(null) } } private fun startCamera() { - val cameraProviderFuture = ProcessCameraProvider.getInstance(this) - cameraProviderFuture.addListener({ - val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val future = ProcessCameraProvider.getInstance(this) - val preview = Preview.Builder() - .build() - .also { - it.setSurfaceProvider(binding.cameraPreview.surfaceProvider) - } + future.addListener({ + val provider = future.get() - imageCapture = ImageCapture.Builder().build() + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(binding.cameraPreview.surfaceProvider) + } - val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + imageCapture = ImageCapture.Builder().build() try { - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture - ) - } catch (exc: Exception) { - Log.e(TAG, "Use case binding failed", exc) + provider.unbindAll() + provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture) + } catch (e: Exception) { + Log.e(TAG, "Use case binding failed", e) } }, ContextCompat.getMainExecutor(this)) } - private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { - ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED - } + private fun allPermissionsGranted() = + REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED + } companion object { - private const val TAG = "CameraXApp" private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private const val REQUEST_CODE_PERMISSIONS = 10 - // TODO("Указать набор требуемых разрешений") - private val REQUIRED_PERMISSIONS: Array = mutableListOf( - // TODO("Добавить требуемые разрешения") - ).toTypedArray() + + private val REQUIRED_PERMISSIONS = arrayOf( + android.Manifest.permission.CAMERA, + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION + ) const val SUCCESS_RESULT_CODE = 15 } diff --git a/gradle.properties b/gradle.properties index a2e90d8..d977798 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,5 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false +android.overridePathCheck=true \ No newline at end of file